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