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']; + } + +}