2012-02-08 18:47:14 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @group maniphest
|
|
|
|
*/
|
|
|
|
final class ManiphestReportController extends ManiphestController {
|
|
|
|
|
|
|
|
private $view;
|
|
|
|
|
|
|
|
public function willProcessRequest(array $data) {
|
|
|
|
$this->view = idx($data, 'view');
|
|
|
|
}
|
|
|
|
|
|
|
|
public function processRequest() {
|
|
|
|
$request = $this->getRequest();
|
|
|
|
$user = $request->getUser();
|
|
|
|
|
2012-03-01 23:19:11 +01:00
|
|
|
if ($request->isFormPost()) {
|
|
|
|
$uri = $request->getRequestURI();
|
|
|
|
|
|
|
|
$project = head($request->getArr('set_project'));
|
2012-03-22 00:58:52 +01:00
|
|
|
$project = nonempty($project, null);
|
2012-03-01 23:19:11 +01:00
|
|
|
$uri = $uri->alter('project', $project);
|
|
|
|
|
2012-03-22 00:58:52 +01:00
|
|
|
$window = $request->getStr('set_window');
|
|
|
|
$uri = $uri->alter('window', $window);
|
|
|
|
|
2012-03-01 23:19:11 +01:00
|
|
|
return id(new AphrontRedirectResponse())->setURI($uri);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$base_nav = $this->buildBaseSideNav();
|
|
|
|
$base_nav->selectFilter('report', 'report');
|
|
|
|
|
2012-02-08 18:47:14 +01:00
|
|
|
$nav = new AphrontSideNavFilterView();
|
|
|
|
$nav->setBaseURI(new PhutilURI('/maniphest/report/'));
|
2012-03-01 23:19:11 +01:00
|
|
|
$nav->addLabel('Open Tasks');
|
|
|
|
$nav->addFilter('user', 'By User');
|
|
|
|
$nav->addFilter('project', 'By Project');
|
|
|
|
$nav->addLabel('Burnup');
|
|
|
|
$nav->addFilter('burn', 'Burnup Rate');
|
2012-02-08 18:47:14 +01:00
|
|
|
|
|
|
|
$this->view = $nav->selectFilter($this->view, 'user');
|
|
|
|
|
2012-03-01 23:19:11 +01:00
|
|
|
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);
|
|
|
|
$base_nav->appendChild($nav);
|
|
|
|
|
|
|
|
return $this->buildStandardPageResponse(
|
|
|
|
$base_nav,
|
|
|
|
array(
|
|
|
|
'title' => 'Maniphest Reports',
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
public function renderBurn() {
|
|
|
|
$request = $this->getRequest();
|
|
|
|
$user = $request->getUser();
|
|
|
|
|
|
|
|
$handle = null;
|
|
|
|
|
|
|
|
$project_phid = $request->getStr('project');
|
|
|
|
if ($project_phid) {
|
|
|
|
$phids = array($project_phid);
|
2012-09-05 04:02:56 +02:00
|
|
|
$handles = $this->loadViewerHandles($phids);
|
2012-03-01 23:19:11 +01:00
|
|
|
$handle = $handles[$project_phid];
|
|
|
|
}
|
|
|
|
|
|
|
|
$table = new ManiphestTransaction();
|
|
|
|
$conn = $table->establishConnection('r');
|
|
|
|
|
|
|
|
$joins = '';
|
|
|
|
if ($project_phid) {
|
|
|
|
$joins = qsprintf(
|
|
|
|
$conn,
|
|
|
|
'JOIN %T t ON x.taskID = t.id
|
|
|
|
JOIN %T p ON p.taskPHID = t.phid AND p.projectPHID = %s',
|
|
|
|
id(new ManiphestTask())->getTableName(),
|
|
|
|
id(new ManiphestTaskProject())->getTableName(),
|
|
|
|
$project_phid);
|
|
|
|
}
|
|
|
|
|
|
|
|
$data = queryfx_all(
|
|
|
|
$conn,
|
2012-03-20 03:19:28 +01:00
|
|
|
'SELECT x.oldValue, x.newValue, x.dateCreated FROM %T x %Q
|
|
|
|
WHERE transactionType = %s
|
2012-03-01 23:19:11 +01:00
|
|
|
ORDER BY x.dateCreated ASC',
|
|
|
|
$table->getTableName(),
|
|
|
|
$joins,
|
|
|
|
ManiphestTransactionType::TYPE_STATUS);
|
|
|
|
|
|
|
|
$stats = array();
|
|
|
|
$day_buckets = array();
|
|
|
|
|
2012-03-20 03:19:28 +01:00
|
|
|
$open_tasks = array();
|
|
|
|
|
|
|
|
foreach ($data as $key => $row) {
|
|
|
|
|
|
|
|
// NOTE: Hack to avoid json_decode().
|
|
|
|
$oldv = trim($row['oldValue'], '"');
|
|
|
|
$newv = trim($row['newValue'], '"');
|
|
|
|
|
|
|
|
$old_is_open = ($oldv === (string)ManiphestTaskStatus::STATUS_OPEN);
|
|
|
|
$new_is_open = ($newv === (string)ManiphestTaskStatus::STATUS_OPEN);
|
|
|
|
|
|
|
|
$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;
|
|
|
|
}
|
|
|
|
|
2012-04-02 23:45:07 +02:00
|
|
|
$day_bucket = phabricator_format_local_time(
|
2012-03-01 23:19:11 +01:00
|
|
|
$row['dateCreated'],
|
|
|
|
$user,
|
2012-04-09 01:33:51 +02:00
|
|
|
'Yz');
|
2012-03-01 23:19:11 +01:00
|
|
|
$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;
|
|
|
|
|
Use head_key() and last_key() to explicitly communicate intent
Summary:
PHP arrays have an internal "current position" marker. (I think because foreach() wasn't introduced until PHP 4 and there was no way to get rid of it by then?)
A few functions affect the position of the marker, like reset(), end(), each(), next(), and prev(). A few functions read the position of the marker, like each(), next(), prev(), current() and key().
For the most part, no one uses any of this because foreach() is vastly easier and more natural. However, we sometimes want to select the first or last key from an array. Since key() returns the key //at the current position//, and you can't guarantee that no one will introduce some next() calls somewhere, the right way to do this is reset() + key(). This is cumbesome, so we introduced head_key() and last_key() (like head() and last()) in D2161.
Switch all the reset()/end() + key() (or omitted reset() since I was feeling like taking risks + key()) calls to head_key() or last_key().
Test Plan: Verified most of these by visiting the affected pages.
Reviewers: btrahan, vrana, jungejason, Koolvin
Reviewed By: jungejason
CC: aran
Differential Revision: https://secure.phabricator.com/D2169
2012-04-09 20:08:59 +02:00
|
|
|
$last = last_key($stats) - 1;
|
2012-03-01 23:19:11 +01:00
|
|
|
$period = $template;
|
|
|
|
|
|
|
|
foreach ($stats as $bucket => $info) {
|
|
|
|
$epoch = $day_buckets[$bucket];
|
|
|
|
|
2012-04-02 23:45:07 +02:00
|
|
|
$week_bucket = phabricator_format_local_time(
|
2012-03-01 23:19:11 +01:00
|
|
|
$epoch,
|
|
|
|
$user,
|
Use head_key() and last_key() to explicitly communicate intent
Summary:
PHP arrays have an internal "current position" marker. (I think because foreach() wasn't introduced until PHP 4 and there was no way to get rid of it by then?)
A few functions affect the position of the marker, like reset(), end(), each(), next(), and prev(). A few functions read the position of the marker, like each(), next(), prev(), current() and key().
For the most part, no one uses any of this because foreach() is vastly easier and more natural. However, we sometimes want to select the first or last key from an array. Since key() returns the key //at the current position//, and you can't guarantee that no one will introduce some next() calls somewhere, the right way to do this is reset() + key(). This is cumbesome, so we introduced head_key() and last_key() (like head() and last()) in D2161.
Switch all the reset()/end() + key() (or omitted reset() since I was feeling like taking risks + key()) calls to head_key() or last_key().
Test Plan: Verified most of these by visiting the affected pages.
Reviewers: btrahan, vrana, jungejason, Koolvin
Reviewed By: jungejason
CC: aran
Differential Revision: https://secure.phabricator.com/D2169
2012-04-09 20:08:59 +02:00
|
|
|
'YW');
|
2012-03-01 23:19:11 +01:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2012-04-02 23:45:07 +02:00
|
|
|
$month_bucket = phabricator_format_local_time(
|
2012-03-01 23:19:11 +01:00
|
|
|
$epoch,
|
|
|
|
$user,
|
Use head_key() and last_key() to explicitly communicate intent
Summary:
PHP arrays have an internal "current position" marker. (I think because foreach() wasn't introduced until PHP 4 and there was no way to get rid of it by then?)
A few functions affect the position of the marker, like reset(), end(), each(), next(), and prev(). A few functions read the position of the marker, like each(), next(), prev(), current() and key().
For the most part, no one uses any of this because foreach() is vastly easier and more natural. However, we sometimes want to select the first or last key from an array. Since key() returns the key //at the current position//, and you can't guarantee that no one will introduce some next() calls somewhere, the right way to do this is reset() + key(). This is cumbesome, so we introduced head_key() and last_key() (like head() and last()) in D2161.
Switch all the reset()/end() + key() (or omitted reset() since I was feeling like taking risks + key()) calls to head_key() or last_key().
Test Plan: Verified most of these by visiting the affected pages.
Reviewers: btrahan, vrana, jungejason, Koolvin
Reviewed By: jungejason
CC: aran
Differential Revision: https://secure.phabricator.com/D2169
2012-04-09 20:08:59 +02:00
|
|
|
'Ym');
|
2012-03-01 23:19:11 +01:00
|
|
|
if ($month_bucket != $last_month) {
|
|
|
|
if ($month) {
|
|
|
|
$rows[] = $this->formatBurnRow(
|
2012-04-02 23:45:07 +02:00
|
|
|
phabricator_format_local_time($last_month_epoch, $user, 'F, Y'),
|
2012-03-01 23:19:11 +01:00
|
|
|
$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(
|
|
|
|
'Week To Date',
|
|
|
|
$week);
|
|
|
|
$rowc[] = 'week';
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($month) {
|
|
|
|
$rows[] = $this->formatBurnRow(
|
|
|
|
'Month To Date',
|
|
|
|
$month);
|
|
|
|
$rowc[] = 'month';
|
|
|
|
}
|
|
|
|
|
|
|
|
$rows[] = $this->formatBurnRow(
|
|
|
|
'All Time',
|
|
|
|
$period);
|
|
|
|
$rowc[] = 'aggregate';
|
|
|
|
|
|
|
|
$rows = array_reverse($rows);
|
|
|
|
$rowc = array_reverse($rowc);
|
|
|
|
|
|
|
|
$table = new AphrontTableView($rows);
|
|
|
|
$table->setRowClasses($rowc);
|
|
|
|
$table->setHeaders(
|
|
|
|
array(
|
|
|
|
'Period',
|
|
|
|
'Opened',
|
|
|
|
'Closed',
|
|
|
|
'Change',
|
|
|
|
));
|
|
|
|
$table->setColumnClasses(
|
|
|
|
array(
|
|
|
|
'right wide',
|
|
|
|
'n',
|
|
|
|
'n',
|
|
|
|
'n',
|
|
|
|
));
|
|
|
|
|
|
|
|
if ($handle) {
|
2013-02-13 23:08:57 +01:00
|
|
|
$header = "Task Burn Rate for Project ".$handle->renderLink();
|
2013-02-05 22:23:05 +01:00
|
|
|
$caption = hsprintf(
|
|
|
|
"<p>NOTE: This table reflects tasks <em>currently</em> 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.</p>");
|
2012-03-01 23:19:11 +01:00
|
|
|
} else {
|
|
|
|
$header = "Task Burn Rate for All Tasks";
|
|
|
|
$caption = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
$panel = new AphrontPanelView();
|
|
|
|
$panel->setHeader($header);
|
|
|
|
$panel->setCaption($caption);
|
|
|
|
$panel->appendChild($table);
|
|
|
|
|
|
|
|
$tokens = array();
|
|
|
|
if ($handle) {
|
|
|
|
$tokens = array(
|
|
|
|
$handle->getPHID() => $handle->getFullName(),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2012-03-22 00:58:52 +01:00
|
|
|
$filter = $this->renderReportFilters($tokens, $has_window = false);
|
2012-03-01 23:19:11 +01:00
|
|
|
|
|
|
|
$id = celerity_generate_unique_node_id();
|
2013-01-18 03:57:09 +01:00
|
|
|
$chart = phutil_tag(
|
2012-03-01 23:19:11 +01:00
|
|
|
'div',
|
|
|
|
array(
|
|
|
|
'id' => $id,
|
|
|
|
'style' => 'border: 1px solid #6f6f6f; '.
|
|
|
|
'margin: 1em 2em; '.
|
|
|
|
'height: 400px; ',
|
|
|
|
),
|
|
|
|
'');
|
|
|
|
|
2012-03-20 03:19:28 +01:00
|
|
|
list($burn_x, $burn_y) = $this->buildSeries($data);
|
2012-03-01 23:19:11 +01:00
|
|
|
|
|
|
|
require_celerity_resource('raphael-core');
|
|
|
|
require_celerity_resource('raphael-g');
|
|
|
|
require_celerity_resource('raphael-g-line');
|
|
|
|
|
2012-07-30 19:44:08 +02:00
|
|
|
Javelin::initBehavior('line-chart', array(
|
2012-03-01 23:19:11 +01:00
|
|
|
'hardpoint' => $id,
|
|
|
|
'x' => array(
|
2012-03-20 03:19:28 +01:00
|
|
|
$burn_x,
|
2012-03-01 23:19:11 +01:00
|
|
|
),
|
|
|
|
'y' => array(
|
2012-03-20 03:19:28 +01:00
|
|
|
$burn_y,
|
2012-03-01 23:19:11 +01:00
|
|
|
),
|
2012-07-30 19:44:08 +02:00
|
|
|
'xformat' => 'epoch',
|
2012-03-01 23:19:11 +01:00
|
|
|
));
|
|
|
|
|
|
|
|
return array($filter, $chart, $panel);
|
|
|
|
}
|
|
|
|
|
2012-03-22 00:58:52 +01:00
|
|
|
private function renderReportFilters(array $tokens, $has_window) {
|
|
|
|
$request = $this->getRequest();
|
|
|
|
$user = $request->getUser();
|
|
|
|
|
|
|
|
$form = id(new AphrontFormView())
|
|
|
|
->setUser($user)
|
|
|
|
->appendChild(
|
|
|
|
id(new AphrontFormTokenizerControl())
|
|
|
|
->setDatasource('/typeahead/common/searchproject/')
|
|
|
|
->setLabel('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('"Recently" Means')
|
|
|
|
->setName('set_window')
|
|
|
|
->setCaption(
|
|
|
|
'Configure the cutoff for the "Recently Closed" column.')
|
|
|
|
->setValue($window_str)
|
|
|
|
->setError($window_error));
|
|
|
|
}
|
|
|
|
|
|
|
|
$form
|
|
|
|
->appendChild(
|
|
|
|
id(new AphrontFormSubmitControl())
|
|
|
|
->setValue('Filter By Project'));
|
|
|
|
|
|
|
|
$filter = new AphrontListFilterView();
|
|
|
|
$filter->appendChild($form);
|
|
|
|
|
|
|
|
return $filter;
|
|
|
|
}
|
|
|
|
|
2012-03-01 23:19:11 +01:00
|
|
|
private function buildSeries(array $data) {
|
2012-03-20 03:19:28 +01:00
|
|
|
$out = array();
|
2012-03-01 23:19:11 +01:00
|
|
|
|
2012-03-20 03:19:28 +01:00
|
|
|
$counter = 0;
|
2012-03-01 23:19:11 +01:00
|
|
|
foreach ($data as $row) {
|
|
|
|
$t = (int)$row['dateCreated'];
|
2012-03-20 03:19:28 +01:00
|
|
|
if ($row['_is_close']) {
|
|
|
|
--$counter;
|
|
|
|
$out[$t] = $counter;
|
|
|
|
} else if ($row['_is_open']) {
|
|
|
|
++$counter;
|
|
|
|
$out[$t] = $counter;
|
2012-03-01 23:19:11 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-03-20 03:19:28 +01:00
|
|
|
return array(array_keys($out), array_values($out));
|
2012-03-01 23:19:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private function formatBurnRow($label, $info) {
|
|
|
|
$delta = $info['open'] - $info['close'];
|
|
|
|
$fmt = number_format($delta);
|
|
|
|
if ($delta > 0) {
|
|
|
|
$fmt = '+'.$fmt;
|
2013-02-13 23:08:57 +01:00
|
|
|
$fmt = '<span class="red">'.$fmt.'</span>';
|
2012-03-01 23:19:11 +01:00
|
|
|
} else {
|
2013-02-13 23:08:57 +01:00
|
|
|
$fmt = '<span class="green">'.$fmt.'</span>';
|
2012-03-01 23:19:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return array(
|
|
|
|
$label,
|
|
|
|
number_format($info['open']),
|
|
|
|
number_format($info['close']),
|
|
|
|
$fmt);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function renderOpenTasks() {
|
|
|
|
$request = $this->getRequest();
|
|
|
|
$user = $request->getUser();
|
|
|
|
|
2012-03-22 00:58:52 +01:00
|
|
|
|
2012-03-01 23:19:11 +01:00
|
|
|
$query = id(new ManiphestTaskQuery())
|
|
|
|
->withStatus(ManiphestTaskQuery::STATUS_OPEN);
|
|
|
|
|
2012-03-22 00:58:52 +01:00
|
|
|
$project_phid = $request->getStr('project');
|
|
|
|
$project_handle = null;
|
|
|
|
if ($project_phid) {
|
|
|
|
$phids = array($project_phid);
|
2012-09-05 04:02:56 +02:00
|
|
|
$handles = $this->loadViewerHandles($phids);
|
2012-03-22 00:58:52 +01:00
|
|
|
$project_handle = $handles[$project_phid];
|
|
|
|
|
2012-10-05 00:31:04 +02:00
|
|
|
$query->withAnyProjects($phids);
|
2012-03-22 00:58:52 +01:00
|
|
|
}
|
|
|
|
|
2012-03-01 23:19:11 +01:00
|
|
|
$tasks = $query->execute();
|
2012-02-08 18:47:14 +01:00
|
|
|
|
2012-03-20 03:46:57 +01:00
|
|
|
$recently_closed = $this->loadRecentlyClosedTasks();
|
|
|
|
|
2012-02-08 18:47:14 +01:00
|
|
|
$date = phabricator_date(time(), $user);
|
|
|
|
|
|
|
|
switch ($this->view) {
|
|
|
|
case 'user':
|
|
|
|
$result = mgroup($tasks, 'getOwnerPHID');
|
|
|
|
$leftover = idx($result, '', array());
|
|
|
|
unset($result['']);
|
2012-03-20 03:46:57 +01:00
|
|
|
|
|
|
|
$result_closed = mgroup($recently_closed, 'getOwnerPHID');
|
|
|
|
$leftover_closed = idx($result_closed, '', array());
|
|
|
|
unset($result_closed['']);
|
|
|
|
|
2012-04-09 10:07:12 +02:00
|
|
|
$base_link = '/maniphest/?users=';
|
2013-01-18 03:57:09 +01:00
|
|
|
$leftover_name = phutil_tag(
|
2012-02-08 18:47:14 +01:00
|
|
|
'a',
|
|
|
|
array(
|
2012-04-09 10:07:12 +02:00
|
|
|
'href' => $base_link.ManiphestTaskOwner::OWNER_UP_FOR_GRABS,
|
2012-02-08 18:47:14 +01:00
|
|
|
),
|
2013-01-18 04:15:06 +01:00
|
|
|
phutil_tag('em', array(), '(Up For Grabs)'));
|
2012-02-08 18:47:14 +01:00
|
|
|
$col_header = 'User';
|
|
|
|
$header = 'Open Tasks by User and Priority ('.$date.')';
|
|
|
|
break;
|
|
|
|
case 'project':
|
|
|
|
$result = array();
|
2012-03-20 03:46:57 +01:00
|
|
|
$leftover = array();
|
2012-02-08 18:47:14 +01:00
|
|
|
foreach ($tasks as $task) {
|
|
|
|
$phids = $task->getProjectPHIDs();
|
|
|
|
if ($phids) {
|
|
|
|
foreach ($phids as $project_phid) {
|
|
|
|
$result[$project_phid][] = $task;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$leftover[] = $task;
|
|
|
|
}
|
|
|
|
}
|
2012-03-20 03:46:57 +01:00
|
|
|
|
|
|
|
$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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-04-09 10:07:12 +02:00
|
|
|
$base_link = '/maniphest/view/all/?projects=';
|
2013-01-18 03:57:09 +01:00
|
|
|
$leftover_name = phutil_tag(
|
2012-03-01 23:19:11 +01:00
|
|
|
'a',
|
|
|
|
array(
|
2012-04-09 10:07:12 +02:00
|
|
|
'href' => $base_link.ManiphestTaskOwner::PROJECT_NO_PROJECT,
|
2012-03-01 23:19:11 +01:00
|
|
|
),
|
2013-01-18 04:15:06 +01:00
|
|
|
phutil_tag('em', array(), '(No Project)'));
|
2012-02-08 18:47:14 +01:00
|
|
|
$col_header = 'Project';
|
|
|
|
$header = 'Open Tasks by Project and Priority ('.$date.')';
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
$phids = array_keys($result);
|
2012-09-05 04:02:56 +02:00
|
|
|
$handles = $this->loadViewerHandles($phids);
|
2012-02-08 18:47:14 +01:00
|
|
|
$handles = msort($handles, 'getName');
|
|
|
|
|
2012-03-01 23:19:11 +01:00
|
|
|
$order = $request->getStr('order', 'name');
|
2012-03-22 00:58:52 +01:00
|
|
|
list($order, $reverse) = AphrontTableView::parseSort($order);
|
2012-03-01 23:19:11 +01:00
|
|
|
|
2012-03-20 03:46:57 +01:00
|
|
|
require_celerity_resource('aphront-tooltip-css');
|
|
|
|
Javelin::initBehavior('phabricator-tooltips', array());
|
|
|
|
|
2012-02-08 18:47:14 +01:00
|
|
|
$rows = array();
|
|
|
|
$pri_total = array();
|
|
|
|
foreach (array_merge($handles, array(null)) as $handle) {
|
|
|
|
if ($handle) {
|
2012-03-22 00:58:52 +01:00
|
|
|
if (($project_handle) &&
|
|
|
|
($project_handle->getPHID() == $handle->getPHID())) {
|
|
|
|
// If filtering by, e.g., "bugs", don't show a "bugs" group.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2012-02-08 18:47:14 +01:00
|
|
|
$tasks = idx($result, $handle->getPHID(), array());
|
2013-01-18 03:43:35 +01:00
|
|
|
$name = phutil_tag(
|
2012-02-08 18:47:14 +01:00
|
|
|
'a',
|
|
|
|
array(
|
2012-03-23 19:08:10 +01:00
|
|
|
'href' => $base_link.$handle->getPHID(),
|
2012-02-08 18:47:14 +01:00
|
|
|
),
|
2013-01-18 03:43:35 +01:00
|
|
|
$handle->getName());
|
2012-03-20 03:46:57 +01:00
|
|
|
$closed = idx($result_closed, $handle->getPHID(), array());
|
2012-02-08 18:47:14 +01:00
|
|
|
} else {
|
|
|
|
$tasks = $leftover;
|
|
|
|
$name = $leftover_name;
|
2012-03-20 03:46:57 +01:00
|
|
|
$closed = $leftover_closed;
|
2012-02-08 18:47:14 +01:00
|
|
|
}
|
|
|
|
|
2012-03-20 03:46:57 +01:00
|
|
|
$taskv = $tasks;
|
2012-02-08 18:47:14 +01:00
|
|
|
$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);
|
|
|
|
|
2012-03-22 00:58:52 +01:00
|
|
|
list($link, $oldest_all) = $this->renderOldest($taskv);
|
|
|
|
$row[] = $link;
|
2012-03-20 03:46:57 +01:00
|
|
|
|
|
|
|
$normal_or_better = array();
|
|
|
|
foreach ($taskv as $id => $task) {
|
|
|
|
if ($task->getPriority() < ManiphestTaskPriority::PRIORITY_NORMAL) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
$normal_or_better[$id] = $task;
|
|
|
|
}
|
|
|
|
|
2012-03-22 00:58:52 +01:00
|
|
|
list($link, $oldest_pri) = $this->renderOldest($normal_or_better);
|
|
|
|
$row[] = $link;
|
2012-03-20 03:46:57 +01:00
|
|
|
|
|
|
|
if ($closed) {
|
|
|
|
$task_ids = implode(',', mpull($closed, 'getID'));
|
2013-01-18 03:43:35 +01:00
|
|
|
$row[] = phutil_tag(
|
2012-03-20 03:46:57 +01:00
|
|
|
'a',
|
|
|
|
array(
|
|
|
|
'href' => '/maniphest/view/custom/?s=oc&tasks='.$task_ids,
|
|
|
|
'target' => '_blank',
|
|
|
|
),
|
2013-01-18 03:43:35 +01:00
|
|
|
number_format(count($closed)));
|
2012-03-20 03:46:57 +01:00
|
|
|
} else {
|
|
|
|
$row[] = '-';
|
|
|
|
}
|
|
|
|
|
2012-03-01 23:19:11 +01:00
|
|
|
switch ($order) {
|
|
|
|
case 'total':
|
|
|
|
$row['sort'] = $total;
|
|
|
|
break;
|
2012-03-22 00:58:52 +01:00
|
|
|
case 'oldest-all':
|
|
|
|
$row['sort'] = $oldest_all;
|
|
|
|
break;
|
|
|
|
case 'oldest-pri':
|
|
|
|
$row['sort'] = $oldest_pri;
|
|
|
|
break;
|
|
|
|
case 'closed':
|
|
|
|
$row['sort'] = count($closed);
|
|
|
|
break;
|
2012-03-01 23:19:11 +01:00
|
|
|
case 'name':
|
|
|
|
default:
|
|
|
|
$row['sort'] = $handle ? $handle->getName() : '~';
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2012-02-08 18:47:14 +01:00
|
|
|
$rows[] = $row;
|
|
|
|
}
|
|
|
|
|
2012-03-01 23:19:11 +01:00
|
|
|
$rows = isort($rows, 'sort');
|
|
|
|
foreach ($rows as $k => $row) {
|
|
|
|
unset($rows[$k]['sort']);
|
|
|
|
}
|
2012-03-22 00:58:52 +01:00
|
|
|
if ($reverse) {
|
|
|
|
$rows = array_reverse($rows);
|
|
|
|
}
|
2012-03-01 23:19:11 +01:00
|
|
|
|
2012-02-08 18:47:14 +01:00
|
|
|
$cname = array($col_header);
|
|
|
|
$cclass = array('pri right wide');
|
2012-03-20 03:46:57 +01:00
|
|
|
$pri_map = ManiphestTaskPriority::getTaskBriefPriorityMap();
|
|
|
|
foreach ($pri_map as $pri => $label) {
|
2012-02-08 18:47:14 +01:00
|
|
|
$cname[] = $label;
|
|
|
|
$cclass[] = 'n';
|
|
|
|
}
|
|
|
|
$cname[] = 'Total';
|
|
|
|
$cclass[] = 'n';
|
2013-01-25 21:57:17 +01:00
|
|
|
$cname[] = javelin_tag(
|
2012-03-20 03:46:57 +01:00
|
|
|
'span',
|
|
|
|
array(
|
|
|
|
'sigil' => 'has-tooltip',
|
|
|
|
'meta' => array(
|
|
|
|
'tip' => 'Oldest open task.',
|
|
|
|
'size' => 200,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
'Oldest (All)');
|
|
|
|
$cclass[] = 'n';
|
2013-01-25 21:57:17 +01:00
|
|
|
$cname[] = javelin_tag(
|
2012-03-20 03:46:57 +01:00
|
|
|
'span',
|
|
|
|
array(
|
|
|
|
'sigil' => 'has-tooltip',
|
|
|
|
'meta' => array(
|
|
|
|
'tip' => 'Oldest open task, excluding those with Low or Wishlist '.
|
|
|
|
'priority.',
|
|
|
|
'size' => 200,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
'Oldest (Pri)');
|
|
|
|
$cclass[] = 'n';
|
2012-03-22 00:58:52 +01:00
|
|
|
|
|
|
|
list($ignored, $window_epoch) = $this->getWindow();
|
2013-01-25 21:57:17 +01:00
|
|
|
$cname[] = javelin_tag(
|
2012-03-22 00:58:52 +01:00
|
|
|
'span',
|
|
|
|
array(
|
|
|
|
'sigil' => 'has-tooltip',
|
|
|
|
'meta' => array(
|
|
|
|
'tip' => 'Closed after '.phabricator_datetime($window_epoch, $user),
|
|
|
|
'size' => 260
|
|
|
|
),
|
|
|
|
),
|
|
|
|
'Recently Closed');
|
2012-03-20 03:46:57 +01:00
|
|
|
$cclass[] = 'n';
|
2012-02-08 18:47:14 +01:00
|
|
|
|
|
|
|
$table = new AphrontTableView($rows);
|
|
|
|
$table->setHeaders($cname);
|
|
|
|
$table->setColumnClasses($cclass);
|
2012-03-22 00:58:52 +01:00
|
|
|
$table->makeSortable(
|
|
|
|
$request->getRequestURI(),
|
|
|
|
'order',
|
|
|
|
$order,
|
|
|
|
$reverse,
|
|
|
|
array(
|
|
|
|
'name',
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
'total',
|
|
|
|
'oldest-all',
|
|
|
|
'oldest-pri',
|
|
|
|
'closed',
|
|
|
|
));
|
2012-02-08 18:47:14 +01:00
|
|
|
|
|
|
|
$panel = new AphrontPanelView();
|
|
|
|
$panel->setHeader($header);
|
|
|
|
$panel->appendChild($table);
|
|
|
|
|
2012-03-22 00:58:52 +01:00
|
|
|
$tokens = array();
|
|
|
|
if ($project_handle) {
|
|
|
|
$tokens = array(
|
|
|
|
$project_handle->getPHID() => $project_handle->getFullName(),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
$filter = $this->renderReportFilters($tokens, $has_window = true);
|
2012-03-01 23:19:11 +01:00
|
|
|
|
|
|
|
return array($filter, $panel);
|
2012-02-08 18:47:14 +01:00
|
|
|
}
|
|
|
|
|
2012-03-20 03:46:57 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Load all the tasks that have been recently closed.
|
|
|
|
*/
|
|
|
|
private function loadRecentlyClosedTasks() {
|
2012-03-22 00:58:52 +01:00
|
|
|
list($ignored, $window_epoch) = $this->getWindow();
|
2012-03-20 03:46:57 +01:00
|
|
|
|
|
|
|
$table = new ManiphestTask();
|
|
|
|
$xtable = new ManiphestTransaction();
|
|
|
|
$conn_r = $table->establishConnection('r');
|
|
|
|
|
|
|
|
$tasks = queryfx_all(
|
|
|
|
$conn_r,
|
|
|
|
'SELECT t.* FROM %T t JOIN %T x ON x.taskID = t.id
|
|
|
|
WHERE t.status != 0
|
|
|
|
AND x.oldValue IN (null, %s, %s)
|
|
|
|
AND x.newValue NOT IN (%s, %s)
|
|
|
|
AND t.dateModified >= %d
|
|
|
|
AND x.dateCreated >= %d',
|
|
|
|
$table->getTableName(),
|
|
|
|
$xtable->getTableName(),
|
|
|
|
|
|
|
|
// TODO: Gross. This table is not meant to be queried like this. Build
|
|
|
|
// real stats tables.
|
|
|
|
json_encode((int)ManiphestTaskStatus::STATUS_OPEN),
|
|
|
|
json_encode((string)ManiphestTaskStatus::STATUS_OPEN),
|
|
|
|
json_encode((int)ManiphestTaskStatus::STATUS_OPEN),
|
|
|
|
json_encode((string)ManiphestTaskStatus::STATUS_OPEN),
|
|
|
|
|
2012-03-22 00:58:52 +01:00
|
|
|
$window_epoch,
|
|
|
|
$window_epoch);
|
2012-03-20 03:46:57 +01:00
|
|
|
|
|
|
|
return id(new ManiphestTask())->loadAllFromArray($tasks);
|
|
|
|
}
|
|
|
|
|
2012-03-22 00:58:52 +01:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
|
|
|
|
$timezone = new DateTimeZone($user->getTimezoneIdentifier());
|
|
|
|
try {
|
|
|
|
$date = new DateTime($window_str, $timezone);
|
|
|
|
$window_epoch = $date->format('U');
|
|
|
|
} catch (Exception $e) {
|
|
|
|
$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);
|
|
|
|
}
|
|
|
|
|
2012-03-20 03:46:57 +01:00
|
|
|
private function renderOldest(array $tasks) {
|
2012-04-03 21:10:45 +02:00
|
|
|
assert_instances_of($tasks, 'ManiphestTask');
|
2012-03-20 03:46:57 +01:00
|
|
|
$oldest = null;
|
|
|
|
foreach ($tasks as $id => $task) {
|
|
|
|
if (($oldest === null) ||
|
|
|
|
($task->getDateCreated() < $tasks[$oldest]->getDateCreated())) {
|
|
|
|
$oldest = $id;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($oldest === null) {
|
2012-03-22 21:55:22 +01:00
|
|
|
return array('-', 0);
|
2012-03-20 03:46:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$oldest = $tasks[$oldest];
|
|
|
|
|
2012-03-22 00:58:52 +01:00
|
|
|
$raw_age = (time() - $oldest->getDateCreated());
|
|
|
|
$age = number_format($raw_age / (24 * 60 * 60)).' d';
|
2012-03-20 03:46:57 +01:00
|
|
|
|
2013-01-25 21:57:17 +01:00
|
|
|
$link = javelin_tag(
|
2012-03-20 03:46:57 +01:00
|
|
|
'a',
|
|
|
|
array(
|
|
|
|
'href' => '/T'.$oldest->getID(),
|
|
|
|
'sigil' => 'has-tooltip',
|
|
|
|
'meta' => array(
|
|
|
|
'tip' => 'T'.$oldest->getID().': '.$oldest->getTitle(),
|
|
|
|
),
|
|
|
|
'target' => '_blank',
|
|
|
|
),
|
2013-01-25 21:57:17 +01:00
|
|
|
$age);
|
2012-03-22 00:58:52 +01:00
|
|
|
|
|
|
|
return array($link, $raw_age);
|
2012-03-20 03:46:57 +01:00
|
|
|
}
|
|
|
|
|
2012-02-08 18:47:14 +01:00
|
|
|
}
|