@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
fork

Configure Feed

Select the types of activity you want to include in your feed.

at recaptime-dev/main 1005 lines 28 kB view raw
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}