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

Implement "Edit Members" and "Join/Leave" with real ApplicationTransactions

Summary:
Ref T4379. Projects has been partially converted to ApplicationTransactions, but the rough state of the world is that all the //storage// is modern, but most of the stuff on top isn't yet. Particularly, there's a `PhabricatorProjectEditor` which is //not// a subclass of `PhabricatorApplicationTransactionEditor`, but which fakes its way through writing reasonable data into modern storage.

This introduces a real transaction editor, `PhabricatorProjectTransactionEditor`, with the eventual goal of moving all of the old functionality into it and deleting the old class. This diff only moves the membership transaction into new code (it doesn't even move all of it -- when we create a project, we add the author as a member, and that can't move quite yet since there are other transactions at the same time).

Test Plan:
- Created a new project.
- Edited members.
- Joined / left project.
- This already has a pile of unit test coverage.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T4379

Differential Revision: https://secure.phabricator.com/D8167

+206 -179
+2
src/__phutil_library_map__.php
··· 1855 1855 'PhabricatorProjectStatus' => 'applications/project/constants/PhabricatorProjectStatus.php', 1856 1856 'PhabricatorProjectTestDataGenerator' => 'applications/project/lipsum/PhabricatorProjectTestDataGenerator.php', 1857 1857 'PhabricatorProjectTransaction' => 'applications/project/storage/PhabricatorProjectTransaction.php', 1858 + 'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php', 1858 1859 'PhabricatorProjectTransactionQuery' => 'applications/project/query/PhabricatorProjectTransactionQuery.php', 1859 1860 'PhabricatorProjectUpdateController' => 'applications/project/controller/PhabricatorProjectUpdateController.php', 1860 1861 'PhabricatorQuery' => 'infrastructure/query/PhabricatorQuery.php', ··· 4593 4594 'PhabricatorProjectSearchIndexer' => 'PhabricatorSearchDocumentIndexer', 4594 4595 'PhabricatorProjectTestDataGenerator' => 'PhabricatorTestDataGenerator', 4595 4596 'PhabricatorProjectTransaction' => 'PhabricatorApplicationTransaction', 4597 + 'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 4596 4598 'PhabricatorProjectTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 4597 4599 'PhabricatorProjectUpdateController' => 'PhabricatorProjectController', 4598 4600 'PhabricatorRecaptchaConfigOptions' => 'PhabricatorApplicationConfigOptions',
+20 -27
src/applications/project/controller/PhabricatorProjectMembersEditController.php
··· 29 29 30 30 $member_phids = $project->getMemberPHIDs(); 31 31 32 - $errors = array(); 33 32 if ($request->isFormPost()) { 34 - $changed_something = false; 35 - $member_map = array_fill_keys($member_phids, true); 33 + $member_spec = array(); 36 34 37 35 $remove = $request->getStr('remove'); 38 36 if ($remove) { 39 - if (isset($member_map[$remove])) { 40 - unset($member_map[$remove]); 41 - $changed_something = true; 42 - } 43 - } else { 44 - $new_members = $request->getArr('phids'); 45 - foreach ($new_members as $member) { 46 - if (empty($member_map[$member])) { 47 - $member_map[$member] = true; 48 - $changed_something = true; 49 - } 50 - } 37 + $member_spec['-'] = array_fuse(array($remove)); 51 38 } 39 + 40 + $add_members = $request->getArr('phids'); 41 + if ($add_members) { 42 + $member_spec['+'] = array_fuse($add_members); 43 + } 44 + 45 + $type_member = PhabricatorEdgeConfig::TYPE_PROJ_MEMBER; 52 46 53 47 $xactions = array(); 54 - if ($changed_something) { 55 - $xaction = new PhabricatorProjectTransaction(); 56 - $xaction->setTransactionType( 57 - PhabricatorProjectTransaction::TYPE_MEMBERS); 58 - $xaction->setNewValue(array_keys($member_map)); 59 - $xactions[] = $xaction; 60 - } 48 + 49 + $xactions[] = id(new PhabricatorProjectTransaction()) 50 + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) 51 + ->setMetadataValue('edge:type', $type_member) 52 + ->setNewValue($member_spec); 61 53 62 - if ($xactions) { 63 - $editor = new PhabricatorProjectEditor($project); 64 - $editor->setActor($user); 65 - $editor->applyTransactions($xactions); 66 - } 54 + $editor = id(new PhabricatorProjectTransactionEditor($project)) 55 + ->setActor($user) 56 + ->setContentSourceFromRequest($request) 57 + ->setContinueOnNoEffect(true) 58 + ->setContinueOnMissingFields(true) 59 + ->applyTransactions($project, $xactions); 67 60 68 61 return id(new AphrontRedirectResponse()) 69 62 ->setURI($request->getRequestURI());
+23 -2
src/applications/project/controller/PhabricatorProjectUpdateController.php
··· 45 45 $project_uri = '/project/view/'.$project->getID().'/'; 46 46 47 47 if ($process_action) { 48 + 49 + $edge_action = null; 48 50 switch ($this->action) { 49 51 case 'join': 50 - PhabricatorProjectEditor::applyJoinProject($project, $user); 52 + $edge_action = '+'; 51 53 break; 52 54 case 'leave': 53 - PhabricatorProjectEditor::applyLeaveProject($project, $user); 55 + $edge_action = '-'; 54 56 break; 55 57 } 58 + 59 + $type_member = PhabricatorEdgeConfig::TYPE_PROJ_MEMBER; 60 + $member_spec = array( 61 + $edge_action => array($user->getPHID() => $user->getPHID()), 62 + ); 63 + 64 + $xactions = array(); 65 + $xactions[] = id(new PhabricatorProjectTransaction()) 66 + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) 67 + ->setMetadataValue('edge:type', $type_member) 68 + ->setNewValue($member_spec); 69 + 70 + $editor = id(new PhabricatorProjectTransactionEditor($project)) 71 + ->setActor($user) 72 + ->setContentSourceFromRequest($request) 73 + ->setContinueOnNoEffect(true) 74 + ->setContinueOnMissingFields(true) 75 + ->applyTransactions($project, $xactions); 76 + 56 77 return id(new AphrontRedirectResponse())->setURI($project_uri); 57 78 } 58 79
+4 -140
src/applications/project/editor/PhabricatorProjectEditor.php
··· 18 18 return $this->shouldArchive; 19 19 } 20 20 21 - public static function applyJoinProject( 22 - PhabricatorProject $project, 23 - PhabricatorUser $user) { 24 - 25 - $members = $project->getMemberPHIDs(); 26 - $members[] = $user->getPHID(); 27 - 28 - self::applyOneTransaction( 29 - $project, 30 - $user, 31 - PhabricatorProjectTransaction::TYPE_MEMBERS, 32 - $members); 33 - } 34 - 35 - public static function applyLeaveProject( 36 - PhabricatorProject $project, 37 - PhabricatorUser $user) { 38 - 39 - $members = array_fill_keys($project->getMemberPHIDs(), true); 40 - unset($members[$user->getPHID()]); 41 - $members = array_keys($members); 42 - 43 - self::applyOneTransaction( 44 - $project, 45 - $user, 46 - PhabricatorProjectTransaction::TYPE_MEMBERS, 47 - $members); 48 - } 49 - 50 - private static function applyOneTransaction( 51 - PhabricatorProject $project, 52 - PhabricatorUser $user, 53 - $type, 54 - $new_value) { 55 - 56 - $xaction = new PhabricatorProjectTransaction(); 57 - $xaction->setTransactionType($type); 58 - $xaction->setNewValue($new_value); 59 - 60 - $editor = new PhabricatorProjectEditor($project); 61 - $editor->setActor($user); 62 - $editor->applyTransactions(array($xaction)); 63 - } 64 - 65 - 66 21 public function __construct(PhabricatorProject $project) { 67 22 $this->project = $project; 68 23 } ··· 94 49 $project, 95 50 PhabricatorPolicyCapability::CAN_VIEW); 96 51 97 - $need_edit = false; 98 - $need_join = false; 99 - foreach ($transactions as $key => $xaction) { 100 - if ($this->getTransactionRequiresEditCapability($xaction)) { 101 - $need_edit = true; 102 - } 103 - if ($this->getTransactionRequiresJoinCapability($xaction)) { 104 - $need_join = true; 105 - } 106 - } 107 - 108 - if ($need_edit) { 109 - PhabricatorPolicyFilter::requireCapability( 110 - $actor, 111 - $project, 112 - PhabricatorPolicyCapability::CAN_EDIT); 113 - } 114 - 115 - if ($need_join) { 116 - PhabricatorPolicyFilter::requireCapability( 117 - $actor, 118 - $project, 119 - PhabricatorPolicyCapability::CAN_JOIN); 120 - } 52 + PhabricatorPolicyFilter::requireCapability( 53 + $actor, 54 + $project, 55 + PhabricatorPolicyCapability::CAN_EDIT); 121 56 } 122 57 123 58 if (!$transactions) { ··· 315 250 private function transactionHasEffect( 316 251 PhabricatorProjectTransaction $xaction) { 317 252 return ($xaction->getOldValue() !== $xaction->getNewValue()); 318 - } 319 - 320 - 321 - /** 322 - * All transactions except joining or leaving a project require edit 323 - * capability. 324 - */ 325 - private function getTransactionRequiresEditCapability( 326 - PhabricatorProjectTransaction $xaction) { 327 - return ($this->isJoinOrLeaveTransaction($xaction) === null); 328 - } 329 - 330 - 331 - /** 332 - * Joining a project requires the join capability. Anyone leave a project. 333 - */ 334 - private function getTransactionRequiresJoinCapability( 335 - PhabricatorProjectTransaction $xaction) { 336 - $type = $this->isJoinOrLeaveTransaction($xaction); 337 - return ($type == 'join'); 338 - } 339 - 340 - 341 - /** 342 - * Returns 'join' if this transaction causes the acting user ONLY to join the 343 - * project. 344 - * 345 - * Returns 'leave' if this transaction causes the acting user ONLY to leave 346 - * the project. 347 - * 348 - * Returns null in all other cases. 349 - */ 350 - private function isJoinOrLeaveTransaction( 351 - PhabricatorProjectTransaction $xaction) { 352 - 353 - $type = $xaction->getTransactionType(); 354 - if ($type != PhabricatorProjectTransaction::TYPE_MEMBERS) { 355 - return null; 356 - } 357 - 358 - switch ($type) { 359 - case PhabricatorProjectTransaction::TYPE_MEMBERS: 360 - $old = $xaction->getOldValue(); 361 - $new = $xaction->getNewValue(); 362 - 363 - $add = array_diff($new, $old); 364 - $rem = array_diff($old, $new); 365 - 366 - if (count($add) > 1) { 367 - return null; 368 - } else if (count($add) == 1) { 369 - if (reset($add) != $this->getActor()->getPHID()) { 370 - return null; 371 - } else { 372 - return 'join'; 373 - } 374 - } 375 - 376 - if (count($rem) > 1) { 377 - return null; 378 - } else if (count($rem) == 1) { 379 - if (reset($rem) != $this->getActor()->getPHID()) { 380 - return null; 381 - } else { 382 - return 'leave'; 383 - } 384 - } 385 - break; 386 - } 387 - 388 - return true; 389 253 } 390 254 391 255 }
+113
src/applications/project/editor/PhabricatorProjectTransactionEditor.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectTransactionEditor 4 + extends PhabricatorApplicationTransactionEditor { 5 + 6 + public function getTransactionTypes() { 7 + $types = parent::getTransactionTypes(); 8 + 9 + $types[] = PhabricatorTransactions::TYPE_EDGE; 10 + 11 + return $types; 12 + } 13 + 14 + protected function getCustomTransactionOldValue( 15 + PhabricatorLiskDAO $object, 16 + PhabricatorApplicationTransaction $xaction) { 17 + 18 + switch ($xaction->getTransactionType()) { 19 + } 20 + 21 + return parent::getCustomTransactionOldValue($object, $xaction); 22 + } 23 + 24 + protected function getCustomTransactionNewValue( 25 + PhabricatorLiskDAO $object, 26 + PhabricatorApplicationTransaction $xaction) { 27 + 28 + switch ($xaction->getTransactionType()) { 29 + } 30 + 31 + return parent::getCustomTransactionNewValue($object, $xaction); 32 + } 33 + 34 + protected function applyCustomInternalTransaction( 35 + PhabricatorLiskDAO $object, 36 + PhabricatorApplicationTransaction $xaction) { 37 + 38 + switch ($xaction->getTransactionType()) { 39 + case PhabricatorTransactions::TYPE_EDGE: 40 + return; 41 + } 42 + 43 + return parent::applyCustomInternalTransaction($object, $xaction); 44 + } 45 + 46 + protected function applyCustomExternalTransaction( 47 + PhabricatorLiskDAO $object, 48 + PhabricatorApplicationTransaction $xaction) { 49 + 50 + switch ($xaction->getTransactionType()) { 51 + case PhabricatorTransactions::TYPE_EDGE: 52 + return; 53 + } 54 + 55 + return parent::applyCustomExternalTransaction($object, $xaction); 56 + } 57 + 58 + protected function validateTransaction( 59 + PhabricatorLiskDAO $object, 60 + $type, 61 + array $xactions) { 62 + 63 + $errors = parent::validateTransaction($object, $type, $xactions); 64 + 65 + switch ($type) { 66 + } 67 + 68 + return $errors; 69 + } 70 + 71 + protected function requireCapabilities( 72 + PhabricatorLiskDAO $object, 73 + PhabricatorApplicationTransaction $xaction) { 74 + 75 + switch ($xaction->getTransactionType()) { 76 + case PhabricatorTransactions::TYPE_EDGE: 77 + switch ($xaction->getMetadataValue('edge:type')) { 78 + case PhabricatorEdgeConfig::TYPE_PROJ_MEMBER: 79 + $old = $xaction->getOldValue(); 80 + $new = $xaction->getNewValue(); 81 + 82 + $add = array_keys(array_diff_key($new, $old)); 83 + $rem = array_keys(array_diff_key($old, $new)); 84 + 85 + $actor_phid = $this->requireActor()->getPHID(); 86 + 87 + $is_join = (($add === array($actor_phid)) && !$rem); 88 + $is_leave = (($rem === array($actor_phid)) && !$add); 89 + 90 + if ($is_join) { 91 + // You need CAN_JOIN to join a project. 92 + PhabricatorPolicyFilter::requireCapability( 93 + $this->requireActor(), 94 + $object, 95 + PhabricatorPolicyCapability::CAN_JOIN); 96 + } else if ($is_leave) { 97 + // You don't need any capabilities to leave a project. 98 + } else { 99 + // You need CAN_EDIT to change members other than yourself. 100 + PhabricatorPolicyFilter::requireCapability( 101 + $this->requireActor(), 102 + $object, 103 + PhabricatorPolicyCapability::CAN_EDIT); 104 + } 105 + return; 106 + } 107 + break; 108 + } 109 + 110 + return parent::requireCapabilities(); 111 + } 112 + 113 + }
+44 -10
src/applications/project/editor/__tests__/PhabricatorProjectEditorTestCase.php
··· 21 21 22 22 $proj = $this->refreshProject($proj, $user, true); 23 23 24 - PhabricatorProjectEditor::applyJoinProject($proj, $user); 24 + $this->joinProject($proj, $user); 25 25 $proj->setViewPolicy(PhabricatorPolicies::POLICY_USER); 26 26 $proj->save(); 27 27 ··· 123 123 'Arbitrary user not member of project.'); 124 124 125 125 // Join the project. 126 - PhabricatorProjectEditor::applyJoinProject($proj, $user); 126 + $this->joinProject($proj, $user); 127 127 128 128 $proj = $this->refreshProject($proj, $user, true); 129 129 $this->assertEqual(true, (bool)$proj); ··· 135 135 136 136 137 137 // Join the project again. 138 - PhabricatorProjectEditor::applyJoinProject($proj, $user); 138 + $this->joinProject($proj, $user); 139 139 140 140 $proj = $this->refreshProject($proj, $user, true); 141 141 $this->assertEqual(true, (bool)$proj); ··· 147 147 148 148 149 149 // Leave the project. 150 - PhabricatorProjectEditor::applyLeaveProject($proj, $user); 150 + $this->leaveProject($proj, $user); 151 151 152 152 $proj = $this->refreshProject($proj, $user, true); 153 153 $this->assertEqual(true, (bool)$proj); ··· 159 159 160 160 161 161 // Leave the project again. 162 - PhabricatorProjectEditor::applyLeaveProject($proj, $user); 162 + $this->leaveProject($proj, $user); 163 163 164 164 $proj = $this->refreshProject($proj, $user, true); 165 165 $this->assertEqual(true, (bool)$proj); ··· 178 178 $proj = $this->refreshProject($proj, $user, true); 179 179 $caught = null; 180 180 try { 181 - PhabricatorProjectEditor::applyJoinProject($proj, $user); 181 + $this->joinProject($proj, $user); 182 182 } catch (Exception $ex) { 183 183 $caught = $ex; 184 184 } ··· 191 191 $proj->save(); 192 192 193 193 $proj = $this->refreshProject($proj, $user, true); 194 - PhabricatorProjectEditor::applyJoinProject($proj, $user); 194 + $this->joinProject($proj, $user); 195 195 $proj = $this->refreshProject($proj, $user, true); 196 196 $this->assertEqual( 197 197 true, 198 198 $proj->isUserMember($user->getPHID()), 199 199 'Join allowed with edit permission.'); 200 - PhabricatorProjectEditor::applyLeaveProject($proj, $user); 200 + $this->leaveProject($proj, $user); 201 201 202 202 203 203 // If a user can join a project, they can join, even if they can't edit. ··· 206 206 $proj->save(); 207 207 208 208 $proj = $this->refreshProject($proj, $user, true); 209 - PhabricatorProjectEditor::applyJoinProject($proj, $user); 209 + $this->joinProject($proj, $user); 210 210 $proj = $this->refreshProject($proj, $user, true); 211 211 $this->assertEqual( 212 212 true, ··· 220 220 $proj->save(); 221 221 222 222 $proj = $this->refreshProject($proj, $user, true); 223 - PhabricatorProjectEditor::applyLeaveProject($proj, $user); 223 + $this->leaveProject($proj, $user); 224 224 $proj = $this->refreshProject($proj, $user, true); 225 225 $this->assertEqual( 226 226 false, ··· 271 271 $user->setRealName('Unit Test User '.$rand); 272 272 273 273 return $user; 274 + } 275 + 276 + private function joinProject( 277 + PhabricatorProject $project, 278 + PhabricatorUser $user) { 279 + $this->joinOrLeaveProject($project, $user, '+'); 280 + } 281 + 282 + private function leaveProject( 283 + PhabricatorProject $project, 284 + PhabricatorUser $user) { 285 + $this->joinOrLeaveProject($project, $user, '-'); 286 + } 287 + 288 + private function joinOrLeaveProject( 289 + PhabricatorProject $project, 290 + PhabricatorUser $user, 291 + $operation) { 292 + 293 + $spec = array( 294 + $operation => array($user->getPHID() => $user->getPHID()), 295 + ); 296 + 297 + $xactions = array(); 298 + $xactions[] = id(new PhabricatorProjectTransaction()) 299 + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) 300 + ->setMetadataValue('edge:type', PhabricatorEdgeConfig::TYPE_PROJ_MEMBER) 301 + ->setNewValue($spec); 302 + 303 + $editor = id(new PhabricatorProjectTransactionEditor()) 304 + ->setActor($user) 305 + ->setContentSource(PhabricatorContentSource::newConsoleSource()) 306 + ->setContinueOnNoEffect(true) 307 + ->applyTransactions($project, $xactions); 274 308 } 275 309 276 310 }