diff --git a/src/applications/herald/editor/HeraldRuleEditor.php b/src/applications/herald/editor/HeraldRuleEditor.php index 8c83ef6597..3ba5c4f8ac 100644 --- a/src/applications/herald/editor/HeraldRuleEditor.php +++ b/src/applications/herald/editor/HeraldRuleEditor.php @@ -1,140 +1,154 @@ getTransactionType()) { case HeraldRuleTransaction::TYPE_DISABLE: return (int)$object->getIsDisabled(); case HeraldRuleTransaction::TYPE_EDIT: return id(new HeraldRuleSerializer()) ->serializeRule($object); case HeraldRuleTransaction::TYPE_NAME: return $object->getName(); } } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case HeraldRuleTransaction::TYPE_DISABLE: return (int)$xaction->getNewValue(); case HeraldRuleTransaction::TYPE_EDIT: case HeraldRuleTransaction::TYPE_NAME: return $xaction->getNewValue(); } } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case HeraldRuleTransaction::TYPE_DISABLE: return $object->setIsDisabled($xaction->getNewValue()); case HeraldRuleTransaction::TYPE_NAME: return $object->setName($xaction->getNewValue()); case HeraldRuleTransaction::TYPE_EDIT: $new_state = id(new HeraldRuleSerializer()) ->deserializeRuleComponents($xaction->getNewValue()); $object->setMustMatchAll((int)$new_state['match_all']); $object->attachConditions($new_state['conditions']); $object->attachActions($new_state['actions']); $new_repetition = $new_state['repetition_policy']; $object->setRepetitionPolicyStringConstant($new_repetition); return $object; } } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case HeraldRuleTransaction::TYPE_EDIT: $object->saveConditions($object->getConditions()); $object->saveActions($object->getActions()); break; } return; } protected function shouldApplyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function buildHeraldAdapter( PhabricatorLiskDAO $object, array $xactions) { return id(new HeraldRuleAdapter()) ->setRule($object); } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function getMailTo(PhabricatorLiskDAO $object) { $phids = array(); $phids[] = $this->getActingAsPHID(); if ($object->isPersonalRule()) { $phids[] = $object->getAuthorPHID(); } return $phids; } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new HeraldRuleReplyHandler()) ->setMailReceiver($object); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $monogram = $object->getMonogram(); $name = $object->getName(); $subject = pht('%s: %s', $monogram, $name); return id(new PhabricatorMetaMTAMail()) ->setSubject($subject); } protected function getMailSubjectPrefix() { return pht('[Herald]'); } + + protected function buildMailBody( + PhabricatorLiskDAO $object, + array $xactions) { + + $body = parent::buildMailBody($object, $xactions); + + $body->addLinkSection( + pht('RULE DETAIL'), + PhabricatorEnv::getProductionURI($object->getURI())); + + return $body; + } + } diff --git a/src/applications/herald/storage/HeraldRule.php b/src/applications/herald/storage/HeraldRule.php index 44ef68ac03..d2b8d36f05 100644 --- a/src/applications/herald/storage/HeraldRule.php +++ b/src/applications/herald/storage/HeraldRule.php @@ -1,400 +1,404 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort255', 'contentType' => 'text255', 'mustMatchAll' => 'bool', 'configVersion' => 'uint32', 'repetitionPolicy' => 'text32', 'ruleType' => 'text32', 'isDisabled' => 'uint32', 'triggerObjectPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_name' => array( 'columns' => array('name(128)'), ), 'key_author' => array( 'columns' => array('authorPHID'), ), 'key_ruletype' => array( 'columns' => array('ruleType'), ), 'key_trigger' => array( 'columns' => array('triggerObjectPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(HeraldRulePHIDType::TYPECONST); } public function getRuleApplied($phid) { return $this->assertAttachedKey($this->ruleApplied, $phid); } public function setRuleApplied($phid, $applied) { if ($this->ruleApplied === self::ATTACHABLE) { $this->ruleApplied = array(); } $this->ruleApplied[$phid] = $applied; return $this; } public function loadConditions() { if (!$this->getID()) { return array(); } return id(new HeraldCondition())->loadAllWhere( 'ruleID = %d', $this->getID()); } public function attachConditions(array $conditions) { assert_instances_of($conditions, 'HeraldCondition'); $this->conditions = $conditions; return $this; } public function getConditions() { // TODO: validate conditions have been attached. return $this->conditions; } public function loadActions() { if (!$this->getID()) { return array(); } return id(new HeraldActionRecord())->loadAllWhere( 'ruleID = %d', $this->getID()); } public function attachActions(array $actions) { // TODO: validate actions have been attached. assert_instances_of($actions, 'HeraldActionRecord'); $this->actions = $actions; return $this; } public function getActions() { return $this->actions; } public function saveConditions(array $conditions) { assert_instances_of($conditions, 'HeraldCondition'); return $this->saveChildren( id(new HeraldCondition())->getTableName(), $conditions); } public function saveActions(array $actions) { assert_instances_of($actions, 'HeraldActionRecord'); return $this->saveChildren( id(new HeraldActionRecord())->getTableName(), $actions); } protected function saveChildren($table_name, array $children) { assert_instances_of($children, 'HeraldDAO'); if (!$this->getID()) { throw new PhutilInvalidStateException('save'); } foreach ($children as $child) { $child->setRuleID($this->getID()); } $this->openTransaction(); queryfx( $this->establishConnection('w'), 'DELETE FROM %T WHERE ruleID = %d', $table_name, $this->getID()); foreach ($children as $child) { $child->save(); } $this->saveTransaction(); } public function delete() { $this->openTransaction(); queryfx( $this->establishConnection('w'), 'DELETE FROM %T WHERE ruleID = %d', id(new HeraldCondition())->getTableName(), $this->getID()); queryfx( $this->establishConnection('w'), 'DELETE FROM %T WHERE ruleID = %d', id(new HeraldActionRecord())->getTableName(), $this->getID()); $result = parent::delete(); $this->saveTransaction(); return $result; } public function hasValidAuthor() { return $this->assertAttached($this->validAuthor); } public function attachValidAuthor($valid) { $this->validAuthor = $valid; return $this; } public function getAuthor() { return $this->assertAttached($this->author); } public function attachAuthor(PhabricatorUser $user) { $this->author = $user; return $this; } public function isGlobalRule() { return ($this->getRuleType() === HeraldRuleTypeConfig::RULE_TYPE_GLOBAL); } public function isPersonalRule() { return ($this->getRuleType() === HeraldRuleTypeConfig::RULE_TYPE_PERSONAL); } public function isObjectRule() { return ($this->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_OBJECT); } public function attachTriggerObject($trigger_object) { $this->triggerObject = $trigger_object; return $this; } public function getTriggerObject() { return $this->assertAttached($this->triggerObject); } /** * Get a sortable key for rule execution order. * * Rules execute in a well-defined order: personal rules first, then object * rules, then global rules. Within each rule type, rules execute from lowest * ID to highest ID. * * This ordering allows more powerful rules (like global rules) to override * weaker rules (like personal rules) when multiple rules exist which try to * affect the same field. Executing from low IDs to high IDs makes * interactions easier to understand when adding new rules, because the newest * rules always happen last. * * @return string A sortable key for this rule. */ public function getRuleExecutionOrderSortKey() { $rule_type = $this->getRuleType(); switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: $type_order = 1; break; case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: $type_order = 2; break; case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: $type_order = 3; break; default: throw new Exception(pht('Unknown rule type "%s"!', $rule_type)); } return sprintf('~%d%010d', $type_order, $this->getID()); } public function getMonogram() { return 'H'.$this->getID(); } + public function getURI() { + return '/'.$this->getMonogram(); + } + /* -( Repetition Policies )------------------------------------------------ */ public function getRepetitionPolicyStringConstant() { return $this->getRepetitionPolicy(); } public function setRepetitionPolicyStringConstant($value) { $map = self::getRepetitionPolicyMap(); if (!isset($map[$value])) { throw new Exception( pht( 'Rule repetition string constant "%s" is unknown.', $value)); } return $this->setRepetitionPolicy($value); } public function isRepeatEvery() { return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_EVERY); } public function isRepeatFirst() { return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_FIRST); } public function isRepeatOnChange() { return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_CHANGE); } public static function getRepetitionPolicySelectOptionMap() { $map = self::getRepetitionPolicyMap(); return ipull($map, 'select'); } private static function getRepetitionPolicyMap() { return array( self::REPEAT_EVERY => array( 'select' => pht('every time this rule matches:'), ), self::REPEAT_FIRST => array( 'select' => pht('only the first time this rule matches:'), ), self::REPEAT_CHANGE => array( 'select' => pht('if this rule did not match the last time:'), ), ); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HeraldRuleEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new HeraldRuleTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { return PhabricatorPolicies::getMostOpenPolicy(); } if ($this->isGlobalRule()) { $app = 'PhabricatorHeraldApplication'; $herald = PhabricatorApplication::getByClass($app); $global = HeraldManageGlobalRulesCapability::CAPABILITY; return $herald->getPolicy($global); } else if ($this->isObjectRule()) { return $this->getTriggerObject()->getPolicy($capability); } else { return $this->getAuthorPHID(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { return null; } if ($this->isGlobalRule()) { return pht( 'Global Herald rules can be edited by users with the "Can Manage '. 'Global Rules" Herald application permission.'); } else if ($this->isObjectRule()) { return pht('Object rules inherit the edit policies of their objects.'); } else { return pht('A personal rule can only be edited by its owner.'); } } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return $this->isPersonalRule() && $phid == $this->getAuthorPHID(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } }