Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/maniphest/controller/ManiphestReportController.php
12256 views
1
<?php
2
3
final class ManiphestReportController extends ManiphestController {
4
5
private $view;
6
7
public function handleRequest(AphrontRequest $request) {
8
$viewer = $this->getViewer();
9
$this->view = $request->getURIData('view');
10
11
if ($request->isFormPost()) {
12
$uri = $request->getRequestURI();
13
14
$project = head($request->getArr('set_project'));
15
$project = nonempty($project, null);
16
17
if ($project !== null) {
18
$uri->replaceQueryParam('project', $project);
19
} else {
20
$uri->removeQueryParam('project');
21
}
22
23
$window = $request->getStr('set_window');
24
if ($window !== null) {
25
$uri->replaceQueryParam('window', $window);
26
} else {
27
$uri->removeQueryParam('window');
28
}
29
30
return id(new AphrontRedirectResponse())->setURI($uri);
31
}
32
33
$nav = new AphrontSideNavFilterView();
34
$nav->setBaseURI(new PhutilURI('/maniphest/report/'));
35
$nav->addLabel(pht('Open Tasks'));
36
$nav->addFilter('user', pht('By User'));
37
$nav->addFilter('project', pht('By Project'));
38
39
$class = 'PhabricatorFactApplication';
40
if (PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) {
41
$nav->addLabel(pht('Burnup'));
42
$nav->addFilter('burn', pht('Burnup Rate'));
43
}
44
45
$this->view = $nav->selectFilter($this->view, 'user');
46
47
require_celerity_resource('maniphest-report-css');
48
49
switch ($this->view) {
50
case 'burn':
51
$core = $this->renderBurn();
52
break;
53
case 'user':
54
case 'project':
55
$core = $this->renderOpenTasks();
56
break;
57
default:
58
return new Aphront404Response();
59
}
60
61
$crumbs = $this->buildApplicationCrumbs()
62
->addTextCrumb(pht('Reports'));
63
64
$nav->appendChild($core);
65
$title = pht('Maniphest Reports');
66
67
return $this->newPage()
68
->setTitle($title)
69
->setCrumbs($crumbs)
70
->setNavigation($nav);
71
72
}
73
74
public function renderBurn() {
75
$request = $this->getRequest();
76
$viewer = $request->getUser();
77
78
$handle = null;
79
80
$project_phid = $request->getStr('project');
81
if ($project_phid) {
82
$phids = array($project_phid);
83
$handles = $this->loadViewerHandles($phids);
84
$handle = $handles[$project_phid];
85
}
86
87
$table = new ManiphestTransaction();
88
$conn = $table->establishConnection('r');
89
90
if ($project_phid) {
91
$joins = qsprintf(
92
$conn,
93
'JOIN %T t ON x.objectPHID = t.phid
94
JOIN %T p ON p.src = t.phid AND p.type = %d AND p.dst = %s',
95
id(new ManiphestTask())->getTableName(),
96
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
97
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
98
$project_phid);
99
$create_joins = qsprintf(
100
$conn,
101
'JOIN %T p ON p.src = t.phid AND p.type = %d AND p.dst = %s',
102
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
103
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
104
$project_phid);
105
} else {
106
$joins = qsprintf($conn, '');
107
$create_joins = qsprintf($conn, '');
108
}
109
110
$data = queryfx_all(
111
$conn,
112
'SELECT x.transactionType, x.oldValue, x.newValue, x.dateCreated
113
FROM %T x %Q
114
WHERE transactionType IN (%Ls)
115
ORDER BY x.dateCreated ASC',
116
$table->getTableName(),
117
$joins,
118
array(
119
ManiphestTaskStatusTransaction::TRANSACTIONTYPE,
120
ManiphestTaskMergedIntoTransaction::TRANSACTIONTYPE,
121
));
122
123
// See PHI273. After the move to EditEngine, we no longer create a
124
// "status" transaction if a task is created directly into the default
125
// status. This likely impacted API/email tasks after 2016 and all other
126
// tasks after late 2017. Until Facts can fix this properly, use the
127
// task creation dates to generate synthetic transactions which look like
128
// the older transactions that this page expects.
129
130
$default_status = ManiphestTaskStatus::getDefaultStatus();
131
$duplicate_status = ManiphestTaskStatus::getDuplicateStatus();
132
133
// Build synthetic transactions which take status from `null` to the
134
// default value.
135
$create_rows = queryfx_all(
136
$conn,
137
'SELECT t.dateCreated FROM %T t %Q',
138
id(new ManiphestTask())->getTableName(),
139
$create_joins);
140
foreach ($create_rows as $key => $create_row) {
141
$create_rows[$key] = array(
142
'transactionType' => 'status',
143
'oldValue' => null,
144
'newValue' => $default_status,
145
'dateCreated' => $create_row['dateCreated'],
146
);
147
}
148
149
// Remove any actual legacy status transactions which take status from
150
// `null` to any open status.
151
foreach ($data as $key => $row) {
152
if ($row['transactionType'] != 'status') {
153
continue;
154
}
155
156
$oldv = trim($row['oldValue'], '"');
157
$newv = trim($row['newValue'], '"');
158
159
// If this is a status change, preserve it.
160
if ($oldv != 'null') {
161
continue;
162
}
163
164
// If this task was created directly into a closed status, preserve
165
// the transaction.
166
if (!ManiphestTaskStatus::isOpenStatus($newv)) {
167
continue;
168
}
169
170
// If this is a legacy "create" transaction, discard it in favor of the
171
// synthetic one.
172
unset($data[$key]);
173
}
174
175
// Merge the synthetic rows into the real transactions.
176
$data = array_merge($create_rows, $data);
177
$data = array_values($data);
178
$data = isort($data, 'dateCreated');
179
180
$stats = array();
181
$day_buckets = array();
182
183
$open_tasks = array();
184
185
foreach ($data as $key => $row) {
186
switch ($row['transactionType']) {
187
case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
188
// NOTE: Hack to avoid json_decode().
189
$oldv = trim($row['oldValue'], '"');
190
$newv = trim($row['newValue'], '"');
191
break;
192
case ManiphestTaskMergedIntoTransaction::TRANSACTIONTYPE:
193
// NOTE: Merging a task does not generate a "status" transaction.
194
// We pretend it did. Note that this is not always accurate: it is
195
// possible to merge a task which was previously closed, but this
196
// fake transaction always counts a merge as a closure.
197
$oldv = $default_status;
198
$newv = $duplicate_status;
199
break;
200
}
201
202
if ($oldv == 'null') {
203
$old_is_open = false;
204
} else {
205
$old_is_open = ManiphestTaskStatus::isOpenStatus($oldv);
206
}
207
208
$new_is_open = ManiphestTaskStatus::isOpenStatus($newv);
209
210
$is_open = ($new_is_open && !$old_is_open);
211
$is_close = ($old_is_open && !$new_is_open);
212
213
$data[$key]['_is_open'] = $is_open;
214
$data[$key]['_is_close'] = $is_close;
215
216
if (!$is_open && !$is_close) {
217
// This is either some kind of bogus event, or a resolution change
218
// (e.g., resolved -> invalid). Just skip it.
219
continue;
220
}
221
222
$day_bucket = phabricator_format_local_time(
223
$row['dateCreated'],
224
$viewer,
225
'Yz');
226
$day_buckets[$day_bucket] = $row['dateCreated'];
227
if (empty($stats[$day_bucket])) {
228
$stats[$day_bucket] = array(
229
'open' => 0,
230
'close' => 0,
231
);
232
}
233
$stats[$day_bucket][$is_close ? 'close' : 'open']++;
234
}
235
236
$template = array(
237
'open' => 0,
238
'close' => 0,
239
);
240
241
$rows = array();
242
$rowc = array();
243
$last_month = null;
244
$last_month_epoch = null;
245
$last_week = null;
246
$last_week_epoch = null;
247
$week = null;
248
$month = null;
249
250
$last = last_key($stats) - 1;
251
$period = $template;
252
253
foreach ($stats as $bucket => $info) {
254
$epoch = $day_buckets[$bucket];
255
256
$week_bucket = phabricator_format_local_time(
257
$epoch,
258
$viewer,
259
'YW');
260
if ($week_bucket != $last_week) {
261
if ($week) {
262
$rows[] = $this->formatBurnRow(
263
pht('Week of %s', phabricator_date($last_week_epoch, $viewer)),
264
$week);
265
$rowc[] = 'week';
266
}
267
$week = $template;
268
$last_week = $week_bucket;
269
$last_week_epoch = $epoch;
270
}
271
272
$month_bucket = phabricator_format_local_time(
273
$epoch,
274
$viewer,
275
'Ym');
276
if ($month_bucket != $last_month) {
277
if ($month) {
278
$rows[] = $this->formatBurnRow(
279
phabricator_format_local_time($last_month_epoch, $viewer, 'F, Y'),
280
$month);
281
$rowc[] = 'month';
282
}
283
$month = $template;
284
$last_month = $month_bucket;
285
$last_month_epoch = $epoch;
286
}
287
288
$rows[] = $this->formatBurnRow(phabricator_date($epoch, $viewer), $info);
289
$rowc[] = null;
290
$week['open'] += $info['open'];
291
$week['close'] += $info['close'];
292
$month['open'] += $info['open'];
293
$month['close'] += $info['close'];
294
$period['open'] += $info['open'];
295
$period['close'] += $info['close'];
296
}
297
298
if ($week) {
299
$rows[] = $this->formatBurnRow(
300
pht('Week To Date'),
301
$week);
302
$rowc[] = 'week';
303
}
304
305
if ($month) {
306
$rows[] = $this->formatBurnRow(
307
pht('Month To Date'),
308
$month);
309
$rowc[] = 'month';
310
}
311
312
$rows[] = $this->formatBurnRow(
313
pht('All Time'),
314
$period);
315
$rowc[] = 'aggregate';
316
317
$rows = array_reverse($rows);
318
$rowc = array_reverse($rowc);
319
320
$table = new AphrontTableView($rows);
321
$table->setRowClasses($rowc);
322
$table->setHeaders(
323
array(
324
pht('Period'),
325
pht('Opened'),
326
pht('Closed'),
327
pht('Change'),
328
));
329
$table->setColumnClasses(
330
array(
331
'right wide',
332
'n',
333
'n',
334
'n',
335
));
336
337
if ($handle) {
338
$inst = pht(
339
'NOTE: This table reflects tasks currently in '.
340
'the project. If a task was opened in the past but added to '.
341
'the project recently, it is counted on the day it was '.
342
'opened, not the day it was categorized. If a task was part '.
343
'of this project in the past but no longer is, it is not '.
344
'counted at all. This table may not agree exactly with the chart '.
345
'above.');
346
$header = pht('Task Burn Rate for Project %s', $handle->renderLink());
347
$caption = phutil_tag('p', array(), $inst);
348
} else {
349
$header = pht('Task Burn Rate for All Tasks');
350
$caption = null;
351
}
352
353
if ($caption) {
354
$caption = id(new PHUIInfoView())
355
->appendChild($caption)
356
->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
357
}
358
359
$panel = new PHUIObjectBoxView();
360
$panel->setHeaderText($header);
361
if ($caption) {
362
$panel->setInfoView($caption);
363
}
364
$panel->setTable($table);
365
366
$tokens = array();
367
if ($handle) {
368
$tokens = array($handle);
369
}
370
371
$filter = $this->renderReportFilters($tokens, $has_window = false);
372
373
$id = celerity_generate_unique_node_id();
374
$chart = phutil_tag(
375
'div',
376
array(
377
'id' => $id,
378
'style' => 'border: 1px solid #BFCFDA; '.
379
'background-color: #fff; '.
380
'margin: 8px 16px; '.
381
'height: 400px; ',
382
),
383
'');
384
385
list($burn_x, $burn_y) = $this->buildSeries($data);
386
387
if ($project_phid) {
388
$projects = id(new PhabricatorProjectQuery())
389
->setViewer($viewer)
390
->withPHIDs(array($project_phid))
391
->execute();
392
} else {
393
$projects = array();
394
}
395
396
$panel = id(new PhabricatorProjectBurndownChartEngine())
397
->setViewer($viewer)
398
->setProjects($projects)
399
->buildChartPanel();
400
401
$panel->setName(pht('Burnup Rate'));
402
403
$chart_view = id(new PhabricatorDashboardPanelRenderingEngine())
404
->setViewer($viewer)
405
->setPanel($panel)
406
->setParentPanelPHIDs(array())
407
->renderPanel();
408
409
return array($filter, $chart_view);
410
}
411
412
private function renderReportFilters(array $tokens, $has_window) {
413
$request = $this->getRequest();
414
$viewer = $request->getUser();
415
416
$form = id(new AphrontFormView())
417
->setUser($viewer)
418
->appendControl(
419
id(new AphrontFormTokenizerControl())
420
->setDatasource(new PhabricatorProjectDatasource())
421
->setLabel(pht('Project'))
422
->setLimit(1)
423
->setName('set_project')
424
// TODO: This is silly, but this is Maniphest reports.
425
->setValue(mpull($tokens, 'getPHID')));
426
427
if ($has_window) {
428
list($window_str, $ignored, $window_error) = $this->getWindow();
429
$form
430
->appendChild(
431
id(new AphrontFormTextControl())
432
->setLabel(pht('Recently Means'))
433
->setName('set_window')
434
->setCaption(
435
pht('Configure the cutoff for the "Recently Closed" column.'))
436
->setValue($window_str)
437
->setError($window_error));
438
}
439
440
$form
441
->appendChild(
442
id(new AphrontFormSubmitControl())
443
->setValue(pht('Filter By Project')));
444
445
$filter = new AphrontListFilterView();
446
$filter->appendChild($form);
447
448
return $filter;
449
}
450
451
private function buildSeries(array $data) {
452
$out = array();
453
454
$counter = 0;
455
foreach ($data as $row) {
456
$t = (int)$row['dateCreated'];
457
if ($row['_is_close']) {
458
--$counter;
459
$out[$t] = $counter;
460
} else if ($row['_is_open']) {
461
++$counter;
462
$out[$t] = $counter;
463
}
464
}
465
466
return array(array_keys($out), array_values($out));
467
}
468
469
private function formatBurnRow($label, $info) {
470
$delta = $info['open'] - $info['close'];
471
$fmt = number_format($delta);
472
if ($delta > 0) {
473
$fmt = '+'.$fmt;
474
$fmt = phutil_tag('span', array('class' => 'red'), $fmt);
475
} else {
476
$fmt = phutil_tag('span', array('class' => 'green'), $fmt);
477
}
478
479
return array(
480
$label,
481
number_format($info['open']),
482
number_format($info['close']),
483
$fmt,
484
);
485
}
486
487
public function renderOpenTasks() {
488
$request = $this->getRequest();
489
$viewer = $request->getUser();
490
491
492
$query = id(new ManiphestTaskQuery())
493
->setViewer($viewer)
494
->withStatuses(ManiphestTaskStatus::getOpenStatusConstants());
495
496
switch ($this->view) {
497
case 'project':
498
$query->needProjectPHIDs(true);
499
break;
500
}
501
502
$project_phid = $request->getStr('project');
503
$project_handle = null;
504
if ($project_phid) {
505
$phids = array($project_phid);
506
$handles = $this->loadViewerHandles($phids);
507
$project_handle = $handles[$project_phid];
508
509
$query->withEdgeLogicPHIDs(
510
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
511
PhabricatorQueryConstraint::OPERATOR_OR,
512
$phids);
513
}
514
515
$tasks = $query->execute();
516
517
$recently_closed = $this->loadRecentlyClosedTasks();
518
519
$date = phabricator_date(time(), $viewer);
520
521
switch ($this->view) {
522
case 'user':
523
$result = mgroup($tasks, 'getOwnerPHID');
524
$leftover = idx($result, '', array());
525
unset($result['']);
526
527
$result_closed = mgroup($recently_closed, 'getOwnerPHID');
528
$leftover_closed = idx($result_closed, '', array());
529
unset($result_closed['']);
530
531
$base_link = '/maniphest/?assigned=';
532
$leftover_name = phutil_tag('em', array(), pht('(Up For Grabs)'));
533
$col_header = pht('User');
534
$header = pht('Open Tasks by User and Priority (%s)', $date);
535
break;
536
case 'project':
537
$result = array();
538
$leftover = array();
539
foreach ($tasks as $task) {
540
$phids = $task->getProjectPHIDs();
541
if ($phids) {
542
foreach ($phids as $project_phid) {
543
$result[$project_phid][] = $task;
544
}
545
} else {
546
$leftover[] = $task;
547
}
548
}
549
550
$result_closed = array();
551
$leftover_closed = array();
552
foreach ($recently_closed as $task) {
553
$phids = $task->getProjectPHIDs();
554
if ($phids) {
555
foreach ($phids as $project_phid) {
556
$result_closed[$project_phid][] = $task;
557
}
558
} else {
559
$leftover_closed[] = $task;
560
}
561
}
562
563
$base_link = '/maniphest/?projects=';
564
$leftover_name = phutil_tag('em', array(), pht('(No Project)'));
565
$col_header = pht('Project');
566
$header = pht('Open Tasks by Project and Priority (%s)', $date);
567
break;
568
}
569
570
$phids = array_keys($result);
571
$handles = $this->loadViewerHandles($phids);
572
$handles = msort($handles, 'getName');
573
574
$order = $request->getStr('order', 'name');
575
list($order, $reverse) = AphrontTableView::parseSort($order);
576
577
require_celerity_resource('aphront-tooltip-css');
578
Javelin::initBehavior('phabricator-tooltips', array());
579
580
$rows = array();
581
$pri_total = array();
582
foreach (array_merge($handles, array(null)) as $handle) {
583
if ($handle) {
584
if (($project_handle) &&
585
($project_handle->getPHID() == $handle->getPHID())) {
586
// If filtering by, e.g., "bugs", don't show a "bugs" group.
587
continue;
588
}
589
590
$tasks = idx($result, $handle->getPHID(), array());
591
$name = phutil_tag(
592
'a',
593
array(
594
'href' => $base_link.$handle->getPHID(),
595
),
596
$handle->getName());
597
$closed = idx($result_closed, $handle->getPHID(), array());
598
} else {
599
$tasks = $leftover;
600
$name = $leftover_name;
601
$closed = $leftover_closed;
602
}
603
604
$taskv = $tasks;
605
$tasks = mgroup($tasks, 'getPriority');
606
607
$row = array();
608
$row[] = $name;
609
$total = 0;
610
foreach (ManiphestTaskPriority::getTaskPriorityMap() as $pri => $label) {
611
$n = count(idx($tasks, $pri, array()));
612
if ($n == 0) {
613
$row[] = '-';
614
} else {
615
$row[] = number_format($n);
616
}
617
$total += $n;
618
}
619
$row[] = number_format($total);
620
621
list($link, $oldest_all) = $this->renderOldest($taskv);
622
$row[] = $link;
623
624
$normal_or_better = array();
625
foreach ($taskv as $id => $task) {
626
// TODO: This is sort of a hard-code for the default "normal" status.
627
// When reports are more powerful, this should be made more general.
628
if ($task->getPriority() < 50) {
629
continue;
630
}
631
$normal_or_better[$id] = $task;
632
}
633
634
list($link, $oldest_pri) = $this->renderOldest($normal_or_better);
635
$row[] = $link;
636
637
if ($closed) {
638
$task_ids = implode(',', mpull($closed, 'getID'));
639
$row[] = phutil_tag(
640
'a',
641
array(
642
'href' => '/maniphest/?ids='.$task_ids,
643
'target' => '_blank',
644
),
645
number_format(count($closed)));
646
} else {
647
$row[] = '-';
648
}
649
650
switch ($order) {
651
case 'total':
652
$row['sort'] = $total;
653
break;
654
case 'oldest-all':
655
$row['sort'] = $oldest_all;
656
break;
657
case 'oldest-pri':
658
$row['sort'] = $oldest_pri;
659
break;
660
case 'closed':
661
$row['sort'] = count($closed);
662
break;
663
case 'name':
664
default:
665
$row['sort'] = $handle ? $handle->getName() : '~';
666
break;
667
}
668
669
$rows[] = $row;
670
}
671
672
$rows = isort($rows, 'sort');
673
foreach ($rows as $k => $row) {
674
unset($rows[$k]['sort']);
675
}
676
if ($reverse) {
677
$rows = array_reverse($rows);
678
}
679
680
$cname = array($col_header);
681
$cclass = array('pri right wide');
682
$pri_map = ManiphestTaskPriority::getShortNameMap();
683
foreach ($pri_map as $pri => $label) {
684
$cname[] = $label;
685
$cclass[] = 'n';
686
}
687
$cname[] = pht('Total');
688
$cclass[] = 'n';
689
$cname[] = javelin_tag(
690
'span',
691
array(
692
'sigil' => 'has-tooltip',
693
'meta' => array(
694
'tip' => pht('Oldest open task.'),
695
'size' => 200,
696
),
697
),
698
pht('Oldest (All)'));
699
$cclass[] = 'n';
700
$cname[] = javelin_tag(
701
'span',
702
array(
703
'sigil' => 'has-tooltip',
704
'meta' => array(
705
'tip' => pht(
706
'Oldest open task, excluding those with Low or Wishlist priority.'),
707
'size' => 200,
708
),
709
),
710
pht('Oldest (Pri)'));
711
$cclass[] = 'n';
712
713
list($ignored, $window_epoch) = $this->getWindow();
714
$edate = phabricator_datetime($window_epoch, $viewer);
715
$cname[] = javelin_tag(
716
'span',
717
array(
718
'sigil' => 'has-tooltip',
719
'meta' => array(
720
'tip' => pht('Closed after %s', $edate),
721
'size' => 260,
722
),
723
),
724
pht('Recently Closed'));
725
$cclass[] = 'n';
726
727
$table = new AphrontTableView($rows);
728
$table->setHeaders($cname);
729
$table->setColumnClasses($cclass);
730
$table->makeSortable(
731
$request->getRequestURI(),
732
'order',
733
$order,
734
$reverse,
735
array(
736
'name',
737
null,
738
null,
739
null,
740
null,
741
null,
742
null,
743
'total',
744
'oldest-all',
745
'oldest-pri',
746
'closed',
747
));
748
749
$panel = new PHUIObjectBoxView();
750
$panel->setHeaderText($header);
751
$panel->setTable($table);
752
753
$tokens = array();
754
if ($project_handle) {
755
$tokens = array($project_handle);
756
}
757
$filter = $this->renderReportFilters($tokens, $has_window = true);
758
759
return array($filter, $panel);
760
}
761
762
763
/**
764
* Load all the tasks that have been recently closed.
765
*/
766
private function loadRecentlyClosedTasks() {
767
list($ignored, $window_epoch) = $this->getWindow();
768
769
$table = new ManiphestTask();
770
$xtable = new ManiphestTransaction();
771
$conn_r = $table->establishConnection('r');
772
773
// TODO: Gross. This table is not meant to be queried like this. Build
774
// real stats tables.
775
776
$open_status_list = array();
777
foreach (ManiphestTaskStatus::getOpenStatusConstants() as $constant) {
778
$open_status_list[] = json_encode((string)$constant);
779
}
780
781
$rows = queryfx_all(
782
$conn_r,
783
'SELECT t.id FROM %T t JOIN %T x ON x.objectPHID = t.phid
784
WHERE t.status NOT IN (%Ls)
785
AND x.oldValue IN (null, %Ls)
786
AND x.newValue NOT IN (%Ls)
787
AND t.dateModified >= %d
788
AND x.dateCreated >= %d',
789
$table->getTableName(),
790
$xtable->getTableName(),
791
ManiphestTaskStatus::getOpenStatusConstants(),
792
$open_status_list,
793
$open_status_list,
794
$window_epoch,
795
$window_epoch);
796
797
if (!$rows) {
798
return array();
799
}
800
801
$ids = ipull($rows, 'id');
802
803
$query = id(new ManiphestTaskQuery())
804
->setViewer($this->getRequest()->getUser())
805
->withIDs($ids);
806
807
switch ($this->view) {
808
case 'project':
809
$query->needProjectPHIDs(true);
810
break;
811
}
812
813
return $query->execute();
814
}
815
816
/**
817
* Parse the "Recently Means" filter into:
818
*
819
* - A string representation, like "12 AM 7 days ago" (default);
820
* - a locale-aware epoch representation; and
821
* - a possible error.
822
*/
823
private function getWindow() {
824
$request = $this->getRequest();
825
$viewer = $request->getUser();
826
827
$window_str = $this->getRequest()->getStr('window', '12 AM 7 days ago');
828
829
$error = null;
830
$window_epoch = null;
831
832
// Do locale-aware parsing so that the user's timezone is assumed for
833
// time windows like "3 PM", rather than assuming the server timezone.
834
835
$window_epoch = PhabricatorTime::parseLocalTime($window_str, $viewer);
836
if (!$window_epoch) {
837
$error = 'Invalid';
838
$window_epoch = time() - (60 * 60 * 24 * 7);
839
}
840
841
// If the time ends up in the future, convert it to the corresponding time
842
// and equal distance in the past. This is so users can type "6 days" (which
843
// means "6 days from now") and get the behavior of "6 days ago", rather
844
// than no results (because the window epoch is in the future). This might
845
// be a little confusing because it causes "tomorrow" to mean "yesterday"
846
// and "2022" (or whatever) to mean "ten years ago", but these inputs are
847
// nonsense anyway.
848
849
if ($window_epoch > time()) {
850
$window_epoch = time() - ($window_epoch - time());
851
}
852
853
return array($window_str, $window_epoch, $error);
854
}
855
856
private function renderOldest(array $tasks) {
857
assert_instances_of($tasks, 'ManiphestTask');
858
$oldest = null;
859
foreach ($tasks as $id => $task) {
860
if (($oldest === null) ||
861
($task->getDateCreated() < $tasks[$oldest]->getDateCreated())) {
862
$oldest = $id;
863
}
864
}
865
866
if ($oldest === null) {
867
return array('-', 0);
868
}
869
870
$oldest = $tasks[$oldest];
871
872
$raw_age = (time() - $oldest->getDateCreated());
873
$age = number_format($raw_age / (24 * 60 * 60)).' d';
874
875
$link = javelin_tag(
876
'a',
877
array(
878
'href' => '/T'.$oldest->getID(),
879
'sigil' => 'has-tooltip',
880
'meta' => array(
881
'tip' => 'T'.$oldest->getID().': '.$oldest->getTitle(),
882
),
883
'target' => '_blank',
884
),
885
$age);
886
887
return array($link, $raw_age);
888
}
889
890
}
891
892