Projects - add "lock membership", which prevents people from leaving
Summary: Fixes T5603. Puts the toggling of locking membership into the editor so we get exceptions and all that. I think the dialogue when you try to leave a project that is locked could be a little better maybe? Right now it just says "You can't leave" and "The membership is locked" more or less; should I surface a link to the policy stuff there too? Test Plan: - made a project, toggled the "lock" setting, observed stickiness and good transactions being made - locked a project and tried to leave as a non-editor - got a dialogue letting me know i couldn't - locked a project and tried to leave as an editor - left successfully Reviewers: epriestley Reviewed By: epriestley Subscribers: epriestley, Korvin Maniphest Tasks: T5603 Differential Revision: https://secure.phabricator.com/D10508
This commit is contained in:
2
resources/sql/autopatches/20140917.project.canlock.sql
Normal file
2
resources/sql/autopatches/20140917.project.canlock.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE {$NAMESPACE}_project.project
|
||||||
|
ADD isMembershipLocked TINYINT(1) NOT NULL DEFAULT 0 AFTER joinPolicy;
|
||||||
@@ -2682,6 +2682,7 @@ phutil_register_library_map(array(
|
|||||||
'PonderVoteEditor' => 'applications/ponder/editor/PonderVoteEditor.php',
|
'PonderVoteEditor' => 'applications/ponder/editor/PonderVoteEditor.php',
|
||||||
'PonderVoteSaveController' => 'applications/ponder/controller/PonderVoteSaveController.php',
|
'PonderVoteSaveController' => 'applications/ponder/controller/PonderVoteSaveController.php',
|
||||||
'ProjectBoardTaskCard' => 'applications/project/view/ProjectBoardTaskCard.php',
|
'ProjectBoardTaskCard' => 'applications/project/view/ProjectBoardTaskCard.php',
|
||||||
|
'ProjectCanLockProjectsCapability' => 'applications/project/capability/ProjectCanLockProjectsCapability.php',
|
||||||
'ProjectConduitAPIMethod' => 'applications/project/conduit/ProjectConduitAPIMethod.php',
|
'ProjectConduitAPIMethod' => 'applications/project/conduit/ProjectConduitAPIMethod.php',
|
||||||
'ProjectCreateConduitAPIMethod' => 'applications/project/conduit/ProjectCreateConduitAPIMethod.php',
|
'ProjectCreateConduitAPIMethod' => 'applications/project/conduit/ProjectCreateConduitAPIMethod.php',
|
||||||
'ProjectCreateProjectsCapability' => 'applications/project/capability/ProjectCreateProjectsCapability.php',
|
'ProjectCreateProjectsCapability' => 'applications/project/capability/ProjectCreateProjectsCapability.php',
|
||||||
@@ -5722,6 +5723,7 @@ phutil_register_library_map(array(
|
|||||||
'PonderVote' => 'PonderConstants',
|
'PonderVote' => 'PonderConstants',
|
||||||
'PonderVoteEditor' => 'PhabricatorEditor',
|
'PonderVoteEditor' => 'PhabricatorEditor',
|
||||||
'PonderVoteSaveController' => 'PonderController',
|
'PonderVoteSaveController' => 'PonderController',
|
||||||
|
'ProjectCanLockProjectsCapability' => 'PhabricatorPolicyCapability',
|
||||||
'ProjectConduitAPIMethod' => 'ConduitAPIMethod',
|
'ProjectConduitAPIMethod' => 'ConduitAPIMethod',
|
||||||
'ProjectCreateConduitAPIMethod' => 'ProjectConduitAPIMethod',
|
'ProjectCreateConduitAPIMethod' => 'ProjectConduitAPIMethod',
|
||||||
'ProjectCreateProjectsCapability' => 'PhabricatorPolicyCapability',
|
'ProjectCreateProjectsCapability' => 'PhabricatorPolicyCapability',
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ final class PhabricatorProjectApplication extends PhabricatorApplication {
|
|||||||
protected function getCustomCapabilities() {
|
protected function getCustomCapabilities() {
|
||||||
return array(
|
return array(
|
||||||
ProjectCreateProjectsCapability::CAPABILITY => array(),
|
ProjectCreateProjectsCapability::CAPABILITY => array(),
|
||||||
|
ProjectCanLockProjectsCapability::CAPABILITY => array(
|
||||||
|
'default' => PhabricatorPolicies::POLICY_ADMIN,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
final class ProjectCanLockProjectsCapability
|
||||||
|
extends PhabricatorPolicyCapability {
|
||||||
|
|
||||||
|
const CAPABILITY = 'project.can.lock';
|
||||||
|
|
||||||
|
public function getCapabilityName() {
|
||||||
|
return pht('Can Lock Project Membership');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function describeCapabilityRejection() {
|
||||||
|
return pht('You do not have permission to lock project membership.');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -49,6 +49,7 @@ final class PhabricatorProjectEditDetailsController
|
|||||||
$v_slugs = $project_slugs;
|
$v_slugs = $project_slugs;
|
||||||
$v_color = $project->getColor();
|
$v_color = $project->getColor();
|
||||||
$v_icon = $project->getIcon();
|
$v_icon = $project->getIcon();
|
||||||
|
$v_locked = $project->getIsMembershipLocked();
|
||||||
|
|
||||||
$validation_exception = null;
|
$validation_exception = null;
|
||||||
|
|
||||||
@@ -63,6 +64,7 @@ final class PhabricatorProjectEditDetailsController
|
|||||||
$v_join = $request->getStr('can_join');
|
$v_join = $request->getStr('can_join');
|
||||||
$v_color = $request->getStr('color');
|
$v_color = $request->getStr('color');
|
||||||
$v_icon = $request->getStr('icon');
|
$v_icon = $request->getStr('icon');
|
||||||
|
$v_locked = $request->getInt('is_membership_locked', 0);
|
||||||
|
|
||||||
$xactions = $field_list->buildFieldTransactionsFromRequest(
|
$xactions = $field_list->buildFieldTransactionsFromRequest(
|
||||||
new PhabricatorProjectTransaction(),
|
new PhabricatorProjectTransaction(),
|
||||||
@@ -73,6 +75,7 @@ final class PhabricatorProjectEditDetailsController
|
|||||||
$type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY;
|
$type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY;
|
||||||
$type_icon = PhabricatorProjectTransaction::TYPE_ICON;
|
$type_icon = PhabricatorProjectTransaction::TYPE_ICON;
|
||||||
$type_color = PhabricatorProjectTransaction::TYPE_COLOR;
|
$type_color = PhabricatorProjectTransaction::TYPE_COLOR;
|
||||||
|
$type_locked = PhabricatorProjectTransaction::TYPE_LOCKED;
|
||||||
|
|
||||||
$xactions[] = id(new PhabricatorProjectTransaction())
|
$xactions[] = id(new PhabricatorProjectTransaction())
|
||||||
->setTransactionType($type_name)
|
->setTransactionType($type_name)
|
||||||
@@ -102,6 +105,10 @@ final class PhabricatorProjectEditDetailsController
|
|||||||
->setTransactionType($type_color)
|
->setTransactionType($type_color)
|
||||||
->setNewValue($v_color);
|
->setNewValue($v_color);
|
||||||
|
|
||||||
|
$xactions[] = id(new PhabricatorProjectTransaction())
|
||||||
|
->setTransactionType($type_locked)
|
||||||
|
->setNewValue($v_locked);
|
||||||
|
|
||||||
$editor = id(new PhabricatorProjectTransactionEditor())
|
$editor = id(new PhabricatorProjectTransactionEditor())
|
||||||
->setActor($viewer)
|
->setActor($viewer)
|
||||||
->setContentSourceFromRequest($request)
|
->setContentSourceFromRequest($request)
|
||||||
@@ -148,6 +155,10 @@ final class PhabricatorProjectEditDetailsController
|
|||||||
|
|
||||||
$icon_uri = $this->getApplicationURI('icon/'.$project->getID().'/');
|
$icon_uri = $this->getApplicationURI('icon/'.$project->getID().'/');
|
||||||
$icon_display = PhabricatorProjectIcon::renderIconForChooser($v_icon);
|
$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
|
$form
|
||||||
->appendChild(
|
->appendChild(
|
||||||
@@ -201,6 +212,16 @@ final class PhabricatorProjectEditDetailsController
|
|||||||
->setPolicyObject($project)
|
->setPolicyObject($project)
|
||||||
->setPolicies($policies)
|
->setPolicies($policies)
|
||||||
->setCapability(PhabricatorPolicyCapability::CAN_JOIN))
|
->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(
|
->appendChild(
|
||||||
id(new AphrontFormSubmitControl())
|
id(new AphrontFormSubmitControl())
|
||||||
->addCancelButton($edit_uri)
|
->addCancelButton($edit_uri)
|
||||||
|
|||||||
@@ -82,12 +82,18 @@ final class PhabricatorProjectUpdateController
|
|||||||
case 'leave':
|
case 'leave':
|
||||||
$dialog = new AphrontDialogView();
|
$dialog = new AphrontDialogView();
|
||||||
$dialog->setUser($user);
|
$dialog->setUser($user);
|
||||||
$dialog->setTitle(pht('Really leave project?'));
|
if ($this->userCannotLeave($project)) {
|
||||||
$dialog->appendChild(phutil_tag('p', array(), pht(
|
$dialog->setTitle(pht('You can not leave this project.'));
|
||||||
'Your tremendous contributions to this project will be sorely '.
|
$body = pht('The membership is locked for this project.');
|
||||||
'missed. Are you sure you want to leave?')));
|
} 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->addCancelButton($project_uri);
|
||||||
$dialog->addSubmitButton(pht('Leave Project'));
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return new Aphront404Response();
|
return new Aphront404Response();
|
||||||
@@ -96,4 +102,18 @@ final class PhabricatorProjectUpdateController
|
|||||||
return id(new AphrontDialogResponse())->setDialog($dialog);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ final class PhabricatorProjectTransactionEditor
|
|||||||
$types[] = PhabricatorProjectTransaction::TYPE_IMAGE;
|
$types[] = PhabricatorProjectTransaction::TYPE_IMAGE;
|
||||||
$types[] = PhabricatorProjectTransaction::TYPE_ICON;
|
$types[] = PhabricatorProjectTransaction::TYPE_ICON;
|
||||||
$types[] = PhabricatorProjectTransaction::TYPE_COLOR;
|
$types[] = PhabricatorProjectTransaction::TYPE_COLOR;
|
||||||
|
$types[] = PhabricatorProjectTransaction::TYPE_LOCKED;
|
||||||
|
|
||||||
return $types;
|
return $types;
|
||||||
}
|
}
|
||||||
@@ -49,6 +50,8 @@ final class PhabricatorProjectTransactionEditor
|
|||||||
return $object->getIcon();
|
return $object->getIcon();
|
||||||
case PhabricatorProjectTransaction::TYPE_COLOR:
|
case PhabricatorProjectTransaction::TYPE_COLOR:
|
||||||
return $object->getColor();
|
return $object->getColor();
|
||||||
|
case PhabricatorProjectTransaction::TYPE_LOCKED:
|
||||||
|
return (int) $object->getIsMembershipLocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
return parent::getCustomTransactionOldValue($object, $xaction);
|
return parent::getCustomTransactionOldValue($object, $xaction);
|
||||||
@@ -65,6 +68,7 @@ final class PhabricatorProjectTransactionEditor
|
|||||||
case PhabricatorProjectTransaction::TYPE_IMAGE:
|
case PhabricatorProjectTransaction::TYPE_IMAGE:
|
||||||
case PhabricatorProjectTransaction::TYPE_ICON:
|
case PhabricatorProjectTransaction::TYPE_ICON:
|
||||||
case PhabricatorProjectTransaction::TYPE_COLOR:
|
case PhabricatorProjectTransaction::TYPE_COLOR:
|
||||||
|
case PhabricatorProjectTransaction::TYPE_LOCKED:
|
||||||
return $xaction->getNewValue();
|
return $xaction->getNewValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +98,9 @@ final class PhabricatorProjectTransactionEditor
|
|||||||
case PhabricatorProjectTransaction::TYPE_COLOR:
|
case PhabricatorProjectTransaction::TYPE_COLOR:
|
||||||
$object->setColor($xaction->getNewValue());
|
$object->setColor($xaction->getNewValue());
|
||||||
return;
|
return;
|
||||||
|
case PhabricatorProjectTransaction::TYPE_LOCKED:
|
||||||
|
$object->setIsMembershipLocked($xaction->getNewValue());
|
||||||
|
return;
|
||||||
case PhabricatorTransactions::TYPE_EDGE:
|
case PhabricatorTransactions::TYPE_EDGE:
|
||||||
return;
|
return;
|
||||||
case PhabricatorTransactions::TYPE_VIEW_POLICY:
|
case PhabricatorTransactions::TYPE_VIEW_POLICY:
|
||||||
@@ -199,6 +206,7 @@ final class PhabricatorProjectTransactionEditor
|
|||||||
case PhabricatorProjectTransaction::TYPE_IMAGE:
|
case PhabricatorProjectTransaction::TYPE_IMAGE:
|
||||||
case PhabricatorProjectTransaction::TYPE_ICON:
|
case PhabricatorProjectTransaction::TYPE_ICON:
|
||||||
case PhabricatorProjectTransaction::TYPE_COLOR:
|
case PhabricatorProjectTransaction::TYPE_COLOR:
|
||||||
|
case PhabricatorProjectTransaction::TYPE_LOCKED:
|
||||||
return;
|
return;
|
||||||
case PhabricatorTransactions::TYPE_EDGE:
|
case PhabricatorTransactions::TYPE_EDGE:
|
||||||
$edge_type = $xaction->getMetadataValue('edge:type');
|
$edge_type = $xaction->getMetadataValue('edge:type');
|
||||||
@@ -360,6 +368,7 @@ final class PhabricatorProjectTransactionEditor
|
|||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $errors;
|
return $errors;
|
||||||
@@ -381,6 +390,12 @@ final class PhabricatorProjectTransactionEditor
|
|||||||
$object,
|
$object,
|
||||||
PhabricatorPolicyCapability::CAN_EDIT);
|
PhabricatorPolicyCapability::CAN_EDIT);
|
||||||
return;
|
return;
|
||||||
|
case PhabricatorProjectTransaction::TYPE_LOCKED:
|
||||||
|
PhabricatorPolicyFilter::requireCapability(
|
||||||
|
$this->requireActor(),
|
||||||
|
newv($this->getEditorApplicationClass(), array()),
|
||||||
|
ProjectCanLockProjectsCapability::CAPABILITY);
|
||||||
|
return;
|
||||||
case PhabricatorTransactions::TYPE_EDGE:
|
case PhabricatorTransactions::TYPE_EDGE:
|
||||||
switch ($xaction->getMetadataValue('edge:type')) {
|
switch ($xaction->getMetadataValue('edge:type')) {
|
||||||
case PhabricatorEdgeConfig::TYPE_PROJ_MEMBER:
|
case PhabricatorEdgeConfig::TYPE_PROJ_MEMBER:
|
||||||
@@ -402,7 +417,14 @@ final class PhabricatorProjectTransactionEditor
|
|||||||
$object,
|
$object,
|
||||||
PhabricatorPolicyCapability::CAN_JOIN);
|
PhabricatorPolicyCapability::CAN_JOIN);
|
||||||
} else if ($is_leave) {
|
} 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 {
|
} else {
|
||||||
// You need CAN_EDIT to change members other than yourself.
|
// You need CAN_EDIT to change members other than yourself.
|
||||||
PhabricatorPolicyFilter::requireCapability(
|
PhabricatorPolicyFilter::requireCapability(
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO
|
|||||||
protected $viewPolicy;
|
protected $viewPolicy;
|
||||||
protected $editPolicy;
|
protected $editPolicy;
|
||||||
protected $joinPolicy;
|
protected $joinPolicy;
|
||||||
|
protected $isMembershipLocked;
|
||||||
|
|
||||||
private $memberPHIDs = self::ATTACHABLE;
|
private $memberPHIDs = self::ATTACHABLE;
|
||||||
private $watcherPHIDs = self::ATTACHABLE;
|
private $watcherPHIDs = self::ATTACHABLE;
|
||||||
@@ -43,6 +44,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO
|
|||||||
->setViewPolicy(PhabricatorPolicies::POLICY_USER)
|
->setViewPolicy(PhabricatorPolicies::POLICY_USER)
|
||||||
->setEditPolicy(PhabricatorPolicies::POLICY_USER)
|
->setEditPolicy(PhabricatorPolicies::POLICY_USER)
|
||||||
->setJoinPolicy(PhabricatorPolicies::POLICY_USER)
|
->setJoinPolicy(PhabricatorPolicies::POLICY_USER)
|
||||||
|
->setIsMembershipLocked(0)
|
||||||
->attachMemberPHIDs(array());
|
->attachMemberPHIDs(array());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ final class PhabricatorProjectTransaction
|
|||||||
const TYPE_IMAGE = 'project:image';
|
const TYPE_IMAGE = 'project:image';
|
||||||
const TYPE_ICON = 'project:icon';
|
const TYPE_ICON = 'project:icon';
|
||||||
const TYPE_COLOR = 'project:color';
|
const TYPE_COLOR = 'project:color';
|
||||||
|
const TYPE_LOCKED = 'project:locked';
|
||||||
|
|
||||||
// NOTE: This is deprecated, members are just a normal edge now.
|
// NOTE: This is deprecated, members are just a normal edge now.
|
||||||
const TYPE_MEMBERS = 'project:members';
|
const TYPE_MEMBERS = 'project:members';
|
||||||
@@ -100,6 +101,17 @@ final class PhabricatorProjectTransaction
|
|||||||
$author_handle,
|
$author_handle,
|
||||||
PHUITagView::getShadeName($new));
|
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:
|
case PhabricatorProjectTransaction::TYPE_SLUGS:
|
||||||
$add = array_diff($new, $old);
|
$add = array_diff($new, $old);
|
||||||
$rem = array_diff($old, $new);
|
$rem = array_diff($old, $new);
|
||||||
|
|||||||
Reference in New Issue
Block a user