Christopher Johnson (WMDE) has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/166735

Change subject: Add query and storage classes
......................................................................

Add query and storage classes

Change-Id: I1a6649cd865b7fb424e28dfd8648c80f001adcd4
---
M __phutil_library_map__.php
A src/query/SprintQuery.php
A src/storage/SprintBuildStats.php
A src/storage/SprintTransaction.php
M src/view/BurndownDataView.php
5 files changed, 409 insertions(+), 86 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/phabricator/extensions/Sprint 
refs/changes/35/166735/1

diff --git a/__phutil_library_map__.php b/__phutil_library_map__.php
index 148ffec..51baaff 100644
--- a/__phutil_library_map__.php
+++ b/__phutil_library_map__.php
@@ -18,14 +18,17 @@
     'BurndownException' => 'src/exception/BurndownException.php',
     'BurndownListController' => 'src/controller/BurndownListController.php',
     'BurndownTestDataGenerator' => 
'src/__tests__/BurndownTestDataGenerator.php',
+    'SprintBuildStats' => 'src/storage/SprintBuildStats.php',
     'SprintConstants' => 'src/constants/SprintConstants.php',
     'SprintEndDateField' => 'src/customfield/SprintEndDateField.php',
     'SprintProjectCustomField' => 
'src/customfield/SprintProjectCustomField.php',
+    'SprintQuery' => 'src/query/SprintQuery.php',
     'SprintReportBurndownView' => 'src/view/SprintReportBurndownView.php',
     'SprintReportController' => 'src/controller/SprintReportController.php',
     'SprintReportOpenTasksView' => 'src/view/SprintReportOpenTasksView.php',
     'SprintStartDateField' => 'src/customfield/SprintStartDateField.php',
     'SprintTaskStoryPointsField' => 
'src/customfield/SprintTaskStoryPointsField.php',
+    'SprintTransaction' => 'src/storage/SprintTransaction.php',
     'SprintView' => 'src/view/SprintView.php',
   ),
   'function' => array(),
@@ -52,6 +55,7 @@
       'ManiphestCustomField',
       'PhabricatorStandardCustomFieldInterface',
     ),
+    'SprintTransaction' => 'PhabricatorApplicationTransaction',
     'SprintView' => 'AphrontView',
   ),
 ));
