mirror of
https://github.com/processwire/processwire.git
synced 2025-08-15 03:05:26 +02:00
Add support for previously unimplemented status Page::statusUnique, which adds support for globally unique page names. This commit also changes the the previous commit Page::statusIncomplete to value 128 because it turns out 256 was used in a 3rd party module and it seemed safer to use 128, which was occuped by Page::statusVersions, which has never been used. I've also changed the name of Page::statusIncomplete to Page::statusFlagged since the status indicates an error occurred during last interactive save rather than specifically incomplete.
This commit is contained in:
@@ -168,24 +168,21 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
* Status levels 1024 and above are excluded from search by the core. Status levels 16384 and above are runtime only and not
|
||||
* stored in the DB unless for logging or page history.
|
||||
*
|
||||
* If the under 1024 status flags are expanded in the future, it must be ensured that the combined value of the searchable flags
|
||||
* never exceeds 1024, otherwise issues in Pages::find() will need to be considered.
|
||||
*
|
||||
* The status levels 16384 and above can safely be changed as needed as they are runtime only.
|
||||
*
|
||||
* Please note that statuses 2, 32, 256, and 4096 are reserved for future use.
|
||||
* Please note that all other statuses are reserved for future use.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base status for pages in use (assigned automatically)
|
||||
* Base status for pages, represents boolean true (1) or false (0) as flag with other statuses, for internal use purposes only
|
||||
* #pw-internal
|
||||
*
|
||||
*/
|
||||
const statusOn = 1;
|
||||
|
||||
/**
|
||||
* Reserved status
|
||||
* Reserved status (internal use)
|
||||
* #pw-internal
|
||||
*
|
||||
*/
|
||||
@@ -226,18 +223,30 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
const statusDraft = 64;
|
||||
|
||||
/**
|
||||
* Page has version data available (name: "versions").
|
||||
* Page is flagged as incomplete, needing review, or having some issue
|
||||
* ProcessPageEdit uses this status to indicate an error message occurred during last internactive save
|
||||
* #pw-internal
|
||||
* @since 3.0.127
|
||||
*
|
||||
*/
|
||||
const statusFlagged = 128;
|
||||
const statusIncomplete = 128; // alias of statusFlagged
|
||||
|
||||
/**
|
||||
* Deprecated, was never used, but kept in case any modules referenced it
|
||||
* #pw-internal
|
||||
* @deprecated
|
||||
*
|
||||
*/
|
||||
const statusVersions = 128;
|
||||
|
||||
/**
|
||||
* Page might have incomplete data because there were errors when last saved interactively or may be missing required fields
|
||||
* Reserved for internal use
|
||||
* #pw-internal
|
||||
* @since 3.0.127
|
||||
*
|
||||
*/
|
||||
const statusIncomplete = 256;
|
||||
const statusInternal = 256;
|
||||
|
||||
/**
|
||||
* Page is temporary. 1+ day old unpublished pages with this status may be automatically deleted (name: "temp").
|
||||
@@ -309,8 +318,8 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
'system' => self::statusSystem,
|
||||
'unique' => self::statusUnique,
|
||||
'draft' => self::statusDraft,
|
||||
'versions' => self::statusVersions,
|
||||
'incomplete' => self::statusIncomplete,
|
||||
'flagged' => self::statusFlagged,
|
||||
'internal' => self::statusInternal,
|
||||
'temp' => self::statusTemp,
|
||||
'hidden' => self::statusHidden,
|
||||
'unpublished' => self::statusUnpublished,
|
||||
@@ -318,6 +327,8 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
'deleted' => self::statusDeleted,
|
||||
'systemOverride' => self::statusSystemOverride,
|
||||
'corrupted' => self::statusCorrupted,
|
||||
'max' => self::statusMax,
|
||||
'on' => self::statusOn,
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -1884,9 +1895,11 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
if($this->settings['status'] & Page::statusSystemID) $value = $value | Page::statusSystemID;
|
||||
if($this->settings['status'] & Page::statusSystem) $value = $value | Page::statusSystem;
|
||||
}
|
||||
if($this->settings['status'] != $value) {
|
||||
if($this->settings['status'] != $value && $this->isLoaded) {
|
||||
$this->trackChange('status', $this->settings['status'], $value);
|
||||
$this->statusPrevious = $this->settings['status'];
|
||||
if($this->statusPrevious === null) {
|
||||
$this->statusPrevious = $this->settings['status'];
|
||||
}
|
||||
}
|
||||
$this->settings['status'] = $value;
|
||||
if($value & Page::statusDeleted) {
|
||||
@@ -3744,6 +3757,7 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
$names = array();
|
||||
$remainder = $status;
|
||||
foreach(self::$statuses as $name => $value) {
|
||||
if($value <= self::statusOn || $value >= self::statusMax) continue;
|
||||
if($status & $value) {
|
||||
$names[$value] = $name;
|
||||
$remainder = $remainder & ~$value;
|
||||
|
@@ -209,14 +209,8 @@ class PageFinder extends Wire {
|
||||
$value = $selector->value;
|
||||
if(!ctype_digit("$value")) {
|
||||
// allow use of some predefined labels for Page statuses
|
||||
if($value == 'hidden') $selector->value = Page::statusHidden;
|
||||
else if($value == 'unpublished') $selector->value = Page::statusUnpublished;
|
||||
else if($value == 'draft') $selector->value = Page::statusDraft;
|
||||
else if($value == 'versions') $selector->value = Page::statusVersions;
|
||||
else if($value == 'locked') $selector->value = Page::statusLocked;
|
||||
else if($value == 'trash') $selector->value = Page::statusTrash;
|
||||
else if($value == 'max') $selector->value = Page::statusMax;
|
||||
else $selector->value = 1;
|
||||
$statuses = Page::getStatuses();
|
||||
$selector->value = isset($statuses[$value]) ? $statuses[$value] : 1;
|
||||
}
|
||||
$not = false;
|
||||
if(($selector->operator == '!=' && !$selector->not) || ($selector->not && $selector->operator == '=')) {
|
||||
@@ -229,8 +223,8 @@ class PageFinder extends Wire {
|
||||
$selectors[$key] = $this->wire(new SelectorBitwiseAnd('status', $selector->value));
|
||||
|
||||
} else {
|
||||
$not = $selector->not;
|
||||
// some other operator like: >, <, >=, <=
|
||||
$not = $selector->not;
|
||||
}
|
||||
if(!$not && (is_null($maxStatus) || $selector->value > $maxStatus)) $maxStatus = (int) $selector->value;
|
||||
|
||||
|
@@ -436,6 +436,7 @@ class PagesEditor extends Wire {
|
||||
}
|
||||
}
|
||||
|
||||
$this->pages->names()->checkNameConflicts($page);
|
||||
if(!$this->savePageQuery($page, $options)) return false;
|
||||
$result = $this->savePageFinish($page, $isNew, $options);
|
||||
if($language) $user->language = $language; // restore language
|
||||
|
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* ProcessWire Pages Names
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2019 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
*/
|
||||
@@ -69,7 +69,6 @@ class PagesNames extends Wire {
|
||||
* @param Page $page
|
||||
* @param string $format
|
||||
* @return string Returns page name that was assigned
|
||||
* @throws WireException
|
||||
*
|
||||
*/
|
||||
public function setupNewPageName(Page $page, $format = '') {
|
||||
@@ -551,6 +550,7 @@ class PagesNames extends Wire {
|
||||
$wheres[] = 'parent_id=:parent_id';
|
||||
$binds[':parent_id'] = $parentID;
|
||||
}
|
||||
|
||||
if($pageID) {
|
||||
$wheres[] = 'id!=:id';
|
||||
$binds[':id'] = $pageID;
|
||||
@@ -654,4 +654,104 @@ class PagesNames extends Wire {
|
||||
return $this->untitledPageName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does given page have a name that has a conflict/collision?
|
||||
*
|
||||
* In multi-language environment this applies to default language only.
|
||||
*
|
||||
* @param Page $page Page to check
|
||||
* @return string|bool Returns string with conflict reason or boolean false if no conflict
|
||||
* @throws WireException If given invalid $options argument
|
||||
* @since 3.0.127
|
||||
*
|
||||
*/
|
||||
public function pageNameHasConflict(Page $page) {
|
||||
|
||||
$reason = '';
|
||||
|
||||
$sql = "SELECT id, status, parent_id FROM pages WHERE name=:name AND id!=:id";
|
||||
$query = $this->wire('database')->prepare($sql);
|
||||
$query->bindValue(':name', $page->name);
|
||||
$query->bindValue(':id', $page->id, \PDO::PARAM_INT);
|
||||
$query->execute();
|
||||
|
||||
if(!$query->rowCount()) {
|
||||
$query->closeCursor();
|
||||
return false;
|
||||
}
|
||||
|
||||
while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
|
||||
if($row['status'] & Page::statusUnique) {
|
||||
// name is already required to be unique globally
|
||||
$reason = sprintf($this->_("Another page is using name “%s” and requires it to be globally unique"), $page->name);
|
||||
}
|
||||
if((int) $row['parent_id'] === $page->parent_id) {
|
||||
// name already consumed by another page with same parent
|
||||
$reason = sprintf($this->_('Another page with same parent is already using name “%s”'), $page->name);
|
||||
}
|
||||
if($reason) break;
|
||||
}
|
||||
|
||||
// page requires that it be the only one with this name, so if others have it, then disallow
|
||||
if(!$reason && $page->hasStatus(Page::statusUnique)) {
|
||||
$reason = sprintf($this->_('Cannot use name “%s” as globally unique because it is already used by other page(s)'), $page->name);
|
||||
}
|
||||
|
||||
$query->closeCursor();
|
||||
|
||||
return $reason ? $reason : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check given page’s name for conflicts and increment as needed while also triggering a warning notice
|
||||
*
|
||||
* @param Page $page
|
||||
* @since 3.0.127
|
||||
*
|
||||
*/
|
||||
public function checkNameConflicts(Page $page) {
|
||||
|
||||
$checkName = false;
|
||||
$checkStatus = false;
|
||||
$namePrevious = $page->namePrevious;
|
||||
$statusPrevious = $page->statusPrevious;
|
||||
$isNew = $page->isNew();
|
||||
$nameChanged = !$isNew && $namePrevious !== null && $namePrevious !== $page->name;
|
||||
|
||||
if($isNew || $nameChanged) {
|
||||
// new page or changed name
|
||||
$checkName = true;
|
||||
} else if($statusPrevious !== null && $page->hasStatus(Page::statusUnique) && !($statusPrevious & Page::statusUnique)) {
|
||||
// page just received 'unique' status
|
||||
$checkStatus = true;
|
||||
}
|
||||
|
||||
if(!$checkName && !$checkStatus) return;
|
||||
|
||||
do {
|
||||
|
||||
$conflict = $this->pageNameHasConflict($page);
|
||||
if(!$conflict) break;
|
||||
|
||||
$this->warning($conflict);
|
||||
|
||||
if($checkName) {
|
||||
if($nameChanged) {
|
||||
// restore previous name
|
||||
$page->name = $page->namePrevious;
|
||||
$nameChanged = false;
|
||||
} else {
|
||||
// increment name
|
||||
$page->name = $this->incrementName($page->name);
|
||||
}
|
||||
|
||||
} else if($checkStatus) {
|
||||
// remove 'unique' status
|
||||
$page->removeStatus(Page::statusUnique);
|
||||
break;
|
||||
}
|
||||
|
||||
} while($conflict);
|
||||
}
|
||||
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@@ -525,6 +525,21 @@ class InputfieldSelector extends Inputfield implements ConfigurableModule {
|
||||
//'parent' => $this->_('parent'),
|
||||
);
|
||||
|
||||
$ignoreStatuses = array(
|
||||
Page::statusOn,
|
||||
Page::statusReserved,
|
||||
Page::statusSystem,
|
||||
Page::statusSystemID,
|
||||
);
|
||||
|
||||
foreach(Page::getStatuses() as $name => $status) {
|
||||
if($status > Page::statusTrash) continue;
|
||||
if($status === Page::statusDraft && !$this->wire('modules')->isInstalled('ProDrafts')) continue;
|
||||
if(in_array($status, $ignoreStatuses)) continue;
|
||||
if(isset($this->systemFields['status']['options'][$name])) continue; // use existing label
|
||||
$this->systemFields['status']['options'][$name] = ucfirst($name);
|
||||
}
|
||||
|
||||
if(!count($users)) {
|
||||
unset($this->systemFields['modified_users_id']['options']);
|
||||
unset($this->systemFields['created_users_id']['options']);
|
||||
|
@@ -419,18 +419,30 @@ class ProcessPageAdd extends Process implements ConfigurableModule, WirePageEdit
|
||||
*
|
||||
*/
|
||||
public function executeExists() {
|
||||
|
||||
/** @var Pages $pages */
|
||||
$pages = $this->wire('pages');
|
||||
|
||||
$parentID = (int) $this->wire('input')->get('parent_id');
|
||||
if(!$parentID) return '';
|
||||
|
||||
$parent = $this->wire('pages')->get($parentID);
|
||||
if(!$parent->addable()) return '';
|
||||
|
||||
$name = $this->wire('sanitizer')->pageNameUTF8($this->wire('input')->get('name'));
|
||||
if(!strlen($name)) return '';
|
||||
|
||||
$parentID = count($this->predefinedParents) ? $this->predefinedParents : $parentID;
|
||||
$page = $this->wire('pages')->get("parent_id=$parentID, name=" . $this->wire('sanitizer')->selectorValue($name) . ", include=all");
|
||||
if($page->id) {
|
||||
$out = "<span class='taken ui-state-error-text'><i class='fa fa-exclamation-triangle'></i> " . $this->_('Already taken') . "</span>";
|
||||
|
||||
$test = new Page();
|
||||
$test->parent_id = $parentID;
|
||||
$test->name = $name;
|
||||
$reason = $pages->names()->pageNameHasConflict($test);
|
||||
|
||||
if($reason) {
|
||||
$out = "<span class='taken ui-state-error-text'>" . wireIconMarkup('exclamation-triangle') . " $reason</span>";
|
||||
} else {
|
||||
$out = "<span class='ui-priority-secondary'><i class='fa fa-check-square-o'></i> " . $this->_('Ok') . "</span>";
|
||||
$out = "<span class='ui-priority-secondary'>" . wireIconMarkup('check-square-o') . ' ' . $this->_('Ok') . "</span>";
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
@@ -350,7 +350,7 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod
|
||||
$this->set('noticeUnknown', $this->_("Unknown page")); // Init error: Unknown page
|
||||
$this->set('noticeLocked', $this->_("This page is locked for edits")); // Init error: Page is locked
|
||||
$this->set('noticeNoAccess', $this->_("You don't have access to edit")); // Init error: User doesn't have access
|
||||
$this->set('noticeIncomplete', $this->_("This page might have one or more incomplete fields (attempt to save or publish for more info)")); // Init error: User doesn't have access
|
||||
$this->set('noticeIncomplete', $this->_("This page might have one or more incomplete fields (attempt to save or publish for more info)"));
|
||||
|
||||
$settings = $this->config->pageEdit;
|
||||
if(is_array($settings)) $this->configSettings = array_merge($this->configSettings, $settings);
|
||||
@@ -541,7 +541,7 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod
|
||||
} else {
|
||||
$this->error($this->noticeLocked); // Page locked error
|
||||
}
|
||||
} else if(!$this->isPost && $this->page->hasStatus(Page::statusIncomplete) && !$this->input->get('s')) {
|
||||
} else if(!$this->isPost && $this->page->hasStatus(Page::statusFlagged) && !$this->input->get('s')) {
|
||||
$this->warning($this->noticeIncomplete);
|
||||
}
|
||||
|
||||
@@ -1607,25 +1607,28 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod
|
||||
|
||||
$status = (int) $this->page->status;
|
||||
$statuses = array();
|
||||
$debug = $this->config->debug;
|
||||
$advanced = $this->config->advanced;
|
||||
|
||||
/** @var InputfieldCheckboxes $field */
|
||||
$field = $this->modules->get('InputfieldCheckboxes');
|
||||
$field->attr('name', 'status');
|
||||
$field->icon = 'sliders';
|
||||
|
||||
if(!$this->page->template->noUnpublish && $this->page->publishable()) {
|
||||
$statuses[Page::statusUnpublished] = $this->_('Unpublished: Not visible on site'); // Settings: Unpublished status checkbox label
|
||||
}
|
||||
if($this->user->hasPermission('page-hide', $this->page)) {
|
||||
$statuses[Page::statusHidden] = $this->_('Hidden: Excluded from lists and searches'); // Settings: Hidden status checkbox label
|
||||
}
|
||||
if($this->user->hasPermission('page-lock', $this->page)) {
|
||||
$statuses[Page::statusLocked] = $this->_('Locked: Not editable'); // Settings: Locked status checkbox label
|
||||
}
|
||||
if(!$this->page->template->noUnpublish && $this->page->publishable()) {
|
||||
$statuses[Page::statusUnpublished] = $this->_('Unpublished: Not visible on site'); // Settings: Unpublished status checkbox label
|
||||
}
|
||||
|
||||
if($this->user->isSuperuser()) {
|
||||
// $statuses[Page::statusUnique] = sprintf($this->_('Unique: Name “%s” may not be used by any other page in the system'), $this->page->name);
|
||||
if($this->config->advanced) {
|
||||
$statuses[Page::statusUnique] = sprintf($this->_('Unique: Require page name “%s” to be globally unique'), $this->page->name) .
|
||||
($this->wire('languages') ? ' ' . $this->_('(in default language only)') : '');
|
||||
if($advanced) {
|
||||
$statuses[Page::statusSystemID] = "System: Non-deleteable and locked ID (status not removeable via API)";
|
||||
$statuses[Page::statusSystem] = "System: Non-deleteable and locked ID, name, template, parent (status not removeable via API)";
|
||||
}
|
||||
@@ -1635,13 +1638,14 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod
|
||||
|
||||
foreach($statuses as $s => $label) {
|
||||
if($s & $status) $value[] = $s;
|
||||
if(strpos($label, ': ')) $label = str_replace(': ', ': [span.detail]', $label) . '[/span]';
|
||||
$field->addOption($s, $label);
|
||||
}
|
||||
|
||||
$field->attr('value', $value);
|
||||
$field->label = $this->_('Status'); // Settings: Status field label
|
||||
|
||||
if($this->config->debug) $field->notes = $this->page->statusStr;
|
||||
if($debug) $field->notes = $this->page->statusStr;
|
||||
|
||||
return $field;
|
||||
}
|
||||
@@ -1916,15 +1920,15 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod
|
||||
if($notice instanceof NoticeError) $formErrors++;
|
||||
}
|
||||
|
||||
// if any Inputfields threw errors during processing, give the page an 'incomplete' status
|
||||
// if any Inputfields threw errors during processing, give the page a 'flagged' status
|
||||
// so that it can later be identified the page may be missing something
|
||||
if($formErrors && count($this->form->getErrors())) {
|
||||
// add incomplete status when form had errors
|
||||
$this->page->addStatus(Page::statusIncomplete);
|
||||
} else if($this->page->hasStatus(Page::statusIncomplete)) {
|
||||
// add flagged status when form had errors
|
||||
$this->page->addStatus(Page::statusFlagged);
|
||||
} else if($this->page->hasStatus(Page::statusFlagged)) {
|
||||
// if no errors, remove incomplete status
|
||||
$this->page->removeStatus(Page::statusIncomplete);
|
||||
$this->message($this->_('Removed incomplete status because no errors reported during save'));
|
||||
$this->page->removeStatus(Page::statusFlagged);
|
||||
$this->message($this->_('Removed flagged status because no errors reported during save'));
|
||||
}
|
||||
|
||||
$isUnpublished = $this->page->hasStatus(Page::statusUnpublished);
|
||||
@@ -2262,7 +2266,7 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod
|
||||
if($this->user->hasPermission('page-lock', $this->page)) $statusFlags[] = Page::statusLocked;
|
||||
|
||||
if($this->user->isSuperuser()) {
|
||||
// $statusFlags[] = Page::statusUnique;
|
||||
$statusFlags[] = Page::statusUnique;
|
||||
if($this->config->advanced) {
|
||||
$statusFlags[] = Page::statusSystemID;
|
||||
$statusFlags[] = Page::statusSystem;
|
||||
|
@@ -125,7 +125,7 @@ class ProcessPageListActions extends Wire {
|
||||
if(!$locked && !$trash && !$noSettings && $statusEditable) {
|
||||
if($page->publishable()) {
|
||||
if($page->isUnpublished()) {
|
||||
if(!$page->hasStatus(Page::statusIncomplete)) {
|
||||
if(!$page->hasStatus(Page::statusFlagged)) {
|
||||
$extras['pub'] = array(
|
||||
'cn' => 'Publish',
|
||||
'name' => $this->actionLabels['pub'],
|
||||
|
@@ -81,7 +81,7 @@ class ProcessPageListRenderJSON extends ProcessPageListRender {
|
||||
if($page->hasStatus(Page::statusTemp)) $icons[] = 'bolt';
|
||||
if($page->hasStatus(Page::statusLocked)) $icons[] = 'lock';
|
||||
if($page->hasStatus(Page::statusDraft)) $icons[] = 'paperclip';
|
||||
if($page->hasStatus(Page::statusIncomplete)) $icons[] = 'exclamation-triangle';
|
||||
if($page->hasStatus(Page::statusFlagged)) $icons[] = 'exclamation-triangle';
|
||||
$numChildren = $this->numChildren($page, 1);
|
||||
$numTotal = strpos($this->qtyType, 'total') !== false ? $page->numDescendants : $numChildren;
|
||||
}
|
||||
|
Reference in New Issue
Block a user