@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class PhabricatorProject extends PhabricatorProjectDAO
4 implements
5 PhabricatorApplicationTransactionInterface,
6 PhabricatorFlaggableInterface,
7 PhabricatorPolicyInterface,
8 PhabricatorExtendedPolicyInterface,
9 PhabricatorCustomFieldInterface,
10 PhabricatorDestructibleInterface,
11 PhabricatorFulltextInterface,
12 PhabricatorFerretInterface,
13 PhabricatorConduitResultInterface,
14 PhabricatorColumnProxyInterface,
15 PhabricatorSpacesInterface,
16 PhabricatorEditEngineSubtypeInterface,
17 PhabricatorWorkboardInterface {
18
19 protected $name;
20 protected $status = PhabricatorProjectStatus::STATUS_ACTIVE;
21 protected $authorPHID;
22 protected $primarySlug;
23 protected $profileImagePHID;
24 protected $icon;
25 protected $color;
26 protected $mailKey;
27
28 protected $viewPolicy;
29 protected $editPolicy;
30 protected $joinPolicy;
31 protected $isMembershipLocked;
32
33 protected $parentProjectPHID;
34 protected $hasWorkboard;
35 protected $hasMilestones;
36 protected $hasSubprojects;
37 protected $milestoneNumber;
38
39 protected $projectPath;
40 protected $projectDepth;
41 protected $projectPathKey;
42
43 protected $properties = array();
44 protected $spacePHID;
45 protected $subtype;
46
47 private $memberPHIDs = self::ATTACHABLE;
48 private $watcherPHIDs = self::ATTACHABLE;
49 private $sparseWatchers = self::ATTACHABLE;
50 private $sparseMembers = self::ATTACHABLE;
51 private $customFields = self::ATTACHABLE;
52 private $profileImageFile = self::ATTACHABLE;
53 private $slugs = self::ATTACHABLE;
54 private $parentProject = self::ATTACHABLE;
55
56 const TABLE_DATASOURCE_TOKEN = 'project_datasourcetoken';
57
58 const ITEM_PICTURE = 'project.picture';
59 const ITEM_PROFILE = 'project.profile';
60 const ITEM_POINTS = 'project.points';
61 const ITEM_WORKBOARD = 'project.workboard';
62 const ITEM_REPORTS = 'project.reports';
63 const ITEM_MEMBERS = 'project.members';
64 const ITEM_MANAGE = 'project.manage';
65 const ITEM_MILESTONES = 'project.milestones';
66 const ITEM_SUBPROJECTS = 'project.subprojects';
67
68 public static function initializeNewProject(
69 PhabricatorUser $actor,
70 ?PhabricatorProject $parent = null) {
71
72 $app = id(new PhabricatorApplicationQuery())
73 ->setViewer(PhabricatorUser::getOmnipotentUser())
74 ->withClasses(array(PhabricatorProjectApplication::class))
75 ->executeOne();
76
77 $view_policy = $app->getPolicy(
78 ProjectDefaultViewCapability::CAPABILITY);
79 $edit_policy = $app->getPolicy(
80 ProjectDefaultEditCapability::CAPABILITY);
81 $join_policy = $app->getPolicy(
82 ProjectDefaultJoinCapability::CAPABILITY);
83
84 // If this is the child of some other project, default the Space to the
85 // Space of the parent.
86 if ($parent) {
87 $space_phid = $parent->getSpacePHID();
88 } else {
89 $space_phid = $actor->getDefaultSpacePHID();
90 }
91
92 $default_icon = PhabricatorProjectIconSet::getDefaultIconKey();
93 $default_color = PhabricatorProjectIconSet::getDefaultColorKey();
94
95 return id(new PhabricatorProject())
96 ->setAuthorPHID($actor->getPHID())
97 ->setIcon($default_icon)
98 ->setColor($default_color)
99 ->setViewPolicy($view_policy)
100 ->setEditPolicy($edit_policy)
101 ->setJoinPolicy($join_policy)
102 ->setSpacePHID($space_phid)
103 ->setIsMembershipLocked(0)
104 ->attachMemberPHIDs(array())
105 ->attachSlugs(array())
106 ->setHasWorkboard(0)
107 ->setHasMilestones(0)
108 ->setHasSubprojects(0)
109 ->setSubtype(PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT)
110 ->attachParentProject($parent);
111 }
112
113 public function getCapabilities() {
114 return array(
115 PhabricatorPolicyCapability::CAN_VIEW,
116 PhabricatorPolicyCapability::CAN_EDIT,
117 PhabricatorPolicyCapability::CAN_JOIN,
118 );
119 }
120
121 public function getPolicy($capability) {
122 if ($this->isMilestone()) {
123 return $this->getParentProject()->getPolicy($capability);
124 }
125
126 switch ($capability) {
127 case PhabricatorPolicyCapability::CAN_VIEW:
128 return $this->getViewPolicy();
129 case PhabricatorPolicyCapability::CAN_EDIT:
130 return $this->getEditPolicy();
131 case PhabricatorPolicyCapability::CAN_JOIN:
132 return $this->getJoinPolicy();
133 }
134 }
135
136 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
137 if ($this->isMilestone()) {
138 return $this->getParentProject()->hasAutomaticCapability(
139 $capability,
140 $viewer);
141 }
142
143 $can_edit = PhabricatorPolicyCapability::CAN_EDIT;
144
145 switch ($capability) {
146 case PhabricatorPolicyCapability::CAN_VIEW:
147 $viewer_phid = $viewer->getPHID();
148 if ($viewer_phid && $this->isUserMember($viewer_phid)) {
149 // Project members can always view a project.
150 return true;
151 }
152 break;
153 case PhabricatorPolicyCapability::CAN_EDIT:
154 $parent = $this->getParentProject();
155 if ($parent) {
156 $can_edit_parent = PhabricatorPolicyFilter::hasCapability(
157 $viewer,
158 $parent,
159 $can_edit);
160 if ($can_edit_parent) {
161 return true;
162 }
163 }
164 break;
165 case PhabricatorPolicyCapability::CAN_JOIN:
166 if (PhabricatorPolicyFilter::hasCapability($viewer, $this, $can_edit)) {
167 // Project editors can always join a project.
168 return true;
169 }
170 break;
171 }
172
173 return false;
174 }
175
176 public function describeAutomaticCapability($capability) {
177
178 // TODO: Clarify the additional rules that parent and subprojects imply.
179
180 switch ($capability) {
181 case PhabricatorPolicyCapability::CAN_VIEW:
182 return pht('Members of a project can always view it.');
183 case PhabricatorPolicyCapability::CAN_JOIN:
184 return pht('Users who can edit a project can always join it.');
185 }
186 return null;
187 }
188
189 public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
190 $extended = array();
191
192 switch ($capability) {
193 case PhabricatorPolicyCapability::CAN_VIEW:
194 $parent = $this->getParentProject();
195 if ($parent) {
196 $extended[] = array(
197 $parent,
198 PhabricatorPolicyCapability::CAN_VIEW,
199 );
200 }
201 break;
202 }
203
204 return $extended;
205 }
206
207 public function isUserMember($user_phid) {
208 if ($this->memberPHIDs !== self::ATTACHABLE) {
209 return in_array($user_phid, $this->memberPHIDs);
210 }
211 return $this->assertAttachedKey($this->sparseMembers, $user_phid);
212 }
213
214 public function setIsUserMember($user_phid, $is_member) {
215 if ($this->sparseMembers === self::ATTACHABLE) {
216 $this->sparseMembers = array();
217 }
218 $this->sparseMembers[$user_phid] = $is_member;
219 return $this;
220 }
221
222 protected function getConfiguration() {
223 return array(
224 self::CONFIG_AUX_PHID => true,
225 self::CONFIG_SERIALIZATION => array(
226 'properties' => self::SERIALIZATION_JSON,
227 ),
228 self::CONFIG_COLUMN_SCHEMA => array(
229 'name' => 'sort128',
230 'status' => 'text32',
231 'primarySlug' => 'text128?',
232 'isMembershipLocked' => 'bool',
233 'profileImagePHID' => 'phid?',
234 'icon' => 'text32',
235 'color' => 'text32',
236 'mailKey' => 'bytes20',
237 'joinPolicy' => 'policy',
238 'parentProjectPHID' => 'phid?',
239 'hasWorkboard' => 'bool',
240 'hasMilestones' => 'bool',
241 'hasSubprojects' => 'bool',
242 'milestoneNumber' => 'uint32?',
243 'projectPath' => 'hashpath64',
244 'projectDepth' => 'uint32',
245 'projectPathKey' => 'bytes4',
246 'subtype' => 'text64',
247 ),
248 self::CONFIG_KEY_SCHEMA => array(
249 'key_icon' => array(
250 'columns' => array('icon'),
251 ),
252 'key_color' => array(
253 'columns' => array('color'),
254 ),
255 'key_milestone' => array(
256 'columns' => array('parentProjectPHID', 'milestoneNumber'),
257 'unique' => true,
258 ),
259 'key_primaryslug' => array(
260 'columns' => array('primarySlug'),
261 'unique' => true,
262 ),
263 'key_path' => array(
264 'columns' => array('projectPath', 'projectDepth'),
265 ),
266 'key_pathkey' => array(
267 'columns' => array('projectPathKey'),
268 'unique' => true,
269 ),
270 ),
271 ) + parent::getConfiguration();
272 }
273
274 public function generatePHID() {
275 return PhabricatorPHID::generateNewPHID(
276 PhabricatorProjectProjectPHIDType::TYPECONST);
277 }
278
279 public function attachMemberPHIDs(array $phids) {
280 $this->memberPHIDs = $phids;
281 return $this;
282 }
283
284 public function getMemberPHIDs() {
285 return $this->assertAttached($this->memberPHIDs);
286 }
287
288 public function isArchived() {
289 return ($this->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED);
290 }
291
292 public function getProfileImageURI() {
293 return $this->getProfileImageFile()->getBestURI();
294 }
295
296 public function attachProfileImageFile(PhabricatorFile $file) {
297 $this->profileImageFile = $file;
298 return $this;
299 }
300
301 public function getProfileImageFile() {
302 return $this->assertAttached($this->profileImageFile);
303 }
304
305
306 public function isUserWatcher($user_phid) {
307 if ($this->watcherPHIDs !== self::ATTACHABLE) {
308 return in_array($user_phid, $this->watcherPHIDs);
309 }
310 return $this->assertAttachedKey($this->sparseWatchers, $user_phid);
311 }
312
313 public function isUserAncestorWatcher($user_phid) {
314 $is_watcher = $this->isUserWatcher($user_phid);
315
316 if (!$is_watcher) {
317 $parent = $this->getParentProject();
318 if ($parent) {
319 return $parent->isUserWatcher($user_phid);
320 }
321 }
322
323 return $is_watcher;
324 }
325
326 public function getWatchedAncestorPHID($user_phid) {
327 if ($this->isUserWatcher($user_phid)) {
328 return $this->getPHID();
329 }
330
331 $parent = $this->getParentProject();
332 if ($parent) {
333 return $parent->getWatchedAncestorPHID($user_phid);
334 }
335
336 return null;
337 }
338
339 public function setIsUserWatcher($user_phid, $is_watcher) {
340 if ($this->sparseWatchers === self::ATTACHABLE) {
341 $this->sparseWatchers = array();
342 }
343 $this->sparseWatchers[$user_phid] = $is_watcher;
344 return $this;
345 }
346
347 public function attachWatcherPHIDs(array $phids) {
348 $this->watcherPHIDs = $phids;
349 return $this;
350 }
351
352 public function getWatcherPHIDs() {
353 return $this->assertAttached($this->watcherPHIDs);
354 }
355
356 public function getAllAncestorWatcherPHIDs() {
357 $parent = $this->getParentProject();
358 if ($parent) {
359 $watchers = $parent->getAllAncestorWatcherPHIDs();
360 } else {
361 $watchers = array();
362 }
363
364 foreach ($this->getWatcherPHIDs() as $phid) {
365 $watchers[$phid] = $phid;
366 }
367
368 return $watchers;
369 }
370
371 public function attachSlugs(array $slugs) {
372 $this->slugs = $slugs;
373 return $this;
374 }
375
376 public function getSlugs() {
377 return $this->assertAttached($this->slugs);
378 }
379
380 public function getColor() {
381 if ($this->isArchived()) {
382 return PHUITagView::COLOR_DISABLED;
383 }
384
385 return $this->color;
386 }
387
388 public function getURI() {
389 $id = $this->getID();
390 return "/project/view/{$id}/";
391 }
392
393 public function getProfileURI() {
394 $id = $this->getID();
395 return "/project/profile/{$id}/";
396 }
397
398 public function getWorkboardURI() {
399 return urisprintf('/project/board/%d/', $this->getID());
400 }
401
402 public function getReportsURI() {
403 return urisprintf('/project/reports/%d/', $this->getID());
404 }
405
406 public function save() {
407 if (!$this->getMailKey()) {
408 $this->setMailKey(Filesystem::readRandomCharacters(20));
409 }
410
411 $phid = $this->getPHID();
412 if ($phid === null || $phid === '') {
413 $this->setPHID($this->generatePHID());
414 }
415
416 $path_key = $this->getProjectPathKey();
417 if ($path_key === null || $path_key === '') {
418 $hash = PhabricatorHash::digestForIndex($this->getPHID());
419 $hash = substr($hash, 0, 4);
420 $this->setProjectPathKey($hash);
421 }
422
423 $path = array();
424 $depth = 0;
425 if ($this->parentProjectPHID) {
426 $parent = $this->getParentProject();
427 $path[] = $parent->getProjectPath();
428 $depth = $parent->getProjectDepth() + 1;
429 }
430 $path[] = $this->getProjectPathKey();
431 $path = implode('', $path);
432
433 $limit = self::getProjectDepthLimit();
434 if ($depth >= $limit) {
435 throw new Exception(pht('Project depth is too great.'));
436 }
437
438 $this->setProjectPath($path);
439 $this->setProjectDepth($depth);
440
441 $this->openTransaction();
442 $result = parent::save();
443 $this->updateDatasourceTokens();
444 $this->saveTransaction();
445
446 return $result;
447 }
448
449 public static function getProjectDepthLimit() {
450 // This is limited by how many path hashes we can fit in the path
451 // column.
452 return 16;
453 }
454
455 public function updateDatasourceTokens() {
456 $table = self::TABLE_DATASOURCE_TOKEN;
457 $conn_w = $this->establishConnection('w');
458 $id = $this->getID();
459
460 $slugs = queryfx_all(
461 $conn_w,
462 'SELECT * FROM %T WHERE projectPHID = %s',
463 id(new PhabricatorProjectSlug())->getTableName(),
464 $this->getPHID());
465
466 $all_strings = ipull($slugs, 'slug');
467 $all_strings[] = $this->getDisplayName();
468 $all_strings = implode(' ', $all_strings);
469
470 $tokens = PhabricatorTypeaheadDatasource::tokenizeString($all_strings);
471
472 $sql = array();
473 foreach ($tokens as $token) {
474 $sql[] = qsprintf($conn_w, '(%d, %s)', $id, $token);
475 }
476
477 $this->openTransaction();
478 queryfx(
479 $conn_w,
480 'DELETE FROM %T WHERE projectID = %d',
481 $table,
482 $id);
483
484 foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
485 queryfx(
486 $conn_w,
487 'INSERT INTO %T (projectID, token) VALUES %LQ',
488 $table,
489 $chunk);
490 }
491 $this->saveTransaction();
492 }
493
494 public function isMilestone() {
495 return ($this->getMilestoneNumber() !== null);
496 }
497
498 public function getParentProject() {
499 return $this->assertAttached($this->parentProject);
500 }
501
502 public function attachParentProject(?PhabricatorProject $project = null) {
503 $this->parentProject = $project;
504 return $this;
505 }
506
507 public function getAncestorProjectPaths() {
508 $parts = array();
509
510 $path = $this->getProjectPath();
511 $parent_length = (strlen($path) - 4);
512
513 for ($ii = $parent_length; $ii > 0; $ii -= 4) {
514 $parts[] = substr($path, 0, $ii);
515 }
516
517 return $parts;
518 }
519
520 public function getAncestorProjects() {
521 $ancestors = array();
522
523 $cursor = $this->getParentProject();
524 while ($cursor) {
525 $ancestors[] = $cursor;
526 $cursor = $cursor->getParentProject();
527 }
528
529 return $ancestors;
530 }
531
532 public function supportsEditMembers() {
533 if ($this->isMilestone()) {
534 return false;
535 }
536
537 if ($this->getHasSubprojects()) {
538 return false;
539 }
540
541 return true;
542 }
543
544 public function supportsMilestones() {
545 if ($this->isMilestone()) {
546 return false;
547 }
548
549 return true;
550 }
551
552 public function supportsSubprojects() {
553 if ($this->isMilestone()) {
554 return false;
555 }
556
557 return true;
558 }
559
560 public function loadNextMilestoneNumber() {
561 $current = queryfx_one(
562 $this->establishConnection('w'),
563 'SELECT MAX(milestoneNumber) n
564 FROM %T
565 WHERE parentProjectPHID = %s',
566 $this->getTableName(),
567 $this->getPHID());
568
569 if (!$current) {
570 $number = 1;
571 } else {
572 $number = (int)$current['n'] + 1;
573 }
574
575 return $number;
576 }
577
578 public function getDisplayName() {
579 $name = $this->getName();
580
581 // If this is a milestone, show it as "Parent > Sprint 99".
582 if ($this->isMilestone()) {
583 $name = pht(
584 '%s (%s)',
585 $this->getParentProject()->getName(),
586 $name);
587 }
588
589 return $name;
590 }
591
592 public function getDisplayIconKey() {
593 if ($this->isMilestone()) {
594 $key = PhabricatorProjectIconSet::getMilestoneIconKey();
595 } else {
596 $key = $this->getIcon();
597 }
598
599 return $key;
600 }
601
602 public function getDisplayIconIcon() {
603 $key = $this->getDisplayIconKey();
604 return PhabricatorProjectIconSet::getIconIcon($key);
605 }
606
607 public function getDisplayIconName() {
608 $key = $this->getDisplayIconKey();
609 return PhabricatorProjectIconSet::getIconName($key);
610 }
611
612 public function getDisplayColor() {
613 if ($this->isMilestone()) {
614 return $this->getParentProject()->getColor();
615 }
616
617 return $this->getColor();
618 }
619
620 public function getDisplayIconComposeIcon() {
621 $icon = $this->getDisplayIconIcon();
622 return $icon;
623 }
624
625 public function getDisplayIconComposeColor() {
626 $color = $this->getDisplayColor();
627
628 $map = array(
629 'grey' => 'charcoal',
630 'checkered' => 'backdrop',
631 );
632
633 return idx($map, $color, $color);
634 }
635
636 public function getProperty($key, $default = null) {
637 return idx($this->properties, $key, $default);
638 }
639
640 public function setProperty($key, $value) {
641 $this->properties[$key] = $value;
642 return $this;
643 }
644
645 public function getDefaultWorkboardSort() {
646 return $this->getProperty('workboard.sort.default');
647 }
648
649 public function setDefaultWorkboardSort($sort) {
650 return $this->setProperty('workboard.sort.default', $sort);
651 }
652
653 public function getDefaultWorkboardFilter() {
654 return $this->getProperty('workboard.filter.default');
655 }
656
657 public function setDefaultWorkboardFilter($filter) {
658 return $this->setProperty('workboard.filter.default', $filter);
659 }
660
661 public function getWorkboardBackgroundColor() {
662 return $this->getProperty('workboard.background');
663 }
664
665 public function setWorkboardBackgroundColor($color) {
666 return $this->setProperty('workboard.background', $color);
667 }
668
669 public function getDisplayWorkboardBackgroundColor() {
670 $color = $this->getWorkboardBackgroundColor();
671
672 if ($color === null) {
673 $parent = $this->getParentProject();
674 if ($parent) {
675 return $parent->getDisplayWorkboardBackgroundColor();
676 }
677 }
678
679 if ($color === 'none') {
680 $color = null;
681 }
682
683 return $color;
684 }
685
686
687/* -( PhabricatorCustomFieldInterface )------------------------------------ */
688
689
690 public function getCustomFieldSpecificationForRole($role) {
691 return PhabricatorEnv::getEnvConfig('projects.fields');
692 }
693
694 public function getCustomFieldBaseClass() {
695 return 'PhabricatorProjectCustomField';
696 }
697
698 public function getCustomFields() {
699 return $this->assertAttached($this->customFields);
700 }
701
702 public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
703 $this->customFields = $fields;
704 return $this;
705 }
706
707
708/* -( PhabricatorApplicationTransactionInterface )------------------------- */
709
710
711 public function getApplicationTransactionEditor() {
712 return new PhabricatorProjectTransactionEditor();
713 }
714
715 public function getApplicationTransactionTemplate() {
716 return new PhabricatorProjectTransaction();
717 }
718
719
720/* -( PhabricatorSpacesInterface )----------------------------------------- */
721
722
723 public function getSpacePHID() {
724 if ($this->isMilestone()) {
725 return $this->getParentProject()->getSpacePHID();
726 }
727 return $this->spacePHID;
728 }
729
730
731/* -( PhabricatorDestructibleInterface )----------------------------------- */
732
733
734 public function destroyObjectPermanently(
735 PhabricatorDestructionEngine $engine) {
736
737 $this->openTransaction();
738 $this->delete();
739
740 $columns = id(new PhabricatorProjectColumn())
741 ->loadAllWhere('projectPHID = %s', $this->getPHID());
742 foreach ($columns as $column) {
743 $engine->destroyObject($column);
744 }
745
746 $slugs = id(new PhabricatorProjectSlug())
747 ->loadAllWhere('projectPHID = %s', $this->getPHID());
748 foreach ($slugs as $slug) {
749 $slug->delete();
750 }
751
752 // If I'm a project with milestones, these milestones cannot live
753 // without me (without their project), so, delete milestones as well.
754 // FAQ: can we just move milestones to my parent?
755 // Nope: milestones are numbered, so it would not make sense
756 // to try to move my milestones to the parent project,
757 // especially since the parent project may have its own milestones,
758 // with conflicting numbering.
759 // To find milestones, do not use PhabricatorProjectQuery, to avoid a
760 // circular dependency, and to avoid a silent fail, since these
761 // milestones do not have their parent anymore.
762 // Micro-optimization: find milestones, only if I support them.
763 if ($this->supportsMilestones()) {
764 $milestones = id(new self())->loadAllWhere(
765 'parentProjectPHID = %s AND milestoneNumber IS NOT NULL',
766 $this->getPHID());
767 foreach ($milestones as $milestone) {
768 $milestone->attachParentProject($this);
769 $engine->destroyObject($milestone);
770 }
771 }
772
773 // Refresh the 'depth' of my children, fix gaps in the tree, etc.
774 $this->onDestroyTouchChildren();
775
776 // After the tree is fixed, update the field 'hasSubProjects'
777 // of my parent project.
778 // In this way, my parent project may become root-project again.
779 if ($this->getParentProject()) {
780 id(new PhabricatorProjectsMembershipIndexEngineExtension())
781 ->rematerialize($this->getParentProject());
782 }
783
784 $this->saveTransaction();
785 }
786
787 /**
788 * On destroy, refresh my children, and their children recursively,
789 * to consolidate depth, path key, etc.
790 * As default, only during the initial call, bubble up my direct children,
791 * to close the gap in our project tree.
792 * @param bool $first_call True, only during our initial call.
793 */
794 private function onDestroyTouchChildren(bool $first_call = true): void {
795 // Micro-optimization: proceed only if we may have at least one child.
796 if (!$this->supportsSubprojects() && !$this->supportsMilestones()) {
797 return;
798 }
799
800 // Find my direct children.
801 // Do not use 'PhabricatorProjectQuery' to avoid a circular dependency.
802 $table_project = new self();
803 if ($first_call) {
804 // We must skip my direct milestones since they are under removal.
805 $children = $table_project->loadAllWhere(
806 'parentProjectPHID = %s AND milestoneNumber IS NULL',
807 $this->getPHID());
808 } else {
809 // We must take all sub-projects and milestones.
810 $children = $table_project->loadAllWhere(
811 'parentProjectPHID = %s',
812 $this->getPHID());
813 }
814
815 // Refresh all children.
816 foreach ($children as $child) {
817
818 if ($first_call) {
819 // This is a direct children of the deleted project.
820 // Bubble up it, to don't stay orphan and broken.
821 $desired_parent = $this->getParentProject();
822 $desired_parent_phid =
823 $desired_parent ? $desired_parent->getPHID() : null;
824 $child->attachParentProject($desired_parent);
825 $child->setParentProjectPHID($desired_parent_phid);
826 }
827
828 // Refresh 'pathKey' and 'depth'.
829 $child->setProjectPathKey(null);
830
831 $child->save();
832
833 // Recursively refresh all its children (if any).
834 $child->onDestroyTouchChildren(false);
835 }
836 }
837
838
839/* -( PhabricatorFulltextInterface )--------------------------------------- */
840
841
842 public function newFulltextEngine() {
843 return new PhabricatorProjectFulltextEngine();
844 }
845
846
847/* -( PhabricatorFerretInterface )--------------------------------------- */
848
849
850 public function newFerretEngine() {
851 return new PhabricatorProjectFerretEngine();
852 }
853
854
855/* -( PhabricatorConduitResultInterface )---------------------------------- */
856
857
858 public function getFieldSpecificationsForConduit() {
859 return array(
860 id(new PhabricatorConduitSearchFieldSpecification())
861 ->setKey('name')
862 ->setType('string')
863 ->setDescription(pht('The name of the project.')),
864 id(new PhabricatorConduitSearchFieldSpecification())
865 ->setKey('slug')
866 ->setType('string')
867 ->setDescription(pht('Primary hashtag.')),
868 id(new PhabricatorConduitSearchFieldSpecification())
869 ->setKey('subtype')
870 ->setType('string')
871 ->setDescription(pht('Subtype of the project.')),
872 id(new PhabricatorConduitSearchFieldSpecification())
873 ->setKey('milestone')
874 ->setType('int?')
875 ->setDescription(pht('For milestones, milestone sequence number.')),
876 id(new PhabricatorConduitSearchFieldSpecification())
877 ->setKey('parent')
878 ->setType('map<string, wild>?')
879 ->setDescription(
880 pht(
881 'For subprojects and milestones, a brief description of the '.
882 'parent project.')),
883 id(new PhabricatorConduitSearchFieldSpecification())
884 ->setKey('depth')
885 ->setType('int')
886 ->setDescription(
887 pht(
888 'For subprojects and milestones, depth of this project in the '.
889 'tree. Root projects have depth 0.')),
890 id(new PhabricatorConduitSearchFieldSpecification())
891 ->setKey('icon')
892 ->setType('map<string, wild>')
893 ->setDescription(pht('Information about the project icon.')),
894 id(new PhabricatorConduitSearchFieldSpecification())
895 ->setKey('color')
896 ->setType('map<string, wild>')
897 ->setDescription(pht('Information about the project color.')),
898 );
899 }
900
901 public function getFieldValuesForConduit() {
902 $color_key = $this->getDisplayColor();
903 $color_name = PhabricatorProjectIconSet::getColorName($color_key);
904
905 if ($this->isMilestone()) {
906 $milestone = (int)$this->getMilestoneNumber();
907 } else {
908 $milestone = null;
909 }
910
911 $parent = $this->getParentProject();
912 if ($parent) {
913 $parent_ref = $parent->getRefForConduit();
914 } else {
915 $parent_ref = null;
916 }
917
918 return array(
919 'name' => $this->getName(),
920 'slug' => $this->getPrimarySlug(),
921 'subtype' => $this->getSubtype(),
922 'milestone' => $milestone,
923 'depth' => (int)$this->getProjectDepth(),
924 'parent' => $parent_ref,
925 'icon' => array(
926 'key' => $this->getDisplayIconKey(),
927 'name' => $this->getDisplayIconName(),
928 'icon' => $this->getDisplayIconIcon(),
929 ),
930 'color' => array(
931 'key' => $color_key,
932 'name' => $color_name,
933 ),
934 'status' => PhabricatorProjectStatus::getKeyForStatus($this->getStatus()),
935 );
936 }
937
938 public function getConduitSearchAttachments() {
939 return array(
940 id(new PhabricatorProjectsMembersSearchEngineAttachment())
941 ->setAttachmentKey('members'),
942 id(new PhabricatorProjectsWatchersSearchEngineAttachment())
943 ->setAttachmentKey('watchers'),
944 id(new PhabricatorProjectsAncestorsSearchEngineAttachment())
945 ->setAttachmentKey('ancestors'),
946 );
947 }
948
949 /**
950 * Get an abbreviated representation of this project for use in providing
951 * "parent" and "ancestor" information.
952 */
953 public function getRefForConduit() {
954 return array(
955 'id' => (int)$this->getID(),
956 'phid' => $this->getPHID(),
957 'name' => $this->getName(),
958 );
959 }
960
961
962/* -( PhabricatorColumnProxyInterface )------------------------------------ */
963
964
965 public function getProxyColumnName() {
966 return $this->getName();
967 }
968
969 public function getProxyColumnIcon() {
970 return $this->getDisplayIconIcon();
971 }
972
973 public function getProxyColumnClass() {
974 if ($this->isMilestone()) {
975 return 'phui-workboard-column-milestone';
976 }
977
978 return null;
979 }
980
981
982/* -( PhabricatorEditEngineSubtypeInterface )------------------------------ */
983
984
985 public function getEditEngineSubtype() {
986 return $this->getSubtype();
987 }
988
989 public function setEditEngineSubtype($value) {
990 return $this->setSubtype($value);
991 }
992
993 public function newEditEngineSubtypeMap() {
994 $config = PhabricatorEnv::getEnvConfig('projects.subtypes');
995 return PhabricatorEditEngineSubtype::newSubtypeMap($config)
996 ->setDatasource(new PhabricatorProjectSubtypeDatasource());
997 }
998
999 public function newSubtypeObject() {
1000 $subtype_key = $this->getEditEngineSubtype();
1001 $subtype_map = $this->newEditEngineSubtypeMap();
1002 return $subtype_map->getSubtype($subtype_key);
1003 }
1004
1005}