Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F91643
D7349.diff
All Users
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
18 KB
Referenced Files
None
Subscribers
None
D7349.diff
View Options
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1958,6 +1958,8 @@
'PhrequentDAO' => 'applications/phrequent/storage/PhrequentDAO.php',
'PhrequentListController' => 'applications/phrequent/controller/PhrequentListController.php',
'PhrequentSearchEngine' => 'applications/phrequent/query/PhrequentSearchEngine.php',
+ 'PhrequentTimeBlock' => 'applications/phrequent/storage/PhrequentTimeBlock.php',
+ 'PhrequentTimeBlockTestCase' => 'applications/phrequent/storage/__tests__/PhrequentTimeBlockTestCase.php',
'PhrequentTrackController' => 'applications/phrequent/controller/PhrequentTrackController.php',
'PhrequentTrackableInterface' => 'applications/phrequent/interface/PhrequentTrackableInterface.php',
'PhrequentUIEventListener' => 'applications/phrequent/event/PhrequentUIEventListener.php',
@@ -4230,6 +4232,8 @@
1 => 'PhabricatorApplicationSearchResultsControllerInterface',
),
'PhrequentSearchEngine' => 'PhabricatorApplicationSearchEngine',
+ 'PhrequentTimeBlock' => 'Phobject',
+ 'PhrequentTimeBlockTestCase' => 'PhabricatorTestCase',
'PhrequentTrackController' => 'PhrequentController',
'PhrequentUIEventListener' => 'PhutilEventListener',
'PhrequentUserTime' =>
diff --git a/src/applications/phrequent/event/PhrequentUIEventListener.php b/src/applications/phrequent/event/PhrequentUIEventListener.php
--- a/src/applications/phrequent/event/PhrequentUIEventListener.php
+++ b/src/applications/phrequent/event/PhrequentUIEventListener.php
@@ -61,9 +61,9 @@
$event->setValue('actions', $actions);
}
- private function handlePropertyEvent($event) {
- $user = $event->getUser();
- $object = $event->getValue('object');
+ private function handlePropertyEvent($ui_event) {
+ $user = $ui_event->getUser();
+ $object = $ui_event->getValue('object');
if (!$object || !$object->getPHID()) {
// No object, or the object has no PHID yet..
@@ -75,51 +75,64 @@
return;
}
- $depth = false;
+ $events = id(new PhrequentUserTimeQuery())
+ ->setViewer($user)
+ ->withObjectPHIDs(array($object->getPHID()))
+ ->needPreemptingEvents(true)
+ ->execute();
+ $event_groups = mgroup($events, 'getUserPHID');
- $stack = PhrequentUserTimeQuery::loadUserStack($user);
- if ($stack) {
- $stack = array_values($stack);
- for ($ii = 0; $ii < count($stack); $ii++) {
- if ($stack[$ii]->getObjectPHID() == $object->getPHID()) {
- $depth = ($ii + 1);
- break;
- }
- }
+ if (!$events) {
+ return;
}
- $time_spent = PhrequentUserTimeQuery::getTotalTimeSpentOnObject(
- $object->getPHID());
+ $handles = id(new PhabricatorHandleQuery())
+ ->setViewer($user)
+ ->withPHIDs(array_keys($event_groups))
+ ->execute();
+
+ $status_view = new PHUIStatusListView();
+
+ foreach ($event_groups as $user_phid => $event_group) {
+ $item = new PHUIStatusItemView();
+ $item->setTarget($handles[$user_phid]->renderLink());
+
+ $state = 'stopped';
+ foreach ($event_group as $event) {
+ if ($event->getDateEnded() === null) {
+ if ($event->isPreempted()) {
+ $state = 'suspended';
+ } else {
+ $state = 'active';
+ break;
+ }
+ }
+ }
- if (!$depth && !$time_spent) {
- return;
- }
+ switch ($state) {
+ case 'active':
+ $item->setIcon('time-green', pht('Working Now'));
+ break;
+ case 'suspended':
+ $item->setIcon('time-yellow', pht('Interrupted'));
+ break;
+ case 'stopped':
+ $item->setIcon('time-orange', pht('Not Working Now'));
+ break;
+ }
- require_celerity_resource('phrequent-css');
-
- $property = array();
- if ($depth == 1) {
- $property[] = phutil_tag(
- 'div',
- array(
- 'class' => 'phrequent-tracking-property phrequent-active',
- ),
- pht('Currently Tracking'));
- } else if ($depth > 1) {
- $property[] = phutil_tag(
- 'div',
- array(
- 'class' => 'phrequent-tracking-property phrequent-on-stack',
- ),
- pht('On Stack'));
- }
+ $block = new PhrequentTimeBlock($event_group);
+ $item->setNote(
+ phabricator_format_relative_time(
+ $block->getTimeSpentOnObject(
+ $object->getPHID(),
+ time())));
- if ($time_spent) {
- $property[] = phabricator_format_relative_time_detailed($time_spent);
+ $status_view->addItem($item);
}
- $view = $event->getValue('view');
- $view->addProperty(pht('Time Spent'), $property);
+ $view = $ui_event->getValue('view');
+ $view->addProperty(pht('Time Spent'), $status_view);
}
}
diff --git a/src/applications/phrequent/query/PhrequentUserTimeQuery.php b/src/applications/phrequent/query/PhrequentUserTimeQuery.php
--- a/src/applications/phrequent/query/PhrequentUserTimeQuery.php
+++ b/src/applications/phrequent/query/PhrequentUserTimeQuery.php
@@ -21,6 +21,8 @@
private $order = self::ORDER_ID_ASC;
private $ended = self::ENDED_ALL;
+ private $needPreemptingEvents;
+
public function withUserPHIDs($user_phids) {
$this->userPHIDs = $user_phids;
return $this;
@@ -41,6 +43,11 @@
return $this;
}
+ public function needPreemptingEvents($need_events) {
+ $this->needPreemptingEvents = $need_events;
+ return $this;
+ }
+
private function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
@@ -150,6 +157,61 @@
return $usertime->loadAllFromArray($data);
}
+ protected function didFilterPage(array $page) {
+ if ($this->needPreemptingEvents) {
+ $usertime = new PhrequentUserTime();
+ $conn_r = $usertime->establishConnection('r');
+
+ $preempt = array();
+ foreach ($page as $event) {
+ $preempt[] = qsprintf(
+ $conn_r,
+ '(userPHID = %s AND
+ (dateStarted BETWEEN %d AND %d) AND
+ (dateEnded IS NULL OR dateEnded > %d))',
+ $event->getUserPHID(),
+ $event->getDateStarted(),
+ nonempty($event->getDateEnded(), PhabricatorTime::getNow()),
+ $event->getDateStarted());
+ }
+
+ $preempting_events = queryfx_all(
+ $conn_r,
+ 'SELECT * FROM %T WHERE %Q ORDER BY dateStarted ASC, id ASC',
+ $usertime->getTableName(),
+ implode(' OR ', $preempt));
+ $preempting_events = $usertime->loadAllFromArray($preempting_events);
+
+ $preempting_events = mgroup($preempting_events, 'getUserPHID');
+
+ foreach ($page as $event) {
+ $e_start = $event->getDateStarted();
+ $e_end = $event->getDateEnded();
+
+ $select = array();
+ $user_events = idx($preempting_events, $event->getUserPHID(), array());
+ foreach ($user_events as $u_event) {
+ if ($u_event->getID() == $event->getID()) {
+ // Don't allow an event to preempt itself.
+ continue;
+ }
+
+ $u_start = $u_event->getDateStarted();
+ $u_end = $u_event->getDateEnded();
+
+ if (($u_start >= $e_start) && ($u_end <= $e_end) &&
+ ($u_end === null || $u_end > $e_start)) {
+ $select[] = $u_event;
+ }
+ }
+
+ $event->attachPreemptingEvents($select);
+ }
+ }
+
+ return $page;
+ }
+
/* -( Helper Functions ) --------------------------------------------------- */
public static function getEndedSearchOptions() {
@@ -204,46 +266,6 @@
return $count['N'] > 0;
}
- public static function loadUserStack(PhabricatorUser $user) {
- if (!$user->isLoggedIn()) {
- return array();
- }
-
- return id(new PhrequentUserTime())->loadAllWhere(
- 'userPHID = %s AND dateEnded IS NULL
- ORDER BY dateStarted DESC, id DESC',
- $user->getPHID());
- }
-
- 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) {
diff --git a/src/applications/phrequent/storage/PhrequentTimeBlock.php b/src/applications/phrequent/storage/PhrequentTimeBlock.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phrequent/storage/PhrequentTimeBlock.php
@@ -0,0 +1,155 @@
+<?php
+
+final class PhrequentTimeBlock extends Phobject {
+
+ private $events;
+
+ public function __construct(array $events) {
+ assert_instances_of($events, 'PhrequentUserTime');
+ $this->events = $events;
+ }
+
+ public function getTimeSpentOnObject($phid, $now) {
+ $ranges = idx($this->getObjectTimeRanges($now), $phid, array());
+
+ $sum = 0;
+ foreach ($ranges as $range) {
+ $sum += ($range[1] - $range[0]);
+ }
+
+ return $sum;
+ }
+
+ public function getObjectTimeRanges($now) {
+ $ranges = array();
+
+ $object_ranges = array();
+ foreach ($this->events as $event) {
+
+ // First, convert each event's preempting stack into a linear timeline
+ // of events.
+
+ $timeline = array();
+ $timeline[] = array(
+ 'at' => $event->getDateStarted(),
+ 'type' => 'start',
+ );
+ $timeline[] = array(
+ 'at' => nonempty($event->getDateEnded(), $now),
+ 'type' => 'end',
+ );
+
+ $base_phid = $event->getObjectPHID();
+
+ $preempts = $event->getPreemptingEvents();
+
+ foreach ($preempts as $preempt) {
+ $same_object = ($preempt->getObjectPHID() == $base_phid);
+ $timeline[] = array(
+ 'at' => $preempt->getDateStarted(),
+ 'type' => $same_object ? 'start' : 'push',
+ );
+ $timeline[] = array(
+ 'at' => nonempty($preempt->getDateEnded(), $now),
+ 'type' => $same_object ? 'end' : 'pop',
+ );
+ }
+
+ // Now, figure out how much time was actually spent working on the
+ // object.
+
+ $timeline = isort($timeline, 'at');
+
+ $stack = array();
+ $depth = null;
+
+ $ranges = array();
+ foreach ($timeline as $timeline_event) {
+ switch ($timeline_event['type']) {
+ case 'start':
+ $stack[] = $depth;
+ $depth = 0;
+ $range_start = $timeline_event['at'];
+ break;
+ case 'end':
+ if ($depth == 0) {
+ $ranges[] = array($range_start, $timeline_event['at']);
+ }
+ $depth = array_pop($stack);
+ break;
+ case 'push':
+ if ($depth == 0) {
+ $ranges[] = array($range_start, $timeline_event['at']);
+ }
+ $depth++;
+ break;
+ case 'pop':
+ $depth--;
+ if ($depth == 0) {
+ $range_start = $timeline_event['at'];
+ }
+ break;
+ }
+ }
+
+ $object_ranges[$base_phid][] = $ranges;
+ }
+
+ // Finally, collapse all the ranges so we don't double-count time.
+
+ foreach ($object_ranges as $phid => $ranges) {
+ $object_ranges[$phid] = self::mergeTimeRanges(array_mergev($ranges));
+ }
+
+ return $object_ranges;
+ }
+
+
+ /**
+ * Merge a list of time ranges (pairs of `<start, end>` epochs) so that no
+ * elements overlap. For example, the ranges:
+ *
+ * array(
+ * array(50, 150),
+ * array(100, 175),
+ * );
+ *
+ * ...are merged to:
+ *
+ * array(
+ * array(50, 175),
+ * );
+ *
+ * This is used to avoid double-counting time on objects which had timers
+ * started multiple times.
+ *
+ * @param list<pair<int, int>> List of possibly overlapping time ranges.
+ * @return list<pair<int, int>> Nonoverlapping time ranges.
+ */
+ public static function mergeTimeRanges(array $ranges) {
+ $ranges = isort($ranges, 0);
+
+ $result = array();
+
+ $current = null;
+ foreach ($ranges as $key => $range) {
+ if ($current === null) {
+ $current = $range;
+ continue;
+ }
+
+ if ($range[0] <= $current[1]) {
+ $current[1] = max($range[1], $current[1]);
+ continue;
+ }
+
+ $result[] = $current;
+ $current = $range;
+ }
+
+ $result[] = $current;
+
+ return $result;
+ }
+
+}
diff --git a/src/applications/phrequent/storage/PhrequentUserTime.php b/src/applications/phrequent/storage/PhrequentUserTime.php
--- a/src/applications/phrequent/storage/PhrequentUserTime.php
+++ b/src/applications/phrequent/storage/PhrequentUserTime.php
@@ -1,8 +1,5 @@
<?php
-/**
- * @group phrequent
- */
final class PhrequentUserTime extends PhrequentDAO
implements PhabricatorPolicyInterface {
@@ -12,6 +9,8 @@
protected $dateStarted;
protected $dateEnded;
+ private $preemptingEvents = self::ATTACHABLE;
+
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
@@ -23,8 +22,11 @@
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
- $policy = PhabricatorPolicies::POLICY_USER;
- break;
+ // Since it's impossible to perform any meaningful computations with
+ // time if a user can't view some of it, visibility on tracked time is
+ // unrestricted. If we eventually lock it down, it should be per-user.
+ // (This doesn't mean that users can see tracked objects.)
+ return PhabricatorPolicies::getMostOpenPolicy();
}
return $policy;
@@ -36,8 +38,29 @@
public function describeAutomaticCapability($capability) {
- return pht(
- 'The user who tracked time can always view it.');
+ return null;
+ }
+
+ public function attachPreemptingEvents(array $events) {
+ $this->preemptingEvents = $events;
+ return $this;
+ }
+
+ public function getPreemptingEvents() {
+ return $this->assertAttached($this->preemptingEvents);
+ }
+
+ public function isPreempted() {
+ if ($this->getDateEnded() !== null) {
+ return false;
+ }
+ foreach ($this->getPreemptingEvents() as $event) {
+ if ($event->getDateEnded() === null &&
+ $event->getObjectPHID() != $this->getObjectPHID()) {
+ return true;
+ }
+ }
+ return false;
}
}
diff --git a/src/applications/phrequent/storage/__tests__/PhrequentTimeBlockTestCase.php b/src/applications/phrequent/storage/__tests__/PhrequentTimeBlockTestCase.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phrequent/storage/__tests__/PhrequentTimeBlockTestCase.php
@@ -0,0 +1,128 @@
+<?php
+
+final class PhrequentTimeBlockTestCase extends PhabricatorTestCase {
+
+ public function testMergeTimeRanges() {
+
+ // Overlapping ranges.
+ $input = array(
+ array(50, 150),
+ array(100, 175),
+ );
+ $expect = array(
+ array(50, 175),
+ );
+
+ $this->assertEqual($expect, PhrequentTimeBlock::mergeTimeRanges($input));
+
+
+ // Identical ranges.
+ $input = array(
+ array(1, 1),
+ array(1, 1),
+ array(1, 1),
+ );
+ $expect = array(
+ array(1, 1),
+ );
+
+ $this->assertEqual($expect, PhrequentTimeBlock::mergeTimeRanges($input));
+
+
+ // Range which is a strict subset of another range.
+ $input = array(
+ array(2, 7),
+ array(1, 10),
+ );
+ $expect = array(
+ array(1, 10),
+ );
+
+ $this->assertEqual($expect, PhrequentTimeBlock::mergeTimeRanges($input));
+
+
+ // These are discontinuous and should not be merged.
+ $input = array(
+ array(5, 6),
+ array(7, 8),
+ );
+ $expect = array(
+ array(5, 6),
+ array(7, 8),
+ );
+
+ $this->assertEqual($expect, PhrequentTimeBlock::mergeTimeRanges($input));
+
+
+ // These overlap only on an edge, but should merge.
+ $input = array(
+ array(5, 6),
+ array(6, 7),
+ );
+ $expect = array(
+ array(5, 7),
+ );
+
+ $this->assertEqual($expect, PhrequentTimeBlock::mergeTimeRanges($input));
+ }
+
+ public function testPreemptingEvents() {
+
+ // Roughly, this is: got into work, started T1, had a meeting from 10-11,
+ // left the office around 2 to meet a client at a coffee shop, worked on
+ // T1 again for about 40 minutes, had the meeting from 3-3:30, finished up
+ // on T1, headed back to the office, got another hour of work in, and
+ // headed home.
+
+ $event = $this->newEvent('T1', 900, 1700);
+
+ $event->attachPreemptingEvents(
+ array(
+ $this->newEvent('meeting', 1000, 1100),
+ $this->newEvent('offsite', 1400, 1600),
+ $this->newEvent('T1', 1420, 1580),
+ $this->newEvent('offsite meeting', 1500, 1550),
+ ));
+
+ $block = new PhrequentTimeBlock(array($event));
+
+ $ranges = $block->getObjectTimeRanges(1800);
+ $this->assertEqual(
+ array(
+ 'T1' => array(
+ array(900, 1000), // Before morning meeting.
+ array(1100, 1400), // After morning meeting.
+ array(1420, 1500), // Coffee, before client meeting.
+ array(1550, 1580), // Coffee, after client meeting.
+ array(1600, 1700), // After returning from off site.
+ ),
+ ),
+ $ranges);
+
+
+ $event = $this->newEvent('T2', 100, 300);
+ $event->attachPreemptingEvents(
+ array(
+ $this->newEvent('meeting', 200, null),
+ ));
+
+ $block = new PhrequentTimeBlock(array($event));
+
+ $ranges = $block->getObjectTimeRanges(1000);
+ $this->assertEqual(
+ array(
+ 'T2' => array(
+ array(100, 200),
+ ),
+ ),
+ $ranges);
+ }
+
+ private function newEvent($object_phid, $start_time, $end_time) {
+ return id(new PhrequentUserTime())
+ ->setObjectPHID($object_phid)
+ ->setDateStarted($start_time)
+ ->setDateEnded($end_time);
+ }
+
+}
File Metadata
Details
Attached
Mime Type
text/x-diff
Storage Engine
amazon-s3
Storage Format
Raw Data
Storage Handle
phabricator/5y/oz/3szwqvptnmk7zwfc
Default Alt Text
D7349.diff (18 KB)
Attached To
Mode
D7349: Make Phrequent time accounting aware of the stack
Attached
Detach File
Event Timeline
Log In to Comment