From e4a07e01b5a6699e6c23c373aebf444707eef8a3 Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Tue, 1 Oct 2013 13:04:47 -0700 Subject: [PATCH] Update Phrequent to use new search infrastructure. Summary: This updates Phrequent to use new the search infrastructure. Now it looks like: {F60141} I've also added the policy infrastructure stubs, but it's probably not even close to being right in terms of enforcing policies (in particular being able to see time tracked against objects the user wouldn't normally be able to see). At some point I'd like to be able to filter on the objects that the time is tracked against, but I don't believe there's a tokenizer / readahead control that allows you to type any kind of object. Test Plan: Clicked around the new interface, created some custom queries and saved them. Reviewers: epriestley CC: Korvin, aran Maniphest Tasks: T3870 Differential Revision: https://secure.phabricator.com/D7163 --- src/__phutil_library_map__.php | 16 +- .../PhabricatorApplicationPhrequent.php | 3 +- .../controller/PhrequentController.php | 17 +- .../controller/PhrequentListController.php | 317 ++++-------------- .../phrequent/query/PhrequentSearchEngine.php | 114 +++++++ .../query/PhrequentUserTimeQuery.php | 145 +++++--- .../phrequent/storage/PhrequentUserTime.php | 31 +- 7 files changed, 331 insertions(+), 312 deletions(-) create mode 100644 src/applications/phrequent/query/PhrequentSearchEngine.php 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.'); + } + }