Christopher Johnson (WMDE) has uploaded a new change for review. https://gerrit.wikimedia.org/r/164943
Change subject: Refactored and reorganized ...................................................................... Refactored and reorganized Change-Id: I3cf2ea3e84367661c7e6e36adecddf9243b70a3d --- M __phutil_library_map__.php M src/BurndownController.php M src/BurndownData.php M src/BurndownDataDate.php M src/BurndownTestDataGenerator.php M src/SprintEndDateField.php M src/SprintProjectCustomField.php M src/SprintStartDateField.php M src/SprintTaskStoryPointsField.php 9 files changed, 266 insertions(+), 156 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/phabricator/extensions/Sprint refs/changes/43/164943/1 diff --git a/__phutil_library_map__.php b/__phutil_library_map__.php index ec65902..b249693 100644 --- a/__phutil_library_map__.php +++ b/__phutil_library_map__.php @@ -17,6 +17,7 @@ 'BurndownException' => 'src/BurndownException.php', 'BurndownListController' => 'src/BurndownListController.php', 'BurndownTestDataGenerator' => 'src/BurndownTestDataGenerator.php', + 'NullObject' => 'src/NullObject.php', 'SprintEndDateField' => 'src/SprintEndDateField.php', 'SprintProjectCustomField' => 'src/SprintProjectCustomField.php', 'SprintStartDateField' => 'src/SprintStartDateField.php', @@ -30,6 +31,7 @@ 'BurndownException' => 'Exception', 'BurndownListController' => 'PhabricatorController', 'BurndownTestDataGenerator' => 'PhabricatorTestDataGenerator', + 'NullObject' => 'BurndownData', 'SprintEndDateField' => 'SprintProjectCustomField', 'SprintProjectCustomField' => array( 'PhabricatorProjectCustomField', diff --git a/src/BurndownController.php b/src/BurndownController.php index 66e723f..63ee240 100644 --- a/src/BurndownController.php +++ b/src/BurndownController.php @@ -8,15 +8,15 @@ // Project data private $projectID; - private $project; + //private $project; // Start and end date for the sprint - private $startdate; - private $enddate; + //private $startdate; + //private $enddate; // Tasks and transactions - private $tasks; - private $xactions; + //private $tasks; + //private $xactions; public function shouldAllowPublic() { return true; @@ -48,10 +48,10 @@ try { $data = new BurndownData($project, $viewer); - + // $data = new BurndownDataChart($project, $viewer); $burndown_chart = $data->buildBurnDownChart(); - $burndown_table = $data->buildBurnDownTable(); $tasks_table = $data->buildTasksTable(); + $burndown_table = $data->buildBurnDownTable(); $events_table = $data->buildEventTable(); } catch (BurndownException $e) { $error_box = id(new AphrontErrorView()) @@ -70,8 +70,8 @@ $crumbs, $error_box, $burndown_chart, - $burndown_table, $tasks_table, + $burndown_table, $events_table, ), array( diff --git a/src/BurndownData.php b/src/BurndownData.php index abc7ce2..72ada66 100644 --- a/src/BurndownData.php +++ b/src/BurndownData.php @@ -6,86 +6,105 @@ class BurndownData { - private $startDate; - private $endDate; + // private $startDate; + //private $endDate; - // Array of BurndownDataDates - // There are two special keys, 'before' and 'after' - // - // Looks like: array( - // 'before' => BurndownDataDate - // 'Tue Jun 3' => BurndownDataDate - // 'Wed Jun 4' => BurndownDataDate - // ... - // 'after' => BurndownDataDate - // ) + // Array of BurndownDataDates + // There are two special keys, 'before' and 'after' + // + // Looks like: array( + // 'before' => BurndownDataDate + // 'Tue Jun 3' => BurndownDataDate + // 'Wed Jun 4' => BurndownDataDate + // ... + // 'after' => BurndownDataDate + // ) private $dates; - - // These hold an array of each task, and how many points are assigned, and - // whether it's open or closed. These values change as we progress through - // time, so that changes to points or status reflect on the graph. + private $data; + // These hold an array of each task, and how many points are assigned, and + // whether it's open or closed. These values change as we progress through + // time, so that changes to points or status reflect on the graph. private $task_points = array(); private $task_statuses = array(); - - // Project associated with this burndown. + private $task_in_sprint= array(); + // Project associated with this burndown. private $project; - + private $viewer; private $tasks; private $events; private $xactions; - public function __construct($project, $viewer) { + + + public function __construct($project, $viewer) + { $this->project = $project; $this->viewer = $viewer; // We need the custom fields so we can pull out the start and end date - $field_list = PhabricatorCustomField::getObjectFields( - $this->project, - PhabricatorCustomField::ROLE_EDIT); - $field_list->setViewer($viewer); - $field_list->readFieldsFromStorage($this->project); - $aux_fields = $field_list->getFields(); + $aux_fields = $this->getAuxFields($project,$viewer); + $start= $this->getStartDate($aux_fields); + $end = $this->getEndDate($aux_fields); + $this->dates = $this->buildDateArray($start,$end); - $start = idx($aux_fields, 'isdc:sprint:startdate') - ->getProxy()->getFieldValue(); - $end = idx($aux_fields, 'isdc:sprint:enddate') - ->getProxy()->getFieldValue(); + $tasks = $this->getTasks($project,$viewer); - if (!$start OR !$end) + $this->checkNull($start, $end, $tasks); + + $xactions = $this->getXactions($tasks,$viewer); + + $this->examineXactions($xactions, $tasks); + + $this->buildDailyData($start,$end,$viewer); + $this->buildTaskArrays(); + + $this->sumSprintStats(); + $this->computeIdealPoints(); + } + + private function getAuxFields($project, $viewer) { - 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."); + $field_list = PhabricatorCustomField::getObjectFields($project, PhabricatorCustomField::ROLE_EDIT); + $field_list->setViewer($viewer); + $field_list->readFieldsFromStorage($project); + $aux_fields = $field_list->getFields(); + return $aux_fields; } - // 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. - $tasks = id(new ManiphestTaskQuery()) - ->setViewer($viewer) - ->withAnyProjects(array($this->project->getPHID())) - ->execute(); - - // 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. - if (! $tasks) { - throw new BurndownException("This project has no tasks."); + private function getStartDate($aux_fields) + { + $start = idx($aux_fields, 'isdc:sprint:startdate') + ->getProxy()->getFieldValue(); + return $start; } - $task_phids = mpull($tasks, 'getPHID'); + private function getEndDate($aux_fields) + { + $end = idx($aux_fields, 'isdc:sprint:enddate') + ->getProxy()->getFieldValue(); + return $end; + } - $xactions = id(new ManiphestTransactionQuery()) - ->setViewer($viewer) - ->withObjectPHIDs($task_phids) - ->execute(); + 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; + } + + private function examineXactions($xactions,$tasks) + { // 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). @@ -95,19 +114,77 @@ $this->xactions = mpull($xactions, null, 'getPHID'); $this->tasks = mpull($tasks, null, 'getPHID'); - // 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)); + } - $this->dates = array('before' => new BurndownDataDate('Start of Sprint')); - foreach ($period as $day) { - $this->dates[$day->format('D M j')] = new BurndownDataDate( - $day->format('D M j')); + private function getTasks ($project, $viewer) + { + // 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. + $tasks = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withAnyProjects(array($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."); + } + } + + private function getXactions ($tasks, $viewer) + { + // 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. + + $task_phids = mpull($tasks, 'getPHID'); + + $xactions = id(new ManiphestTransactionQuery()) + ->setViewer($viewer) + ->withObjectPHIDs($task_phids) + ->execute(); + return $xactions; + } + + private function sumSprintStats () + { + // Now that we have the data for each day, we need to loop over and sum + // up the relevant columns + $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; } - $this->dates['after'] = new BurndownDataDate('After end of Sprint'); + return; + } + private function buildTaskArrays () + { // Build arrays to store current point and closed status of tasks as we // progress through time, so that these changes reflect on the graph $this->task_points = array(); @@ -117,14 +194,18 @@ $this->task_statuses[$task->getPHID()] = null; $this->task_in_sprint[$task->getPHID()] = 0; } + return; + } + private function buildDailyData ($start, $end, $viewer) + { // Now loop through the events and build the data for each day foreach ($this->events as $event) { $xaction = $this->xactions[$event['transactionPHID']]; $xaction_date = $xaction->getDateCreated(); $task_phid = $xaction->getObjectPHID(); - $task = $this->tasks[$task_phid]; + // $task = $this->tasks[$task_phid]; // Determine which date to attach this data to if ($xaction_date < $start) { @@ -163,67 +244,51 @@ break; } } - - // Now that we have the data for each day, we need to loop over and sum - // up the relevant columns - $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; - } - - $this->computeIdealPoints(); - } /** * Compute the values for the "Ideal Points" line. */ - private function computeIdealPoints() { + private function computeIdealPoints() + { // This is a cheap hacky way to get business days, and does not account for // holidays at all. - $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++; - } - } + $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++; - } + 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; + } - $date->points_ideal_remaining = round($date->points_total * - (1 - ($elapsed_business_days / $total_business_days)), 1); + $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; @@ -267,13 +332,14 @@ } } - public function buildBurnDownChart() { + private function buildChartDataSet() + { $data = array(array( - pht('Date'), - pht('Total Points'), - pht('Remaining Points'), - pht('Ideal Points'), - pht('Points Today'), + pht('Date'), + pht('Total Points'), + pht('Remaining Points'), + pht('Ideal Points'), + pht('Points Today'), )); $future = false; @@ -283,17 +349,25 @@ $future = new DateTime($date->getDate()) > id(new DateTime())->setTime(0,0); } $data[] = array( - $date->getDate(), - $future ? null: $date->points_total, - $future ? null: $date->points_remaining, - $date->points_ideal_remaining, - $future ? null: $date->points_closed_today, + $date->getDate(), + $future ? null: $date->points_total, + $future ? null: $date->points_remaining, + $date->points_ideal_remaining, + $future ? null: $date->points_closed_today, ); + } + return $data; + + } + public function buildBurnDownChart() { + + $this->data = $this->buildChartDataSet(); // Format the data for the chart - $data = json_encode($data); + $data = json_encode($this->data); // This should probably use celerity and/or javelin + $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Burndown for '.$this->project->getName())) // Calling phutil_safe_html and passing in <script> tags is a potential @@ -392,6 +466,8 @@ array( pht('Task'), pht('Assigned to'), + pht('Priority'), + // pht('Points'), pht('Status'), )); @@ -479,7 +555,7 @@ // If this task is already is this tree, this is a repeat. $repeat = isset($included[$task->getPHID()]); - + $priority_name = new ManiphestTaskPriority(); $depth_indent=''; for($i=0; $i<$depth; $i++) { $depth_indent.=' '; @@ -498,8 +574,10 @@ $task->getMonogram().': '.$task->getTitle() ).($repeat? ' <em title="This task is a child of more than one task in this list. Children are only shown on '. 'the first occurance">[Repeat]</em>':'')), - $task->getOwnerPHID() ? $owner->renderLink() : 'none assigned', - $task->getStatus(), + $task->getOwnerPHID() ? $owner->renderLink() : 'none assigned', + $priority_name->getTaskPriorityName($task->getPriority()), + // $task->getPoints(), + $task->getStatus(), ); $included[$task->getPHID()] = $task->getPHID(); @@ -518,6 +596,7 @@ */ public function buildEventTable() { + $rows = array(); foreach ($this->events as $event) { $task_phid = $this->xactions[$event['transactionPHID']]->getObjectPHID(); $task = $this->tasks[$task_phid]; @@ -559,9 +638,9 @@ * Extract important events (the times when tasks were opened or closed) * from a list of transactions. * - * @param list<ManiphestTransaction> List of transactions. - * @param list<phid> List of project PHIDs to emit "scope" events for. - * @return list<dict> Chronologically sorted events. + * @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'); @@ -639,6 +718,7 @@ // POINTS! $event_type = 'points'; } + break; default: // This is something else (comment, subscription change, etc) that diff --git a/src/BurndownDataDate.php b/src/BurndownDataDate.php index 6386327..ca7d7ee 100644 --- a/src/BurndownDataDate.php +++ b/src/BurndownDataDate.php @@ -27,6 +27,10 @@ return $this; } + /** + * @return string|null + */ + public function getDate() { return $this->date; } diff --git a/src/BurndownTestDataGenerator.php b/src/BurndownTestDataGenerator.php index 32490e5..163655a 100644 --- a/src/BurndownTestDataGenerator.php +++ b/src/BurndownTestDataGenerator.php @@ -14,7 +14,6 @@ // Prepend or append 'Sprint' $title = (mt_rand(0,1)) ? $title.' Sprint':'Sprint '.$title; $author = $this->loadPhabrictorUser(); - $authorPHID = $author->getPHID(); $project = PhabricatorProject::initializeNewProject($author); $this->addTransaction( @@ -57,13 +56,6 @@ ->setOldValue(null) ->setNewValue($end); - $editor = id(new PhabricatorProjectTransactionEditor()) - ->setActor($author) - ->setContentSource(PhabricatorContentSource::newConsoleSource()) - ->setContinueOnNoEffect(true) - ->setContinueOnMissingFields(true) - ->applyTransactions($project, $this->xactions); - $project->save(); // Generate a bunch of tasks created the before the sprint starts @@ -99,6 +91,12 @@ ->generateSeveral(rand(30, 40)); } + /** + * @param $project + * @param integer $start + * @param integer $end + * @return + */ public function generateTask($project, $start, $end) { // Decide when the task was created switch (mt_rand(0,10)) { diff --git a/src/SprintEndDateField.php b/src/SprintEndDateField.php index 200b9b2..97a69dc 100644 --- a/src/SprintEndDateField.php +++ b/src/SprintEndDateField.php @@ -33,7 +33,7 @@ public function renderPropertyViewValue(array $handles) { if (!$this->shouldShowSprintFields()) { - return; + return null; } if ($this->getProxy()->getFieldValue()) diff --git a/src/SprintProjectCustomField.php b/src/SprintProjectCustomField.php index 1248724..86b7681 100644 --- a/src/SprintProjectCustomField.php +++ b/src/SprintProjectCustomField.php @@ -38,7 +38,7 @@ */ public function renderPropertyViewLabel() { if (!$this->shouldShowSprintFields()) { - return; + return null; } if ($this->getProxy()) { @@ -50,10 +50,13 @@ /** * Each subclass must either declare a proxy or implement this method + * @param array $handles + * @throws PhabricatorCustomFieldImplementationIncompleteException + * @return */ public function renderPropertyViewValue(array $handles) { if (!$this->shouldShowSprintFields()) { - return; + return null; } if ($this->getProxy()) { @@ -69,10 +72,13 @@ /** * Each subclass must either declare a proxy or implement this method + * @param array $handles + * @throws PhabricatorCustomFieldImplementationIncompleteException + * @return */ public function renderEditControl(array $handles) { if (!$this->shouldShowSprintFields()) { - return; + return null; } if ($this->getProxy()) { diff --git a/src/SprintStartDateField.php b/src/SprintStartDateField.php index 86f7a7f..fc17335 100644 --- a/src/SprintStartDateField.php +++ b/src/SprintStartDateField.php @@ -33,7 +33,7 @@ public function renderPropertyViewValue(array $handles) { if (!$this->shouldShowSprintFields()) { - return; + return null; } if ($this->getProxy()->getFieldValue()) diff --git a/src/SprintTaskStoryPointsField.php b/src/SprintTaskStoryPointsField.php index c28cff2..27e671e 100644 --- a/src/SprintTaskStoryPointsField.php +++ b/src/SprintTaskStoryPointsField.php @@ -7,6 +7,9 @@ final class SprintTaskStoryPointsField extends ManiphestCustomField implements PhabricatorStandardCustomFieldInterface { + // == General field identity stuff + private $customFields; + public function __construct() { $proxy = id(new PhabricatorStandardCustomFieldText()) ->setFieldKey($this->getFieldKey()) @@ -23,7 +26,24 @@ return true; } - // == General field identity stuff + public function getCustomFieldSpecificationForRole($role) { + return PhabricatorEnv::getEnvConfig('phabricator.serious-business'); + } + + public function getCustomFieldBaseClass() { + return 'ManiphestCustomField'; + } + + public function getCustomFields() { + return $this->assertAttached($this->customFields); + } + + public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { + $this->customFields = $fields; + return $this; + } + + public function getFieldKey() { return 'isdc:sprint:storypoints'; } @@ -70,7 +90,7 @@ public function renderPropertyViewLabel() { if (!$this->showField()) { - return; + return null; } if ($this->getProxy()) { @@ -81,7 +101,7 @@ public function renderPropertyViewValue(array $handles) { if (!$this->showField()) { - return; + return null; } if ($this->getProxy()) { @@ -96,7 +116,7 @@ public function renderEditControl(array $handles) { if (!$this->showField()) { - return; + return null; } if ($this->getProxy()) { -- To view, visit https://gerrit.wikimedia.org/r/164943 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I3cf2ea3e84367661c7e6e36adecddf9243b70a3d Gerrit-PatchSet: 1 Gerrit-Project: phabricator/extensions/Sprint Gerrit-Branch: master Gerrit-Owner: Christopher Johnson (WMDE) <christopher.john...@wikimedia.de> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits