diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 6c4b09993b..dbedd9b0b3 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1917,6 +1917,7 @@ phutil_register_library_map(array( 'PhrequentController' => 'applications/phrequent/controller/PhrequentController.php', 'PhrequentDAO' => 'applications/phrequent/storage/PhrequentDAO.php', 'PhrequentListController' => 'applications/phrequent/controller/PhrequentListController.php', + 'PhrequentSearchEngine' => 'applications/phrequent/query/PhrequentSearchEngine.php', 'PhrequentTrackController' => 'applications/phrequent/controller/PhrequentTrackController.php', 'PhrequentTrackableInterface' => 'applications/phrequent/interface/PhrequentTrackableInterface.php', 'PhrequentUIEventListener' => 'applications/phrequent/event/PhrequentUIEventListener.php', @@ -4130,11 +4131,20 @@ phutil_register_library_map(array( 'PhortuneWePayPaymentProvider' => 'PhortunePaymentProvider', 'PhrequentController' => 'PhabricatorController', 'PhrequentDAO' => 'PhabricatorLiskDAO', - 'PhrequentListController' => 'PhrequentController', + 'PhrequentListController' => + array( + 0 => 'PhrequentController', + 1 => 'PhabricatorApplicationSearchResultsControllerInterface', + ), + 'PhrequentSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhrequentTrackController' => 'PhrequentController', 'PhrequentUIEventListener' => 'PhutilEventListener', - 'PhrequentUserTime' => 'PhrequentDAO', - 'PhrequentUserTimeQuery' => 'PhabricatorOffsetPagedQuery', + 'PhrequentUserTime' => + array( + 0 => 'PhrequentDAO', + 1 => 'PhabricatorPolicyInterface', + ), + 'PhrequentUserTimeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhrictionActionConstants' => 'PhrictionConstants', 'PhrictionActionMenuEventListener' => 'PhutilEventListener', 'PhrictionChangeType' => 'PhrictionConstants', diff --git a/src/applications/phrequent/application/PhabricatorApplicationPhrequent.php b/src/applications/phrequent/application/PhabricatorApplicationPhrequent.php index b1227f2662..bf406cd703 100644 --- a/src/applications/phrequent/application/PhabricatorApplicationPhrequent.php +++ b/src/applications/phrequent/application/PhabricatorApplicationPhrequent.php @@ -35,8 +35,7 @@ final class PhabricatorApplicationPhrequent extends PhabricatorApplication { public function getRoutes() { return array( '/phrequent/' => array( - '' => 'PhrequentListController', - 'view/(?P\w+)/' => 'PhrequentListController', + '(?:query/(?P[^/]+)/)?' => 'PhrequentListController', 'track/(?P[a-z]+)/(?P[^/]+)/' => 'PhrequentTrackController' ), diff --git a/src/applications/phrequent/controller/PhrequentController.php b/src/applications/phrequent/controller/PhrequentController.php index 0a71ffc9af..bb8673cdf5 100644 --- a/src/applications/phrequent/controller/PhrequentController.php +++ b/src/applications/phrequent/controller/PhrequentController.php @@ -2,18 +2,17 @@ abstract class PhrequentController extends PhabricatorController { - protected function buildNav($view) { + protected function buildSideNavView() { + $user = $this->getRequest()->getUser(); + $nav = new AphrontSideNavFilterView(); - $nav->setBaseURI(new PhutilURI('/phrequent/view/')); + $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); - $nav->addLabel(pht('User Times')); - $nav->addFilter('current', pht('Currently Tracking')); - $nav->addFilter('recent', pht('Recent Activity')); - $nav->addLabel('All Times'); - $nav->addFilter('allcurrent', pht('Currently Tracking')); - $nav->addFilter('allrecent', pht('Recent Activity')); + id(new PhrequentSearchEngine()) + ->setViewer($user) + ->addNavigationItems($nav->getMenu()); - $nav->selectFilter($view); + $nav->selectFilter(null); return $nav; } diff --git a/src/applications/phrequent/controller/PhrequentListController.php b/src/applications/phrequent/controller/PhrequentListController.php index f79fcd53c5..8bb277d2e2 100644 --- a/src/applications/phrequent/controller/PhrequentListController.php +++ b/src/applications/phrequent/controller/PhrequentListController.php @@ -1,290 +1,95 @@ view = idx($data, 'view', "current"); + public function shouldAllowPublic() { + return true; } - private function getArrToStrList($key) { - $arr = $this->getRequest()->getArr($key); - $arr = implode(',', $arr); - return nonempty($arr, null); + public function willProcessRequest(array $data) { + $this->queryKey = idx($data, 'queryKey'); } public function processRequest() { $request = $this->getRequest(); - $user = $request->getUser(); - - if ($request->isFormPost()) { - // Redirect to GET so URIs can be copy/pasted. - - $order = $request->getStr('o'); - $order = nonempty($order, null); - - $ended = $request->getStr('e'); - $ended = nonempty($ended, null); - - $uri = $request->getRequestURI() - ->alter('users', $this->getArrToStrList('set_users')) - ->alter('o', $order) - ->alter('e', $ended); - - return id(new AphrontRedirectResponse())->setURI($uri); - } - - $nav = $this->buildNav($this->view); - - $has_user_filter = array( - "current" => true, - "recent" => true, - ); - - $user_phids = $request->getStrList('users', array()); - if (isset($has_user_filter[$this->view])) { - $user_phids = array($user->getPHID()); - } - - switch ($this->view) { - case "current": - case "allcurrent": - $order_key_default = "s"; - $ended_key_default = "n"; - break; - case "recent": - case "allrecent": - $order_key_default = "s"; - $ended_key_default = "y"; - break; - default: - $order_key_default = "s"; - $ended_key_default = "a"; - break; - } - - switch ($request->getStr('o', $order_key_default)) { - case 's': - $order = PhrequentUserTimeQuery::ORDER_STARTED; - break; - case 'e': - $order = PhrequentUserTimeQuery::ORDER_ENDED; - break; - case 'd': - $order = PhrequentUserTimeQuery::ORDER_DURATION; - break; - default: - throw new Exception("Unknown order!"); - } - - switch ($request->getStr('e', $ended_key_default)) { - case 'a': - $ended = PhrequentUserTimeQuery::ENDED_ALL; - break; - case 'y': - $ended = PhrequentUserTimeQuery::ENDED_YES; - break; - case 'n': - $ended = PhrequentUserTimeQuery::ENDED_NO; - break; - default: - throw new Exception("Unknown ended!"); - } - - $filter = new AphrontListFilterView(); - $filter->appendChild( - $this->buildForm($user_phids, $order_key_default, $ended_key_default)); - - $query = new PhrequentUserTimeQuery(); - $query->setOrder($order); - $query->setEnded($ended); - $query->setUsers($user_phids); - - $pager = new AphrontPagerView(); - $pager->setPageSize(500); - $pager->setOffset($request->getInt('offset')); - $pager->setURI($request->getRequestURI(), 'offset'); - - $logs = $query->executeWithOffsetPager($pager); - - $title = pht('Time Tracked'); - - $table = $this->buildTableView($logs); - $table->appendChild($pager); - - $nav->appendChild( - array( - $filter, - $table, - $pager, - )); - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($this->getApplicationURI('/'))); - - $nav->setCrumbs($crumbs); - - return $this->buildApplicationPage( - $nav, - array( - 'title' => $title, - 'device' => true, - )); + $controller = id(new PhabricatorApplicationSearchController($request)) + ->setQueryKey($this->queryKey) + ->setSearchEngine(new PhrequentSearchEngine()) + ->setNavigation($this->buildSideNavView()); + return $this->delegateToController($controller); } - protected function buildForm(array $user_phids, $order_key_default, - $ended_key_default) { - $request = $this->getRequest(); - $user = $request->getUser(); - - $form = id(new AphrontFormView()) - ->setUser($user) - ->setAction($this->getApplicationURI("/view/custom/")); - - $user_handles = id(new PhabricatorHandleQuery()) - ->setViewer($user) - ->withPHIDs($user_phids) - ->execute(); - $tokens = array(); - foreach ($user_phids as $phid) { - $tokens[$phid] = $user_handles[$phid]->getFullName(); - } - $form->appendChild( - id(new AphrontFormTokenizerControl()) - ->setDatasource('/typeahead/common/searchowner/') - ->setName('set_users') - ->setLabel(pht('Users')) - ->setValue($tokens)); - - $form->appendChild( - id(new AphrontFormToggleButtonsControl()) - ->setName('o') - ->setLabel(pht('Sort Order')) - ->setBaseURI($request->getRequestURI(), 'o') - ->setValue($request->getStr('o', $order_key_default)) - ->setButtons( - array( - 's' => pht('Started'), - 'e' => pht('Ended'), - 'd' => pht('Duration'), - ))); - - $form->appendChild( - id(new AphrontFormToggleButtonsControl()) - ->setName('e') - ->setLabel(pht('Ended')) - ->setBaseURI($request->getRequestURI(), 'e') - ->setValue($request->getStr('e', $ended_key_default)) - ->setButtons( - array( - 'y' => pht('Yes'), - 'n' => pht('No'), - 'a' => pht('All'), - ))); - - $form->appendChild( - id(new AphrontFormSubmitControl())->setValue(pht('Filter Objects'))); - - return $form; - } - - protected function buildTableView(array $usertimes) { + public function renderResultsList( + array $usertimes, + PhabricatorSavedQuery $query) { assert_instances_of($usertimes, 'PhrequentUserTime'); - - $user = $this->getRequest()->getUser(); + $viewer = $this->getRequest()->getUser(); $phids = array(); - foreach ($usertimes as $usertime) { - $phids[] = $usertime->getUserPHID(); - $phids[] = $usertime->getObjectPHID(); - } + $phids[] = mpull($usertimes, 'getUserPHID'); + $phids[] = mpull($usertimes, 'getObjectPHID'); + $phids = array_mergev($phids); + $handles = $this->loadViewerHandles($phids); - $rows = array(); + $view = id(new PHUIObjectItemListView()) + ->setUser($viewer); + foreach ($usertimes as $usertime) { + $item = new PHUIObjectItemView(); + + if ($usertime->getObjectPHID() === null) { + $item->setHeader($usertime->getNote()); + } else { + $obj = $handles[$usertime->getObjectPHID()]; + $item->setHeader($obj->getLinkName()); + $item->setHref($obj->getURI()); + } + $item->setObject($usertime); + + $item->addByline( + pht( + 'Tracked: %s', + $handles[$usertime->getUserPHID()]->renderLink())); + + $started_date = phabricator_date($usertime->getDateStarted(), $viewer); + $item->addIcon('none', $started_date); if ($usertime->getDateEnded() !== null) { $time_spent = $usertime->getDateEnded() - $usertime->getDateStarted(); - $time_ended = phabricator_datetime($usertime->getDateEnded(), $user); + $time_ended = phabricator_datetime($usertime->getDateEnded(), $viewer); } else { $time_spent = time() - $usertime->getDateStarted(); - $time_ended = phutil_tag( - 'em', - array(), - pht('Ongoing')); } - $usertime_user = $handles[$usertime->getUserPHID()]; - $usertime_object = null; - $object = null; - if ($usertime->getObjectPHID() !== null) { - $usertime_object = $handles[$usertime->getObjectPHID()]; - $object = phutil_tag( - 'a', - array( - 'href' => $usertime_object->getURI() - ), - $usertime_object->getFullName()); + $time_spent = $time_spent == 0 ? 'none' : + phabricator_format_relative_time_detailed($time_spent); + + if ($usertime->getDateEnded() !== null) { + $item->addAttribute( + pht( + 'Tracked %s', + $time_spent)); + $item->addAttribute( + pht( + 'Ended on %s', + $time_ended)); } else { - $object = phutil_tag( - 'em', - array(), - pht('None')); + $item->addAttribute( + pht( + 'Tracked %s so far', + $time_spent)); + $item->setBarColor('green'); } - $rows[] = array( - $object, - phutil_tag( - 'a', - array( - 'href' => $usertime_user->getURI() - ), - $usertime_user->getFullName()), - phabricator_datetime($usertime->getDateStarted(), $user), - $time_ended, - $time_spent == 0 ? 'none' : - phabricator_format_relative_time_detailed($time_spent), - $usertime->getNote() - ); + $view->addItem($item); } - $table = new AphrontTableView($rows); - $table->setDeviceReadyTable(true); - $table->setHeaders( - array( - 'Object', - 'User', - 'Started', - 'Ended', - 'Duration', - 'Note' - )); - $table->setShortHeaders( - array( - 'O', - 'U', - 'S', - 'E', - 'D', - 'Note', - '', - )); - $table->setColumnClasses( - array( - '', - '', - '', - '', - '', - 'wide' - )); - - return $table; + return $view; } } diff --git a/src/applications/phrequent/query/PhrequentSearchEngine.php b/src/applications/phrequent/query/PhrequentSearchEngine.php new file mode 100644 index 0000000000..5e3d152f3a --- /dev/null +++ b/src/applications/phrequent/query/PhrequentSearchEngine.php @@ -0,0 +1,114 @@ +getParameter('limit', 1000); + } + + public function buildSavedQueryFromRequest(AphrontRequest $request) { + $saved = new PhabricatorSavedQuery(); + + $saved->setParameter( + 'userPHIDs', + $this->readUsersFromRequest($request, 'users')); + + $saved->setParameter('ended', $request->getStr('ended')); + + $saved->setParameter('order', $request->getStr('order')); + + return $saved; + } + + public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { + $query = id(new PhrequentUserTimeQuery()); + + $user_phids = $saved->getParameter('userPHIDs'); + if ($user_phids) { + $query->withUserPHIDs($user_phids); + } + + $ended = $saved->getParameter('ended'); + if ($ended != null) { + $query->withEnded($ended); + } + + $order = $saved->getParameter('order'); + if ($order != null) { + $query->setOrder($order); + } + + return $query; + } + + public function buildSearchForm( + AphrontFormView $form, + PhabricatorSavedQuery $saved_query) { + + $user_phids = $saved_query->getParameter('userPHIDs', array()); + $ended = $saved_query->getParameter( + 'ended', PhrequentUserTimeQuery::ENDED_ALL); + $order = $saved_query->getParameter( + 'order', PhrequentUserTimeQuery::ORDER_ENDED_DESC); + + $phids = array_merge($user_phids); + $handles = id(new PhabricatorHandleQuery()) + ->setViewer($this->requireViewer()) + ->withPHIDs($phids) + ->execute(); + + $form + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setDatasource('/typeahead/common/users/') + ->setName('users') + ->setLabel(pht('Users')) + ->setValue($handles)) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel(pht('Ended')) + ->setName('ended') + ->setValue($ended) + ->setOptions(PhrequentUserTimeQuery::getEndedSearchOptions())) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel(pht('Order')) + ->setName('order') + ->setValue($order) + ->setOptions(PhrequentUserTimeQuery::getOrderSearchOptions())); + } + + protected function getURI($path) { + return '/phrequent/'.$path; + } + + public function getBuiltinQueryNames() { + $names = array( + 'tracking' => pht('Currently Tracking'), + 'all' => pht('All Tracked'), + ); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query + ->setParameter('order', PhrequentUserTimeQuery::ORDER_ENDED_DESC); + case 'tracking': + return $query + ->setParameter('ended', PhrequentUserTimeQuery::ENDED_NO) + ->setParameter('order', PhrequentUserTimeQuery::ORDER_ENDED_DESC); + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + +} + diff --git a/src/applications/phrequent/query/PhrequentUserTimeQuery.php b/src/applications/phrequent/query/PhrequentUserTimeQuery.php index 4ab0351309..a6321906df 100644 --- a/src/applications/phrequent/query/PhrequentUserTimeQuery.php +++ b/src/applications/phrequent/query/PhrequentUserTimeQuery.php @@ -1,56 +1,46 @@ userPHIDs = $user_phids; return $this; } - public function setObjects($object_phids) { + public function withObjectPHIDs($object_phids) { $this->objectPHIDs = $object_phids; return $this; } + public function withEnded($ended) { + $this->ended = $ended; + return $this; + } + public function setOrder($order) { $this->order = $order; return $this; } - public function setEnded($ended) { - $this->ended = $ended; - return $this; - } - - public function execute() { - $usertime_dao = new PhrequentUserTime(); - $conn = $usertime_dao->establishConnection('r'); - - $data = queryfx_all( - $conn, - 'SELECT usertime.* FROM %T usertime %Q %Q %Q', - $usertime_dao->getTableName(), - $this->buildWhereClause($conn), - $this->buildOrderClause($conn), - $this->buildLimitClause($conn)); - - return $usertime_dao->loadAllFromArray($data); - } - private function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); @@ -85,27 +75,100 @@ final class PhrequentUserTimeQuery extends PhabricatorOffsetPagedQuery { throw new Exception("Unknown ended '{$this->ended}'!"); } + $where[] = $this->buildPagingClause($conn); + return $this->formatWhereClause($where); } - private function buildOrderClause(AphrontDatabaseConnection $conn) { + protected function getPagingColumn() { switch ($this->order) { - case self::ORDER_ID: - return 'ORDER BY id ASC'; - case self::ORDER_STARTED: - return 'ORDER BY dateStarted DESC'; - case self::ORDER_ENDED: - return 'ORDER BY dateEnded IS NULL, dateEnded DESC, dateStarted DESC'; - case self::ORDER_DURATION: - return 'ORDER BY COALESCE(dateEnded, UNIX_TIMESTAMP()) - dateStarted '. - 'DESC'; + case self::ORDER_ID_ASC: + case self::ORDER_ID_DESC: + return 'id'; + case self::ORDER_STARTED_ASC: + case self::ORDER_STARTED_DESC: + return 'dateStarted'; + case self::ORDER_ENDED_ASC: + case self::ORDER_ENDED_DESC: + return 'dateEnded'; + case self::ORDER_DURATION_ASC: + case self::ORDER_DURATION_DESC: + return 'COALESCE(dateEnded, UNIX_TIMESTAMP()) - dateStarted'; default: throw new Exception("Unknown order '{$this->order}'!"); } } + protected function getPagingValue($result) { + switch ($this->order) { + case self::ORDER_ID_ASC: + case self::ORDER_ID_DESC: + return $result->getID(); + case self::ORDER_STARTED_ASC: + case self::ORDER_STARTED_DESC: + return $result->getDateStarted(); + case self::ORDER_ENDED_ASC: + case self::ORDER_ENDED_DESC: + return $result->getDateEnded(); + case self::ORDER_DURATION_ASC: + case self::ORDER_DURATION_DESC: + return ($result->getDateEnded() || time()) - $result->getDateStarted(); + default: + throw new Exception("Unknown order '{$this->order}'!"); + } + } + + protected function getReversePaging() { + switch ($this->order) { + case self::ORDER_ID_ASC: + case self::ORDER_STARTED_ASC: + case self::ORDER_ENDED_ASC: + case self::ORDER_DURATION_ASC: + return true; + case self::ORDER_ID_DESC: + case self::ORDER_STARTED_DESC: + case self::ORDER_ENDED_DESC: + case self::ORDER_DURATION_DESC: + return false; + default: + throw new Exception("Unknown order '{$this->order}'!"); + } + } + + protected function loadPage() { + $usertime = new PhrequentUserTime(); + $conn = $usertime->establishConnection('r'); + + $data = queryfx_all( + $conn, + 'SELECT usertime.* FROM %T usertime %Q %Q %Q', + $usertime->getTableName(), + $this->buildWhereClause($conn), + $this->buildOrderClause($conn), + $this->buildLimitClause($conn)); + + return $usertime->loadAllFromArray($data); + } + /* -( Helper Functions ) --------------------------------------------------- */ + public static function getEndedSearchOptions() { + return array( + self::ENDED_ALL => pht('All'), + self::ENDED_NO => pht('No'), + self::ENDED_YES => pht('Yes')); + } + + public static function getOrderSearchOptions() { + return array( + self::ORDER_STARTED_ASC => pht('by furthest start date'), + self::ORDER_STARTED_DESC => pht('by nearest start date'), + self::ORDER_ENDED_ASC => pht('by furthest end date'), + self::ORDER_ENDED_DESC => pht('by nearest end date'), + self::ORDER_DURATION_ASC => pht('by smallest duration'), + self::ORDER_DURATION_DESC => pht('by largest duration')); + } + public static function getUserTotalObjectsTracked( PhabricatorUser $user) { diff --git a/src/applications/phrequent/storage/PhrequentUserTime.php b/src/applications/phrequent/storage/PhrequentUserTime.php index f467168dac..3fd32593e4 100644 --- a/src/applications/phrequent/storage/PhrequentUserTime.php +++ b/src/applications/phrequent/storage/PhrequentUserTime.php @@ -3,7 +3,8 @@ /** * @group phrequent */ -final class PhrequentUserTime extends PhrequentDAO { +final class PhrequentUserTime extends PhrequentDAO + implements PhabricatorPolicyInterface { protected $userPHID; protected $objectPHID; @@ -11,4 +12,32 @@ final class PhrequentUserTime extends PhrequentDAO { protected $dateStarted; protected $dateEnded; + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + $policy = PhabricatorPolicies::POLICY_NOONE; + + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + $policy = PhabricatorPolicies::POLICY_USER; + break; + } + + return $policy; + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return ($viewer->getPHID() == $this->getUserPHID()); + } + + + public function describeAutomaticCapability($capability) { + return pht( + 'The user who tracked time can always view it.'); + } + }