Christopher Johnson (WMDE) has submitted this change and it was merged.

Change subject: Add points to Tasks Table Change base URLs Add 
SprintReportController Class Add Nav Menu to BurndownListController Change-Id: 
I9148971fb50a407cb843efd046817855a2365e9e
......................................................................


Add points to Tasks Table
Change base URLs
Add SprintReportController Class
Add Nav Menu to BurndownListController
Change-Id: I9148971fb50a407cb843efd046817855a2365e9e
---
M __phutil_library_map__.php
M src/BurndownActionMenuEventListener.php
M src/BurndownApplication.php
M src/BurndownData.php
M src/BurndownListController.php
A src/SprintReportController.php
6 files changed, 1,031 insertions(+), 195 deletions(-)

Approvals:
  Christopher Johnson (WMDE): Verified; Looks good to me, approved



diff --git a/__phutil_library_map__.php b/__phutil_library_map__.php
index b49e603..d3477bd 100644
--- a/__phutil_library_map__.php
+++ b/__phutil_library_map__.php
@@ -20,6 +20,7 @@
     'SprintStartDateField' => 'src/SprintStartDateField.php',
     'SprintEndDateField' => 'src/SprintEndDateField.php',
     'SprintProjectCustomField' => 'src/SprintProjectCustomField.php',
+    'SprintReportController' => 'src/SprintReportController.php',
     'SprintTaskStoryPointsField' => 'src/SprintTaskStoryPointsField.php',
   ),
   'function' => array(),
@@ -36,6 +37,7 @@
       'PhabricatorProjectCustomField',
       'PhabricatorStandardCustomFieldInterface',
     ),
