diff --git a/resources/sql/autopatches/20140917.project.canlock.sql b/resources/sql/autopatches/20140917.project.canlock.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20140917.project.canlock.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_project.project + ADD isMembershipLocked TINYINT(1) NOT NULL DEFAULT 0 AFTER joinPolicy; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2682,6 +2682,7 @@ 'PonderVoteEditor' => 'applications/ponder/editor/PonderVoteEditor.php', 'PonderVoteSaveController' => 'applications/ponder/controller/PonderVoteSaveController.php', 'ProjectBoardTaskCard' => 'applications/project/view/ProjectBoardTaskCard.php', + 'ProjectCanLockProjectsCapability' => 'applications/project/capability/ProjectCanLockProjectsCapability.php', 'ProjectConduitAPIMethod' => 'applications/project/conduit/ProjectConduitAPIMethod.php', 'ProjectCreateConduitAPIMethod' => 'applications/project/conduit/ProjectCreateConduitAPIMethod.php', 'ProjectCreateProjectsCapability' => 'applications/project/capability/ProjectCreateProjectsCapability.php', @@ -5722,6 +5723,7 @@ 'PonderVote' => 'PonderConstants', 'PonderVoteEditor' => 'PhabricatorEditor', 'PonderVoteSaveController' => 'PonderController', + 'ProjectCanLockProjectsCapability' => 'PhabricatorPolicyCapability', 'ProjectConduitAPIMethod' => 'ConduitAPIMethod', 'ProjectCreateConduitAPIMethod' => 'ProjectConduitAPIMethod', 'ProjectCreateProjectsCapability' => 'PhabricatorPolicyCapability', diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -93,6 +93,9 @@ protected function getCustomCapabilities() { return array( ProjectCreateProjectsCapability::CAPABILITY => array(), + ProjectCanLockProjectsCapability::CAPABILITY => array( + 'default' => PhabricatorPolicies::POLICY_ADMIN, + ), ); } diff --git a/src/applications/project/capability/ProjectCanLockProjectsCapability.php b/src/applications/project/capability/ProjectCanLockProjectsCapability.php new file mode 100644 --- /dev/null +++ b/src/applications/project/capability/ProjectCanLockProjectsCapability.php @@ -0,0 +1,16 @@ +getColor(); $v_icon = $project->getIcon(); + $v_locked = $project->getIsMembershipLocked(); $validation_exception = null; @@ -63,6 +64,7 @@ $v_join = $request->getStr('can_join'); $v_color = $request->getStr('color'); $v_icon = $request->getStr('icon'); + $v_locked = $request->getInt('is_membership_locked', 0); $xactions = $field_list->buildFieldTransactionsFromRequest( new PhabricatorProjectTransaction(), @@ -73,6 +75,7 @@ $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY; $type_icon = PhabricatorProjectTransaction::TYPE_ICON; $type_color = PhabricatorProjectTransaction::TYPE_COLOR; + $type_locked = PhabricatorProjectTransaction::TYPE_LOCKED; $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType($type_name) @@ -102,6 +105,10 @@ ->setTransactionType($type_color) ->setNewValue($v_color); + $xactions[] = id(new PhabricatorProjectTransaction()) + ->setTransactionType($type_locked) + ->setNewValue($v_locked); + $editor = id(new PhabricatorProjectTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) @@ -148,6 +155,10 @@ $icon_uri = $this->getApplicationURI('icon/'.$project->getID().'/'); $icon_display = PhabricatorProjectIcon::renderIconForChooser($v_icon); + list($can_lock, $lock_message) = $this->explainApplicationCapability( + ProjectCanLockProjectsCapability::CAPABILITY, + pht('You can update the Lock Project setting.'), + pht('You can not update the Lock Project setting.')); $form ->appendChild( @@ -202,6 +213,16 @@ ->setPolicies($policies) ->setCapability(PhabricatorPolicyCapability::CAN_JOIN)) ->appendChild( + id(new AphrontFormCheckboxControl()) + ->setLabel(pht('Lock Project')) + ->setDisabled(!$can_lock) + ->addCheckbox( + 'is_membership_locked', + 1, + pht('Prevent members from leaving this project.'), + $v_locked) + ->setCaption($lock_message)) + ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($edit_uri) ->setValue(pht('Save'))); diff --git a/src/applications/project/controller/PhabricatorProjectUpdateController.php b/src/applications/project/controller/PhabricatorProjectUpdateController.php --- a/src/applications/project/controller/PhabricatorProjectUpdateController.php +++ b/src/applications/project/controller/PhabricatorProjectUpdateController.php @@ -82,12 +82,18 @@ case 'leave': $dialog = new AphrontDialogView(); $dialog->setUser($user); - $dialog->setTitle(pht('Really leave project?')); - $dialog->appendChild(phutil_tag('p', array(), pht( - 'Your tremendous contributions to this project will be sorely '. - 'missed. Are you sure you want to leave?'))); + if ($this->userCannotLeave($project)) { + $dialog->setTitle(pht('You can not leave this project.')); + $body = pht('The membership is locked for this project.'); + } else { + $dialog->setTitle(pht('Really leave project?')); + $body = pht( + 'Your tremendous contributions to this project will be sorely '. + 'missed. Are you sure you want to leave?'); + $dialog->addSubmitButton(pht('Leave Project')); + } + $dialog->appendParagraph($body); $dialog->addCancelButton($project_uri); - $dialog->addSubmitButton(pht('Leave Project')); break; default: return new Aphront404Response(); @@ -96,4 +102,18 @@ return id(new AphrontDialogResponse())->setDialog($dialog); } + /** + * This is enforced in @{class:PhabricatorProjectTransactionEditor}. We use + * this logic to render a better form for users hitting this case. + */ + private function userCannotLeave(PhabricatorProject $project) { + $user = $this->getRequest()->getUser(); + + return + $project->getIsMembershipLocked() && + !PhabricatorPolicyFilter::hasCapability( + $user, + $project, + PhabricatorPolicyCapability::CAN_EDIT); + } } diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php --- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php +++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php @@ -25,6 +25,7 @@ $types[] = PhabricatorProjectTransaction::TYPE_IMAGE; $types[] = PhabricatorProjectTransaction::TYPE_ICON; $types[] = PhabricatorProjectTransaction::TYPE_COLOR; + $types[] = PhabricatorProjectTransaction::TYPE_LOCKED; return $types; } @@ -49,6 +50,8 @@ return $object->getIcon(); case PhabricatorProjectTransaction::TYPE_COLOR: return $object->getColor(); + case PhabricatorProjectTransaction::TYPE_LOCKED: + return (int) $object->getIsMembershipLocked(); } return parent::getCustomTransactionOldValue($object, $xaction); @@ -65,6 +68,7 @@ case PhabricatorProjectTransaction::TYPE_IMAGE: case PhabricatorProjectTransaction::TYPE_ICON: case PhabricatorProjectTransaction::TYPE_COLOR: + case PhabricatorProjectTransaction::TYPE_LOCKED: return $xaction->getNewValue(); } @@ -94,6 +98,9 @@ case PhabricatorProjectTransaction::TYPE_COLOR: $object->setColor($xaction->getNewValue()); return; + case PhabricatorProjectTransaction::TYPE_LOCKED: + $object->setIsMembershipLocked($xaction->getNewValue()); + return; case PhabricatorTransactions::TYPE_EDGE: return; case PhabricatorTransactions::TYPE_VIEW_POLICY: @@ -199,6 +206,7 @@ case PhabricatorProjectTransaction::TYPE_IMAGE: case PhabricatorProjectTransaction::TYPE_ICON: case PhabricatorProjectTransaction::TYPE_COLOR: + case PhabricatorProjectTransaction::TYPE_LOCKED: return; case PhabricatorTransactions::TYPE_EDGE: $edge_type = $xaction->getMetadataValue('edge:type'); @@ -360,6 +368,7 @@ } break; + } return $errors; @@ -381,6 +390,12 @@ $object, PhabricatorPolicyCapability::CAN_EDIT); return; + case PhabricatorProjectTransaction::TYPE_LOCKED: + PhabricatorPolicyFilter::requireCapability( + $this->requireActor(), + newv($this->getEditorApplicationClass(), array()), + ProjectCanLockProjectsCapability::CAPABILITY); + return; case PhabricatorTransactions::TYPE_EDGE: switch ($xaction->getMetadataValue('edge:type')) { case PhabricatorEdgeConfig::TYPE_PROJ_MEMBER: @@ -402,7 +417,14 @@ $object, PhabricatorPolicyCapability::CAN_JOIN); } else if ($is_leave) { - // You don't need any capabilities to leave a project. + // You usually don't need any capabilities to leave a project. + if ($object->getIsMembershipLocked()) { + // you must be able to edit though to leave locked projects + PhabricatorPolicyFilter::requireCapability( + $this->requireActor(), + $object, + PhabricatorPolicyCapability::CAN_EDIT); + } } else { // You need CAN_EDIT to change members other than yourself. PhabricatorPolicyFilter::requireCapability( diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -20,6 +20,7 @@ protected $viewPolicy; protected $editPolicy; protected $joinPolicy; + protected $isMembershipLocked; private $memberPHIDs = self::ATTACHABLE; private $watcherPHIDs = self::ATTACHABLE; @@ -43,6 +44,7 @@ ->setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy(PhabricatorPolicies::POLICY_USER) ->setJoinPolicy(PhabricatorPolicies::POLICY_USER) + ->setIsMembershipLocked(0) ->attachMemberPHIDs(array()); } diff --git a/src/applications/project/storage/PhabricatorProjectTransaction.php b/src/applications/project/storage/PhabricatorProjectTransaction.php --- a/src/applications/project/storage/PhabricatorProjectTransaction.php +++ b/src/applications/project/storage/PhabricatorProjectTransaction.php @@ -9,6 +9,7 @@ const TYPE_IMAGE = 'project:image'; const TYPE_ICON = 'project:icon'; const TYPE_COLOR = 'project:color'; + const TYPE_LOCKED = 'project:locked'; // NOTE: This is deprecated, members are just a normal edge now. const TYPE_MEMBERS = 'project:members'; @@ -100,6 +101,17 @@ $author_handle, PHUITagView::getShadeName($new)); + case PhabricatorProjectTransaction::TYPE_LOCKED: + if ($new) { + return pht( + '%s locked this project\'s membership.', + $author_handle); + } else { + return pht( + '%s unlocked this project\'s membership.', + $author_handle); + } + case PhabricatorProjectTransaction::TYPE_SLUGS: $add = array_diff($new, $old); $rem = array_diff($old, $new);