From e555b9025fcf7fc27b0707d1102af701e1aa0a73 Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Sat, 30 Mar 2013 09:32:29 -0700 Subject: [PATCH] Implemented Phrequent time tracking functionality. Summary: This differential implements Phrequent's time tracking functionality for users and hooks it up to Maniphest. It also includes a basic "Time Tracked" list for the Phrequent application, where users can review what they've spent time working on. Test Plan: Apply the patch and track some things in Maniphest. They should appear in the "Time Tracked" view of Phrequent. There is also a `phrequent.show-prompt` option which toggles whether to display a prompt when tracking time. I'm unsure of whether the prompt is useful or is more likely to cause people to click "Track Time", go off and do the task and then come back to the prompt still waiting for them to confirm. A potential solution to the "accidentally clicking the button and recording 2 seconds of time" might be to show a prompt on stop if the total time is under 10 seconds, asking whether the user wants to keep or discard the tracked time. Reviewers: epriestley Reviewed By: epriestley CC: aran, Korvin Maniphest Tasks: T2857 Differential Revision: https://secure.phabricator.com/D5479 --- src/__phutil_library_map__.php | 21 +++ .../storage/DifferentialRevision.php | 5 +- .../maniphest/storage/ManiphestTask.php | 3 +- .../PhabricatorApplicationPhrequent.php | 73 ++++++++ .../PhabricatorPhrequentConfigOptions.php | 18 ++ .../controller/PhrequentController.php | 15 ++ .../controller/PhrequentListController.php | 146 +++++++++++++++ .../controller/PhrequentTrackController.php | 70 ++++++++ .../event/PhrequentUIEventListener.php | 81 +++++++++ .../interface/PhrequentTrackableInterface.php | 5 + .../query/PhrequentUserTimeQuery.php | 166 ++++++++++++++++++ 11 files changed, 601 insertions(+), 2 deletions(-) create mode 100644 src/applications/phrequent/application/PhabricatorApplicationPhrequent.php create mode 100644 src/applications/phrequent/config/PhabricatorPhrequentConfigOptions.php create mode 100644 src/applications/phrequent/controller/PhrequentController.php create mode 100644 src/applications/phrequent/controller/PhrequentListController.php create mode 100644 src/applications/phrequent/controller/PhrequentTrackController.php create mode 100644 src/applications/phrequent/event/PhrequentUIEventListener.php create mode 100644 src/applications/phrequent/interface/PhrequentTrackableInterface.php create mode 100644 src/applications/phrequent/query/PhrequentUserTimeQuery.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index ffdcb510af..12e3440a2b 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -703,6 +703,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationPhlux' => 'applications/phlux/application/PhabricatorApplicationPhlux.php', 'PhabricatorApplicationPholio' => 'applications/pholio/application/PhabricatorApplicationPholio.php', 'PhabricatorApplicationPhortune' => 'applications/phortune/application/PhabricatorApplicationPhortune.php', + 'PhabricatorApplicationPhrequent' => 'applications/phrequent/application/PhabricatorApplicationPhrequent.php', 'PhabricatorApplicationPhriction' => 'applications/phriction/application/PhabricatorApplicationPhriction.php', 'PhabricatorApplicationPonder' => 'applications/ponder/application/PhabricatorApplicationPonder.php', 'PhabricatorApplicationProject' => 'applications/project/application/PhabricatorApplicationProject.php', @@ -1181,6 +1182,7 @@ phutil_register_library_map(array( 'PhabricatorPhabricatorOAuthConfigOptions' => 'applications/config/option/PhabricatorPhabricatorOAuthConfigOptions.php', 'PhabricatorPhameConfigOptions' => 'applications/phame/config/PhabricatorPhameConfigOptions.php', 'PhabricatorPholioConfigOptions' => 'applications/pholio/config/PhabricatorPholioConfigOptions.php', + 'PhabricatorPhrequentConfigOptions' => 'applications/phrequent/config/PhabricatorPhrequentConfigOptions.php', 'PhabricatorPhrictionConfigOptions' => 'applications/phriction/config/PhabricatorPhrictionConfigOptions.php', 'PhabricatorPinboardItemView' => 'view/layout/PhabricatorPinboardItemView.php', 'PhabricatorPinboardView' => 'view/layout/PhabricatorPinboardView.php', @@ -1556,6 +1558,14 @@ phutil_register_library_map(array( 'PhortunePurchase' => 'applications/phortune/storage/PhortunePurchase.php', 'PhortuneStripePaymentFormView' => 'applications/phortune/view/PhortuneStripePaymentFormView.php', 'PhortuneUtil' => 'applications/phortune/util/PhortuneUtil.php', + 'PhrequentController' => 'applications/phrequent/controller/PhrequentController.php', + 'PhrequentDAO' => 'applications/phrequent/storage/PhrequentDAO.php', + 'PhrequentListController' => 'applications/phrequent/controller/PhrequentListController.php', + 'PhrequentTrackController' => 'applications/phrequent/controller/PhrequentTrackController.php', + 'PhrequentTrackableInterface' => 'applications/phrequent/interface/PhrequentTrackableInterface.php', + 'PhrequentUIEventListener' => 'applications/phrequent/event/PhrequentUIEventListener.php', + 'PhrequentUserTime' => 'applications/phrequent/storage/PhrequentUserTime.php', + 'PhrequentUserTimeQuery' => 'applications/phrequent/query/PhrequentUserTimeQuery.php', 'PhrictionActionConstants' => 'applications/phriction/constants/PhrictionActionConstants.php', 'PhrictionChangeType' => 'applications/phriction/constants/PhrictionChangeType.php', 'PhrictionConstants' => 'applications/phriction/constants/PhrictionConstants.php', @@ -2052,6 +2062,7 @@ phutil_register_library_map(array( 0 => 'DifferentialDAO', 1 => 'PhabricatorTokenReceiverInterface', 2 => 'PhabricatorPolicyInterface', + 3 => 'PhrequentTrackableInterface', ), 'DifferentialRevisionCommentListView' => 'AphrontView', 'DifferentialRevisionCommentView' => 'AphrontView', @@ -2276,6 +2287,7 @@ phutil_register_library_map(array( 1 => 'PhabricatorMarkupInterface', 2 => 'PhabricatorPolicyInterface', 3 => 'PhabricatorTokenReceiverInterface', + 4 => 'PhrequentTrackableInterface', ), 'ManiphestTaskAuxiliaryStorage' => 'ManiphestDAO', 'ManiphestTaskDescriptionChangeController' => 'ManiphestController', @@ -2354,6 +2366,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationPhlux' => 'PhabricatorApplication', 'PhabricatorApplicationPholio' => 'PhabricatorApplication', 'PhabricatorApplicationPhortune' => 'PhabricatorApplication', + 'PhabricatorApplicationPhrequent' => 'PhabricatorApplication', 'PhabricatorApplicationPhriction' => 'PhabricatorApplication', 'PhabricatorApplicationPonder' => 'PhabricatorApplication', 'PhabricatorApplicationProject' => 'PhabricatorApplication', @@ -2821,6 +2834,7 @@ phutil_register_library_map(array( 'PhabricatorPhabricatorOAuthConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPhameConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPholioConfigOptions' => 'PhabricatorApplicationConfigOptions', + 'PhabricatorPhrequentConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPhrictionConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPinboardItemView' => 'AphrontView', 'PhabricatorPinboardView' => 'AphrontView', @@ -3222,6 +3236,13 @@ phutil_register_library_map(array( 'PhortuneProductViewController' => 'PhabricatorController', 'PhortunePurchase' => 'PhortuneDAO', 'PhortuneStripePaymentFormView' => 'AphrontView', + 'PhrequentController' => 'PhabricatorController', + 'PhrequentDAO' => 'PhabricatorLiskDAO', + 'PhrequentListController' => 'PhrequentController', + 'PhrequentTrackController' => 'PhabricatorApplicationsController', + 'PhrequentUIEventListener' => 'PhutilEventListener', + 'PhrequentUserTime' => 'PhrequentDAO', + 'PhrequentUserTimeQuery' => 'PhabricatorOffsetPagedQuery', 'PhrictionActionConstants' => 'PhrictionConstants', 'PhrictionChangeType' => 'PhrictionConstants', 'PhrictionContent' => diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index a0af974267..d134149252 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -1,7 +1,10 @@ array( + '' => 'PhrequentListController', + 'track/(?P[a-z]+)/(?P[^/]+)/' + => 'PhrequentTrackController' + ), + ); + } + + public function loadStatus(PhabricatorUser $user) { + $status = array(); + + // TODO: Show number of timers that are currently + // running for a user. + + /* + + $query = id(new ManiphestTaskQuery()) + ->withStatus(ManiphestTaskQuery::STATUS_OPEN) + ->withOwners(array($user->getPHID())) + ->setLimit(1) + ->setCalculateRows(true); + $query->execute(); + + $count = $query->getRowCount(); + $type = PhabricatorApplicationStatusView::TYPE_WARNING; + $status[] = id(new PhabricatorApplicationStatusView()) + ->setType($type) + ->setText(pht('%d Assigned Task(s)', $count)) + ->setCount($count); + + */ + + return $status; + } + +} + diff --git a/src/applications/phrequent/config/PhabricatorPhrequentConfigOptions.php b/src/applications/phrequent/config/PhabricatorPhrequentConfigOptions.php new file mode 100644 index 0000000000..7ec707eaa8 --- /dev/null +++ b/src/applications/phrequent/config/PhabricatorPhrequentConfigOptions.php @@ -0,0 +1,18 @@ +setBaseURI(new PhutilURI('/phrequent/')); + + $nav->addFilter('usertime', 'Time Tracked'); + + $nav->selectFilter($view); + + return $nav; + } +} diff --git a/src/applications/phrequent/controller/PhrequentListController.php b/src/applications/phrequent/controller/PhrequentListController.php new file mode 100644 index 0000000000..eb1e6b548f --- /dev/null +++ b/src/applications/phrequent/controller/PhrequentListController.php @@ -0,0 +1,146 @@ +getRequest(); + $user = $request->getUser(); + + $nav = $this->buildNav('usertime'); + + $query = new PhrequentUserTimeQuery(); + $query->setOrder(PhrequentUserTimeQuery::ORDER_ENDED); + + $pager = new AphrontPagerView(); + $pager->setPageSize(500); + $pager->setOffset($request->getInt('offset')); + $pager->setURI($request->getRequestURI(), 'offset'); + + $logs = $query->executeWithOffsetPager($pager); + + $title = pht('Time Tracked'); + + $header = id(new PhabricatorHeaderView()) + ->setHeader($title); + + $table = $this->buildTableView($logs); + $table->appendChild($pager); + + $nav->appendChild( + array( + $header, + $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, + )); + + } + + protected function buildTableView(array $usertimes) { + assert_instances_of($usertimes, 'PhrequentUserTime'); + + $user = $this->getRequest()->getUser(); + + $phids = array(); + foreach ($usertimes as $usertime) { + $phids[] = $usertime->getUserPHID(); + $phids[] = $usertime->getObjectPHID(); + } + $handles = $this->loadViewerHandles($phids); + + $rows = array(); + foreach ($usertimes as $usertime) { + + if ($usertime->getDateEnded() !== null) { + $time_spent = $usertime->getDateEnded() - $usertime->getDateStarted(); + $time_ended = phabricator_date($usertime->getDateEnded(), $user); + } 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()); + } else { + $object = phutil_tag( + 'em', + array(), + pht('None')); + } + + $rows[] = array( + $object, + phutil_tag( + 'a', + array( + 'href' => $usertime_user->getURI() + ), + $usertime_user->getFullName()), + phabricator_date($usertime->getDateStarted(), $user), + $time_ended, + $time_spent == 0 ? 'none' : + phabricator_format_relative_time_detailed($time_spent), + $usertime->getNote() + ); + } + + $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; + } + +} diff --git a/src/applications/phrequent/controller/PhrequentTrackController.php b/src/applications/phrequent/controller/PhrequentTrackController.php new file mode 100644 index 0000000000..5c8c1c5f52 --- /dev/null +++ b/src/applications/phrequent/controller/PhrequentTrackController.php @@ -0,0 +1,70 @@ +phid = $data['phid']; + $this->verb = $data['verb']; + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + + if (!$this->isStartingTracking() && + !$this->isStoppingTracking()) { + throw new Exception('Unrecognized verb: ' . $this->verb); + } + + if ($this->isStartingTracking()) { + $this->startTracking($user, $this->phid); + } else if ($this->isStoppingTracking()) { + $this->stopTracking($user, $this->phid); + } + return id(new AphrontRedirectResponse()); + } + + private function isStartingTracking() { + return $this->verb === 'start'; + } + + private function isStoppingTracking() { + return $this->verb === 'stop'; + } + + private function startTracking($user, $phid) { + $usertime = new PhrequentUserTime(); + $usertime->setDateStarted(time()); + $usertime->setUserPHID($user->getPHID()); + $usertime->setObjectPHID($phid); + $usertime->save(); + } + + private function stopTracking($user, $phid) { + if (!PhrequentUserTimeQuery::isUserTrackingObject($user, $phid)) { + // Don't do anything, it's not being tracked. + return; + } + + $usertime_dao = new PhrequentUserTime(); + $conn = $usertime_dao->establishConnection('r'); + + queryfx( + $conn, + 'UPDATE %T usertime '. + 'SET usertime.dateEnded = UNIX_TIMESTAMP() '. + 'WHERE usertime.userPHID = %s '. + 'AND usertime.objectPHID = %s '. + 'AND usertime.dateEnded IS NULL '. + 'ORDER BY usertime.dateStarted, usertime.id DESC '. + 'LIMIT 1', + $usertime_dao->getTableName(), + $user->getPHID(), + $phid); + } + +} diff --git a/src/applications/phrequent/event/PhrequentUIEventListener.php b/src/applications/phrequent/event/PhrequentUIEventListener.php new file mode 100644 index 0000000000..ed43ab3caf --- /dev/null +++ b/src/applications/phrequent/event/PhrequentUIEventListener.php @@ -0,0 +1,81 @@ +listen(PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS); + $this->listen(PhabricatorEventType::TYPE_UI_WILLRENDERPROPERTIES); + } + + public function handleEvent(PhutilEvent $event) { + switch ($event->getType()) { + case PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS: + $this->handleActionEvent($event); + break; + case PhabricatorEventType::TYPE_UI_WILLRENDERPROPERTIES: + $this->handlePropertyEvent($event); + break; + } + } + + private function handleActionEvent($event) { + $user = $event->getUser(); + $object = $event->getValue('object'); + + if (!$object || !$object->getPHID()) { + // No object, or the object has no PHID yet.. + return; + } + + if (!($object instanceof PhrequentTrackableInterface)) { + // This object isn't a time trackable object. + return; + } + + $tracking = PhrequentUserTimeQuery::isUserTrackingObject( + $user, + $object->getPHID()); + if (!$tracking) { + $track_action = id(new PhabricatorActionView()) + ->setName(pht('Track Time')) + ->setIcon('history') + ->setWorkflow(true) + ->setHref('/phrequent/track/start/'.$object->getPHID().'/'); + } else { + $track_action = id(new PhabricatorActionView()) + ->setName(pht('Stop Tracking')) + ->setIcon('history') + ->setWorkflow(true) + ->setHref('/phrequent/track/stop/'.$object->getPHID().'/'); + } + + $actions = $event->getValue('actions'); + $actions[] = $track_action; + $event->setValue('actions', $actions); + } + + private function handlePropertyEvent($event) { + $user = $event->getUser(); + $object = $event->getValue('object'); + + if (!$object || !$object->getPHID()) { + // No object, or the object has no PHID yet.. + return; + } + + if (!($object instanceof PhrequentTrackableInterface)) { + // This object isn't a time trackable object. + return; + } + + $time_spent = PhrequentUserTimeQuery::getTotalTimeSpentOnObject( + $object->getPHID()); + $view = $event->getValue('view'); + $view->addProperty( + pht('Time Spent'), + $time_spent == 0 ? 'none' : + phabricator_format_relative_time_detailed($time_spent)); + } + +} diff --git a/src/applications/phrequent/interface/PhrequentTrackableInterface.php b/src/applications/phrequent/interface/PhrequentTrackableInterface.php new file mode 100644 index 0000000000..0501d205a0 --- /dev/null +++ b/src/applications/phrequent/interface/PhrequentTrackableInterface.php @@ -0,0 +1,5 @@ +userPHIDs = $user_phids; + return $this; + } + + public function setObjects($object_phids) { + $this->objectPHIDs = $object_phids; + return $this; + } + + public function setOrder($order) { + $this->order = $order; + 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(); + + if ($this->userPHIDs) { + $where[] = qsprintf( + $conn, + 'userPHID IN (%Ls)', + $this->userPHIDs); + } + + if ($this->objectPHIDs) { + $where[] = qsprintf( + $conn, + 'objectPHID IN (%Ls)', + $this->objectPHIDs); + } + + return $this->formatWhereClause($where); + } + + private function buildOrderClause(AphrontDatabaseConnection $conn) { + 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'; + default: + throw new Exception("Unknown order '{$this->order}'!"); + } + } + +/* -( Helper Functions ) --------------------------------------------------- */ + + public static function isUserTrackingObject( + PhabricatorUser $user, + $phid) { + + $usertime_dao = new PhrequentUserTime(); + $conn = $usertime_dao->establishConnection('r'); + + $count = queryfx_one( + $conn, + 'SELECT COUNT(usertime.id) N FROM %T usertime '. + 'WHERE usertime.userPHID = %s '. + 'AND usertime.objectPHID = %s '. + 'AND usertime.dateEnded IS NULL', + $usertime_dao->getTableName(), + $user->getPHID(), + $phid); + return $count['N'] > 0; + } + + public static function getTotalTimeSpentOnObject($phid) { + $usertime_dao = new PhrequentUserTime(); + $conn = $usertime_dao->establishConnection('r'); + + // First calculate all the time spent where the + // usertime blocks have ended. + $sum_ended = queryfx_one( + $conn, + 'SELECT SUM(usertime.dateEnded - usertime.dateStarted) N '. + 'FROM %T usertime '. + 'WHERE usertime.objectPHID = %s '. + 'AND usertime.dateEnded IS NOT NULL', + $usertime_dao->getTableName(), + $phid); + + // Now calculate the time spent where the usertime + // blocks have not yet ended. + $sum_not_ended = queryfx_one( + $conn, + 'SELECT SUM(UNIX_TIMESTAMP() - usertime.dateStarted) N '. + 'FROM %T usertime '. + 'WHERE usertime.objectPHID = %s '. + 'AND usertime.dateEnded IS NULL', + $usertime_dao->getTableName(), + $phid); + + return $sum_ended['N'] + $sum_not_ended['N']; + } + + public static function getUserTimeSpentOnObject( + PhabricatorUser $user, + $phid) { + + $usertime_dao = new PhrequentUserTime(); + $conn = $usertime_dao->establishConnection('r'); + + // First calculate all the time spent where the + // usertime blocks have ended. + $sum_ended = queryfx_one( + $conn, + 'SELECT SUM(usertime.dateEnded - usertime.dateStarted) N '. + 'FROM %T usertime '. + 'WHERE usertime.userPHID = %s '. + 'AND usertime.objectPHID = %s '. + 'AND usertime.dateEnded IS NOT NULL', + $usertime_dao->getTableName(), + $user->getPHID(), + $phid); + + // Now calculate the time spent where the usertime + // blocks have not yet ended. + $sum_not_ended = queryfx_one( + $conn, + 'SELECT SUM(UNIX_TIMESTAMP() - usertime.dateStarted) N '. + 'FROM %T usertime '. + 'WHERE usertime.userPHID = %s '. + 'AND usertime.objectPHID = %s '. + 'AND usertime.dateEnded IS NULL', + $usertime_dao->getTableName(), + $user->getPHID(), + $phid); + + return $sum_ended['N'] + $sum_not_ended['N']; + } + +}