Write workboard trigger rules to the database
Summary: Ref T5474. Read and write trigger rules so users can actually edit them. Test Plan: Added, modified, and removed trigger rules. Saved changes, used "Show Details" to review edits. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T5474 Differential Revision: https://secure.phabricator.com/D20302
This commit is contained in:
		| @@ -4186,6 +4186,7 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorProjectTriggerQuery' => 'applications/project/query/PhabricatorProjectTriggerQuery.php', |     'PhabricatorProjectTriggerQuery' => 'applications/project/query/PhabricatorProjectTriggerQuery.php', | ||||||
|     'PhabricatorProjectTriggerRule' => 'applications/project/trigger/PhabricatorProjectTriggerRule.php', |     'PhabricatorProjectTriggerRule' => 'applications/project/trigger/PhabricatorProjectTriggerRule.php', | ||||||
|     'PhabricatorProjectTriggerRuleRecord' => 'applications/project/trigger/PhabricatorProjectTriggerRuleRecord.php', |     'PhabricatorProjectTriggerRuleRecord' => 'applications/project/trigger/PhabricatorProjectTriggerRuleRecord.php', | ||||||
|  |     'PhabricatorProjectTriggerRulesetTransaction' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerRulesetTransaction.php', | ||||||
|     'PhabricatorProjectTriggerSearchEngine' => 'applications/project/query/PhabricatorProjectTriggerSearchEngine.php', |     'PhabricatorProjectTriggerSearchEngine' => 'applications/project/query/PhabricatorProjectTriggerSearchEngine.php', | ||||||
|     'PhabricatorProjectTriggerTransaction' => 'applications/project/storage/PhabricatorProjectTriggerTransaction.php', |     'PhabricatorProjectTriggerTransaction' => 'applications/project/storage/PhabricatorProjectTriggerTransaction.php', | ||||||
|     'PhabricatorProjectTriggerTransactionQuery' => 'applications/project/query/PhabricatorProjectTriggerTransactionQuery.php', |     'PhabricatorProjectTriggerTransactionQuery' => 'applications/project/query/PhabricatorProjectTriggerTransactionQuery.php', | ||||||
| @@ -10319,6 +10320,7 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorProjectTriggerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', |     'PhabricatorProjectTriggerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', | ||||||
|     'PhabricatorProjectTriggerRule' => 'Phobject', |     'PhabricatorProjectTriggerRule' => 'Phobject', | ||||||
|     'PhabricatorProjectTriggerRuleRecord' => 'Phobject', |     'PhabricatorProjectTriggerRuleRecord' => 'Phobject', | ||||||
|  |     'PhabricatorProjectTriggerRulesetTransaction' => 'PhabricatorProjectTriggerTransactionType', | ||||||
|     'PhabricatorProjectTriggerSearchEngine' => 'PhabricatorApplicationSearchEngine', |     'PhabricatorProjectTriggerSearchEngine' => 'PhabricatorApplicationSearchEngine', | ||||||
|     'PhabricatorProjectTriggerTransaction' => 'PhabricatorModularTransaction', |     'PhabricatorProjectTriggerTransaction' => 'PhabricatorModularTransaction', | ||||||
|     'PhabricatorProjectTriggerTransactionQuery' => 'PhabricatorApplicationTransactionQuery', |     'PhabricatorProjectTriggerTransactionQuery' => 'PhabricatorApplicationTransactionQuery', | ||||||
|   | |||||||
| @@ -55,6 +55,7 @@ final class PhabricatorProjectTriggerEditController | |||||||
|  |  | ||||||
|     $v_name = $trigger->getName(); |     $v_name = $trigger->getName(); | ||||||
|     $v_edit = $trigger->getEditPolicy(); |     $v_edit = $trigger->getEditPolicy(); | ||||||
|  |     $v_rules = $trigger->getTriggerRules(); | ||||||
|  |  | ||||||
|     $e_name = null; |     $e_name = null; | ||||||
|     $e_edit = null; |     $e_edit = null; | ||||||
| @@ -65,8 +66,15 @@ final class PhabricatorProjectTriggerEditController | |||||||
|         $v_name = $request->getStr('name'); |         $v_name = $request->getStr('name'); | ||||||
|         $v_edit = $request->getStr('editPolicy'); |         $v_edit = $request->getStr('editPolicy'); | ||||||
|  |  | ||||||
|         $v_rules = $request->getStr('rules'); |         // Read the JSON rules from the request and convert them back into | ||||||
|         $v_rules = phutil_json_decode($v_rules); |         // "TriggerRule" objects so we can render the correct form state | ||||||
|  |         // if the user is modifying the rules | ||||||
|  |         $raw_rules = $request->getStr('rules'); | ||||||
|  |         $raw_rules = phutil_json_decode($raw_rules); | ||||||
|  |  | ||||||
|  |         $copy = clone $trigger; | ||||||
|  |         $copy->setRuleset($raw_rules); | ||||||
|  |         $v_rules = $copy->getTriggerRules(); | ||||||
|  |  | ||||||
|         $xactions = array(); |         $xactions = array(); | ||||||
|         if (!$trigger->getID()) { |         if (!$trigger->getID()) { | ||||||
| @@ -84,7 +92,10 @@ final class PhabricatorProjectTriggerEditController | |||||||
|           ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) |           ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) | ||||||
|           ->setNewValue($v_edit); |           ->setNewValue($v_edit); | ||||||
|  |  | ||||||
|         // TODO: Actually write the new rules to the database. |         $xactions[] = $trigger->getApplicationTransactionTemplate() | ||||||
|  |           ->setTransactionType( | ||||||
|  |             PhabricatorProjectTriggerRulesetTransaction::TRANSACTIONTYPE) | ||||||
|  |           ->setNewValue($raw_rules); | ||||||
|  |  | ||||||
|         $editor = $trigger->getApplicationTransactionEditor() |         $editor = $trigger->getApplicationTransactionEditor() | ||||||
|           ->setActor($viewer) |           ->setActor($viewer) | ||||||
| @@ -207,6 +218,7 @@ final class PhabricatorProjectTriggerEditController | |||||||
|  |  | ||||||
|     $this->setupEditorBehavior( |     $this->setupEditorBehavior( | ||||||
|       $trigger, |       $trigger, | ||||||
|  |       $v_rules, | ||||||
|       $form_id, |       $form_id, | ||||||
|       $table_id, |       $table_id, | ||||||
|       $create_id, |       $create_id, | ||||||
| @@ -250,12 +262,12 @@ final class PhabricatorProjectTriggerEditController | |||||||
|  |  | ||||||
|   private function setupEditorBehavior( |   private function setupEditorBehavior( | ||||||
|     PhabricatorProjectTrigger $trigger, |     PhabricatorProjectTrigger $trigger, | ||||||
|  |     array $rule_list, | ||||||
|     $form_id, |     $form_id, | ||||||
|     $table_id, |     $table_id, | ||||||
|     $create_id, |     $create_id, | ||||||
|     $input_id) { |     $input_id) { | ||||||
|  |  | ||||||
|     $rule_list = $trigger->getTriggerRules(); |  | ||||||
|     $rule_list = mpull($rule_list, 'toDictionary'); |     $rule_list = mpull($rule_list, 'toDictionary'); | ||||||
|     $rule_list = array_values($rule_list); |     $rule_list = array_values($rule_list); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -62,107 +62,19 @@ final class PhabricatorProjectTrigger | |||||||
|     return pht('Trigger %d', $this->getID()); |     return pht('Trigger %d', $this->getID()); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public function setRuleset(array $ruleset) { | ||||||
|  |     // Clear any cached trigger rules, since we're changing the ruleset | ||||||
|  |     // for the trigger. | ||||||
|  |     $this->triggerRules = null; | ||||||
|  |  | ||||||
|  |     parent::setRuleset($ruleset); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public function getTriggerRules() { |   public function getTriggerRules() { | ||||||
|     if ($this->triggerRules === null) { |     if ($this->triggerRules === null) { | ||||||
|  |       $trigger_rules = self::newTriggerRulesFromRuleSpecifications( | ||||||
|       // TODO: Temporary hard-coded rule specification. |         $this->getRuleset(), | ||||||
|       $rule_specifications = array( |         $allow_invalid = true); | ||||||
|         array( |  | ||||||
|           'type' => 'status', |  | ||||||
|           'value' => ManiphestTaskStatus::getDefaultClosedStatus(), |  | ||||||
|         ), |  | ||||||
|         // This is an intentionally unknown rule. |  | ||||||
|         array( |  | ||||||
|           'type' => 'quack', |  | ||||||
|           'value' => 'aaa', |  | ||||||
|         ), |  | ||||||
|         // This is an intentionally invalid rule. |  | ||||||
|         array( |  | ||||||
|           'type' => 'status', |  | ||||||
|           'value' => 'quack', |  | ||||||
|         ), |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       // NOTE: We're trying to preserve the database state in the rule |  | ||||||
|       // structure, even if it includes rule types we don't have implementations |  | ||||||
|       // for, or rules with invalid rule values. |  | ||||||
|  |  | ||||||
|       // If an administrator adds or removes extensions which add rules, or |  | ||||||
|       // an upgrade affects rule validity, existing rules may become invalid. |  | ||||||
|       // When they do, we still want the UI to reflect the ruleset state |  | ||||||
|       // accurately and "Edit" + "Save" shouldn't destroy data unless the |  | ||||||
|       // user explicitly modifies the ruleset. |  | ||||||
|  |  | ||||||
|       // When we run into rules which are structured correctly but which have |  | ||||||
|       // types we don't know about, we replace them with "Unknown Rules". If |  | ||||||
|       // we know about the type of a rule but the value doesn't validate, we |  | ||||||
|       // replace it with "Invalid Rules". These two rule types don't take any |  | ||||||
|       // actions when a card is dropped into the column, but they show the user |  | ||||||
|       // what's wrong with the ruleset and can be saved without causing any |  | ||||||
|       // collateral damage. |  | ||||||
|  |  | ||||||
|       $rule_map = PhabricatorProjectTriggerRule::getAllTriggerRules(); |  | ||||||
|  |  | ||||||
|       // If the stored rule data isn't a list of rules (or we encounter other |  | ||||||
|       // fundamental structural problems, below), there isn't much we can do |  | ||||||
|       // to try to represent the state. |  | ||||||
|       if (!is_array($rule_specifications)) { |  | ||||||
|         throw new PhabricatorProjectTriggerCorruptionException( |  | ||||||
|           pht( |  | ||||||
|             'Trigger ("%s") has a corrupt ruleset: expected a list of '. |  | ||||||
|             'rule specifications, found "%s".', |  | ||||||
|             $this->getPHID(), |  | ||||||
|             phutil_describe_type($rule_specifications))); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       $trigger_rules = array(); |  | ||||||
|       foreach ($rule_specifications as $key => $rule) { |  | ||||||
|         if (!is_array($rule)) { |  | ||||||
|           throw new PhabricatorProjectTriggerCorruptionException( |  | ||||||
|             pht( |  | ||||||
|               'Trigger ("%s") has a corrupt ruleset: rule (with key "%s") '. |  | ||||||
|               'should be a rule specification, but is actually "%s".', |  | ||||||
|               $this->getPHID(), |  | ||||||
|               $key, |  | ||||||
|               phutil_describe_type($rule))); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         try { |  | ||||||
|           PhutilTypeSpec::checkMap( |  | ||||||
|             $rule, |  | ||||||
|             array( |  | ||||||
|               'type' => 'string', |  | ||||||
|               'value' => 'wild', |  | ||||||
|             )); |  | ||||||
|         } catch (PhutilTypeCheckException $ex) { |  | ||||||
|           throw new PhabricatorProjectTriggerCorruptionException( |  | ||||||
|             pht( |  | ||||||
|               'Trigger ("%s") has a corrupt ruleset: rule (with key "%s") '. |  | ||||||
|               'is not a valid rule specification: %s', |  | ||||||
|               $this->getPHID(), |  | ||||||
|               $key, |  | ||||||
|               $ex->getMessage())); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         $record = id(new PhabricatorProjectTriggerRuleRecord()) |  | ||||||
|           ->setType(idx($rule, 'type')) |  | ||||||
|           ->setValue(idx($rule, 'value')); |  | ||||||
|  |  | ||||||
|         if (!isset($rule_map[$record->getType()])) { |  | ||||||
|           $rule = new PhabricatorProjectTriggerUnknownRule(); |  | ||||||
|         } else { |  | ||||||
|           $rule = clone $rule_map[$record->getType()]; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         try { |  | ||||||
|           $rule->setRecord($record); |  | ||||||
|         } catch (Exception $ex) { |  | ||||||
|           $rule = id(new PhabricatorProjectTriggerInvalidRule()) |  | ||||||
|             ->setRecord($record); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         $trigger_rules[] = $rule; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       $this->triggerRules = $trigger_rules; |       $this->triggerRules = $trigger_rules; | ||||||
|     } |     } | ||||||
| @@ -170,6 +82,108 @@ final class PhabricatorProjectTrigger | |||||||
|     return $this->triggerRules; |     return $this->triggerRules; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public static function newTriggerRulesFromRuleSpecifications( | ||||||
|  |     array $list, | ||||||
|  |     $allow_invalid) { | ||||||
|  |  | ||||||
|  |     // NOTE: With "$allow_invalid" set, we're  trying to preserve the database | ||||||
|  |     // state in the rule structure, even if it includes rule types we don't | ||||||
|  |     // ha ve implementations for, or rules with invalid rule values. | ||||||
|  |  | ||||||
|  |     // If an administrator adds or removes extensions which add rules, or | ||||||
|  |     // an upgrade affects rule validity, existing rules may become invalid. | ||||||
|  |     // When they do, we still want the UI to reflect the ruleset state | ||||||
|  |     // accurately and "Edit" + "Save" shouldn't destroy data unless the | ||||||
|  |     // user explicitly modifies the ruleset. | ||||||
|  |  | ||||||
|  |     // In this mode, when we run into rules which are structured correctly but | ||||||
|  |     // which have types we don't know about, we replace them with "Unknown | ||||||
|  |     // Rules". If we know about the type of a rule but the value doesn't | ||||||
|  |     // validate, we replace it with "Invalid Rules". These two rule types don't | ||||||
|  |     // take any actions when a card is dropped into the column, but they show | ||||||
|  |     // the user what's wrong with the ruleset and can be saved without causing | ||||||
|  |     // any collateral damage. | ||||||
|  |  | ||||||
|  |     $rule_map = PhabricatorProjectTriggerRule::getAllTriggerRules(); | ||||||
|  |  | ||||||
|  |     // If the stored rule data isn't a list of rules (or we encounter other | ||||||
|  |     // fundamental structural problems, below), there isn't much we can do | ||||||
|  |     // to try to represent the state. | ||||||
|  |     if (!is_array($list)) { | ||||||
|  |       throw new PhabricatorProjectTriggerCorruptionException( | ||||||
|  |         pht( | ||||||
|  |           'Trigger ruleset is corrupt: expected a list of rule '. | ||||||
|  |           'specifications, found "%s".', | ||||||
|  |           phutil_describe_type($list))); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     $trigger_rules = array(); | ||||||
|  |     foreach ($list as $key => $rule) { | ||||||
|  |       if (!is_array($rule)) { | ||||||
|  |         throw new PhabricatorProjectTriggerCorruptionException( | ||||||
|  |           pht( | ||||||
|  |             'Trigger ruleset is corrupt: rule (with key "%s") should be a '. | ||||||
|  |             'rule specification, but is actually "%s".', | ||||||
|  |             $key, | ||||||
|  |             phutil_describe_type($rule))); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       try { | ||||||
|  |         PhutilTypeSpec::checkMap( | ||||||
|  |           $rule, | ||||||
|  |           array( | ||||||
|  |             'type' => 'string', | ||||||
|  |             'value' => 'wild', | ||||||
|  |           )); | ||||||
|  |       } catch (PhutilTypeCheckException $ex) { | ||||||
|  |         throw new PhabricatorProjectTriggerCorruptionException( | ||||||
|  |           pht( | ||||||
|  |             'Trigger ruleset is corrupt: rule (with key "%s") is not a '. | ||||||
|  |             'valid rule specification: %s', | ||||||
|  |             $key, | ||||||
|  |             $ex->getMessage())); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       $record = id(new PhabricatorProjectTriggerRuleRecord()) | ||||||
|  |         ->setType(idx($rule, 'type')) | ||||||
|  |         ->setValue(idx($rule, 'value')); | ||||||
|  |  | ||||||
|  |       if (!isset($rule_map[$record->getType()])) { | ||||||
|  |         if (!$allow_invalid) { | ||||||
|  |           throw new PhabricatorProjectTriggerCorruptionException( | ||||||
|  |             pht( | ||||||
|  |               'Trigger ruleset is corrupt: rule type "%s" is unknown.', | ||||||
|  |               $record->getType())); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $rule = new PhabricatorProjectTriggerUnknownRule(); | ||||||
|  |       } else { | ||||||
|  |         $rule = clone $rule_map[$record->getType()]; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       try { | ||||||
|  |         $rule->setRecord($record); | ||||||
|  |       } catch (Exception $ex) { | ||||||
|  |         if (!$allow_invalid) { | ||||||
|  |           throw new PhabricatorProjectTriggerCorruptionException( | ||||||
|  |             pht( | ||||||
|  |               'Trigger ruleset is corrupt, rule (of type "%s") does not '. | ||||||
|  |               'validate: %s', | ||||||
|  |               $record->getType(), | ||||||
|  |               $ex->getMessage())); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $rule = id(new PhabricatorProjectTriggerInvalidRule()) | ||||||
|  |           ->setRecord($record); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       $trigger_rules[] = $rule; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return $trigger_rules; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|   public function getDropEffects() { |   public function getDropEffects() { | ||||||
|     $effects = array(); |     $effects = array(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -0,0 +1,65 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | final class PhabricatorProjectTriggerRulesetTransaction | ||||||
|  |   extends PhabricatorProjectTriggerTransactionType { | ||||||
|  |  | ||||||
|  |   const TRANSACTIONTYPE = 'ruleset'; | ||||||
|  |  | ||||||
|  |   public function generateOldValue($object) { | ||||||
|  |     return $object->getRuleset(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public function applyInternalEffects($object, $value) { | ||||||
|  |     $object->setRuleset($value); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public function getTitle() { | ||||||
|  |     return pht( | ||||||
|  |       '%s updated the ruleset for this trigger.', | ||||||
|  |       $this->renderAuthor()); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public function validateTransactions($object, array $xactions) { | ||||||
|  |     $errors = array(); | ||||||
|  |  | ||||||
|  |     foreach ($xactions as $xaction) { | ||||||
|  |       $ruleset = $xaction->getNewValue(); | ||||||
|  |  | ||||||
|  |       try { | ||||||
|  |         PhabricatorProjectTrigger::newTriggerRulesFromRuleSpecifications( | ||||||
|  |           $ruleset, | ||||||
|  |           $allow_invalid = false); | ||||||
|  |       } catch (PhabricatorProjectTriggerCorruptionException $ex) { | ||||||
|  |         $errors[] = $this->newInvalidError( | ||||||
|  |           pht( | ||||||
|  |             'Ruleset specification is not valid. %s', | ||||||
|  |             $ex->getMessage()), | ||||||
|  |           $xaction); | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return $errors; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public function hasChangeDetailView() { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public function newChangeDetailView() { | ||||||
|  |     $viewer = $this->getViewer(); | ||||||
|  |  | ||||||
|  |     $old = $this->getOldValue(); | ||||||
|  |     $new = $this->getNewValue(); | ||||||
|  |  | ||||||
|  |     $json = new PhutilJSON(); | ||||||
|  |     $old_json = $json->encodeAsList($old); | ||||||
|  |     $new_json = $json->encodeAsList($new); | ||||||
|  |  | ||||||
|  |     return id(new PhabricatorApplicationTransactionTextDiffDetailView()) | ||||||
|  |       ->setViewer($viewer) | ||||||
|  |       ->setOldText($old_json) | ||||||
|  |       ->setNewText($new_json); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 epriestley
					epriestley