+    'SprintReportController' => 'ManiphestController',
     'SprintTaskStoryPointsField' => array(
       'ManiphestCustomField',
       'PhabricatorStandardCustomFieldInterface',
diff --git a/src/BurndownActionMenuEventListener.php 
b/src/BurndownActionMenuEventListener.php
index f528e15..1422171 100644
--- a/src/BurndownActionMenuEventListener.php
+++ b/src/BurndownActionMenuEventListener.php
@@ -37,7 +37,7 @@
 
     $project = $event->getValue('object');
 
-    $view_uri = '/burndown/view/'.$project->getId();
+    $view_uri = '/sprint/view/'.$project->getId();
 
     return id(new PhabricatorActionView())
       ->setIcon('fa-bar-chart-o')
diff --git a/src/BurndownApplication.php b/src/BurndownApplication.php
index f81fae6..67455da 100644
--- a/src/BurndownApplication.php
+++ b/src/BurndownApplication.php
@@ -7,11 +7,11 @@
 final class BurndownApplication extends PhabricatorApplication {
 
   public function getName() {
-    return pht('Burndown Extensions');
+    return pht('Sprint');
   }
 
   public function getBaseURI() {
-    return '/burndown/list/';
+    return '/sprint/list/';
   }
 
   public function getIconName() {
@@ -30,8 +30,9 @@
 
   public function getRoutes() {
     return array(
-      '/burndown/' => array(
+      '/sprint/' => array(
         'list/' => 'BurndownListController',
+        'report/(?:(?P<view>\w+)/)?' => 'SprintReportController',
         'view/(?P<id>\d+)/' => 'BurndownController',
       ),
     );
diff --git a/src/BurndownData.php b/src/BurndownData.php
index 10b8ae2..f9dd8ef 100644
--- a/src/BurndownData.php
+++ b/src/BurndownData.php
@@ -6,60 +6,62 @@
 
 class BurndownData {
 
-   // 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 $type_status = 'core:customfield';
+  private $storypoints;
   private $dates;
   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.
+  // 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();
-  private $task_in_sprint= 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
-    $aux_fields = $this->getAuxFields($project,$viewer);
-    $start= $this->getStartDate($aux_fields);
+    $aux_fields = $this->getAuxFields($project, $viewer);
+    $start = $this->getStartDate($aux_fields);
     $end = $this->getEndDate($aux_fields);
-    $this->dates = $this->buildDateArray($start,$end);
+    $this->dates = $this->buildDateArray($start, $end);
 
-    $tasks = $this->getTasks($project,$viewer);
+    $tasks = $this->getTasks($project, $viewer);
 
     $this->checkNull($start, $end, $tasks);
 
-    $xactions = $this->getXactions($tasks,$viewer);
+    $xactions = $this->getXactions($tasks, $viewer);
 
     $this->examineXactions($xactions, $tasks);
 
-    $this->buildDailyData($start,$end,$viewer);
+    $this->buildDailyData($start, $end, $viewer);
     $this->buildTaskArrays();
+
 
     $this->sumSprintStats();
     $this->computeIdealPoints();
+
   }
 
-  private function getAuxFields($project, $viewer)
-  {
+  private function getAuxFields($project, $viewer) {
     $field_list = PhabricatorCustomField::getObjectFields($project, 
PhabricatorCustomField::ROLE_EDIT);
     $field_list->setViewer($viewer);
     $field_list->readFieldsFromStorage($project);
@@ -67,27 +69,24 @@
     return $aux_fields;
   }
 
-  private function getStartDate($aux_fields)
-  {
+  private function getStartDate($aux_fields) {
     $start = idx($aux_fields, 'isdc:sprint:startdate')
-            ->getProxy()->getFieldValue();
+        ->getProxy()->getFieldValue();
     return $start;
   }
 
-  private function getEndDate($aux_fields)
-  {
+  private function getEndDate($aux_fields) {
     $end = idx($aux_fields, 'isdc:sprint:enddate')
-            ->getProxy()->getFieldValue();
+        ->getProxy()->getFieldValue();
     return $end;
   }
 
-  private function buildDateArray ($start, $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),
+        id(new DateTime("@" . $start))->setTime(0, 0),
         new DateInterval('P1D'), // 1 day interval
-        id(new DateTime("@".$end))->modify('+1 day')->setTime(0,0));
+        id(new DateTime("@" . $end))->modify('+1 day')->setTime(0, 0));
 
     $dates = array('before' => new BurndownDataDate('Start of Sprint'));
     foreach ($period as $day) {
@@ -101,8 +100,7 @@
   // 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).
-  private function examineXactions($xactions,$tasks)
-  {
+  private function examineXactions($xactions, $tasks) {
     $scope_phids = array($this->project->getPHID());
     $this->events = $this->extractEvents($xactions, $scope_phids);
 
@@ -119,35 +117,30 @@
   // 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 ($project, $viewer)
-  {
+  private function getTasks($project, $viewer) {
     $tasks = id(new ManiphestTaskQuery())
-            ->setViewer($viewer)
-            ->withAnyProjects(array($project->getPHID()))
-            ->execute();
+        ->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.");
+  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.");
-     }
+    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, $viewer)
-  {
+  private function getXactions($tasks, $viewer) {
     $task_phids = mpull($tasks, 'getPHID');
 
     $xactions = id(new ManiphestTransactionQuery())
@@ -159,18 +152,17 @@
 
   // Now that we have the data for each day, we need to loop over and sum
   // up the relevant columns
-  private function sumSprintStats ()
-  {
+  private 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->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->tasks_remaining += $previous->tasks_remaining - 
$current->tasks_closed_today;
         $current->points_remaining += $previous->points_remaining - 
$current->points_closed_today;
       }
       $previous = $current;
@@ -180,11 +172,10 @@
 
   // Build arrays to store current point and closed status of tasks as we
   // progress through time, so that these changes reflect on the graph
-  private function buildTaskArrays ()
-  {
+  private function buildTaskArrays() {
     $this->task_points = array();
     $this->task_statuses = array();
-    foreach($this->tasks as $task) {
+    foreach ($this->tasks as $task) {
       $this->task_points[$task->getPHID()] = 0;
       $this->task_statuses[$task->getPHID()] = null;
       $this->task_in_sprint[$task->getPHID()] = 0;
@@ -193,8 +184,7 @@
   }
 
   // Now loop through the events and build the data for each day
-  private function buildDailyData ($start, $end, $viewer)
-  {
+  private function buildDailyData($start, $end, $viewer) {
     foreach ($this->events as $event) {
 
       $xaction = $this->xactions[$event['transactionPHID']];
@@ -212,7 +202,7 @@
         $date = phabricator_format_local_time($xaction_date, $viewer, 'D M j');
       }
 
-      switch($event['type']) {
+      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
@@ -247,37 +237,36 @@
 
   // This is a cheap hacky way to get business days, and does not account for
   // holidays at all.
-   private 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++;
-            }
-        }
+  private 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;
-             }
+    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++;
-                 }
+      $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);
-         }
+      $date->points_ideal_remaining = round($date->points_total *
+          (1 - ($elapsed_business_days / $total_business_days)), 1);
     }
+  }
 
 
   /**
@@ -317,18 +306,17 @@
 
       // Adjust points for that day
       $this->dates[$date]->points_added_today +=
-        $xaction->getNewValue() - $xaction->getOldValue();
+          $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();
+            $xaction->getNewValue() - $xaction->getOldValue();
       }
     }
   }
 
-  private function buildChartDataSet()
-  {
+  private function buildChartDataSet() {
     $data = array(array(
         pht('Date'),
         pht('Total Points'),
@@ -338,23 +326,23 @@
     ));
 
     $future = false;
-    foreach($this->dates as $key => $date)
-    {
+    foreach ($this->dates as $key => $date) {
       if ($key != 'before' AND $key != 'after') {
-        $future = new DateTime($date->getDate()) > id(new 
DateTime())->setTime(0,0);
+        $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,
+          $future ? null : $date->points_total,
+          $future ? null : $date->points_remaining,
           $date->points_ideal_remaining,
-          $future ? null: $date->points_closed_today,
+          $future ? null : $date->points_closed_today,
       );
 
     }
     return $data;
 
   }
+
   public function buildBurnDownChart() {
 
     $this->data = $this->buildChartDataSet();
@@ -364,11 +352,11 @@
     // 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
-      // security hole. None of this data is direct user input, so we should
-      // be fine.
-      ->appendChild(phutil_safe_html(<<<HERE
+        ->setHeaderText(pht('Burndown for ' . $this->project->getName()))
+        // Calling phutil_safe_html and passing in <script> tags is a potential
+        // security hole. None of this data is direct user input, so we should
+        // be fine.
+        ->appendChild(phutil_safe_html(<<<HERE
 <script type="text/javascript" src="//www.google.com/jsapi"></script>
 <script type="text/javascript">
   google.load('visualization', '1', {packages: ['corechart']});
@@ -400,11 +388,11 @@
 </script>
 HERE
         ))
-      ->appendChild(phutil_tag('div',
-        array(
-          'id' => 'visualization',
-          'style' => 'width: 100%; height:400px'
-        ),''));
+        ->appendChild(phutil_tag('div',
+            array(
+                'id' => 'visualization',
+                'style' => 'width: 100%; height:400px'
+            ), ''));
 
     return $box;
   }
@@ -416,7 +404,7 @@
    */
   public function buildBurnDownTable() {
     $data = array();
-    foreach($this->dates as $date) {
+    foreach ($this->dates as $date) {
       $data[] = array(
           $date->getDate(),
           $date->tasks_total,
@@ -425,24 +413,24 @@
           $date->points_remaining,
           $date->points_ideal_remaining,
           $date->points_closed_today,
-        );
+      );
     }
 
     $table = id(new AphrontTableView($data))
-      ->setHeaders(
-        array(
-          pht('Date'),
-          pht('Total Tasks'),
-          pht('Remaining Tasks'),
-          pht('Total Points'),
-          pht('Remaining Points'),
-          pht('Ideal Remaining Points'),
-          pht('Points Completed Today'),
-        ));
+        ->setHeaders(
+            array(
+                pht('Date'),
+                pht('Total Tasks'),
+                pht('Remaining Tasks'),
+                pht('Total Points'),
+                pht('Remaining Points'),
+                pht('Ideal Remaining Points'),
+                pht('Points Completed Today'),
+            ));
 
     $box = id(new PHUIObjectBoxView())
-      ->setHeaderText(pht('DATA'))
-      ->appendChild($table);
+        ->setHeaderText(pht('DATA'))
+        ->appendChild($table);
 
     return $box;
   }
@@ -457,18 +445,18 @@
     $rows = $this->buildTasksTree();
 
     $table = id(new AphrontTableView($rows))
-      ->setHeaders(
-        array(
-          pht('Task'),
-          pht('Assigned to'),
-          pht('Priority'),
-       //   pht('Points'),
-          pht('Status'),
-        ));
+        ->setHeaders(
+            array(
+                pht('Task'),
+                pht('Assigned to'),
+                pht('Priority'),
+                pht('Points'),
+                pht('Status'),
+            ));
 
     $box = id(new PHUIObjectBoxView())
-      ->setHeaderText(pht('Tasks in this Sprint'))
-      ->appendChild($table);
+        ->setHeaderText(pht('Tasks in this Sprint'))
+        ->appendChild($table);
 
     return $box;
   }
@@ -488,9 +476,9 @@
 
     // Load all edges of depends and depended on tasks
     $edges = id(new PhabricatorEdgeQuery())
-      ->withSourcePHIDs(array_keys($this->tasks))
-      ->withEdgeTypes(array($DEPENDS_ON, $DEPENDED_ON))
-      ->execute();
+        ->withSourcePHIDs(array_keys($this->tasks))
+        ->withEdgeTypes(array($DEPENDS_ON, $DEPENDED_ON))
+        ->execute();
 
     // First we build a flat map. Each task is in the map at the root level,
     // and lists it's parents and children.
@@ -516,15 +504,15 @@
 
     // We also collect the phids we need to fetch owner information
     $handle_phids = array();
-    foreach($this->tasks as $task) {
+    foreach ($this->tasks as $task) {
       // Get the owner (assigned to) phid
       $handle_phids[$task->getOwnerPHID()] = $task->getOwnerPHID();
     }
 
     $handles = id(new PhabricatorHandleQuery())
-      ->setViewer($this->viewer)
-      ->withPHIDs($handle_phids)
-      ->execute();
+        ->setViewer($this->viewer)
+        ->withPHIDs($handle_phids)
+        ->execute();
 
     // Now we loop through the tasks, and add them to the output
     $output = array();
@@ -541,8 +529,7 @@
     return $output;
   }
 
-  private function addTaskToTree(&$output, $task, &$map, $handles,
-    $depth = 0) {
+  private function addTaskToTree(&$output, $task, &$map, $handles, $depth = 0) 
{
     static $included = array();
 
     // Get the owner object so we can render the owner username/link
@@ -550,36 +537,41 @@
 
     // If this task is already is this tree, this is a repeat.
     $repeat = isset($included[$task->getPHID()]);
+
+    $points_data = $this->getPointsData();
+    $points = $this->getTaskStoryPoints($task->getPHID(),$points_data);
+    $points = trim($points, '"');
+
     $priority_name = new ManiphestTaskPriority();
-    $depth_indent='';
-    for($i=0; $i<$depth; $i++) {
-      $depth_indent.='&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
+    $depth_indent = '';
+    for ($i = 0; $i < $depth; $i++) {
+      $depth_indent .= '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
     }
 
     // Build the row
     $output[] = array(
-      phutil_safe_html($depth_indent.phutil_tag(
-        'a',
-        array(
-          'href' => '/'.$task->getMonogram(),
-          'class' => $task->getStatus() !== 'open'
-            ? 'phui-tag-core-closed'
-            : '',
-        ),
-        $task->getMonogram().': '.$task->getTitle()
-        ).($repeat? '&nbsp;&nbsp;<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>':'')),
+        phutil_safe_html($depth_indent . phutil_tag(
+                'a',
+                array(
+                    'href' => '/' . $task->getMonogram(),
+                    'class' => $task->getStatus() !== 'open'
+                        ? 'phui-tag-core-closed'
+                        : '',
+                ),
+                $task->getMonogram() . ': ' . $task->getTitle()
+            ) . ($repeat ? '&nbsp;&nbsp;<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',
         $priority_name->getTaskPriorityName($task->getPriority()),
-      //  $task->getPoints(),
+        $points,
         $task->getStatus(),
     );
     $included[$task->getPHID()] = $task->getPHID();
 
     if (isset($map[$task->getPHID()]['children'])) {
-      foreach($map[$task->getPHID()]['children'] as $child) {
+      foreach ($map[$task->getPHID()]['children'] as $child) {
         $child = $this->tasks[$child];
-        $this->addTaskToTree($output, $child, $map, $handles, $depth+1);
+        $this->addTaskToTree($output, $child, $map, $handles, $depth + 1);
       }
     }
   }
@@ -589,46 +581,88 @@
    *
    * @returns PHUIObjectBoxView
    */
-  public function buildEventTable()
-  {
+  public function buildEventTable() {
     $rows = array();
     foreach ($this->events as $event) {
       $task_phid = $this->xactions[$event['transactionPHID']]->getObjectPHID();
       $task = $this->tasks[$task_phid];
 
       $rows[] = array(
-        phabricator_datetime($event['epoch'], $this->viewer),
-        phutil_tag(
-          'a',
-          array(
-            'href' => '/'.$task->getMonogram(),
-          ),
-          $task->getMonogram().': '.$task->getTitle()),
-        $event['title'],
+          phabricator_datetime($event['epoch'], $this->viewer),
+          phutil_tag(
+              'a',
+              array(
+                  'href' => '/' . $task->getMonogram(),
+              ),
+              $task->getMonogram() . ': ' . $task->getTitle()),
+          $event['title'],
       );
     }
 
     $table = id(new AphrontTableView($rows))
-      ->setHeaders(
-        array(
-          pht('When'),
-          pht('Task'),
-          pht('Action'),
-        ))
-      ->setColumnClasses(
-        array(
-          '',
-          '',
-          'wide',
-        ));
+        ->setHeaders(
+            array(
+                pht('When'),
+                pht('Task'),
+                pht('Action'),
+            ))
+        ->setColumnClasses(
+            array(
+                '',
+                '',
+                'wide',
+            ));
 
     $box = id(new PHUIObjectBoxView())
-      ->setHeaderText(pht('Events related to this sprint'))
-      ->appendChild($table);
+        ->setHeaderText(pht('Events related to this sprint'))
+        ->appendChild($table);
 
     return $box;
   }
 
+  private function getTaskStoryPoints($task,$points_data) {
+    $storypoints = array();
+       foreach ($points_data as $k=>$subarray) {
+         if (isset ($subarray['objectPHID']) && $subarray['objectPHID'] == 
$task) {
+           $points_data[$k] = $subarray;
+           $storypoints = $subarray['newValue'];
+         }
+       }
+    return $storypoints;
+  }
+
+
+  private function getPointsData () {
+    $handle = null;
+
+    $project_phid = $this->project->getPHID();
+    $table = new ManiphestTransaction();
+    $conn = $table->establishConnection('r');
+
+    $joins = '';
+    if ($project_phid) {
+      $joins = qsprintf(
+          $conn,
+          'JOIN %T t ON x.objectPHID = t.phid
+          JOIN %T p ON p.src = t.phid AND p.type = %d AND p.dst = %s',
+          id(new ManiphestTask())->getTableName(),
+          PhabricatorEdgeConfig::TABLE_NAME_EDGE,
+          PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
+          $project_phid);
+    }
+
+    $points_data = queryfx_all(
+        $conn,
+        'SELECT x.objectPHID, x.oldValue, x.newValue, x.dateCreated FROM %T x 
%Q
+        WHERE transactionType = %s
+        ORDER BY x.dateCreated ASC',
+        $table->getTableName(),
+        $joins,
+        $this->type_status);
+
+    return $points_data;
+  }
+
   /**
    * Extract important events (the times when tasks were opened or closed)
    * from a list of transactions.
diff --git a/src/BurndownListController.php b/src/BurndownListController.php
index fb18e0e..0e7e287 100644
--- a/src/BurndownListController.php
+++ b/src/BurndownListController.php
@@ -6,12 +6,24 @@
 
 final class BurndownListController extends PhabricatorController {
 
+  private $view;
   public function willProcessRequest(array $data) {
   }
 
   public function processRequest() {
     $request = $this->getRequest();
     $viewer = $request->getUser();
+
+    $nav = new AphrontSideNavFilterView();
+    $nav->setBaseURI(new PhutilURI('/sprint/report/'));
+    $nav->addLabel(pht('Open Tasks'));
+    $nav->addFilter('user', pht('By User'));
+    $nav->addFilter('project', pht('By Project'));
+    $nav->addLabel(pht('Burndown'));
+    $nav->addFilter('burn', pht('Burndown Rate'));
+
+    $this->view = $nav->selectFilter($this->view, 'user');
+
 
     // Load all projects with "Sprint" in the name.
     $projects = id(new PhabricatorProjectQuery())
@@ -38,7 +50,7 @@
       $rows[] = array(
         'project' => phutil_tag('a',
           array(
-            'href'  => '/burndown/view/'.$project->getId(),
+            'href'  => '/sprint/view/'.$project->getId(),
             'style' => 'font-weight:bold',
           ),
           $project->getName()
@@ -62,8 +74,10 @@
           'date',
         ));
 
+
     $crumbs = $this->buildApplicationCrumbs();
     $crumbs->addTextCrumb(pht('Burndown List'));
+
 
     $help = id(new PHUIBoxView())
       ->appendChild(phutil_tag('p', array(),
@@ -72,18 +86,24 @@
       ))
       ->addMargin(PHUI::MARGIN_LARGE);
 
-    $box = id(new PHUIBoxView())
+    $box= id(new PHUIBoxView())
       ->appendChild($projects_table)
       ->addMargin(PHUI::MARGIN_LARGE);
 
+    $nav->appendChild(
+        array(
+            $crumbs,
+            $help,
+            $box,
+        ));
+
     return $this->buildApplicationPage(
+
       array(
-        $crumbs,
-        $help,
-        $box,
+        $nav,
       ),
       array(
-        'title' => array(pht('Burndown List')),
+        'title' => array(pht('Sprint List')),
         'device' => true,
       ));
   }
diff --git a/src/SprintReportController.php b/src/SprintReportController.php
new file mode 100644
index 0000000..055ca3f
--- /dev/null
+++ b/src/SprintReportController.php
@@ -0,0 +1,779 @@
+<?php
+
+final class SprintReportController extends ManiphestController {
+
+  private $view;
+  //private $type_status = 'core:customfield';
+  public function willProcessRequest(array $data) {
+    $this->view = idx($data, 'view');
+  }
+
+  public function processRequest() {
+    $request = $this->getRequest();
+    $user = $request->getUser();
+
+    if ($request->isFormPost()) {
+      $uri = $request->getRequestURI();
+
+      $project = head($request->getArr('set_project'));
+      $project = nonempty($project, null);
+      $uri = $uri->alter('project', $project);
+
+      $window = $request->getStr('set_window');
+      $uri = $uri->alter('window', $window);
+
+      return id(new AphrontRedirectResponse())->setURI($uri);
+    }
+
+    $nav = new AphrontSideNavFilterView();
+    $nav->setBaseURI(new PhutilURI('/sprint/report/'));
+    $nav->addLabel(pht('Open Tasks'));
+    $nav->addFilter('project', pht('By Project'));
+    $nav->addFilter('user', pht('By User'));
+    $nav->addLabel(pht('Burndown'));
+    $nav->addFilter('burn', pht('Burndown Rate'));
+
+    $this->view = $nav->selectFilter($this->view, 'user');
+
+    require_celerity_resource('maniphest-report-css');
+
+    switch ($this->view) {
+      case 'burn':
+        $core = $this->renderBurn();
+        break;
+      case 'user':
+      case 'project':
+        $core = $this->renderOpenTasks();
+        break;
+      default:
+        return new Aphront404Response();
+    }
+
+    $nav->appendChild($core);
+    $nav->setCrumbs(
+        $this->buildApplicationCrumbs()
+            ->addTextCrumb(pht('Reports')));
+
+    return $this->buildApplicationPage(
+        $nav,
+        array(
+            'title' => pht('Sprint Reports'),
+            'device' => false,
+        ));
+  }
+
+  public function renderBurn() {
+
+
+    $request = $this->getRequest();
+    $user = $request->getUser();
+
+    $handle = null;
+
+    $project_phid = $request->getStr('project');
+    if ($project_phid) {
+      $phids = array($project_phid);
+      $handles = $this->loadViewerHandles($phids);
+      $handle = $handles[$project_phid];
+    }
+
+    $table = new ManiphestTransaction();
+    $conn = $table->establishConnection('r');
+
+    $joins = '';
+    if ($project_phid) {
+      $joins = qsprintf(
+          $conn,
+          'JOIN %T t ON x.objectPHID = t.phid
+          JOIN %T p ON p.src = t.phid AND p.type = %d AND p.dst = %s',
+          id(new ManiphestTask())->getTableName(),
+          PhabricatorEdgeConfig::TABLE_NAME_EDGE,
+          PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
+          $project_phid);
+    }
+
+    $data = queryfx_all(
+        $conn,
+        'SELECT x.objectPHID, x.oldValue, x.newValue, x.dateCreated FROM %T x 
%Q
+        WHERE transactionType = %s
+        ORDER BY x.dateCreated ASC',
+        $table->getTableName(),
+        $joins,
+        ManiphestTransaction::TYPE_STATUS);
+
+    $stats = array();
+    $day_buckets = array();
+
+    $open_tasks = array();
+
+    foreach ($data as $key => $row) {
+
+      // NOTE: Hack to avoid json_decode().
+      $oldv = trim($row['oldValue'], '"');
+      $newv = trim($row['newValue'], '"');
+
+      if ($oldv == 'null') {
+        $old_is_open = false;
+      } else {
+        $old_is_open = ManiphestTaskStatus::isOpenStatus($oldv);
+      }
+
+      $new_is_open = ManiphestTaskStatus::isOpenStatus($newv);
+
+      $is_open  = ($new_is_open && !$old_is_open);
+      $is_close = ($old_is_open && !$new_is_open);
+
+      $data[$key]['_is_open'] = $is_open;
+      $data[$key]['_is_close'] = $is_close;
+
+      if (!$is_open && !$is_close) {
+        // This is either some kind of bogus event, or a resolution change
+        // (e.g., resolved -> invalid). Just skip it.
+        continue;
+      }
+
+      $day_bucket = phabricator_format_local_time(
+          $row['dateCreated'],
+          $user,
+          'Yz');
+      $day_buckets[$day_bucket] = $row['dateCreated'];
+      if (empty($stats[$day_bucket])) {
+        $stats[$day_bucket] = array(
+            'open'  => 0,
+            'close' => 0,
+        );
+      }
+      $stats[$day_bucket][$is_close ? 'close' : 'open']++;
+    }
+
+    $template = array(
+        'open'  => 0,
+        'close' => 0,
+    );
+
+    $rows = array();
+    $rowc = array();
+    $last_month = null;
+    $last_month_epoch = null;
+    $last_week = null;
+    $last_week_epoch = null;
+    $week = null;
+    $month = null;
+
+    $last = last_key($stats) - 1;
+    $period = $template;
+
+    foreach ($stats as $bucket => $info) {
+      $epoch = $day_buckets[$bucket];
+
+      $week_bucket = phabricator_format_local_time(
+          $epoch,
+          $user,
+          'YW');
+      if ($week_bucket != $last_week) {
+        if ($week) {
+          $rows[] = $this->formatBurnRow(
+              'Week of '.phabricator_date($last_week_epoch, $user),
+              $week);
+          $rowc[] = 'week';
+        }
+        $week = $template;
+        $last_week = $week_bucket;
+        $last_week_epoch = $epoch;
+      }
+
+      $month_bucket = phabricator_format_local_time(
+          $epoch,
+          $user,
+          'Ym');
+      if ($month_bucket != $last_month) {
+        if ($month) {
+          $rows[] = $this->formatBurnRow(
+              phabricator_format_local_time($last_month_epoch, $user, 'F, Y'),
+              $month);
+          $rowc[] = 'month';
+        }
+        $month = $template;
+        $last_month = $month_bucket;
+        $last_month_epoch = $epoch;
+      }
+
+      $rows[] = $this->formatBurnRow(phabricator_date($epoch, $user), $info);
+      $rowc[] = null;
+      $week['open'] += $info['open'];
+      $week['close'] += $info['close'];
+      $month['open'] += $info['open'];
+      $month['close'] += $info['close'];
+      $period['open'] += $info['open'];
+      $period['close'] += $info['close'];
+    }
+
+    if ($week) {
+      $rows[] = $this->formatBurnRow(
+          pht('Week To Date'),
+          $week);
+      $rowc[] = 'week';
+    }
+
+    if ($month) {
+      $rows[] = $this->formatBurnRow(
+          pht('Month To Date'),
+          $month);
+      $rowc[] = 'month';
+    }
+
+    $rows[] = $this->formatBurnRow(
+        pht('All Time'),
+        $period);
+    $rowc[] = 'aggregate';
+
+    $rows = array_reverse($rows);
+    $rowc = array_reverse($rowc);
+
+    $table = new AphrontTableView($rows);
+    $table->setRowClasses($rowc);
+    $table->setHeaders(
+        array(
+            pht('Period'),
+            pht('Opened'),
+            pht('Closed'),
+            pht('Change'),
+        ));
+    $table->setColumnClasses(
+        array(
+            'left narrow',
+            'center narrow',
+            'center narrow',
+            'center narrow',
+        ));
+
+    if ($handle) {
+      $inst = pht(
+          'NOTE: This table reflects tasks currently in '.
+          'the project. If a task was opened in the past but added to '.
+          'the project recently, it is counted on the day it was '.
+          'opened, not the day it was categorized. If a task was part '.
+          'of this project in the past but no longer is, it is not '.
+          'counted at all.');
+      $header = pht('Task Burn Rate for Project %s', $handle->renderLink());
+      $caption = phutil_tag('p', array(), $inst);
+    } else {
+      $header = pht('Task Burn Rate for All Tasks');
+      $caption = null;
+    }
+
+    if ($caption) {
+      $caption = id(new AphrontErrorView())
+          ->appendChild($caption)
+          ->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
+    }
+
+    $panel = new PHUIObjectBoxView();
+    $panel->setHeaderText($header);
+    if ($caption) {
+      $panel->setErrorView($caption);
+    }
+    $panel->appendChild($table);
+
+    $tokens = array();
+    if ($handle) {
+      $tokens = array($handle);
+    }
+
+    $filter = $this->renderReportFilters($tokens, $has_window = false);
+
+    $id = celerity_generate_unique_node_id();
+    $chart = phutil_tag(
+        'div',
+        array(
+            'id' => $id,
+            'style' => 'border: 1px solid #BFCFDA; '.
+                'background-color: #fff; '.
+                'margin: 8px 16px; '.
+                'height: 400px; ',
+        ),
+        '');
+
+    list($burn_x, $burn_y) = $this->buildSeries($data);
+
+    require_celerity_resource('raphael-core');
+    require_celerity_resource('raphael-g');
+    require_celerity_resource('raphael-g-line');
+
+    Javelin::initBehavior('line-chart', array(
+        'hardpoint' => $id,
+        'x' => array(
+            $burn_x,
+        ),
+        'y' => array(
+            $burn_y,
+        ),
+        'xformat' => 'epoch',
+        'yformat' => 'int',
+    ));
+
+    return array($filter, $chart, $panel);
+  }
+
+  private function renderReportFilters(array $tokens, $has_window) {
+    $request = $this->getRequest();
+    $user = $request->getUser();
+
+    $form = id(new AphrontFormView())
+        ->setUser($user)
+        ->appendChild(
+            id(new AphrontFormTokenizerControl())
+                ->setDatasource(new PhabricatorProjectDatasource())
+                ->setLabel(pht('Project'))
+                ->setLimit(1)
+                ->setName('set_project')
+                ->setValue($tokens));
+
+    if ($has_window) {
+      list($window_str, $ignored, $window_error) = $this->getWindow();
+      $form
+          ->appendChild(
+              id(new AphrontFormTextControl())
+                  ->setLabel(pht('Recently Means'))
+                  ->setName('set_window')
+                  ->setCaption(
+                      pht('Configure the cutoff for the "Recently Closed" 
column.'))
+                  ->setValue($window_str)
+                  ->setError($window_error));
+    }
+
+    $form
+        ->appendChild(
+            id(new AphrontFormSubmitControl())
+                ->setValue(pht('Filter By Project')));
+
+    $filter = new AphrontListFilterView();
+    $filter->appendChild($form);
+
+    return $filter;
+  }
+
+  private function buildSeries(array $data) {
+    $out = array();
+
+    $counter = 0;
+    foreach ($data as $row) {
+      $t = (int)$row['dateCreated'];
+      if ($row['_is_close']) {
+        --$counter;
+        $out[$t] = $counter;
+      } else if ($row['_is_open']) {
+        ++$counter;
+        $out[$t] = $counter;
+      }
+    }
+
+    return array(array_keys($out), array_values($out));
+  }
+
+  private function formatBurnRow($label, $info) {
+    $delta = $info['open'] - $info['close'];
+    $fmt = number_format($delta);
+    if ($delta > 0) {
+      $fmt = '+'.$fmt;
+      $fmt = phutil_tag('span', array('class' => 'red'), $fmt);
+    } else {
+      $fmt = phutil_tag('span', array('class' => 'green'), $fmt);
+    }
+
+    return array(
+        $label,
+        number_format($info['open']),
+        number_format($info['close']),
+        $fmt);
+  }
+
+  public function renderOpenTasks() {
+    $request = $this->getRequest();
+    $user = $request->getUser();
+
+
+    $query = id(new ManiphestTaskQuery())
+        ->setViewer($user)
+        ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants());
+
+    $project_phid = $request->getStr('project');
+    $project_handle = null;
+    if ($project_phid) {
+      $phids = array($project_phid);
+      $handles = $this->loadViewerHandles($phids);
+      $project_handle = $handles[$project_phid];
+
+      $query->withAnyProjects($phids);
+    }
+
+    $tasks = $query->execute();
+
+    $recently_closed = $this->loadRecentlyClosedTasks();
+
+    $date = phabricator_date(time(), $user);
+
+    switch ($this->view) {
+      case 'user':
+        $result = mgroup($tasks, 'getOwnerPHID');
+        $leftover = idx($result, '', array());
+        unset($result['']);
+
+        $result_closed = mgroup($recently_closed, 'getOwnerPHID');
+        $leftover_closed = idx($result_closed, '', array());
+        unset($result_closed['']);
+
+        $base_link = '/maniphest/?assigned=';
+        $leftover_name = phutil_tag('em', array(), pht('(Up For Grabs)'));
+        $col_header = pht('User');
+        $header = pht('Open Tasks by User and Priority (%s)', $date);
+        break;
+      case 'project':
+        $result = array();
+        $leftover = array();
+        foreach ($tasks as $task) {
+          $phids = $task->getProjectPHIDs();
+          if ($phids) {
+            foreach ($phids as $project_phid) {
+              $result[$project_phid][] = $task;
+            }
+          } else {
+            $leftover[] = $task;
+          }
+        }
+
+        $result_closed = array();
+        $leftover_closed = array();
+        foreach ($recently_closed as $task) {
+          $phids = $task->getProjectPHIDs();
+          if ($phids) {
+            foreach ($phids as $project_phid) {
+              $result_closed[$project_phid][] = $task;
+            }
+          } else {
+            $leftover_closed[] = $task;
+          }
+        }
+
+        $base_link = '/maniphest/?allProjects=';
+        $leftover_name = phutil_tag('em', array(), pht('(No Project)'));
+        $col_header = pht('Project');
+        $header = pht('Open Tasks by Project and Priority (%s)', $date);
+        break;
+    }
+
+    $phids = array_keys($result);
+    $handles = $this->loadViewerHandles($phids);
+    $handles = msort($handles, 'getName');
+
+    $order = $request->getStr('order', 'name');
+    list($order, $reverse) = AphrontTableView::parseSort($order);
+
+    require_celerity_resource('aphront-tooltip-css');
+    Javelin::initBehavior('phabricator-tooltips', array());
+
+    $rows = array();
+    $pri_total = array();
+    foreach (array_merge($handles, array(null)) as $handle) {
+      if ($handle) {
+        if (($project_handle) &&
+            ($project_handle->getPHID() == $handle->getPHID())) {
+          // If filtering by, e.g., "bugs", don't show a "bugs" group.
+          continue;
+        }
+
+        $tasks = idx($result, $handle->getPHID(), array());
+        $name = phutil_tag(
+            'a',
+            array(
+                'href' => $base_link.$handle->getPHID(),
+            ),
+            $handle->getName());
+        $closed = idx($result_closed, $handle->getPHID(), array());
+      } else {
+        $tasks = $leftover;
+        $name  = $leftover_name;
+        $closed = $leftover_closed;
+      }
+
+      $taskv = $tasks;
+      $tasks = mgroup($tasks, 'getPriority');
+
+      $row = array();
+      $row[] = $name;
+      $total = 0;
+      foreach (ManiphestTaskPriority::getTaskPriorityMap() as $pri => $label) {
+        $n = count(idx($tasks, $pri, array()));
+        if ($n == 0) {
+          $row[] = '-';
+        } else {
+          $row[] = number_format($n);
+        }
+        $total += $n;
+      }
+      $row[] = number_format($total);
+
+      list($link, $oldest_all) = $this->renderOldest($taskv);
+      $row[] = $link;
+
+      $normal_or_better = array();
+      foreach ($taskv as $id => $task) {
+        // TODO: This is sort of a hard-code for the default "normal" status.
+        // When reports are more powerful, this should be made more general.
+        if ($task->getPriority() < 50) {
+          continue;
+        }
+        $normal_or_better[$id] = $task;
+      }
+
+      list($link, $oldest_pri) = $this->renderOldest($normal_or_better);
+      $row[] = $link;
+
+      if ($closed) {
+        $task_ids = implode(',', mpull($closed, 'getID'));
+        $row[] = phutil_tag(
+            'a',
+            array(
+                'href' => '/maniphest/?ids='.$task_ids,
+                'target' => '_blank',
+            ),
+            number_format(count($closed)));
+      } else {
+        $row[] = '-';
+      }
+
+      switch ($order) {
+        case 'total':
+          $row['sort'] = $total;
+          break;
+        case 'oldest-all':
+          $row['sort'] = $oldest_all;
+          break;
+        case 'oldest-pri':
+          $row['sort'] = $oldest_pri;
+          break;
+        case 'closed':
+          $row['sort'] = count($closed);
+          break;
+        case 'name':
+        default:
+          $row['sort'] = $handle ? $handle->getName() : '~';
+          break;
+      }
+
+      $rows[] = $row;
+    }
+
+    $rows = isort($rows, 'sort');
+    foreach ($rows as $k => $row) {
+      unset($rows[$k]['sort']);
+    }
+    if ($reverse) {
+      $rows = array_reverse($rows);
+    }
+
+    $cname = array($col_header);
+    $cclass = array('pri left narrow');
+    $pri_map = ManiphestTaskPriority::getShortNameMap();
+    foreach ($pri_map as $pri => $label) {
+      $cname[] = $label;
+      $cclass[] = 'center narrow';
+    }
+    $cname[] = 'Total';
+    $cclass[] = 'center narrow';
+    $cname[] = javelin_tag(
+        'span',
+        array(
+            'sigil' => 'has-tooltip',
+            'meta'  => array(
+                'tip' => pht('Oldest open task.'),
+                'size' => 200,
+            ),
+        ),
+        pht('Oldest (All)'));
+    $cclass[] = 'center narrow';
+    $cname[] = javelin_tag(
+        'span',
+        array(
+            'sigil' => 'has-tooltip',
+            'meta'  => array(
+                'tip' => pht('Oldest open task, excluding those with Low or '.
+                    'Wishlist priority.'),
+                'size' => 200,
+            ),
+        ),
+        pht('Oldest (Pri)'));
+    $cclass[] = 'center narrow';
+
+    list($ignored, $window_epoch) = $this->getWindow();
+    $edate = phabricator_datetime($window_epoch, $user);
+    $cname[] = javelin_tag(
+        'span',
+        array(
+            'sigil' => 'has-tooltip',
+            'meta'  => array(
+                'tip'  => pht('Closed after %s', $edate),
+                'size' => 260
+            ),
+        ),
+        pht('Recently Closed'));
+    $cclass[] = 'center narrow';
+
+    $table = new AphrontTableView($rows);
+    $table->setHeaders($cname);
+    $table->setColumnClasses($cclass);
+    $table->makeSortable(
+        $request->getRequestURI(),
+        'order',
+        $order,
+        $reverse,
+        array(
+            'name',
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            'total',
+            'oldest-all',
+            'oldest-pri',
+            'closed',
+        ));
+
+    $panel = new PHUIObjectBoxView();
+    $panel->setHeaderText($header);
+    $panel->appendChild($table);
+
+    $tokens = array();
+    if ($project_handle) {
+      $tokens = array($project_handle);
+    }
+    $filter = $this->renderReportFilters($tokens, $has_window = true);
+
+    return array($filter, $panel);
+  }
+
+
+  /**
+   * Load all the tasks that have been recently closed.
+   */
+  private function loadRecentlyClosedTasks() {
+    list($ignored, $window_epoch) = $this->getWindow();
+
+    $table = new ManiphestTask();
+    $xtable = new ManiphestTransaction();
+    $conn_r = $table->establishConnection('r');
+
+    // TODO: Gross. This table is not meant to be queried like this. Build
+    // real stats tables.
+
+    $open_status_list = array();
+    foreach (ManiphestTaskStatus::getOpenStatusConstants() as $constant) {
+      $open_status_list[] = json_encode((string)$constant);
+    }
+
+    $rows = queryfx_all(
+        $conn_r,
+        'SELECT t.id FROM %T t JOIN %T x ON x.objectPHID = t.phid
+        WHERE t.status NOT IN (%Ls)
+        AND x.oldValue IN (null, %Ls)
+        AND x.newValue NOT IN (%Ls)
+        AND t.dateModified >= %d
+        AND x.dateCreated >= %d',
+        $table->getTableName(),
+        $xtable->getTableName(),
+        ManiphestTaskStatus::getOpenStatusConstants(),
+        $open_status_list,
+        $open_status_list,
+        $window_epoch,
+        $window_epoch);
+
+    if (!$rows) {
+      return array();
+    }
+
+    $ids = ipull($rows, 'id');
+
+    return id(new ManiphestTaskQuery())
+        ->setViewer($this->getRequest()->getUser())
+        ->withIDs($ids)
+        ->execute();
+  }
+
+  /**
+   * Parse the "Recently Means" filter into:
+   *
+   *    - A string representation, like "12 AM 7 days ago" (default);
+   *    - a locale-aware epoch representation; and
+   *    - a possible error.
+   */
+  private function getWindow() {
+    $request = $this->getRequest();
+    $user = $request->getUser();
+
+    $window_str = $this->getRequest()->getStr('window', '12 AM 7 days ago');
+
+    $error = null;
+    $window_epoch = null;
+
+    // Do locale-aware parsing so that the user's timezone is assumed for
+    // time windows like "3 PM", rather than assuming the server timezone.
+
+    $window_epoch = PhabricatorTime::parseLocalTime($window_str, $user);
+    if (!$window_epoch) {
+      $error = 'Invalid';
+      $window_epoch = time() - (60 * 60 * 24 * 7);
+    }
+
+    // If the time ends up in the future, convert it to the corresponding time
+    // and equal distance in the past. This is so users can type "6 days" 
(which
+    // means "6 days from now") and get the behavior of "6 days ago", rather
+    // than no results (because the window epoch is in the future). This might
+    // be a little confusing because it casues "tomorrow" to mean "yesterday"
+    // and "2022" (or whatever) to mean "ten years ago", but these inputs are
+    // nonsense anyway.
+
+    if ($window_epoch > time()) {
+      $window_epoch = time() - ($window_epoch - time());
+    }
+
+    return array($window_str, $window_epoch, $error);
+  }
+
+  private function renderOldest(array $tasks) {
+    assert_instances_of($tasks, 'ManiphestTask');
+    $oldest = null;
+    foreach ($tasks as $id => $task) {
+      if (($oldest === null) ||
+          ($task->getDateCreated() < $tasks[$oldest]->getDateCreated())) {
+        $oldest = $id;
+      }
+    }
+
+    if ($oldest === null) {
+      return array('-', 0);
+    }
+
+    $oldest = $tasks[$oldest];
+
+    $raw_age = (time() - $oldest->getDateCreated());
+    $age = number_format($raw_age / (24 * 60 * 60)).' d';
+
+    $link = javelin_tag(
+        'a',
+        array(
+            'href'  => '/T'.$oldest->getID(),
+            'sigil' => 'has-tooltip',
+            'meta'  => array(
+                'tip' => 'T'.$oldest->getID().': '.$oldest->getTitle(),
+            ),
+            'target' => '_blank',
+        ),
+        $age);
+
+    return array($link, $raw_age);
+  }
+
+}
+

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

Gerrit-MessageType: merged
Gerrit-Change-Id: I9148971fb50a407cb843efd046817855a2365e9e
Gerrit-PatchSet: 3
Gerrit-Project: phabricator/extensions/Sprint
Gerrit-Branch: master
Gerrit-Owner: Christopher Johnson (WMDE) <[email protected]>
Gerrit-Reviewer: Christopher Johnson (WMDE) <[email protected]>

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

Reply via email to