diff --git a/src/query/SprintQuery.php b/src/query/SprintQuery.php
new file mode 100644
index 0000000..27739eb
--- /dev/null
+++ b/src/query/SprintQuery.php
@@ -0,0 +1,69 @@
+<?php
+
+final class SprintQuery  {
+
+  private $viewer;
+  private $project;
+
+
+  public function setProject ($project) {
+    $this->project = $project;
+    return $this;
+  }
+
+  public function setViewer ($viewer) {
+    $this->viewer = $viewer;
+    return $this;
+  }
+
+  public function getAuxFields() {
+    $field_list = PhabricatorCustomField::getObjectFields($this->project, 
PhabricatorCustomField::ROLE_EDIT);
+    $field_list->setViewer($this->viewer);
+    $field_list->readFieldsFromStorage($this->project);
+    $aux_fields = $field_list->getFields();
+    return $aux_fields;
+  }
+
+  public function getStartDate($aux_fields) {
+    $start = idx($aux_fields, 'isdc:sprint:startdate')
+        ->getProxy()->getFieldValue();
+    return $start;
+  }
+
+  public function getEndDate($aux_fields) {
+    $end = idx($aux_fields, 'isdc:sprint:enddate')
+        ->getProxy()->getFieldValue();
+    return $end;
+  }
+
+  public function getTasks() {
+    $tasks = id(new ManiphestTaskQuery())
+        ->setViewer($this->viewer)
+        ->withAnyProjects(array($this->project->getPHID()))
+        ->execute();
+    return $tasks;
+  }
+
+  public function getXactions($tasks) {
+    $task_phids = mpull($tasks, 'getPHID');
+
+    $xactions = id(new ManiphestTransactionQuery())
+        ->setViewer($this->viewer)
+        ->withObjectPHIDs($task_phids)
+        ->execute();
+    return $xactions;
+  }
+
+  public function checkNull($start, $end, $tasks) {
+    if (!$start OR !$end) {
+      throw new BurndownException("This project is not set up for Burndowns, "
+          . "make sure it has 'Sprint' in the name, and then edit it to add 
the "
+          . "sprint start and end date.");
+    }
+
+    if (!$tasks) {
+      throw new BurndownException("This project has no tasks.");
+    }
+  }
+}
+
diff --git a/src/storage/SprintBuildStats.php b/src/storage/SprintBuildStats.php
new file mode 100644
index 0000000..3ff32f4
--- /dev/null
+++ b/src/storage/SprintBuildStats.php
@@ -0,0 +1,316 @@
+<?php
+
+final class SprintBuildStats {
+
+  private $project;
+  private $viewer;
+  private $tasks;
+  private $events;
+  private $xactions;
+  private $dates;
+
+  public function buildDateArray($start, $end) {
+    // Build an array of dates between start and end
+    $period = new DatePeriod(
+        id(new DateTime("@" . $start))->setTime(0, 0),
+        new DateInterval('P1D'), // 1 day interval
+        id(new DateTime("@" . $end))->modify('+1 day')->setTime(0, 0));
+
+    $dates = array('before' => new BurndownDataDate('Start of Sprint'));
+    foreach ($period as $day) {
+      $dates[$day->format('D M j')] = new BurndownDataDate(
+          $day->format('D M j'));
+    }
+    $dates['after'] = new BurndownDataDate('After end of Sprint');
+    return $dates;
+  }
+
+  // Examine all the transactions and extract "events" out of them. These are
+  // times when a task was opened or closed. Make some effort to also track
+  // "scope" events (when a task was added or removed from a project).
+  public function examineXactions($xactions, $tasks) {
+    $scope_phids = array($this->project->getPHID());
+    $this->events = $this->extractEvents($xactions, $scope_phids);
+
+    $this->xactions = mpull($xactions, null, 'getPHID');
+    $this->tasks = mpull($tasks, null, 'getPHID');
+
+  }
+
+   // Now that we have the data for each day, we need to loop over and sum
+  // up the relevant columns
+  public function sumSprintStats() {
+    $previous = null;
+    foreach ($this->dates as $current) {
+      $current->tasks_total = $current->tasks_added_today;
+      $current->points_total = $current->points_added_today;
+      $current->tasks_remaining = $current->tasks_added_today;
+      $current->points_remaining = $current->points_added_today;
+      if ($previous) {
+        $current->tasks_total += $previous->tasks_total;
+        $current->points_total += $previous->points_total;
+        $current->tasks_remaining += $previous->tasks_remaining - 
$current->tasks_closed_today;
+        $current->points_remaining += $previous->points_remaining - 
$current->points_closed_today;
+      }
+      $previous = $current;
+    }
+    return;
+  }
+
+  // Build arrays to store current point and closed status of tasks as we
+  // progress through time, so that these changes reflect on the graph
+  public function buildTaskArrays() {
+    $this->task_points = array();
+    $this->task_statuses = array();
+    foreach ($this->tasks as $task) {
+      $this->task_points[$task->getPHID()] = 0;
+      $this->task_statuses[$task->getPHID()] = null;
+      $this->task_in_sprint[$task->getPHID()] = 0;
+    }
+    return;
+  }
+
+  // Now loop through the events and build the data for each day
+  public function buildDailyData($start, $end) {
+    foreach ($this->events as $event) {
+
+      $xaction = $this->xactions[$event['transactionPHID']];
+      $xaction_date = $xaction->getDateCreated();
+      $task_phid = $xaction->getObjectPHID();
+      // $task = $this->tasks[$task_phid];
+
+      // Determine which date to attach this data to
+      if ($xaction_date < $start) {
+        $date = 'before';
+      } else if ($xaction_date > $end) {
+        $date = 'after';
+      } else {
+        //$date = id(new DateTime("@".$xaction_date))->format('D M j');
+        $date = phabricator_format_local_time($xaction_date, $this->viewer, 'D 
M j');
+      }
+
+      switch ($event['type']) {
+        case "create":
+          // Will be accounted for by "task-add" when the project is added
+          // Bet we still include it so it shows on the Events list
+          break;
+        case "task-add":
+          // A task was added to the sprint
+          $this->addTaskToSprint($date, $task_phid);
+          break;
+        case "task-remove":
+          // A task was removed from the sprint
+          $this->removeTaskFromSprint($date, $task_phid);
+          break;
+        case "close":
+          // A task was closed, mark it as done
+          $this->closeTask($date, $task_phid);
+          break;
+        case "reopen":
+          // A task was reopened, subtract from done
+          $this->reopenTask($date, $task_phid);
+          break;
+        case "points":
+          // Points were changed
+          $this->changePoints($date, $task_phid, $xaction);
+          break;
+      }
+    }
+  }
+
+  /**
+   * Compute the values for the "Ideal Points" line.
+   */
+
+  // This is a cheap hacky way to get business days, and does not account for
+  // holidays at all.
+  public function computeIdealPoints() {
+    $total_business_days = 0;
+    foreach ($this->dates as $key => $date) {
+      if ($key == 'before' OR $key == 'after')
+        continue;
+      $day_of_week = id(new DateTime($date->getDate()))->format('w');
+      if ($day_of_week != 0 AND $day_of_week != 6) {
+        $total_business_days++;
+      }
+    }
+
+    $elapsed_business_days = 0;
+    foreach ($this->dates as $key => $date) {
+      if ($key == 'before') {
+        $date->points_ideal_remaining = $date->points_total;
+        continue;
+      } else if ($key == 'after') {
+        $date->points_ideal_remaining = 0;
+        continue;
+      }
+
+      $day_of_week = id(new DateTime($date->getDate()))->format('w');
+      if ($day_of_week != 0 AND $day_of_week != 6) {
+        $elapsed_business_days++;
+      }
+
+      $date->points_ideal_remaining = round($date->points_total *
+          (1 - ($elapsed_business_days / $total_business_days)), 1);
+    }
+  }
+
+
+  /**
+   * These handle the relevant math for adding, removing, closing, etc.
+   * @param $date
+   * @param $task_phid
+   */
+  private function addTaskToSprint($date, $task_phid) {
+    $this->dates[$date]->tasks_added_today += 1;
+    $this->dates[$date]->points_added_today += $this->task_points[$task_phid];
+    $this->task_in_sprint[$task_phid] = 1;
+  }
+
+  private function removeTaskFromSprint($date, $task_phid) {
+    $this->dates[$date]->tasks_added_today -= 1;
+    $this->dates[$date]->points_added_today -= $this->task_points[$task_phid];
+    $this->task_in_sprint[$task_phid] = 0;
+  }
+
+  private function closeTask($date, $task_phid) {
+    $this->dates[$date]->tasks_closed_today += 1;
+    $this->dates[$date]->points_closed_today += $this->task_points[$task_phid];
+    $this->task_statuses[$task_phid] = 'closed';
+  }
+
+  private function reopenTask($date, $task_phid) {
+    $this->dates[$date]->tasks_closed_today -= 1;
+    $this->dates[$date]->points_closed_today -= $this->task_points[$task_phid];
+    $this->task_statuses[$task_phid] = 'open';
+  }
+
+  private function changePoints($date, $task_phid, $xaction) {
+    $this->task_points[$task_phid] = $xaction->getNewValue();
+
+    // Only make changes if the task is in the sprint
+    if ($this->task_in_sprint[$task_phid]) {
+
+      // Adjust points for that day
+      $this->dates[$date]->points_added_today +=
+          $xaction->getNewValue() - $xaction->getOldValue();
+
+      // If the task is closed, adjust completed points as well
+      if ($this->task_statuses[$task_phid] == 'closed') {
+        $this->dates[$date]->points_closed_today +=
+            $xaction->getNewValue() - $xaction->getOldValue();
+      }
+    }
+  }
+
+  /**
+   * Extract important events (the times when tasks were opened or closed)
+   * from a list of transactions.
+   *
+   * @param array<ManiphestTransaction> List of transactions.
+   * @param array<phid> List of project PHIDs to emit "scope" events for.
+   * @return array<dict> Chronologically sorted events.
+   */
+  private function extractEvents($xactions, array $scope_phids) {
+    assert_instances_of($xactions, 'ManiphestTransaction');
+
+    $scope_phids = array_fuse($scope_phids);
+
+    $events = array();
+    foreach ($xactions as $xaction) {
+      $old = $xaction->getOldValue();
+      $new = $xaction->getNewValue();
+
+      $event_type = null;
+      switch ($xaction->getTransactionType()) {
+        case ManiphestTransaction::TYPE_STATUS:
+          $old_is_closed = ($old === null) ||
+              ManiphestTaskStatus::isClosedStatus($old);
+          $new_is_closed = ManiphestTaskStatus::isClosedStatus($new);
+
+          if ($old_is_closed == $new_is_closed) {
+            // This was just a status change from one open status to another,
+            // or from one closed status to another, so it's not an events we
+            // care about.
+            break;
+          }
+          if ($old === null) {
+            // This would show as "reopened" even though it's when the task was
+            // created so we skip it. Instead we will use the title for created
+            // events
+            break;
+          }
+
+          if ($new_is_closed) {
+            $event_type = 'close';
+          } else {
+            $event_type = 'reopen';
+          }
+          break;
+
+        case ManiphestTransaction::TYPE_TITLE:
+          if ($old === null)
+          {
+            $event_type = 'create';
+          }
+          break;
+
+        // Project changes are "core:edge" transactions
+        case PhabricatorTransactions::TYPE_EDGE:
+
+          // We only care about ProjectEdgeType
+          if (idx($xaction->getMetadata(), 'edge:type') !==
+              PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
+            break;
+
+          $old = ipull($old, 'dst');
+          $new = ipull($new, 'dst');
+
+          $in_old_scope = array_intersect_key($scope_phids, $old);
+          $in_new_scope = array_intersect_key($scope_phids, $new);
+
+          if ($in_new_scope && !$in_old_scope) {
+            $event_type = 'task-add';
+          } else if ($in_old_scope && !$in_new_scope) {
+            // NOTE: We will miss some of these events, becuase we are only
+            // examining tasks that are currently in the project. If a task
+            // is removed from the project and not added again later, it will
+            // just vanish from the chart completely, not show up as a
+            // scope contraction. We can't do better until the Facts 
application
+            // is avialable without examining *every* task.
+            $event_type = 'task-remove';
+          }
+          break;
+
+        case PhabricatorTransactions::TYPE_CUSTOMFIELD:
+          if ($xaction->getMetadataValue('customfield:key') == 
'isdc:sprint:storypoints') {
+            // POINTS!
+            $event_type = 'points';
+          }
+          break;
+
+        default:
+          // This is something else (comment, subscription change, etc) that
+          // we don't care about for now.
+          break;
+      }
+
+      // If we found some kind of events that we care about, stick it in the
+      // list of events.
+      if ($event_type !== null) {
+        $events[] = array(
+            'transactionPHID' => $xaction->getPHID(),
+            'epoch' => $xaction->getDateCreated(),
+            'key'   => $xaction->getMetadataValue('customfield:key'),
+            'type'  => $event_type,
+            'title' => $xaction->getTitle(),
+        );
+      }
+    }
+
+    // Sort all events chronologically.
+    $events = isort($events, 'epoch');
+
+    return $events;
+  }
+}
diff --git a/src/storage/SprintTransaction.php 
b/src/storage/SprintTransaction.php
new file mode 100644
index 0000000..80b4221
--- /dev/null
+++ b/src/storage/SprintTransaction.php
@@ -0,0 +1,8 @@
+<?php
+
+final class SprintTransaction extends PhabricatorApplicationTransaction {
+
+  public function getApplicationTransactionType() {
+    return DifferentialRevisionPHIDType::TYPECONST;
+  }
+}
\ No newline at end of file
diff --git a/src/view/BurndownDataView.php b/src/view/BurndownDataView.php
index c1014a1..cef960d 100644
--- a/src/view/BurndownDataView.php
+++ b/src/view/BurndownDataView.php
@@ -52,42 +52,6 @@
     return array ($chart, $tasks_table, $burndown_table, $event_table);
   }
 
-  private function getAuxFields() {
-    $field_list = PhabricatorCustomField::getObjectFields($this->project, 
PhabricatorCustomField::ROLE_EDIT);
-    $field_list->setViewer($this->viewer);
-    $field_list->readFieldsFromStorage($this->project);
-    $aux_fields = $field_list->getFields();
-    return $aux_fields;
-  }
-
-  private function getStartDate($aux_fields) {
-    $start = idx($aux_fields, 'isdc:sprint:startdate')
-        ->getProxy()->getFieldValue();
-    return $start;
-  }
-
-  private function getEndDate($aux_fields) {
-    $end = idx($aux_fields, 'isdc:sprint:enddate')
-        ->getProxy()->getFieldValue();
-    return $end;
-  }
-
-  private function buildDateArray($start, $end) {
-    // Build an array of dates between start and end
-    $period = new DatePeriod(
-        id(new DateTime("@" . $start))->setTime(0, 0),
-        new DateInterval('P1D'), // 1 day interval
-        id(new DateTime("@" . $end))->modify('+1 day')->setTime(0, 0));
-
-    $dates = array('before' => new BurndownDataDate('Start of Sprint'));
-    foreach ($period as $day) {
-      $dates[$day->format('D M j')] = new BurndownDataDate(
-          $day->format('D M j'));
-    }
-    $dates['after'] = new BurndownDataDate('After end of Sprint');
-    return $dates;
-  }
-
   // Examine all the transactions and extract "events" out of them. These are
   // times when a task was opened or closed. Make some effort to also track
   // "scope" events (when a task was added or removed from a project).
@@ -100,48 +64,7 @@
 
   }
 
