Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/maniphest/query/ManiphestTaskQuery.php
12262 views
1
<?php
2
3
/**
4
* Query tasks by specific criteria. This class uses the higher-performance
5
* but less-general Maniphest indexes to satisfy queries.
6
*/
7
final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
8
9
private $taskIDs;
10
private $taskPHIDs;
11
private $authorPHIDs;
12
private $ownerPHIDs;
13
private $noOwner;
14
private $anyOwner;
15
private $subscriberPHIDs;
16
private $dateCreatedAfter;
17
private $dateCreatedBefore;
18
private $dateModifiedAfter;
19
private $dateModifiedBefore;
20
private $bridgedObjectPHIDs;
21
private $hasOpenParents;
22
private $hasOpenSubtasks;
23
private $parentTaskIDs;
24
private $subtaskIDs;
25
private $subtypes;
26
private $closedEpochMin;
27
private $closedEpochMax;
28
private $closerPHIDs;
29
private $columnPHIDs;
30
private $specificGroupByProjectPHID;
31
32
private $status = 'status-any';
33
const STATUS_ANY = 'status-any';
34
const STATUS_OPEN = 'status-open';
35
const STATUS_CLOSED = 'status-closed';
36
const STATUS_RESOLVED = 'status-resolved';
37
const STATUS_WONTFIX = 'status-wontfix';
38
const STATUS_INVALID = 'status-invalid';
39
const STATUS_SPITE = 'status-spite';
40
const STATUS_DUPLICATE = 'status-duplicate';
41
42
private $statuses;
43
private $priorities;
44
private $subpriorities;
45
46
private $groupBy = 'group-none';
47
const GROUP_NONE = 'group-none';
48
const GROUP_PRIORITY = 'group-priority';
49
const GROUP_OWNER = 'group-owner';
50
const GROUP_STATUS = 'group-status';
51
const GROUP_PROJECT = 'group-project';
52
53
const ORDER_PRIORITY = 'order-priority';
54
const ORDER_CREATED = 'order-created';
55
const ORDER_MODIFIED = 'order-modified';
56
const ORDER_TITLE = 'order-title';
57
58
private $needSubscriberPHIDs;
59
private $needProjectPHIDs;
60
61
public function withAuthors(array $authors) {
62
$this->authorPHIDs = $authors;
63
return $this;
64
}
65
66
public function withIDs(array $ids) {
67
$this->taskIDs = $ids;
68
return $this;
69
}
70
71
public function withPHIDs(array $phids) {
72
$this->taskPHIDs = $phids;
73
return $this;
74
}
75
76
public function withOwners(array $owners) {
77
if ($owners === array()) {
78
throw new Exception(pht('Empty withOwners() constraint is not valid.'));
79
}
80
81
$no_owner = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN;
82
$any_owner = PhabricatorPeopleAnyOwnerDatasource::FUNCTION_TOKEN;
83
84
foreach ($owners as $k => $phid) {
85
if ($phid === $no_owner || $phid === null) {
86
$this->noOwner = true;
87
unset($owners[$k]);
88
break;
89
}
90
if ($phid === $any_owner) {
91
$this->anyOwner = true;
92
unset($owners[$k]);
93
break;
94
}
95
}
96
97
if ($owners) {
98
$this->ownerPHIDs = $owners;
99
}
100
101
return $this;
102
}
103
104
public function withStatus($status) {
105
$this->status = $status;
106
return $this;
107
}
108
109
public function withStatuses(array $statuses) {
110
$this->statuses = $statuses;
111
return $this;
112
}
113
114
public function withPriorities(array $priorities) {
115
$this->priorities = $priorities;
116
return $this;
117
}
118
119
public function withSubpriorities(array $subpriorities) {
120
$this->subpriorities = $subpriorities;
121
return $this;
122
}
123
124
public function withSubscribers(array $subscribers) {
125
$this->subscriberPHIDs = $subscribers;
126
return $this;
127
}
128
129
public function setGroupBy($group) {
130
$this->groupBy = $group;
131
132
switch ($this->groupBy) {
133
case self::GROUP_NONE:
134
$vector = array();
135
break;
136
case self::GROUP_PRIORITY:
137
$vector = array('priority');
138
break;
139
case self::GROUP_OWNER:
140
$vector = array('owner');
141
break;
142
case self::GROUP_STATUS:
143
$vector = array('status');
144
break;
145
case self::GROUP_PROJECT:
146
$vector = array('project');
147
break;
148
}
149
150
$this->setGroupVector($vector);
151
152
return $this;
153
}
154
155
public function withOpenSubtasks($value) {
156
$this->hasOpenSubtasks = $value;
157
return $this;
158
}
159
160
public function withOpenParents($value) {
161
$this->hasOpenParents = $value;
162
return $this;
163
}
164
165
public function withParentTaskIDs(array $ids) {
166
$this->parentTaskIDs = $ids;
167
return $this;
168
}
169
170
public function withSubtaskIDs(array $ids) {
171
$this->subtaskIDs = $ids;
172
return $this;
173
}
174
175
public function withDateCreatedBefore($date_created_before) {
176
$this->dateCreatedBefore = $date_created_before;
177
return $this;
178
}
179
180
public function withDateCreatedAfter($date_created_after) {
181
$this->dateCreatedAfter = $date_created_after;
182
return $this;
183
}
184
185
public function withDateModifiedBefore($date_modified_before) {
186
$this->dateModifiedBefore = $date_modified_before;
187
return $this;
188
}
189
190
public function withDateModifiedAfter($date_modified_after) {
191
$this->dateModifiedAfter = $date_modified_after;
192
return $this;
193
}
194
195
public function withClosedEpochBetween($min, $max) {
196
$this->closedEpochMin = $min;
197
$this->closedEpochMax = $max;
198
return $this;
199
}
200
201
public function withCloserPHIDs(array $phids) {
202
$this->closerPHIDs = $phids;
203
return $this;
204
}
205
206
public function needSubscriberPHIDs($bool) {
207
$this->needSubscriberPHIDs = $bool;
208
return $this;
209
}
210
211
public function needProjectPHIDs($bool) {
212
$this->needProjectPHIDs = $bool;
213
return $this;
214
}
215
216
public function withBridgedObjectPHIDs(array $phids) {
217
$this->bridgedObjectPHIDs = $phids;
218
return $this;
219
}
220
221
public function withSubtypes(array $subtypes) {
222
$this->subtypes = $subtypes;
223
return $this;
224
}
225
226
public function withColumnPHIDs(array $column_phids) {
227
$this->columnPHIDs = $column_phids;
228
return $this;
229
}
230
231
public function withSpecificGroupByProjectPHID($project_phid) {
232
$this->specificGroupByProjectPHID = $project_phid;
233
return $this;
234
}
235
236
public function newResultObject() {
237
return new ManiphestTask();
238
}
239
240
protected function loadPage() {
241
$task_dao = new ManiphestTask();
242
$conn = $task_dao->establishConnection('r');
243
244
$where = $this->buildWhereClause($conn);
245
246
$group_column = qsprintf($conn, '');
247
switch ($this->groupBy) {
248
case self::GROUP_PROJECT:
249
$group_column = qsprintf(
250
$conn,
251
', projectGroupName.indexedObjectPHID projectGroupPHID');
252
break;
253
}
254
255
$rows = queryfx_all(
256
$conn,
257
'%Q %Q FROM %T task %Q %Q %Q %Q %Q %Q',
258
$this->buildSelectClause($conn),
259
$group_column,
260
$task_dao->getTableName(),
261
$this->buildJoinClause($conn),
262
$where,
263
$this->buildGroupClause($conn),
264
$this->buildHavingClause($conn),
265
$this->buildOrderClause($conn),
266
$this->buildLimitClause($conn));
267
268
switch ($this->groupBy) {
269
case self::GROUP_PROJECT:
270
$data = ipull($rows, null, 'id');
271
break;
272
default:
273
$data = $rows;
274
break;
275
}
276
277
$data = $this->didLoadRawRows($data);
278
$tasks = $task_dao->loadAllFromArray($data);
279
280
switch ($this->groupBy) {
281
case self::GROUP_PROJECT:
282
$results = array();
283
foreach ($rows as $row) {
284
$task = clone $tasks[$row['id']];
285
$task->attachGroupByProjectPHID($row['projectGroupPHID']);
286
$results[] = $task;
287
}
288
$tasks = $results;
289
break;
290
}
291
292
return $tasks;
293
}
294
295
protected function willFilterPage(array $tasks) {
296
if ($this->groupBy == self::GROUP_PROJECT) {
297
// We should only return project groups which the user can actually see.
298
$project_phids = mpull($tasks, 'getGroupByProjectPHID');
299
$projects = id(new PhabricatorProjectQuery())
300
->setViewer($this->getViewer())
301
->withPHIDs($project_phids)
302
->execute();
303
$projects = mpull($projects, null, 'getPHID');
304
305
foreach ($tasks as $key => $task) {
306
if (!$task->getGroupByProjectPHID()) {
307
// This task is either not tagged with any projects, or only tagged
308
// with projects which we're ignoring because they're being queried
309
// for explicitly.
310
continue;
311
}
312
313
if (empty($projects[$task->getGroupByProjectPHID()])) {
314
unset($tasks[$key]);
315
}
316
}
317
}
318
319
return $tasks;
320
}
321
322
protected function didFilterPage(array $tasks) {
323
$phids = mpull($tasks, 'getPHID');
324
325
if ($this->needProjectPHIDs) {
326
$edge_query = id(new PhabricatorEdgeQuery())
327
->withSourcePHIDs($phids)
328
->withEdgeTypes(
329
array(
330
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
331
));
332
$edge_query->execute();
333
334
foreach ($tasks as $task) {
335
$project_phids = $edge_query->getDestinationPHIDs(
336
array($task->getPHID()));
337
$task->attachProjectPHIDs($project_phids);
338
}
339
}
340
341
if ($this->needSubscriberPHIDs) {
342
$subscriber_sets = id(new PhabricatorSubscribersQuery())
343
->withObjectPHIDs($phids)
344
->execute();
345
foreach ($tasks as $task) {
346
$subscribers = idx($subscriber_sets, $task->getPHID(), array());
347
$task->attachSubscriberPHIDs($subscribers);
348
}
349
}
350
351
return $tasks;
352
}
353
354
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
355
$where = parent::buildWhereClauseParts($conn);
356
357
$where[] = $this->buildStatusWhereClause($conn);
358
$where[] = $this->buildOwnerWhereClause($conn);
359
360
if ($this->taskIDs !== null) {
361
$where[] = qsprintf(
362
$conn,
363
'task.id in (%Ld)',
364
$this->taskIDs);
365
}
366
367
if ($this->taskPHIDs !== null) {
368
$where[] = qsprintf(
369
$conn,
370
'task.phid in (%Ls)',
371
$this->taskPHIDs);
372
}
373
374
if ($this->statuses !== null) {
375
$where[] = qsprintf(
376
$conn,
377
'task.status IN (%Ls)',
378
$this->statuses);
379
}
380
381
if ($this->authorPHIDs !== null) {
382
$where[] = qsprintf(
383
$conn,
384
'task.authorPHID in (%Ls)',
385
$this->authorPHIDs);
386
}
387
388
if ($this->dateCreatedAfter) {
389
$where[] = qsprintf(
390
$conn,
391
'task.dateCreated >= %d',
392
$this->dateCreatedAfter);
393
}
394
395
if ($this->dateCreatedBefore) {
396
$where[] = qsprintf(
397
$conn,
398
'task.dateCreated <= %d',
399
$this->dateCreatedBefore);
400
}
401
402
if ($this->dateModifiedAfter) {
403
$where[] = qsprintf(
404
$conn,
405
'task.dateModified >= %d',
406
$this->dateModifiedAfter);
407
}
408
409
if ($this->dateModifiedBefore) {
410
$where[] = qsprintf(
411
$conn,
412
'task.dateModified <= %d',
413
$this->dateModifiedBefore);
414
}
415
416
if ($this->closedEpochMin !== null) {
417
$where[] = qsprintf(
418
$conn,
419
'task.closedEpoch >= %d',
420
$this->closedEpochMin);
421
}
422
423
if ($this->closedEpochMax !== null) {
424
$where[] = qsprintf(
425
$conn,
426
'task.closedEpoch <= %d',
427
$this->closedEpochMax);
428
}
429
430
if ($this->closerPHIDs !== null) {
431
$where[] = qsprintf(
432
$conn,
433
'task.closerPHID IN (%Ls)',
434
$this->closerPHIDs);
435
}
436
437
if ($this->priorities !== null) {
438
$where[] = qsprintf(
439
$conn,
440
'task.priority IN (%Ld)',
441
$this->priorities);
442
}
443
444
if ($this->bridgedObjectPHIDs !== null) {
445
$where[] = qsprintf(
446
$conn,
447
'task.bridgedObjectPHID IN (%Ls)',
448
$this->bridgedObjectPHIDs);
449
}
450
451
if ($this->subtypes !== null) {
452
$where[] = qsprintf(
453
$conn,
454
'task.subtype IN (%Ls)',
455
$this->subtypes);
456
}
457
458
459
if ($this->columnPHIDs !== null) {
460
$viewer = $this->getViewer();
461
462
$columns = id(new PhabricatorProjectColumnQuery())
463
->setParentQuery($this)
464
->setViewer($viewer)
465
->withPHIDs($this->columnPHIDs)
466
->execute();
467
if (!$columns) {
468
throw new PhabricatorEmptyQueryException();
469
}
470
471
// We must do board layout before we move forward because the column
472
// positions may not yet exist otherwise. An example is that newly
473
// created tasks may not yet be positioned in the backlog column.
474
475
$projects = mpull($columns, 'getProject');
476
$projects = mpull($projects, null, 'getPHID');
477
478
// The board layout engine needs to know about every object that it's
479
// going to be asked to do layout for. For now, we're just doing layout
480
// on every object on the boards. In the future, we could do layout on a
481
// smaller set of objects by using the constraints on this Query. For
482
// example, if the caller is only asking for open tasks, we only need
483
// to do layout on open tasks.
484
485
// This fetches too many objects (every type of object tagged with the
486
// project, not just tasks). We could narrow it by querying the edge
487
// table on the Maniphest side, but there's currently no way to build
488
// that query with EdgeQuery.
489
$edge_query = id(new PhabricatorEdgeQuery())
490
->withSourcePHIDs(array_keys($projects))
491
->withEdgeTypes(
492
array(
493
PhabricatorProjectProjectHasObjectEdgeType::EDGECONST,
494
));
495
496
$edge_query->execute();
497
$all_phids = $edge_query->getDestinationPHIDs();
498
499
// Since we overfetched PHIDs, filter out any non-tasks we got back.
500
foreach ($all_phids as $key => $phid) {
501
if (phid_get_type($phid) !== ManiphestTaskPHIDType::TYPECONST) {
502
unset($all_phids[$key]);
503
}
504
}
505
506
// If there are no tasks on the relevant boards, this query can't
507
// possibly hit anything so we're all done.
508
$task_phids = array_fuse($all_phids);
509
if (!$task_phids) {
510
throw new PhabricatorEmptyQueryException();
511
}
512
513
// We know everything we need to know, so perform board layout.
514
$engine = id(new PhabricatorBoardLayoutEngine())
515
->setViewer($viewer)
516
->setFetchAllBoards(true)
517
->setBoardPHIDs(array_keys($projects))
518
->setObjectPHIDs($task_phids)
519
->executeLayout();
520
521
// Find the tasks that are in the constraint columns after board layout
522
// completes.
523
$select_phids = array();
524
foreach ($columns as $column) {
525
$in_column = $engine->getColumnObjectPHIDs(
526
$column->getProjectPHID(),
527
$column->getPHID());
528
foreach ($in_column as $phid) {
529
$select_phids[$phid] = $phid;
530
}
531
}
532
533
if (!$select_phids) {
534
throw new PhabricatorEmptyQueryException();
535
}
536
537
$where[] = qsprintf(
538
$conn,
539
'task.phid IN (%Ls)',
540
$select_phids);
541
}
542
543
if ($this->specificGroupByProjectPHID !== null) {
544
$where[] = qsprintf(
545
$conn,
546
'projectGroupName.indexedObjectPHID = %s',
547
$this->specificGroupByProjectPHID);
548
}
549
550
return $where;
551
}
552
553
private function buildStatusWhereClause(AphrontDatabaseConnection $conn) {
554
static $map = array(
555
self::STATUS_RESOLVED => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
556
self::STATUS_WONTFIX => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
557
self::STATUS_INVALID => ManiphestTaskStatus::STATUS_CLOSED_INVALID,
558
self::STATUS_SPITE => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
559
self::STATUS_DUPLICATE => ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE,
560
);
561
562
switch ($this->status) {
563
case self::STATUS_ANY:
564
return null;
565
case self::STATUS_OPEN:
566
return qsprintf(
567
$conn,
568
'task.status IN (%Ls)',
569
ManiphestTaskStatus::getOpenStatusConstants());
570
case self::STATUS_CLOSED:
571
return qsprintf(
572
$conn,
573
'task.status IN (%Ls)',
574
ManiphestTaskStatus::getClosedStatusConstants());
575
default:
576
$constant = idx($map, $this->status);
577
if (!$constant) {
578
throw new Exception(pht("Unknown status query '%s'!", $this->status));
579
}
580
return qsprintf(
581
$conn,
582
'task.status = %s',
583
$constant);
584
}
585
}
586
587
private function buildOwnerWhereClause(AphrontDatabaseConnection $conn) {
588
$subclause = array();
589
590
if ($this->noOwner) {
591
$subclause[] = qsprintf(
592
$conn,
593
'task.ownerPHID IS NULL');
594
}
595
596
if ($this->anyOwner) {
597
$subclause[] = qsprintf(
598
$conn,
599
'task.ownerPHID IS NOT NULL');
600
}
601
602
if ($this->ownerPHIDs !== null) {
603
$subclause[] = qsprintf(
604
$conn,
605
'task.ownerPHID IN (%Ls)',
606
$this->ownerPHIDs);
607
}
608
609
if (!$subclause) {
610
return qsprintf($conn, '');
611
}
612
613
return qsprintf($conn, '%LO', $subclause);
614
}
615
616
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
617
$open_statuses = ManiphestTaskStatus::getOpenStatusConstants();
618
$edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE;
619
$task_table = $this->newResultObject()->getTableName();
620
621
$parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST;
622
$subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST;
623
624
$joins = array();
625
if ($this->hasOpenParents !== null) {
626
if ($this->hasOpenParents) {
627
$join_type = qsprintf($conn, 'JOIN');
628
} else {
629
$join_type = qsprintf($conn, 'LEFT JOIN');
630
}
631
632
$joins[] = qsprintf(
633
$conn,
634
'%Q %T e_parent
635
ON e_parent.src = task.phid
636
AND e_parent.type = %d
637
%Q %T parent
638
ON e_parent.dst = parent.phid
639
AND parent.status IN (%Ls)',
640
$join_type,
641
$edge_table,
642
$parent_type,
643
$join_type,
644
$task_table,
645
$open_statuses);
646
}
647
648
if ($this->hasOpenSubtasks !== null) {
649
if ($this->hasOpenSubtasks) {
650
$join_type = qsprintf($conn, 'JOIN');
651
} else {
652
$join_type = qsprintf($conn, 'LEFT JOIN');
653
}
654
655
$joins[] = qsprintf(
656
$conn,
657
'%Q %T e_subtask
658
ON e_subtask.src = task.phid
659
AND e_subtask.type = %d
660
%Q %T subtask
661
ON e_subtask.dst = subtask.phid
662
AND subtask.status IN (%Ls)',
663
$join_type,
664
$edge_table,
665
$subtask_type,
666
$join_type,
667
$task_table,
668
$open_statuses);
669
}
670
671
if ($this->subscriberPHIDs !== null) {
672
$joins[] = qsprintf(
673
$conn,
674
'JOIN %T e_ccs ON e_ccs.src = task.phid '.
675
'AND e_ccs.type = %s '.
676
'AND e_ccs.dst in (%Ls)',
677
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
678
PhabricatorObjectHasSubscriberEdgeType::EDGECONST,
679
$this->subscriberPHIDs);
680
}
681
682
switch ($this->groupBy) {
683
case self::GROUP_PROJECT:
684
$ignore_group_phids = $this->getIgnoreGroupedProjectPHIDs();
685
if ($ignore_group_phids) {
686
$joins[] = qsprintf(
687
$conn,
688
'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src
689
AND projectGroup.type = %d
690
AND projectGroup.dst NOT IN (%Ls)',
691
$edge_table,
692
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
693
$ignore_group_phids);
694
} else {
695
$joins[] = qsprintf(
696
$conn,
697
'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src
698
AND projectGroup.type = %d',
699
$edge_table,
700
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
701
}
702
$joins[] = qsprintf(
703
$conn,
704
'LEFT JOIN %T projectGroupName
705
ON projectGroup.dst = projectGroupName.indexedObjectPHID',
706
id(new ManiphestNameIndex())->getTableName());
707
break;
708
}
709
710
if ($this->parentTaskIDs !== null) {
711
$joins[] = qsprintf(
712
$conn,
713
'JOIN %T e_has_parent
714
ON e_has_parent.src = task.phid
715
AND e_has_parent.type = %d
716
JOIN %T has_parent
717
ON e_has_parent.dst = has_parent.phid
718
AND has_parent.id IN (%Ld)',
719
$edge_table,
720
$parent_type,
721
$task_table,
722
$this->parentTaskIDs);
723
}
724
725
if ($this->subtaskIDs !== null) {
726
$joins[] = qsprintf(
727
$conn,
728
'JOIN %T e_has_subtask
729
ON e_has_subtask.src = task.phid
730
AND e_has_subtask.type = %d
731
JOIN %T has_subtask
732
ON e_has_subtask.dst = has_subtask.phid
733
AND has_subtask.id IN (%Ld)',
734
$edge_table,
735
$subtask_type,
736
$task_table,
737
$this->subtaskIDs);
738
}
739
740
$joins[] = parent::buildJoinClauseParts($conn);
741
742
return $joins;
743
}
744
745
protected function buildGroupClause(AphrontDatabaseConnection $conn) {
746
$joined_multiple_rows =
747
($this->hasOpenParents !== null) ||
748
($this->hasOpenSubtasks !== null) ||
749
($this->parentTaskIDs !== null) ||
750
($this->subtaskIDs !== null) ||
751
$this->shouldGroupQueryResultRows();
752
753
$joined_project_name = ($this->groupBy == self::GROUP_PROJECT);
754
755
// If we're joining multiple rows, we need to group the results by the
756
// task IDs.
757
if ($joined_multiple_rows) {
758
if ($joined_project_name) {
759
return qsprintf($conn, 'GROUP BY task.phid, projectGroup.dst');
760
} else {
761
return qsprintf($conn, 'GROUP BY task.phid');
762
}
763
}
764
765
return qsprintf($conn, '');
766
}
767
768
769
protected function buildHavingClauseParts(AphrontDatabaseConnection $conn) {
770
$having = parent::buildHavingClauseParts($conn);
771
772
if ($this->hasOpenParents !== null) {
773
if (!$this->hasOpenParents) {
774
$having[] = qsprintf(
775
$conn,
776
'COUNT(parent.phid) = 0');
777
}
778
}
779
780
if ($this->hasOpenSubtasks !== null) {
781
if (!$this->hasOpenSubtasks) {
782
$having[] = qsprintf(
783
$conn,
784
'COUNT(subtask.phid) = 0');
785
}
786
}
787
788
return $having;
789
}
790
791
792
/**
793
* Return project PHIDs which we should ignore when grouping tasks by
794
* project. For example, if a user issues a query like:
795
*
796
* Tasks tagged with all projects: Frontend, Bugs
797
*
798
* ...then we don't show "Frontend" or "Bugs" groups in the result set, since
799
* they're meaningless as all results are in both groups.
800
*
801
* Similarly, for queries like:
802
*
803
* Tasks tagged with any projects: Public Relations
804
*
805
* ...we ignore the single project, as every result is in that project. (In
806
* the case that there are several "any" projects, we do not ignore them.)
807
*
808
* @return list<phid> Project PHIDs which should be ignored in query
809
* construction.
810
*/
811
private function getIgnoreGroupedProjectPHIDs() {
812
// Maybe we should also exclude the "OPERATOR_NOT" PHIDs? It won't
813
// impact the results, but we might end up with a better query plan.
814
// Investigate this on real data? This is likely very rare.
815
816
$edge_types = array(
817
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
818
);
819
820
$phids = array();
821
822
$phids[] = $this->getEdgeLogicValues(
823
$edge_types,
824
array(
825
PhabricatorQueryConstraint::OPERATOR_AND,
826
));
827
828
$any = $this->getEdgeLogicValues(
829
$edge_types,
830
array(
831
PhabricatorQueryConstraint::OPERATOR_OR,
832
));
833
if (count($any) == 1) {
834
$phids[] = $any;
835
}
836
837
return array_mergev($phids);
838
}
839
840
public function getBuiltinOrders() {
841
$orders = array(
842
'priority' => array(
843
'vector' => array('priority', 'id'),
844
'name' => pht('Priority'),
845
'aliases' => array(self::ORDER_PRIORITY),
846
),
847
'updated' => array(
848
'vector' => array('updated', 'id'),
849
'name' => pht('Date Updated (Latest First)'),
850
'aliases' => array(self::ORDER_MODIFIED),
851
),
852
'outdated' => array(
853
'vector' => array('-updated', '-id'),
854
'name' => pht('Date Updated (Oldest First)'),
855
),
856
'closed' => array(
857
'vector' => array('closed', 'id'),
858
'name' => pht('Date Closed (Latest First)'),
859
),
860
'title' => array(
861
'vector' => array('title', 'id'),
862
'name' => pht('Title'),
863
'aliases' => array(self::ORDER_TITLE),
864
),
865
) + parent::getBuiltinOrders();
866
867
// Alias the "newest" builtin to the historical key for it.
868
$orders['newest']['aliases'][] = self::ORDER_CREATED;
869
870
$orders = array_select_keys(
871
$orders,
872
array(
873
'priority',
874
'updated',
875
'outdated',
876
'newest',
877
'oldest',
878
'closed',
879
'title',
880
)) + $orders;
881
882
return $orders;
883
}
884
885
public function getOrderableColumns() {
886
return parent::getOrderableColumns() + array(
887
'priority' => array(
888
'table' => 'task',
889
'column' => 'priority',
890
'type' => 'int',
891
),
892
'owner' => array(
893
'table' => 'task',
894
'column' => 'ownerOrdering',
895
'null' => 'head',
896
'reverse' => true,
897
'type' => 'string',
898
),
899
'status' => array(
900
'table' => 'task',
901
'column' => 'status',
902
'type' => 'string',
903
'reverse' => true,
904
),
905
'project' => array(
906
'table' => 'projectGroupName',
907
'column' => 'indexedObjectName',
908
'type' => 'string',
909
'null' => 'head',
910
'reverse' => true,
911
),
912
'title' => array(
913
'table' => 'task',
914
'column' => 'title',
915
'type' => 'string',
916
'reverse' => true,
917
),
918
'updated' => array(
919
'table' => 'task',
920
'column' => 'dateModified',
921
'type' => 'int',
922
),
923
'closed' => array(
924
'table' => 'task',
925
'column' => 'closedEpoch',
926
'type' => 'int',
927
'null' => 'tail',
928
),
929
);
930
}
931
932
protected function newPagingMapFromCursorObject(
933
PhabricatorQueryCursor $cursor,
934
array $keys) {
935
936
$task = $cursor->getObject();
937
938
$map = array(
939
'id' => (int)$task->getID(),
940
'priority' => (int)$task->getPriority(),
941
'owner' => $task->getOwnerOrdering(),
942
'status' => $task->getStatus(),
943
'title' => $task->getTitle(),
944
'updated' => (int)$task->getDateModified(),
945
'closed' => $task->getClosedEpoch(),
946
);
947
948
if (isset($keys['project'])) {
949
$value = null;
950
951
$group_phid = $task->getGroupByProjectPHID();
952
if ($group_phid) {
953
$paging_projects = id(new PhabricatorProjectQuery())
954
->setViewer($this->getViewer())
955
->withPHIDs(array($group_phid))
956
->execute();
957
if ($paging_projects) {
958
$value = head($paging_projects)->getName();
959
}
960
}
961
962
$map['project'] = $value;
963
}
964
965
foreach ($keys as $key) {
966
if ($this->isCustomFieldOrderKey($key)) {
967
$map += $this->getPagingValueMapForCustomFields($task);
968
break;
969
}
970
}
971
972
return $map;
973
}
974
975
protected function newExternalCursorStringForResult($object) {
976
$id = $object->getID();
977
978
if ($this->groupBy == self::GROUP_PROJECT) {
979
return rtrim($id.'.'.$object->getGroupByProjectPHID(), '.');
980
}
981
982
return $id;
983
}
984
985
protected function newInternalCursorFromExternalCursor($cursor) {
986
list($task_id, $group_phid) = $this->parseCursor($cursor);
987
988
$cursor_object = parent::newInternalCursorFromExternalCursor($cursor);
989
990
if ($group_phid !== null) {
991
$project = id(new PhabricatorProjectQuery())
992
->setViewer($this->getViewer())
993
->withPHIDs(array($group_phid))
994
->execute();
995
996
if (!$project) {
997
$this->throwCursorException(
998
pht(
999
'Group PHID ("%s") component of cursor ("%s") is not valid.',
1000
$group_phid,
1001
$cursor));
1002
}
1003
1004
$cursor_object->getObject()->attachGroupByProjectPHID($group_phid);
1005
}
1006
1007
return $cursor_object;
1008
}
1009
1010
protected function applyExternalCursorConstraintsToQuery(
1011
PhabricatorCursorPagedPolicyAwareQuery $subquery,
1012
$cursor) {
1013
list($task_id, $group_phid) = $this->parseCursor($cursor);
1014
1015
$subquery->withIDs(array($task_id));
1016
1017
if ($group_phid) {
1018
$subquery->setGroupBy(self::GROUP_PROJECT);
1019
1020
// The subquery needs to return exactly one result. If a task is in
1021
// several projects, the query may naturally return several results.
1022
// Specify that we want only the particular instance of the task in
1023
// the specified project.
1024
$subquery->withSpecificGroupByProjectPHID($group_phid);
1025
}
1026
}
1027
1028
1029
private function parseCursor($cursor) {
1030
// Split a "123.PHID-PROJ-abcd" cursor into a "Task ID" part and a
1031
// "Project PHID" part.
1032
1033
$parts = explode('.', $cursor, 2);
1034
1035
if (count($parts) < 2) {
1036
$parts[] = null;
1037
}
1038
1039
if (!strlen($parts[1])) {
1040
$parts[1] = null;
1041
}
1042
1043
return $parts;
1044
}
1045
1046
protected function getPrimaryTableAlias() {
1047
return 'task';
1048
}
1049
1050
public function getQueryApplicationClass() {
1051
return 'PhabricatorManiphestApplication';
1052
}
1053
1054
}
1055
1056