mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-23 14:00:56 +01:00
Allow custom fields to have validation logic
Summary: Ref T418. This is fairly messy, but basically: - Add a validation phase to TransactionEditor. - Add a validation phase to CustomField. - Bring it to StandardField. - Add validation logic for the int field. - Provide support in related classes. Test Plan: See screenshot. Reviewers: btrahan Reviewed By: btrahan CC: aran Maniphest Tasks: T418 Differential Revision: https://secure.phabricator.com/D7028
This commit is contained in:
parent
a025050e87
commit
3f24232d2b
9 changed files with 265 additions and 8 deletions
|
@ -868,6 +868,8 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorApplicationTransactionQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionQuery.php',
|
'PhabricatorApplicationTransactionQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionQuery.php',
|
||||||
'PhabricatorApplicationTransactionResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionResponse.php',
|
'PhabricatorApplicationTransactionResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionResponse.php',
|
||||||
'PhabricatorApplicationTransactionTextDiffDetailView' => 'applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php',
|
'PhabricatorApplicationTransactionTextDiffDetailView' => 'applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php',
|
||||||
|
'PhabricatorApplicationTransactionValidationError' => 'applications/transactions/error/PhabricatorApplicationTransactionValidationError.php',
|
||||||
|
'PhabricatorApplicationTransactionValidationException' => 'applications/transactions/exception/PhabricatorApplicationTransactionValidationException.php',
|
||||||
'PhabricatorApplicationTransactionView' => 'applications/transactions/view/PhabricatorApplicationTransactionView.php',
|
'PhabricatorApplicationTransactionView' => 'applications/transactions/view/PhabricatorApplicationTransactionView.php',
|
||||||
'PhabricatorApplicationTransactions' => 'applications/transactions/application/PhabricatorApplicationTransactions.php',
|
'PhabricatorApplicationTransactions' => 'applications/transactions/application/PhabricatorApplicationTransactions.php',
|
||||||
'PhabricatorApplicationUIExamples' => 'applications/uiexample/application/PhabricatorApplicationUIExamples.php',
|
'PhabricatorApplicationUIExamples' => 'applications/uiexample/application/PhabricatorApplicationUIExamples.php',
|
||||||
|
@ -2960,6 +2962,8 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorApplicationTransactionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
'PhabricatorApplicationTransactionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||||
'PhabricatorApplicationTransactionResponse' => 'AphrontProxyResponse',
|
'PhabricatorApplicationTransactionResponse' => 'AphrontProxyResponse',
|
||||||
'PhabricatorApplicationTransactionTextDiffDetailView' => 'AphrontView',
|
'PhabricatorApplicationTransactionTextDiffDetailView' => 'AphrontView',
|
||||||
|
'PhabricatorApplicationTransactionValidationError' => 'Phobject',
|
||||||
|
'PhabricatorApplicationTransactionValidationException' => 'Exception',
|
||||||
'PhabricatorApplicationTransactionView' => 'AphrontView',
|
'PhabricatorApplicationTransactionView' => 'AphrontView',
|
||||||
'PhabricatorApplicationTransactions' => 'PhabricatorApplication',
|
'PhabricatorApplicationTransactions' => 'PhabricatorApplication',
|
||||||
'PhabricatorApplicationUIExamples' => 'PhabricatorApplication',
|
'PhabricatorApplicationUIExamples' => 'PhabricatorApplication',
|
||||||
|
|
|
@ -39,6 +39,7 @@ final class PhabricatorPeopleProfileEditController
|
||||||
->setViewer($user)
|
->setViewer($user)
|
||||||
->readFieldsFromStorage($user);
|
->readFieldsFromStorage($user);
|
||||||
|
|
||||||
|
$validation_exception = null;
|
||||||
if ($request->isFormPost()) {
|
if ($request->isFormPost()) {
|
||||||
$xactions = $field_list->buildFieldTransactionsFromRequest(
|
$xactions = $field_list->buildFieldTransactionsFromRequest(
|
||||||
new PhabricatorUserTransaction(),
|
new PhabricatorUserTransaction(),
|
||||||
|
@ -50,9 +51,12 @@ final class PhabricatorPeopleProfileEditController
|
||||||
PhabricatorContentSource::newFromRequest($request))
|
PhabricatorContentSource::newFromRequest($request))
|
||||||
->setContinueOnNoEffect(true);
|
->setContinueOnNoEffect(true);
|
||||||
|
|
||||||
|
try {
|
||||||
$editor->applyTransactions($user, $xactions);
|
$editor->applyTransactions($user, $xactions);
|
||||||
|
|
||||||
return id(new AphrontRedirectResponse())->setURI($profile_uri);
|
return id(new AphrontRedirectResponse())->setURI($profile_uri);
|
||||||
|
} catch (PhabricatorApplicationTransactionValidationException $ex) {
|
||||||
|
$validation_exception = $ex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$title = pht('Edit Profile');
|
$title = pht('Edit Profile');
|
||||||
|
@ -78,6 +82,7 @@ final class PhabricatorPeopleProfileEditController
|
||||||
|
|
||||||
$form_box = id(new PHUIFormBoxView())
|
$form_box = id(new PHUIFormBoxView())
|
||||||
->setHeaderText(pht('Edit Your Profile'))
|
->setHeaderText(pht('Edit Your Profile'))
|
||||||
|
->setValidationException($validation_exception)
|
||||||
->setForm($form);
|
->setForm($form);
|
||||||
|
|
||||||
return $this->buildApplicationPage(
|
return $this->buildApplicationPage(
|
||||||
|
|
|
@ -185,7 +185,6 @@ abstract class PhabricatorApplicationTransactionEditor
|
||||||
PhabricatorLiskDAO $object,
|
PhabricatorLiskDAO $object,
|
||||||
array $xactions) {
|
array $xactions) {
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function applyInitialEffects(
|
protected function applyInitialEffects(
|
||||||
|
@ -356,6 +355,18 @@ abstract class PhabricatorApplicationTransactionEditor
|
||||||
$transaction_open = false;
|
$transaction_open = false;
|
||||||
|
|
||||||
if (!$is_preview) {
|
if (!$is_preview) {
|
||||||
|
$errors = array();
|
||||||
|
$type_map = mgroup($xactions, 'getTransactionType');
|
||||||
|
foreach ($this->getTransactionTypes() as $type) {
|
||||||
|
$type_xactions = idx($type_map, $type, array());
|
||||||
|
$errors[] = $this->validateTransaction($object, $type, $type_xactions);
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = array_mergev($errors);
|
||||||
|
if ($errors) {
|
||||||
|
throw new PhabricatorApplicationTransactionValidationException($errors);
|
||||||
|
}
|
||||||
|
|
||||||
if ($object->getID()) {
|
if ($object->getID()) {
|
||||||
foreach ($xactions as $xaction) {
|
foreach ($xactions as $xaction) {
|
||||||
|
|
||||||
|
@ -1003,6 +1014,55 @@ abstract class PhabricatorApplicationTransactionEditor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for validating transactions. This callback will be invoked for each
|
||||||
|
* available transaction type, even if an edit does not apply any transactions
|
||||||
|
* of that type. This allows you to raise exceptions when required fields are
|
||||||
|
* missing, by detecting that the object has no field value and there is no
|
||||||
|
* transaction which sets one.
|
||||||
|
*
|
||||||
|
* @param PhabricatorLiskDAO Object being edited.
|
||||||
|
* @param string Transaction type to validate.
|
||||||
|
* @param list<PhabricatorApplicationTransaction> Transactions of given type,
|
||||||
|
* which may be empty if the edit does not apply any transactions of the
|
||||||
|
* given type.
|
||||||
|
* @return list<PhabricatorApplicationTransactionValidationError> List of
|
||||||
|
* validation errors.
|
||||||
|
*/
|
||||||
|
protected function validateTransaction(
|
||||||
|
PhabricatorLiskDAO $object,
|
||||||
|
$type,
|
||||||
|
array $xactions) {
|
||||||
|
|
||||||
|
$errors = array();
|
||||||
|
switch ($type) {
|
||||||
|
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
|
||||||
|
$groups = array();
|
||||||
|
foreach ($xactions as $xaction) {
|
||||||
|
$groups[$xaction->getMetadataValue('customfield:key')][] = $xaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
$field_list = PhabricatorCustomField::getObjectFields(
|
||||||
|
$object,
|
||||||
|
PhabricatorCustomField::ROLE_EDIT);
|
||||||
|
|
||||||
|
$role_xactions = PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS;
|
||||||
|
foreach ($field_list->getFields() as $field) {
|
||||||
|
if (!$field->shouldEnableForRole($role_xactions)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$errors[] = $field->validateApplicationTransactions(
|
||||||
|
$this,
|
||||||
|
$type,
|
||||||
|
idx($groups, $field->getFieldKey(), array()));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_mergev($errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* -( Implicit CCs )------------------------------------------------------- */
|
/* -( Implicit CCs )------------------------------------------------------- */
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorApplicationTransactionValidationError
|
||||||
|
extends Phobject {
|
||||||
|
|
||||||
|
private $type;
|
||||||
|
private $transaction;
|
||||||
|
private $shortMessage;
|
||||||
|
private $message;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
$type,
|
||||||
|
$short_message,
|
||||||
|
$message,
|
||||||
|
PhabricatorApplicationTransaction $xaction = null) {
|
||||||
|
|
||||||
|
$this->type = $type;
|
||||||
|
$this->shortMessage = $short_message;
|
||||||
|
$this->message = $message;
|
||||||
|
$this->transaction = $xaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getType() {
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTransaction() {
|
||||||
|
return $this->tranaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getShortMessage() {
|
||||||
|
return $this->shortMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMessage() {
|
||||||
|
return $this->message;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorApplicationTransactionValidationException
|
||||||
|
extends Exception {
|
||||||
|
|
||||||
|
private $errors;
|
||||||
|
|
||||||
|
public function __construct(array $errors) {
|
||||||
|
assert_instances_of(
|
||||||
|
$errors,
|
||||||
|
'PhabricatorApplicationTransactionValidationError');
|
||||||
|
|
||||||
|
$this->errors = $errors;
|
||||||
|
|
||||||
|
$message = array();
|
||||||
|
$message[] = 'Validation errors:';
|
||||||
|
foreach ($this->errors as $error) {
|
||||||
|
$message[] = ' - '.$error->getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::__construct(implode("\n", $message));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getErrors() {
|
||||||
|
return $this->errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getErrorMessages() {
|
||||||
|
return mpull($this->errors, 'getMessage');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getShortMessage($type) {
|
||||||
|
foreach ($this->errors as $error) {
|
||||||
|
if ($error->getType() === $type) {
|
||||||
|
if ($error->getShortMessage() !== null) {
|
||||||
|
return $error->getShortMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -844,6 +844,36 @@ abstract class PhabricatorCustomField {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate transactions for an object. This allows you to raise an error
|
||||||
|
* when a transaction would set a field to an invalid value, or when a field
|
||||||
|
* is required but no transactions provide value.
|
||||||
|
*
|
||||||
|
* @param PhabricatorLiskDAO Editor applying the transactions.
|
||||||
|
* @param string Transaction type. This type is always
|
||||||
|
* `PhabricatorTransactions::TYPE_CUSTOMFIELD`, it is provided for
|
||||||
|
* convenience when constructing exceptions.
|
||||||
|
* @param list<PhabricatorApplicationTransaction> Transactions being applied,
|
||||||
|
* which may be empty if this field is not being edited.
|
||||||
|
* @return list<PhabricatorApplicationTransactionValidationError> Validation
|
||||||
|
* errors.
|
||||||
|
*
|
||||||
|
* @task appxaction
|
||||||
|
*/
|
||||||
|
public function validateApplicationTransactions(
|
||||||
|
PhabricatorApplicationTransactionEditor $editor,
|
||||||
|
$type,
|
||||||
|
array $xactions) {
|
||||||
|
if ($this->proxy) {
|
||||||
|
return $this->proxy->validateApplicationTransactions(
|
||||||
|
$editor,
|
||||||
|
$type,
|
||||||
|
$xactions);
|
||||||
|
}
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* -( Edit View )---------------------------------------------------------- */
|
/* -( Edit View )---------------------------------------------------------- */
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ abstract class PhabricatorStandardCustomField
|
||||||
private $applicationField;
|
private $applicationField;
|
||||||
private $strings;
|
private $strings;
|
||||||
private $caption;
|
private $caption;
|
||||||
|
private $fieldError;
|
||||||
|
|
||||||
abstract public function getFieldType();
|
abstract public function getFieldType();
|
||||||
|
|
||||||
|
@ -114,6 +115,14 @@ abstract class PhabricatorStandardCustomField
|
||||||
return idx($this->fieldConfig, $key, $default);
|
return idx($this->fieldConfig, $key, $default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setFieldError($field_error) {
|
||||||
|
$this->fieldError = $field_error;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFieldError() {
|
||||||
|
return $this->fieldError;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* -( PhabricatorCustomField )--------------------------------------------- */
|
/* -( PhabricatorCustomField )--------------------------------------------- */
|
||||||
|
@ -178,6 +187,7 @@ abstract class PhabricatorStandardCustomField
|
||||||
->setName($this->getFieldKey())
|
->setName($this->getFieldKey())
|
||||||
->setCaption($this->getCaption())
|
->setCaption($this->getCaption())
|
||||||
->setValue($this->getFieldValue())
|
->setValue($this->getFieldValue())
|
||||||
|
->setError($this->getFieldError())
|
||||||
->setLabel($this->getFieldName());
|
->setLabel($this->getFieldName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,4 +240,19 @@ abstract class PhabricatorStandardCustomField
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function validateApplicationTransactions(
|
||||||
|
PhabricatorApplicationTransactionEditor $editor,
|
||||||
|
$type,
|
||||||
|
array $xactions) {
|
||||||
|
|
||||||
|
$this->setFieldError(null);
|
||||||
|
|
||||||
|
$errors = parent::validateApplicationTransactions(
|
||||||
|
$editor,
|
||||||
|
$type,
|
||||||
|
$xactions);
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ final class PhabricatorStandardCustomFieldInt
|
||||||
public function getValueForStorage() {
|
public function getValueForStorage() {
|
||||||
$value = $this->getFieldValue();
|
$value = $this->getFieldValue();
|
||||||
if (strlen($value)) {
|
if (strlen($value)) {
|
||||||
return (int)$value;
|
return $value;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -68,4 +68,31 @@ final class PhabricatorStandardCustomFieldInt
|
||||||
->setValue($value));
|
->setValue($value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function validateApplicationTransactions(
|
||||||
|
PhabricatorApplicationTransactionEditor $editor,
|
||||||
|
$type,
|
||||||
|
array $xactions) {
|
||||||
|
|
||||||
|
$errors = parent::validateApplicationTransactions(
|
||||||
|
$editor,
|
||||||
|
$type,
|
||||||
|
$xactions);
|
||||||
|
|
||||||
|
foreach ($xactions as $xaction) {
|
||||||
|
$value = $xaction->getNewValue();
|
||||||
|
if (strlen($value)) {
|
||||||
|
if (!preg_match('/^-?\d+/', $value)) {
|
||||||
|
$errors[] = new PhabricatorApplicationTransactionValidationError(
|
||||||
|
$type,
|
||||||
|
pht('Invalid'),
|
||||||
|
pht('%s must be an integer.', $this->getFieldName()),
|
||||||
|
$xaction);
|
||||||
|
$this->setFieldError(pht('Invalid'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ final class PHUIFormBoxView extends AphrontView {
|
||||||
private $headerText;
|
private $headerText;
|
||||||
private $formError = null;
|
private $formError = null;
|
||||||
private $form;
|
private $form;
|
||||||
|
private $validationException;
|
||||||
|
|
||||||
public function setHeaderText($text) {
|
public function setHeaderText($text) {
|
||||||
$this->headerText = $text;
|
$this->headerText = $text;
|
||||||
|
@ -21,16 +22,39 @@ final class PHUIFormBoxView extends AphrontView {
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render() {
|
public function setValidationException(
|
||||||
|
PhabricatorApplicationTransactionValidationException $ex = null) {
|
||||||
|
$this->validationException = $ex;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
$error = $this->formError ? $this->formError : null;
|
public function render() {
|
||||||
|
|
||||||
$header = id(new PhabricatorActionHeaderView())
|
$header = id(new PhabricatorActionHeaderView())
|
||||||
->setHeaderTitle($this->headerText)
|
->setHeaderTitle($this->headerText)
|
||||||
->setHeaderColor(PhabricatorActionHeaderView::HEADER_LIGHTBLUE);
|
->setHeaderColor(PhabricatorActionHeaderView::HEADER_LIGHTBLUE);
|
||||||
|
|
||||||
|
$ex = $this->validationException;
|
||||||
|
$exception_errors = null;
|
||||||
|
if ($ex) {
|
||||||
|
$messages = array();
|
||||||
|
foreach ($ex->getErrors() as $error) {
|
||||||
|
$messages[] = $error->getMessage();
|
||||||
|
}
|
||||||
|
if ($messages) {
|
||||||
|
$exception_errors = id(new AphrontErrorView())
|
||||||
|
->setErrors($messages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$content = id(new PHUIBoxView())
|
$content = id(new PHUIBoxView())
|
||||||
->appendChild(array($header, $error, $this->form))
|
->appendChild(
|
||||||
|
array(
|
||||||
|
$header,
|
||||||
|
$this->formError,
|
||||||
|
$exception_errors,
|
||||||
|
$this->form,
|
||||||
|
))
|
||||||
->setBorder(true)
|
->setBorder(true)
|
||||||
->addMargin(PHUI::MARGIN_LARGE_TOP)
|
->addMargin(PHUI::MARGIN_LARGE_TOP)
|
||||||
->addMargin(PHUI::MARGIN_LARGE_LEFT)
|
->addMargin(PHUI::MARGIN_LARGE_LEFT)
|
||||||
|
|
Loading…
Reference in a new issue