diff --git a/src/applications/maniphest/controller/ManiphestReportController.php b/src/applications/maniphest/controller/ManiphestReportController.php --- a/src/applications/maniphest/controller/ManiphestReportController.php +++ b/src/applications/maniphest/controller/ManiphestReportController.php @@ -92,295 +92,6 @@ $handle = $handles[$project_phid]; } - $xtable = new ManiphestTransaction(); - $conn = $xtable->establishConnection('r'); - - // Get legacy data: Querying the task transaction table is only needed for - // code before rPd321cc81 got merged on 2017-11-22. - if ($project_phid) { - $legacy_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); - } else { - $legacy_joins = qsprintf($conn, ''); - } - - $legacy_data = queryfx_all( - $conn, - 'SELECT x.transactionType, x.oldValue, x.newValue, x.dateCreated - FROM %T x %Q - WHERE transactionType IN (%Ls) - ORDER BY x.dateCreated ASC', - $xtable->getTableName(), - $legacy_joins, - array( - ManiphestTaskStatusTransaction::TRANSACTIONTYPE, - ManiphestTaskMergedIntoTransaction::TRANSACTIONTYPE, - )); - - // Remove any actual legacy status transactions which take status from - // `null` to any open status. - foreach ($legacy_data as $key => $row) { - if ($row['transactionType'] != 'status') { - continue; - } - - $oldv = trim($row['oldValue'], '"'); - $newv = trim($row['newValue'], '"'); - - // If this is a status change, preserve it. - if ($oldv != 'null') { - continue; - } - - // If this task was created directly into a closed status, preserve - // the transaction. - if (!ManiphestTaskStatus::isOpenStatus($newv)) { - continue; - } - - // If this is a legacy "create" transaction, discard it in favor of the - // synthetic transaction to be created below. - unset($legacy_data[$key]); - } - - // Since rPd321cc81, after the move to EditEngine, we no longer create a - // "status" transaction if a task is created directly into the default - // status. This likely impacted API/email tasks after 2016 and all other - // tasks after deploying the Phorge codebase from 2017-11-22. - // Until Facts can fix this properly, use the task creation dates to - // generate synthetic transactions which look like the older transactions - // that this page expects. - - $default_status = ManiphestTaskStatus::getDefaultStatus(); - $duplicate_status = ManiphestTaskStatus::getDuplicateStatus(); - - if ($project_phid) { - $synth_joins = qsprintf( - $conn, - 'JOIN %T p ON p.src = t.phid AND p.type = %d AND p.dst = %s', - PhabricatorEdgeConfig::TABLE_NAME_EDGE, - PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, - $project_phid); - } else { - $synth_joins = qsprintf($conn, ''); - } - - // Build synthetic transactions which take status from `null` to the - // default value. - $synth_data = queryfx_all( - $conn, - 'SELECT t.dateCreated FROM %T t %Q', - id(new ManiphestTask())->getTableName(), - $synth_joins); - foreach ($synth_data as $key => $synth_row) { - $synth_data[$key] = array( - 'transactionType' => 'status', - 'oldValue' => null, - 'newValue' => $default_status, - 'dateCreated' => $synth_row['dateCreated'], - ); - } - - // Merge the synthetic transactions into the legacy transactions. - $data = array_merge($synth_data, $legacy_data); - $data = array_values($data); - $data = isort($data, 'dateCreated'); - - $stats = array(); - $day_buckets = array(); - - $open_tasks = array(); - - foreach ($data as $key => $row) { - switch ($row['transactionType']) { - case ManiphestTaskStatusTransaction::TRANSACTIONTYPE: - // NOTE: Hack to avoid json_decode(). - $oldv = $row['oldValue']; - if ($oldv !== null) { - $oldv = trim($oldv, '"'); - } - $newv = trim($row['newValue'], '"'); - break; - case ManiphestTaskMergedIntoTransaction::TRANSACTIONTYPE: - // NOTE: Merging a task does not generate a "status" transaction. - // We pretend it did. Note that this is not always accurate: it is - // possible to merge a task which was previously closed, but this - // fake transaction always counts a merge as a closure. - $oldv = $default_status; - $newv = $duplicate_status; - break; - } - - 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'], - $viewer, - '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, - $viewer, - 'YW'); - if ($week_bucket != $last_week) { - if ($week) { - $rows[] = $this->formatBurnRow( - pht('Week of %s', phabricator_date($last_week_epoch, $viewer)), - $week); - $rowc[] = 'week'; - } - $week = $template; - $last_week = $week_bucket; - $last_week_epoch = $epoch; - } - - $month_bucket = phabricator_format_local_time( - $epoch, - $viewer, - 'Ym'); - if ($month_bucket != $last_month) { - if ($month) { - $rows[] = $this->formatBurnRow( - phabricator_format_local_time($last_month_epoch, $viewer, 'F, Y'), - $month); - $rowc[] = 'month'; - } - $month = $template; - $last_month = $month_bucket; - $last_month_epoch = $epoch; - } - - $rows[] = $this->formatBurnRow(phabricator_date($epoch, $viewer), $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( - 'right wide', - 'n', - 'n', - 'n', - )); - - 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. This table may not agree exactly with the chart '. - 'above.'); - $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 PHUIInfoView()) - ->appendChild($caption) - ->setSeverity(PHUIInfoView::SEVERITY_NOTICE); - } - - $panel = new PHUIObjectBoxView(); - $panel->setHeaderText($header); - if ($caption) { - $panel->setInfoView($caption); - } - $panel->setTable($table); - $tokens = array(); if ($handle) { $tokens = array($handle); @@ -388,20 +99,6 @@ $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); - if ($project_phid) { $projects = id(new PhabricatorProjectQuery()) ->setViewer($viewer) @@ -471,52 +168,8 @@ 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)); - } - - /** - * @param $label string Time representation for the row, e.g. "Feb 29 2024", - * "All Time", "Week of May 10 2024", "Month To Date", etc. - * @param $info array open|close; number of tasks in timespan - * @return array Row text label; number - * of open tasks as string; number of closed tasks as string; - * PhutilSafeHTML such as "+144" - */ - 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, - ); - } - /** - * @return int 50 + * @return int 50, the default value of the default "normal" Priority */ private function getAveragePriority() { // TODO: This is sort of a hard-code for the default "normal" status.