1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-02 03:32:42 +01:00

Implement basic object rules for Herald

Summary:
Ref T4264. Allows you to create "Object" rules, in addition to Global and Personal rules. If you choose to create an Object rule, you'll be prompted to select an object on a new screen. You must be able to edit and object in order to create rules for it.

Ref T3506. This makes "All" the default filter for the transcript view, which should reduce confusion on smaller installs.

Test Plan:
  - Created non-object rules.
  - Created object rules.
  - Triggered object rules against matching and unmatching objects.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T3506, T4264

Differential Revision: https://secure.phabricator.com/D7853
This commit is contained in:
epriestley 2013-12-30 16:48:14 -08:00
parent 472b0f983e
commit 140c88e971
11 changed files with 269 additions and 40 deletions

View file

@ -44,14 +44,33 @@ final class HeraldPreCommitContentAdapter extends HeraldAdapter {
public function supportsRuleType($rule_type) { public function supportsRuleType($rule_type) {
switch ($rule_type) { switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
return true; return true;
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
default: default:
return false; return false;
} }
} }
public function canTriggerOnObject($object) {
if ($object instanceof PhabricatorRepository) {
return true;
}
return false;
}
public function explainValidTriggerObjects() {
return pht(
'This rule can trigger for **repositories**.');
}
public function getTriggerObjectPHIDs() {
return array(
$this->hookEngine->getRepository()->getPHID(),
$this->getPHID(),
);
}
public function getFieldNameMap() { public function getFieldNameMap() {
return array( return array(
) + parent::getFieldNameMap(); ) + parent::getFieldNameMap();
@ -92,6 +111,7 @@ final class HeraldPreCommitContentAdapter extends HeraldAdapter {
public function getActions($rule_type) { public function getActions($rule_type) {
switch ($rule_type) { switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
return array( return array(
self::ACTION_BLOCK, self::ACTION_BLOCK,
self::ACTION_NOTHING self::ACTION_NOTHING

View file

@ -47,14 +47,33 @@ final class HeraldPreCommitRefAdapter extends HeraldAdapter {
public function supportsRuleType($rule_type) { public function supportsRuleType($rule_type) {
switch ($rule_type) { switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
return true; return true;
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
default: default:
return false; return false;
} }
} }
public function canTriggerOnObject($object) {
if ($object instanceof PhabricatorRepository) {
return true;
}
return false;
}
public function explainValidTriggerObjects() {
return pht(
'This rule can trigger for **repositories**.');
}
public function getTriggerObjectPHIDs() {
return array(
$this->hookEngine->getRepository()->getPHID(),
$this->getPHID(),
);
}
public function getFieldNameMap() { public function getFieldNameMap() {
return array( return array(
self::FIELD_REF_TYPE => pht('Ref type'), self::FIELD_REF_TYPE => pht('Ref type'),
@ -103,6 +122,7 @@ final class HeraldPreCommitRefAdapter extends HeraldAdapter {
public function getActions($rule_type) { public function getActions($rule_type) {
switch ($rule_type) { switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
return array( return array(
self::ACTION_BLOCK, self::ACTION_BLOCK,
self::ACTION_NOTHING self::ACTION_NOTHING

View file

@ -144,6 +144,18 @@ abstract class HeraldAdapter {
return false; return false;
} }
public function canTriggerOnObject($object) {
return false;
}
public function explainValidTriggerObjects() {
return pht('This adapter can not trigger on objects.');
}
public function getTriggerObjectPHIDs() {
return array($this->getPHID());
}
public function getAdapterSortKey() { public function getAdapterSortKey() {
return sprintf( return sprintf(
'%08d%s', '%08d%s',
@ -577,6 +589,7 @@ abstract class HeraldAdapter {
public function getActionNameMap($rule_type) { public function getActionNameMap($rule_type) {
switch ($rule_type) { switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
return array( return array(
self::ACTION_NOTHING => pht('Do nothing'), self::ACTION_NOTHING => pht('Do nothing'),
self::ACTION_ADD_CC => pht('Add emails to CC'), self::ACTION_ADD_CC => pht('Add emails to CC'),
@ -1002,6 +1015,11 @@ abstract class HeraldAdapter {
} }
} }
} }
if ($rule->isObjectRule()) {
$phids[] = $rule->getTriggerObjectPHID();
}
return $phids; return $phids;
} }

View file

@ -53,13 +53,32 @@ final class HeraldCommitAdapter extends HeraldAdapter {
switch ($rule_type) { switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
return true;
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
return true;
default: default:
return false; return false;
} }
} }
public function canTriggerOnObject($object) {
if ($object instanceof PhabricatorRepository) {
return true;
}
return false;
}
public function getTriggerObjectPHIDs() {
return array(
$this->repository->getPHID(),
$this->getPHID(),
);
}
public function explainValidTriggerObjects() {
return pht(
'This rule can trigger for **repositories**.');
}
public function getFieldNameMap() { public function getFieldNameMap() {
return array( return array(
self::FIELD_NEED_AUDIT_FOR_PACKAGE => self::FIELD_NEED_AUDIT_FOR_PACKAGE =>
@ -111,6 +130,7 @@ final class HeraldCommitAdapter extends HeraldAdapter {
public function getActions($rule_type) { public function getActions($rule_type) {
switch ($rule_type) { switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
return array( return array(
self::ACTION_ADD_CC, self::ACTION_ADD_CC,
self::ACTION_EMAIL, self::ACTION_EMAIL,

View file

@ -4,19 +4,19 @@ final class HeraldNewController extends HeraldController {
public function processRequest() { public function processRequest() {
$request = $this->getRequest(); $request = $this->getRequest();
$user = $request->getUser(); $viewer = $request->getUser();
$content_type_map = HeraldAdapter::getEnabledAdapterMap($user); $content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer);
$rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap(); $rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap();
$errors = array(); $errors = array();
$e_type = null; $e_type = null;
$e_rule = null; $e_rule = null;
$e_object = null;
$step = 0; $step = $request->getInt('step');
if ($request->isFormPost()) { if ($request->isFormPost()) {
$step = $request->getInt('step');
$content_type = $request->getStr('content_type'); $content_type = $request->getStr('content_type');
if (empty($content_type_map[$content_type])) { if (empty($content_type_map[$content_type])) {
$errors[] = pht('You must choose a content type for this rule.'); $errors[] = pht('You must choose a content type for this rule.');
@ -33,24 +33,77 @@ final class HeraldNewController extends HeraldController {
} }
} }
if (!$errors && $step == 2) { if (!$errors && $step >= 2) {
$uri = id(new PhutilURI('edit/')) $target_phid = null;
->setQueryParams( $object_name = $request->getStr('objectName');
array( $done = false;
'content_type' => $content_type, if ($rule_type != HeraldRuleTypeConfig::RULE_TYPE_OBJECT) {
'rule_type' => $rule_type, $done = true;
)); } else if (strlen($object_name)) {
$uri = $this->getApplicationURI($uri); $target_object = id(new PhabricatorObjectQuery())
return id(new AphrontRedirectResponse())->setURI($uri); ->setViewer($viewer)
->withNames(array($object_name))
->executeOne();
if ($target_object) {
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$target_object,
PhabricatorPolicyCapability::CAN_EDIT);
if (!$can_edit) {
$errors[] = pht(
'You can not create a rule for that object, because you do '.
'not have permission to edit it. You can only create rules '.
'for objects you can edit.');
$e_object = pht('Not Editable');
$step = 2;
} else {
$adapter = HeraldAdapter::getAdapterForContentType($content_type);
if (!$adapter->canTriggerOnObject($target_object)) {
$errors[] = pht(
'This object is not of an allowed type for the rule. '.
'Rules can only trigger on certain objects.');
$e_object = pht('Invalid');
$step = 2;
} else {
$target_phid = $target_object->getPHID();
$done = true;
}
}
} else {
$errors[] = pht('No object exists by that name.');
$e_object = pht('Invalid');
$step = 2;
}
} else if ($step > 2) {
$errors[] = pht(
'You must choose an object to associate this rule with.');
$e_object = pht('Required');
$step = 2;
}
if (!$errors && $done) {
$uri = id(new PhutilURI('edit/'))
->setQueryParams(
array(
'content_type' => $content_type,
'rule_type' => $rule_type,
'targetPHID' => $target_phid,
));
$uri = $this->getApplicationURI($uri);
return id(new AphrontRedirectResponse())->setURI($uri);
}
} }
} }
$content_type = $request->getStr('content_type');
$rule_type = $request->getStr('rule_type');
if ($errors) { if ($errors) {
$errors = id(new AphrontErrorView())->setErrors($errors); $errors = id(new AphrontErrorView())->setErrors($errors);
} }
$form = id(new AphrontFormView()) $form = id(new AphrontFormView())
->setUser($user) ->setUser($viewer)
->setAction($this->getApplicationURI('new/')); ->setAction($this->getApplicationURI('new/'));
switch ($step) { switch ($step) {
@ -73,7 +126,7 @@ final class HeraldNewController extends HeraldController {
$e_rule); $e_rule);
$form $form
->addHiddenInput('content_type', $request->getStr('content_type')) ->addHiddenInput('content_type', $content_type)
->addHiddenInput('step', 2) ->addHiddenInput('step', 2)
->appendChild( ->appendChild(
id(new AphrontFormStaticControl()) id(new AphrontFormStaticControl())
@ -89,7 +142,52 @@ final class HeraldNewController extends HeraldController {
$cancel_uri = id(new PhutilURI('new/')) $cancel_uri = id(new PhutilURI('new/'))
->setQueryParams( ->setQueryParams(
array( array(
'content_type' => $request->getStr('content_type'), 'content_type' => $content_type,
'step' => 0,
));
$cancel_uri = $this->getApplicationURI($cancel_uri);
break;
case 2:
$adapter = HeraldAdapter::getAdapterForContentType($content_type);
$form
->addHiddenInput('content_type', $content_type)
->addHiddenInput('rule_type', $rule_type)
->addHiddenInput('step', 3)
->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Rule for'))
->setValue(
phutil_tag(
'strong',
array(),
idx($content_type_map, $content_type))))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Rule Type'))
->setValue(
phutil_tag(
'strong',
array(),
idx($rule_type_map, $rule_type))))
->appendRemarkupInstructions(
pht(
'Choose the object this rule will act on (for example, enter '.
'`rX` to act on the `rX` repository).'))
->appendRemarkupInstructions(
$adapter->explainValidTriggerObjects())
->appendChild(
id(new AphrontFormTextControl())
->setName('objectName')
->setError($e_object)
->setValue($request->getStr('objectName'))
->setLabel(pht('Object')));
$cancel_text = pht('Back');
$cancel_uri = id(new PhutilURI('new/'))
->setQueryParams(
array(
'content_type' => $content_type,
'rule_type' => $rule_type,
'step' => 1, 'step' => 1,
)); ));
$cancel_uri = $this->getApplicationURI($cancel_uri); $cancel_uri = $this->getApplicationURI($cancel_uri);
@ -156,9 +254,6 @@ final class HeraldNewController extends HeraldController {
HeraldRuleTypeConfig::RULE_TYPE_GLOBAL, HeraldRuleTypeConfig::RULE_TYPE_GLOBAL,
)) + $rule_type_map; )) + $rule_type_map;
// TODO: Enable this.
unset($rule_type_map[HeraldRuleTypeConfig::RULE_TYPE_OBJECT]);
list($can_global, $global_link) = $this->explainApplicationCapability( list($can_global, $global_link) = $this->explainApplicationCapability(
HeraldCapabilityManageGlobalRules::CAPABILITY, HeraldCapabilityManageGlobalRules::CAPABILITY,
pht('You have permission to create and manage global rules.'), pht('You have permission to create and manage global rules.'),
@ -170,6 +265,12 @@ final class HeraldNewController extends HeraldController {
'Personal rules notify you about events. You own them, but they can '. 'Personal rules notify you about events. You own them, but they can '.
'only affect you. Personal rules only trigger for objects you have '. 'only affect you. Personal rules only trigger for objects you have '.
'permission to see.'), 'permission to see.'),
HeraldRuleTypeConfig::RULE_TYPE_OBJECT =>
pht(
'Object rules notify anyone about events. They are bound to an '.
'object (like a repository) and can only act on that object. You '.
'must be able to edit an object to create object rules for it. '.
'Other users who an edit the object can edit its rules.'),
HeraldRuleTypeConfig::RULE_TYPE_GLOBAL => HeraldRuleTypeConfig::RULE_TYPE_GLOBAL =>
array( array(
pht( pht(
@ -180,7 +281,7 @@ final class HeraldNewController extends HeraldController {
); );
$radio = id(new AphrontFormRadioButtonControl()) $radio = id(new AphrontFormRadioButtonControl())
->setLabel(pht('Type')) ->setLabel(pht('Rule Type'))
->setName('rule_type') ->setName('rule_type')
->setValue($request->getStr('rule_type')) ->setValue($request->getStr('rule_type'))
->setError($e_rule); ->setError($e_rule);

View file

@ -46,6 +46,38 @@ final class HeraldRuleController extends HeraldController {
} }
$rule->setRuleType($rule_type); $rule->setRuleType($rule_type);
$adapter = HeraldAdapter::getAdapterForContentType(
$rule->getContentType());
if (!$adapter->supportsRuleType($rule->getRuleType())) {
throw new Exception(
pht(
"This rule's content type does not support the selected rule ".
"type."));
}
if ($rule->isObjectRule()) {
$rule->setTriggerObjectPHID($request->getStr('targetPHID'));
$object = id(new PhabricatorObjectQuery())
->setViewer($user)
->withPHIDs(array($rule->getTriggerObjectPHID()))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$object) {
throw new Exception(
pht('No valid object provided for object rule!'));
}
if (!$adapter->canTriggerOnObject($object)) {
throw new Exception(
pht('Object is of wrong type for adapter!'));
}
}
$cancel_uri = $this->getApplicationURI(); $cancel_uri = $this->getApplicationURI();
} }
@ -65,12 +97,6 @@ final class HeraldRuleController extends HeraldController {
"deployment.")); "deployment."));
} }
if (!$adapter->supportsRuleType($rule->getRuleType())) {
throw new Exception(
pht(
"This rule's content type does not support the selected rule type."));
}
// Upgrade rule version to our version, since we might add newly-defined // Upgrade rule version to our version, since we might add newly-defined
// conditions, etc. // conditions, etc.
$rule->setConfigVersion($local_version); $rule->setConfigVersion($local_version);
@ -133,6 +159,16 @@ final class HeraldRuleController extends HeraldController {
->setError($e_name) ->setError($e_name)
->setValue($rule->getName())); ->setValue($rule->getName()));
$trigger_object_control = false;
if ($rule->isObjectRule()) {
$trigger_object_control = id(new AphrontFormStaticControl())
->setValue(
pht(
'This rule triggers for %s.',
$handles[$rule->getTriggerObjectPHID()]->renderLink()));
}
$form $form
->appendChild( ->appendChild(
id(new AphrontFormMarkupControl()) id(new AphrontFormMarkupControl())
@ -140,6 +176,7 @@ final class HeraldRuleController extends HeraldController {
"This %s rule triggers for %s.", "This %s rule triggers for %s.",
phutil_tag('strong', array(), $rule_type_name), phutil_tag('strong', array(), $rule_type_name),
phutil_tag('strong', array(), $content_type_name)))) phutil_tag('strong', array(), $content_type_name))))
->appendChild($trigger_object_control)
->appendChild( ->appendChild(
id(new AphrontFormInsetView()) id(new AphrontFormInsetView())
->setTitle(pht('Conditions')) ->setTitle(pht('Conditions'))
@ -489,6 +526,10 @@ final class HeraldRuleController extends HeraldController {
$phids[] = $rule->getAuthorPHID(); $phids[] = $rule->getAuthorPHID();
if ($rule->isObjectRule()) {
$phids[] = $rule->getTriggerObjectPHID();
}
return $this->loadViewerHandles($phids); return $this->loadViewerHandles($phids);
} }

View file

@ -131,6 +131,7 @@ final class HeraldRuleViewController extends HeraldController {
$this->getHandle($rule->getAuthorPHID())->renderLink()); $this->getHandle($rule->getAuthorPHID())->renderLink());
} }
$adapter = HeraldAdapter::getAdapterForContentType($rule->getContentType()); $adapter = HeraldAdapter::getAdapterForContentType($rule->getContentType());
if ($adapter) { if ($adapter) {
$view->addProperty( $view->addProperty(
@ -139,6 +140,12 @@ final class HeraldRuleViewController extends HeraldController {
HeraldAdapter::getEnabledAdapterMap($viewer), HeraldAdapter::getEnabledAdapterMap($viewer),
$rule->getContentType())); $rule->getContentType()));
if ($rule->isObjectRule()) {
$view->addProperty(
pht('Trigger Object'),
$this->getHandle($rule->getTriggerObjectPHID())->renderLink());
}
$view->invokeWillRenderEvent(); $view->invokeWillRenderEvent();
$view->addSectionHeader(pht('Rule Description')); $view->addSectionHeader(pht('Rule Description'));

View file

@ -16,7 +16,7 @@ final class HeraldTranscriptController extends HeraldController {
$map = $this->getFilterMap(); $map = $this->getFilterMap();
$this->filter = idx($data, 'filter'); $this->filter = idx($data, 'filter');
if (empty($map[$this->filter])) { if (empty($map[$this->filter])) {
$this->filter = self::FILTER_AFFECTED; $this->filter = self::FILTER_ALL;
} }
} }
@ -149,9 +149,9 @@ final class HeraldTranscriptController extends HeraldController {
protected function getFilterMap() { protected function getFilterMap() {
return array( return array(
self::FILTER_AFFECTED => pht('Rules that Affected Me'),
self::FILTER_OWNED => pht('Rules I Own'),
self::FILTER_ALL => pht('All Rules'), self::FILTER_ALL => pht('All Rules'),
self::FILTER_OWNED => pht('Rules I Own'),
self::FILTER_AFFECTED => pht('Rules that Affected Me'),
); );
} }

View file

@ -420,14 +420,14 @@ final class HeraldEngine {
} }
$trigger_phid = $rule->getTriggerObjectPHID(); $trigger_phid = $rule->getTriggerObjectPHID();
$object_phid = $adapter->getPHID(); $object_phids = $adapter->getTriggerObjectPHIDs();
if ($trigger_phid == $object_phid) { if ($object_phids) {
return true; if (in_array($trigger_phid, $object_phids)) {
return true;
}
} }
// TODO: We should also handle projects.
return false; return false;
} }

View file

@ -17,7 +17,7 @@ final class HeraldRule extends HeraldDAO
protected $isDisabled = 0; protected $isDisabled = 0;
protected $triggerObjectPHID; protected $triggerObjectPHID;
protected $configVersion = 22; protected $configVersion = 23;
// phids for which this rule has been applied // phids for which this rule has been applied
private $ruleApplied = self::ATTACHABLE; private $ruleApplied = self::ATTACHABLE;

View file

@ -36,8 +36,10 @@ final class PhabricatorRepositoryCommitHeraldWorker
$commit->getID()); $commit->getID());
if (!$data) { if (!$data) {
// TODO: Permanent failure. throw new PhabricatorWorkerPermanentFailureException(
return; pht(
'Unable to load commit data. The data for this task is invalid '.
'or no longer exists.'));
} }
$adapter = HeraldCommitAdapter::newLegacyAdapter( $adapter = HeraldCommitAdapter::newLegacyAdapter(