1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-24 07:13:08 +02:00

Fix issue processwire/processwire-issues#639 where template configured to unpublish when missing required field was not applying to missing required fields within repeater items.

This commit is contained in:
Ryan Cramer
2018-08-08 06:16:04 -04:00
parent 69424df158
commit 6d1dc83a45
3 changed files with 157 additions and 37 deletions

View File

@@ -966,6 +966,34 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre
return $empty;
}
/**
* Return Inputfields in this wrapper that are required and have empty values
*
* This method includes all children up through the tree, not just direct children.
*
* #pw-internal
*
* @param bool $required Only include empty Inputfields that are required? (default=true)
* @return array of Inputfield instances indexed by name attributes
*
*/
public function getEmpty($required = true) {
$a = array();
static $n = 0;
foreach($this->children as $child) {
if($child instanceof InputfieldWrapper) {
$a = array_merge($a, $child->getEmpty($required));
} else {
if($required && !$child->getSetting('required')) continue;
if(!$child->isEmpty()) continue;
$name = $child->attr('name');
if(empty($name)) $name = "_unknown" . (++$n);
$a[$name] = $child;
}
}
return $a;
}
/**
* Return an array of errors that occurred on any of the children during input processing.
*

View File

@@ -82,6 +82,14 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
*/
protected $renderValueMode = false;
/**
* Number of required empty Inputfields after processing
*
* @var int
*
*/
protected $numRequiredEmpty = 0;
/**
* Set config defaults
*
@@ -271,14 +279,18 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
$label = $this->field->getLabel();
if(!$label) $label = ucfirst($this->field->name);
// remember which repeater items are open, when enabled
// remember which repeater items are open (as stored in cookie), when enabled
$openIDs = array();
if((int) $this->field->get('rememberOpen')) {
$this->addClass('InputfieldRepeaterRememberOpen', 'wrapClass');
$openIDs = $this->wire('input')->cookie('repeaters_open');
$openIDs = $this->wire('input')->cookie('repeaters_open');
if($openIDs) $openIDs = explode('|', trim($openIDs, '|'));
if(!is_array($openIDs)) $openIDs = array();
} else {
$openIDs = array();
}
// merge with any open IDs in session
$_openIDs = $this->wire('session')->getFor($this, 'openIDs');
if(is_array($_openIDs) && !empty($_openIDs)) {
$openIDs = array_merge($openIDs, array_values($_openIDs));
}
$minItems = $this->repeaterMinItems;
@@ -754,6 +766,11 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
$sortChanged = false;
$value->setTrackChanges(true);
$pageIDs = array();
$_openIDs = $this->wire('session')->getFor($this, 'openIDs');
if(!is_array($_openIDs)) $_openIDs = array();
$openIDs = $_openIDs; // these two are compared with each other at the end
$this->numRequiredEmpty = 0;
$this->getErrors(true);
// existing items
foreach($value as $key => $page) {
@@ -802,9 +819,11 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
/** @var InputfieldWrapper $wrapper */
$wrapper = $this->wrappers[$page->id];
$wrapper->resetTrackChanges(true);
$wrapper->getErrors(true); // clear out any errors
$wrapper->processInput($input);
$numErrors = count($wrapper->getErrors());
$numRequiredEmpty = count($wrapper->getEmpty(true));
$page->setQuietly('_repeater_errors', $numErrors); // signal to FieldtypeRepeater::savePageField() that page has errors
$page->setQuietly('_repeater_processed', true); // signal to FieldtypeRepeater::savePageField() that page had input processed
$this->formToPage($wrapper, $page);
@@ -829,6 +848,14 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
$page->addStatus(Page::statusOn);
}
}
if($numErrors || $numRequiredEmpty) {
$this->error(sprintf($this->_('Errors in “%s” item %d'), $this->label, $key + 1));
if(!$page->hasStatus(Page::statusUnpublished)) $this->numRequiredEmpty += $numRequiredEmpty;
$openIDs[$page->id] = $page->id; // force item with error to be open on next request
} else if(isset($openIDs[$page->id])) {
unset($openIDs[$page->id]);
}
if($page->isChanged() && $this->page->id) $numChanges++;
}
@@ -858,6 +885,9 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
$this->page->trackChange($this->attr('name'));
$this->trackChange('value');
}
// if openIDs value changed, update the session variable
if($_openIDs !== $openIDs) $this->wire('session')->setFor($this, 'openIDs', $openIDs);
return $this;
}
@@ -923,6 +953,33 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
return $cnt === 0;
}
/**
* Return quantity of published items
*
* @return int
*
*/
public function numPublished() {
/** @var PageArray $value */
$value = $this->attr('value');
if(empty($value) || !count($value)) return 0;
$num = 0;
foreach($value as $item) {
if(!$item->hasStatus(Page::statusUnpublished)) $num++;
}
return $num;
}
/**
* Get number of required but empty Inputfields (across all repeater items)
*
* @return int
*
*/
public function numRequiredEmpty() {
return $this->numRequiredEmpty;
}
/**
* Override the default set() to capture the required $page variable that the repeaters field lives on.
*

View File

@@ -1825,39 +1825,8 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod
if($name == 'status' && $this->processInputStatus($inputfield)) continue;
}
if($name && $errorAction
&& $inputfield->getSetting('required') && $inputfield->isEmpty()
&& !$inputfield->getSetting('requiredSkipped')
&& !$this->page->isUnpublished()) {
if($errorAction === 1) {
// restore existing value by skipping processing of empty when required
$value = $inputfield->attr('value');
if($value instanceof Wire) $value->resetTrackChanges();
if($this->page->getField($name)) $this->page->remove($name); // force fresh copy to reload
$previousValue = $this->page->get($name);
$this->page->untrackChange($name);
if($previousValue) {
// we should have a previous value to restore
if(WireArray::iterable($previousValue) && !count($previousValue)) {
// previous value still empty
} else {
// previous value restored by simply not setting new value to $page
$inputfield->error($this->_('Restored previous value'));
continue;
}
}
} else if($errorAction === 2 && $this->page->publishable() && $this->page->id > 1) {
// unpublish page missing required value
$this->page->setQuietly('_forceAddStatus', Page::statusUnpublished);
$label = $inputfield->getSetting('label');
if(empty($label)) $label = $inputfield->attr('name');
$this->error(sprintf($this->_('Page unpublished because field "%s" is required'), $label));
continue;
}
}
if($this->processInputErrorAction($this->page, $inputfield, $name, $errorAction)) continue;
if($name && $inputfield->isChanged()) {
if($languages && $inputfield->getSetting('useLanguages')) {
@@ -1887,6 +1856,72 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod
}
}
/**
* Process required error actions as configured with pages template
*
* @param Page $page
* @param Inputfield|InputfieldRepeater $inputfield Inputfield that has already had its processInput() method called.
* @param string $name Name of field that we are checking.
* @param null|int $errorAction Error action from $page->template->errorAction, or omit to auto-detect.
* @return bool Returns true if field $name should be skipped over during processing, or false if not
*
*/
public function processInputErrorAction(Page $page, Inputfield $inputfield, $name, $errorAction = null) {
if(empty($name)) return false;
if($errorAction === null) $errorAction = (int) $page->template->get('errorAction');
if(!$errorAction) return false;
if($page->isUnpublished()) return false;
$isRequired = $inputfield->getSetting('required');
$isRepeater = strpos($inputfield->className(), 'Repeater') > 0 && wireInstanceOf($inputfield, 'InputfieldRepeater', false);
if(!$isRepeater && !$isRequired) return false;
if($inputfield->getSetting('requiredSkipped')) return false;
if($isRepeater) {
if($inputfield->numRequiredEmpty() > 0) {
// repeater has required fields that are empty
} else if($isRequired && $inputfield->numPublished() < 1) {
// repeater is required and has no published items
} else {
// repeater is okay for now
return false;
}
} else if(!$inputfield->isEmpty()) {
return false;
}
if($errorAction === 1) {
// restore existing value by skipping processing of empty when required
$value = $inputfield->attr('value');
if($value instanceof Wire) $value->resetTrackChanges();
if($page->getField($name)) $page->remove($name); // force fresh copy to reload
$previousValue = $page->get($name);
$page->untrackChange($name);
if($previousValue) {
// we should have a previous value to restore
if(WireArray::iterable($previousValue) && !count($previousValue)) {
// previous value still empty
} else {
// previous value restored by simply not setting new value to $page
$inputfield->error($this->_('Restored previous value'));
return true;
}
}
} else if($errorAction === 2 && $page->publishable() && $page->id > 1) {
// unpublish page missing required value
$page->setQuietly('_forceAddStatus', Page::statusUnpublished);
$label = $inputfield->getSetting('label');
if(empty($label)) $label = $inputfield->attr('name');
$inputfield->error(sprintf($this->_('Page unpublished because field "%s" is required'), $label));
return false;
}
return false;
}
/**
* Check to see if the page's created user has changed and make sure it's valid
*