-  // Load the data for the chart. This approach tries to be simple, but loads
-  // and processes large amounts of unnecessary data, so it is not especially
-  // fast. Some performance improvements can be made at the cost of fragility
-  // by using raw SQL; real improvements can be made once Facts comes online.
-
-  // First, load *every task* in the project. We have to do something like
-  // this because there's no straightforward way to determine which tasks
-  // have activity in the project period.
-  private function getTasks() {
-    $tasks = id(new ManiphestTaskQuery())
-        ->setViewer($this->viewer)
-        ->withAnyProjects(array($this->project->getPHID()))
-        ->execute();
-    return $tasks;
-  }
-
-  private function checkNull($start, $end, $tasks) {
-    if (!$start OR !$end) {
-      throw new BurndownException("This project is not set up for Burndowns, "
-          . "make sure it has 'Sprint' in the name, and then edit it to add 
the "
-          . "sprint start and end date.");
-    }
-
-    if (!$tasks) {
-      throw new BurndownException("This project has no tasks.");
-    }
-  }
-
-  // Now load *every transaction* for those tasks. This loads all the
-  // comments, etc., for every one of the tasks. Again, not very fast, but
-  // we largely do not have ways to select this data more narrowly yet.
-  private function getXactions($tasks) {
-    $task_phids = mpull($tasks, 'getPHID');
-
-    $xactions = id(new ManiphestTransactionQuery())
-        ->setViewer($this->viewer)
-        ->withObjectPHIDs($task_phids)
-        ->execute();
-    return $xactions;
-  }
-
-  // Now that we have the data for each day, we need to loop over and sum
+    // Now that we have the data for each day, we need to loop over and sum
   // up the relevant columns
   private function sumSprintStats() {
     $previous = null;
@@ -181,7 +104,6 @@
       $xaction = $this->xactions[$event['transactionPHID']];
       $xaction_date = $xaction->getDateCreated();
       $task_phid = $xaction->getObjectPHID();
-      // $task = $this->tasks[$task_phid];
 
       // Determine which date to attach this data to
       if ($xaction_date < $start) {
@@ -311,17 +233,21 @@
 
   private function buildChartDataSet() {
 
+    $query = id(new SprintQuery())
+         ->setProject($this->project)
+         ->setViewer($this->viewer);
+    $aux_fields = $query->getAuxFields();
+    $start = $query->getStartDate($aux_fields);
+    $end = $query->getEndDate($aux_fields);
 
-    $aux_fields = $this->getAuxFields();
-    $start = $this->getStartDate($aux_fields);
-    $end = $this->getEndDate($aux_fields);
-    $this->dates = $this->buildDateArray($start, $end);
+    $tasks = $query->getTasks();
 
-    $tasks = $this->getTasks();
+    $query->checkNull($start, $end, $tasks);
 
-    $this->checkNull($start, $end, $tasks);
+    $xactions = $query->getXactions($tasks);
 
-    $xactions = $this->getXactions($tasks);
+    $stats = id(new SprintBuildStats());
+    $this->dates = $stats->buildDateArray($start, $end);
 
     $this->examineXactions($xactions, $tasks);
 

-- 
To view, visit https://gerrit.wikimedia.org/r/166735
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I1a6649cd865b7fb424e28dfd8648c80f001adcd4
Gerrit-PatchSet: 1
Gerrit-Project: phabricator/extensions/Sprint
Gerrit-Branch: master
Gerrit-Owner: Christopher Johnson (WMDE) <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to