├── .gitattributes ├── .gitignore ├── cockpitql.php ├── views ├── partials │ ├── settings.php │ └── entry-aside.php └── settings │ └── index.php ├── assets ├── moderation.js ├── field-moderation.tag └── moderation.css ├── composer.json ├── Controller ├── Admin.php └── RestApi.php ├── LICENSE ├── admin.php ├── actions.php ├── bootstrap.php └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | /vendor/ 3 | 4 | # Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control 5 | # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file 6 | composer.lock 7 | -------------------------------------------------------------------------------- /cockpitql.php: -------------------------------------------------------------------------------- 1 | on('cockpitql.type.moderation', function ($field, &$def) use ($app) { 6 | $def['type'] = Type::string(); 7 | }); 8 | 9 | // API includes. 10 | if (COCKPIT_API_REQUEST) { 11 | include_once __DIR__ . '/cockpitql.php'; 12 | } 13 | -------------------------------------------------------------------------------- /views/partials/settings.php: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 |
11 | @lang('Moderation') 12 |
13 | @lang('Moderation') 14 |
15 | 16 |
17 |
18 | -------------------------------------------------------------------------------- /assets/moderation.js: -------------------------------------------------------------------------------- 1 | App.Utils.renderer.moderation = function(v) { 2 | switch (v) { 3 | case "Published": 4 | icon = "uk-icon-circle"; 5 | break; 6 | case "Unpublished": 7 | icon = "uk-icon-circle-o"; 8 | break; 9 | case "Draft": 10 | icon = "uk-icon-pencil"; 11 | break; 12 | } 13 | return ' ' + v + ""; 14 | }; 15 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pauloamgomes/cockpitcms-moderation", 3 | "description": "Moderation addon for Cockpit CMS, provides status capabilities (Unpublished, Draft or Published) for collections", 4 | "keywords": ["cms", "headless", "api", "cockpit", "moderation-addon"], 5 | "homepage": "https://github.com/pauloamgomes/CockpitCMS-Moderation", 6 | "license": "MIT", 7 | "type": "cockpit-module", 8 | "authors": [ 9 | { 10 | "name": "Paulo Gomes", 11 | "email": "pauloamgomes@gmail.com", 12 | "homepage": "https://www.pauloamgomes.net" 13 | } 14 | ], 15 | "require": { 16 | "php": ">= 7.3", 17 | "composer/installers": "^1.10" 18 | }, 19 | "suggest": { 20 | "aheinze/cockpit": "Please install Cockpit before installing this addon" 21 | }, 22 | "extra": { 23 | "installer-name": "Moderation" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Controller/Admin.php: -------------------------------------------------------------------------------- 1 | app->module('cockpit')->hasaccess('moderation', 'manage')) { 17 | return FALSE; 18 | } 19 | 20 | $keys = $this->app->module('cockpit')->loadApiKeys(); 21 | 22 | $key = $keys['moderation'] ?? ''; 23 | 24 | return $this->render('moderation:views/settings/index.php', ['key' => $key]); 25 | } 26 | 27 | public function save() { 28 | $key = $this->param('key', false); 29 | 30 | if (!$key) { 31 | return false; 32 | } 33 | 34 | $keys = $this->app->module('cockpit')->loadApiKeys(); 35 | $keys['moderation'] = $keys; 36 | 37 | return ['success' => $this->app->module('cockpit')->saveApiKeys($keys)]; 38 | 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /assets/field-moderation.tag: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 24 | 25 | -------------------------------------------------------------------------------- /assets/moderation.css: -------------------------------------------------------------------------------- 1 | .with-schedule { 2 | padding-bottom: 15px; 3 | border-bottom: 1px solid #e0e0e0; 4 | } 5 | 6 | .schedule-container { 7 | min-height: 250px; 8 | } 9 | 10 | .schedule-container .uk-dropdown-scrollable { 11 | position: fixed; 12 | } 13 | 14 | .schedule-container .schedule-time { 15 | position: relative; 16 | } 17 | 18 | .uk-moderation-element .uk-badge { 19 | min-width: 90px; 20 | text-align: left; 21 | padding: 6px 8px; 22 | } 23 | .uk-moderation-list { 24 | width: auto; 25 | max-width: 80px; 26 | } 27 | .uk-moderation-list .uk-badge { 28 | font-size: 8px; 29 | padding: 2px 6px; 30 | border-radius: 2px; 31 | } 32 | .uk-moderation-Unpublished .uk-badge { 33 | background-color: #d85030; 34 | color: #ffffff; 35 | } 36 | .uk-moderation-Draft .uk-badge { 37 | background-color: #e28327; 38 | color: #ffffff !important; 39 | } 40 | .uk-moderation-Published .uk-badge { 41 | background-color: #659f13; 42 | color: #ffffff !important; 43 | } 44 | .icon-Unpublished { 45 | color: #d85030; 46 | } 47 | .icon-Published { 48 | color: #659f13; 49 | } 50 | .icon-Draft { 51 | color: #e28327; 52 | } 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Paulo Gomes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /admin.php: -------------------------------------------------------------------------------- 1 | addResource('moderation', [ 5 | 'manage', 6 | 'publish', 7 | 'unpublish', 8 | 'schedule', 9 | ]); 10 | 11 | /** 12 | * Add moderation markup to collections sidebar. 13 | */ 14 | $this->on('collections.entry.aside', function($name) use ($app) { 15 | $canSchedule = $app->module('cockpit')->hasaccess('moderation', ['manage', 'schedule']); 16 | $settings = $this->retrieve('config/moderation', ['schedule' => []]); 17 | 18 | $scheduleEnabled = $canSchedule && isset($settings['schedule']) && ($settings['schedule'] === '*' || in_array($name, $settings['schedule'])); 19 | 20 | $this->renderView("moderation:views/partials/entry-aside.php", ['enabled' => $scheduleEnabled]); 21 | }); 22 | 23 | /** 24 | * Initialize addon for admin pages. 25 | */ 26 | $app->on('admin.init', function () use ($app) { 27 | // Check moderation capabilities for the user. 28 | $canPublish = $app->module('cockpit')->hasaccess('moderation', ['manage', 'publish']); 29 | $canUnpublish = $app->module('cockpit')->hasaccess('moderation', ['manage', 'unpublish']); 30 | 31 | $this('admin')->data["extract/moderation"] = [ 32 | 'canPublish' => $canPublish, 33 | 'canUnpublish' => $canUnpublish, 34 | ]; 35 | 36 | // Add field tag. 37 | $this->helper('admin')->addAssets('moderation:assets/field-moderation.tag'); 38 | $this->helper('admin')->addAssets('moderation:assets/moderation.css'); 39 | $this->helper('admin')->addAssets('moderation:assets/moderation.js'); 40 | // Bind admin routes. 41 | $this->bindClass('Moderation\\Controller\\Admin', 'settings/moderation'); 42 | }); 43 | 44 | /* 45 | * Add menu entry if the user has access to group stuff. 46 | */ 47 | $this->on('cockpit.view.settings.item', function () use ($app) { 48 | if ($app->module('cockpit')->hasaccess('moderation', 'manage')) { 49 | $this->renderView("moderation:views/partials/settings.php"); 50 | } 51 | }); 52 | 53 | /** 54 | * Provide modififications on the preview url (Helpers addon). 55 | */ 56 | $this->on('helpers.preview.url', function(&$preview) use ($app) { 57 | $keys = $app->module('cockpit')->loadApiKeys(); 58 | $preview['token'] = $keys['moderation'] ?? ''; 59 | }); 60 | 61 | -------------------------------------------------------------------------------- /Controller/RestApi.php: -------------------------------------------------------------------------------- 1 | 'Published', 14 | 'Unpublish' => 'Unpublished', 15 | ]; 16 | 17 | /** 18 | * Run schedulling. 19 | */ 20 | public function list() { 21 | $range = $this->param('range', 10); 22 | $results = (array) $this->app->storage->find('moderation/schedule'); 23 | $outdated = []; 24 | $scheduled = []; 25 | $active = []; 26 | $ago = strtotime("-{$range} minutes"); 27 | $now = time(); 28 | foreach ($results as $result) { 29 | $time = strtotime("{$result['schedule']['date']}T{$result['schedule']['time']}Z"); 30 | if ($time < $ago) { 31 | $outdated[] = $result; 32 | } 33 | elseif ($time > $now) { 34 | $scheduled[] = $result; 35 | } 36 | else { 37 | $active[] = $result; 38 | } 39 | } 40 | return [ 41 | 'outdated' => $outdated, 42 | 'active' => $active, 43 | 'scheduled' => $scheduled, 44 | ]; 45 | } 46 | 47 | /** 48 | * Run schedulling. 49 | */ 50 | public function run() { 51 | $results = $this->list(); 52 | $processed = []; 53 | $collections = []; 54 | $entries = []; 55 | foreach ($results['active'] as $data) { 56 | $type = $data['schedule']['type']; 57 | if (!isset($collections[$data['_collection']])) { 58 | $collection = $this->app->module('collections')->collection($data['_collection']); 59 | $collections[$data['_collection']] = $collection; 60 | } 61 | else { 62 | $collection = $collections[$data['_collection']]; 63 | } 64 | 65 | $entry = (array) $this->app->storage->findOne("collections/{$collection['_id']}", ['_id' => $data['_oid']]); 66 | $field = $data['_field']; 67 | if ($data['_lang']) { 68 | $field .= "_{$data['_lang']}"; 69 | } 70 | if (isset($entry[$field]) && isset($this->moderation[$type])) { 71 | $old_status = $entry[$field]; 72 | $entry[$field] = $this->moderation[$type]; 73 | $this->app->module('collections')->save($data['_collection'], $entry, ['revision' => TRUE]); 74 | $this->app->storage->remove('moderation/schedule', ['_id' => $data['_id']]); 75 | $processed[] = [ 76 | '_id' => $entry['_id'], 77 | 'old_status' => $old_status, 78 | 'new_status' => $entry[$field], 79 | 'schedule_data' => $data, 80 | ]; 81 | } 82 | } 83 | return $processed; 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /views/settings/index.php: -------------------------------------------------------------------------------- 1 |
2 | 6 |
7 | 8 |
9 | 10 |
11 |
12 | 13 |
14 | @lang('Moderation API-Key') 15 | @lang('Share with caution') 16 |
17 | 18 |
19 |
20 | 21 |
22 | @lang('Use the api token as an extra query parameter (previewToken) in your API requests') 23 |
24 |
25 |
26 | 27 | 28 |
29 |
30 | 31 |
32 | 33 |
34 | 35 |
36 | 37 | @lang('Close') 38 |
39 | 40 |
41 | 42 |
43 |
44 |
45 | 46 | 47 | 86 | 87 |
88 | -------------------------------------------------------------------------------- /actions.php: -------------------------------------------------------------------------------- 1 | on('collections.find.before', function ($name, &$options) use ($app) { 11 | 12 | // Get the collection. 13 | $collection = $this->module('collections')->collection($name); 14 | // Exclude on unpublished state. 15 | foreach ($collection['fields'] as $field) { 16 | if ($field['type'] === 'moderation') { 17 | $field_name = $field_name_lang = $field['name']; 18 | if ($field['localize'] && $lang = $app->param("lang", false)) { 19 | $field_name_lang .= "_{$lang}"; 20 | } 21 | 22 | $options['filter']['$and'][] = [$field_name_lang => ['$exists' => TRUE]]; 23 | $options['filter']['$and'][] = [$field_name_lang => ['$ne' => 'Unpublished']]; 24 | break; 25 | } 26 | } 27 | 28 | if (!isset($field_name)) { 29 | return; 30 | } 31 | 32 | // If we are using filter/fields we need to use original field name. 33 | if (!empty($options['fields'])) { 34 | $options['fields'][$field_name] = 1; 35 | } 36 | 37 | $app->trigger("moderation.find.before", [$name, &$options]); 38 | }); 39 | 40 | /** 41 | * Iterate over the collection entries. 42 | * 43 | * For the draft ones check if we have a previous published revision. 44 | */ 45 | $app->on('collections.find.after', function ($name, &$entries) use ($app) { 46 | $token = $app->param('previewToken', FALSE); 47 | // If we have a valid previewToken don't need to go further. 48 | if ($token && $app->module('moderation')->validateToken($token)) { 49 | return; 50 | } 51 | 52 | // Get the moderation field. 53 | $field = $app->module('moderation')->getModerationField($name); 54 | 55 | if (!$field) { 56 | return; 57 | } 58 | 59 | $lang = $app->param('lang', FALSE); 60 | $ignoreDefaultFallback = $app->param('ignoreDefaultFallback', FALSE); 61 | if ($ignoreDefaultFallback = $this->param('ignoreDefaultFallback', false)) { 62 | $ignoreDefaultFallback = \in_array($ignoreDefaultFallback, ['1', '0']) ? \boolval($ignoreDefaultFallback) : $ignoreDefaultFallback; 63 | } 64 | $moderation_field = $field['name']; 65 | $localize = $field['localize'] ?? FALSE; 66 | $populate = $app->param('populate', 1); 67 | 68 | foreach ($entries as $idx => $entry) { 69 | if (!isset($entry[$moderation_field])) { 70 | continue; 71 | } 72 | 73 | // If Draft ensure we retrieve the latest published revision. 74 | // Please note that the entry being checked has already been thru lang filtering. 75 | if ($entry[$moderation_field] == 'Draft') { 76 | $revisions = $app->helper('revisions')->getList($entry['_id']); 77 | 78 | if ($lang && $localize) { 79 | $moderation_field .= "_{$lang}"; 80 | } 81 | // However, this has not been filtered: 82 | $published = $app->module('moderation')->getLastPublished($entry['_id'], $moderation_field, $revisions); 83 | 84 | if ($published) { 85 | $published = $app->module('moderation')->removeLangSuffix($name, $published, $lang, $ignoreDefaultFallback); 86 | $published = array_merge($entry, array_intersect_key($published, $entry)); 87 | $published = [$published]; 88 | $populated = cockpit_populate_collection($published, $populate); 89 | $published = current($populated); 90 | $entries[$idx] = $published; 91 | } 92 | else { 93 | unset($entries[$idx]); 94 | } 95 | } 96 | } 97 | // Rebuild array indices. 98 | $entries = array_values($entries); 99 | }); 100 | -------------------------------------------------------------------------------- /bootstrap.php: -------------------------------------------------------------------------------- 1 | module('moderation')->extend([ 17 | 18 | 'getLastPublished' => function ($id, $moderation_field, array $revisions = []) { 19 | $revisons = array_reverse($revisions); 20 | 21 | foreach ($revisions as $revision) { 22 | 23 | // If we have an Unpublished version before draft entry is ignored. 24 | if ($revision['data'][$moderation_field] === 'Unpublished') { 25 | return FALSE; 26 | } 27 | 28 | // If we have at least one Published revision. 29 | // And we don't have an unpublished one before we return the entry. 30 | if ($revision['data'][$moderation_field] === 'Published') { 31 | return $revision['data']; 32 | } 33 | } 34 | 35 | // In all other cases (no revisions, only drafts we ignore the entry). 36 | return FALSE; 37 | }, 38 | 39 | 'validateToken' => function($token) { 40 | $keys = $this->app->module('cockpit')->loadApiKeys(); 41 | if (!empty($keys['moderation']) && $keys['moderation'] === $token) { 42 | return TRUE; 43 | } 44 | return FALSE; 45 | }, 46 | 47 | 'getModerationField' => function($name) { 48 | $collection = $this->app->module('collections')->collection($name); 49 | if ($collection && !empty($collection['fields'])) { 50 | foreach ($collection['fields'] as $field) { 51 | if ($field['type'] === 'moderation') { 52 | return $field; 53 | } 54 | } 55 | } 56 | return FALSE; 57 | }, 58 | 59 | 'saveSettings' => function($settings) { 60 | $keys = $this->app->module('cockpit')->loadApiKeys(); 61 | $keys['moderation'] = $settings['key']; 62 | 63 | return ['success' => $this->app->module('cockpit')->saveApiKeys($keys)]; 64 | }, 65 | 66 | 'removeLangSuffix' => function($name, $entry, $lang, $ignoreDefaultFallback) { 67 | if ($lang) { 68 | 69 | $collection = $this->app->module('collections')->collection($name); 70 | 71 | foreach ($collection['fields'] as $field) { 72 | 73 | if ($field['localize']) { 74 | $fieldName = $field['name']; 75 | $suffixedFieldName = $fieldName."_$lang"; 76 | 77 | if ( 78 | isset($entry[$suffixedFieldName]) && 79 | $entry[$suffixedFieldName] !== '' 80 | ) { 81 | $entry[$fieldName] = $entry[$suffixedFieldName]; 82 | 83 | if (isset($entry["{$suffixedFieldName}_slug"]) && $entry["{$suffixedFieldName}_slug"] !== '') { 84 | $entry["{$fieldName}_slug"] = $entry["{$suffixedFieldName}_slug"]; 85 | } 86 | } elseif ( 87 | $ignoreDefaultFallback === true || 88 | ( 89 | is_array($ignoreDefaultFallback) && 90 | in_array($fieldName, $ignoreDefaultFallback) 91 | ) 92 | ) { 93 | $entry[$fieldName] = null; 94 | } 95 | } 96 | } 97 | } 98 | return $entry; 99 | }, 100 | 101 | 'setSchedule' => function(array $data) { 102 | $id = $data['id']; 103 | $lang = $data['lang'] ?? ""; 104 | 105 | $user = $this->app->module('cockpit')->getUser(); 106 | 107 | $existing = $this->app->storage->findOne('moderation/schedule', ['_oid' => $id, 'lang' => $lang]); 108 | 109 | $entry = [ 110 | '_oid' => trim($id), 111 | 'schedule' => $data['schedule'], 112 | '_field' => $data['field'], 113 | '_collection' => $data['collection'], 114 | '_lang' => trim($data['lang']), 115 | '_creator' => $user['_id'] ?? NULL, 116 | '_modified' => time() 117 | ]; 118 | 119 | if ($existing) { 120 | $entry['_id'] = $existing['_id']; 121 | $this->app->storage->save('moderation/schedule', $entry); 122 | } 123 | else { 124 | $this->app->storage->insert('moderation/schedule', $entry); 125 | } 126 | 127 | return $entry; 128 | }, 129 | 130 | 'getSchedule' => function(array $data) { 131 | $filter = ['_oid' => $data['id'], '_lang' => $data['lang']]; 132 | return $this->app->storage->findOne('moderation/schedule', $filter); 133 | }, 134 | 135 | 'removeSchedule' => function(array $data) { 136 | $id = $data['id']; 137 | $lang = $data['lang']; 138 | return $this->app->storage->remove('moderation/schedule', ['_oid' => $id, '_lang' => $lang]); 139 | }, 140 | 141 | 'getLastPublishedStatus' => function (array $params) { 142 | $revisions = $this->app->helper('revisions')->getList($params['id']); 143 | if ($revisions) { 144 | $moderationField = $this->getModerationField($params['collection']); 145 | if ($moderationField) { 146 | $field = $moderationField['name']; 147 | if ($moderationField['localize'] && $params['lang']) { 148 | $field .= "_{$params['lang']}"; 149 | } 150 | foreach ($revisions as $revision) { 151 | if ($revision['data'][$field] != "Draft") { 152 | return $revision['data'][$field]; 153 | } 154 | } 155 | } 156 | } 157 | return "Unpublished"; 158 | }, 159 | 160 | ]); 161 | 162 | // Incldude admin. 163 | if (COCKPIT_ADMIN && !COCKPIT_API_REQUEST) { 164 | include_once __DIR__ . '/admin.php'; 165 | } 166 | 167 | // Include actions. 168 | if (COCKPIT_API_REQUEST) { 169 | include_once __DIR__ . '/actions.php'; 170 | include_once __DIR__ . '/cockpitql.php'; 171 | $this->on('cockpit.rest.init', function ($routes) { 172 | $routes['schedule'] = 'Moderation\\Controller\\RestApi'; 173 | }); 174 | } 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cockpit CMS Moderation Add-on 2 | 3 | This addon extends Cockpit CMS core functionality by introducing the possibility to have moderation of collections. It means that its possible to create collections with a status (Unpublished, Draft or Published) affecting the way that entries are retrieved: 4 | 5 | - **Unpublished** - Any collection entry in unpublished state will be filtered out. 6 | - **Draft** - Any collection entry in Draft that doesn't have a previous revision in published status will be also filtered out. If there is a previous revision with published status the revision will be returned instead. However on a scenario that we have a published > unpublished > draft sequence no entry will be returned. 7 | - **Published** - They are always returned. 8 | 9 | ## Installation 10 | 11 | ### Manual 12 | 13 | Download [latest release](https://github.com/pauloamgomes/CockpitCMS-Moderation) and extract to `COCKPIT_PATH/addons/Moderation` directory 14 | 15 | ### Git 16 | 17 | ```sh 18 | git clone https://github.com/pauloamgomes/CockpitCMS-Moderation.git ./addons/Moderation 19 | ``` 20 | 21 | ### Cockpit CLI 22 | 23 | ```sh 24 | php ./cp install/addon --name Moderation --url https://github.com/pauloamgomes/CockpitCMS-Moderation.git 25 | ``` 26 | 27 | ### Composer 28 | 29 | 1. Make sure path to cockpit addons is defined in your projects' _composer.json_ file: 30 | 31 | ```json 32 | { 33 | "name": "MY_PROJECT", 34 | "extra": { 35 | "installer-paths": { 36 | "cockpit/addons/{$name}": ["type:cockpit-module"] 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | 2. In your project root run: 43 | 44 | ```sh 45 | composer require pauloamgomes/cockpitcms-moderation 46 | ``` 47 | 48 | --- 49 | 50 | ## Configuration 51 | 52 | To use the main functionality of the addon no extra configuration is required. 53 | To use the preview mode (Draft entries will be also returned) is required to configure an API key 54 | on the addon settings page. You can use the moderation api key in your requests like: 55 | 56 | ``` 57 | http://your-cockpit-site/api/collections/get/?token=&previewToken= 58 | ``` 59 | 60 | Additional addon settings are available at: http://your-cockpit-site/settings/moderation 61 | 62 | ### Permissions 63 | 64 | The following permissions (ACL's) are defined: 65 | 66 | * **manage** - access to all moderation states and addons settings page 67 | * **publish** - can change entries to Published state 68 | * **unpublish** - can change entries to Unpublished state 69 | 70 | Example of configuration for 3 groups of editors where `editor` can only create/update entries to `Draft` state, `approver` can create/update `Draft` and move to `Published` state, and finally `manager` can publish and unpublish entries. 71 | 72 | ```yaml 73 | groups: 74 | editor: 75 | approver: 76 | moderation: 77 | publish: true 78 | manager: 79 | moderation: 80 | publish: true 81 | unpublish: true 82 | ``` 83 | 84 | By default admins have super access, any other groups that have not the permissions specificed in the configuration, can only create/edit 85 | entries only in Draft mode. 86 | 87 | ### Scheduling 88 | 89 | Scheduling is supported since version v1.3, to enable scheduling add a new entry in the config.yml like below: 90 | 91 | ```yaml 92 | moderation: 93 | schedule: 94 | - page 95 | - article 96 | ``` 97 | above configuration enables scheduling on collections page and article, if you want to enable for all collections by default use: 98 | 99 | ```yaml 100 | moderation: 101 | schedule: * 102 | ``` 103 | 104 | If using scheduling, its required to provide a `schedule` permission for non admin roles: 105 | 106 | ```yaml 107 | groups: 108 | editor: 109 | moderation: 110 | schedule: true 111 | ``` 112 | The Scheduling just defines in Cockpit the operation and date/time for a specific collection entry, to run the scheduling is required 113 | to invoke a REST API endpoint, that endpoing can be invoked using cron or other mechanism. By default when executed, it will run against 114 | all scheduled entries between a range of time in the past (default of 10m, but can be changed in the request) and current date. 115 | 116 | The following example illustrates how that works: 117 | 118 | ![Scheduling example](https://monosnap.com/image/6szBmxoUUUZwO7QT5kf5xVYteo9n3C) 119 | 120 | In the above example, the schedule operation was executed at 22:55 and detected 2 operations to run at 22:52 (in the range of 10m) performing 121 | the defined moderation status change. 122 | 123 | 124 | ## Usage 125 | 126 | 1. Add a new field to your collection of type Moderation. 127 | You can name whatever you want for your moderation field, e.g. status, moderation, etc.. But you need to keep in mind 128 | if you change the field later you may need to manually update all existing collection entries. 129 | 2. When creating a new collection entry the moderation value will be `Draft`, can be changed to `Unpublished` or `Published`. 130 | 3. When editing an existing collection the moderation value will change automatically to `Draft`. 131 | 4. When retrieving a collection entry (or list of entries) only entries with moderation value of `Published` or `Draft` (if there is a `Published` revision before the `Draft` one) will be returned. 132 | 133 | ### Options 134 | The moderation field supports the following options: 135 | 136 | * `autodraft` (default: _true_) If set to _false_, entries that are being edited wont be set to `Draft` 137 | 138 | ### Localization 139 | 140 | The moderation fields supports localization as any other Cockpit field, just enable the localization option in the field configuration. 141 | When editing an entry, the moderation status will be saved according to the selected option on each language. 142 | 143 | If Scheduling is enabled and the moderation field is set to be localized, the scheduling will be defined for each language. 144 | 145 | ## Demo 146 | 147 | [![Screencast](https://monosnap.com/image/o9F3WihH3NtOk1VfszARSa402sD12U)](http://www.youtube.com/watch?v=TdhoThghRRY "Screencast") 148 | 149 | - [Demo v1.3](http://www.youtube.com/watch?v=TdhoThghRRY "Screencast v1.3") 150 | - [Demo v1.0](https://youtu.be/LywGxJqUJkg "Screencast v1.0") 151 | 152 | 153 | ## Copyright and license 154 | 155 | Copyright since 2018 pauloamgomes under the MIT license. 156 | -------------------------------------------------------------------------------- /views/partials/entry-aside.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

@lang('Set Schedule')

4 |
5 |
6 | @lang('Configure the scheduling moderation status and the date/time.') 7 |
8 |
9 |
10 |
11 |
12 | { getLangLabel(lang) } 13 |
14 |
15 |
16 |
17 |
18 | 19 | 20 | 25 |
26 |
27 |
28 |
29 |
30 | 31 | 32 |
33 |
34 |
35 | 36 |
37 |
38 |
39 |
40 | 51 |
52 |
53 | 54 |
55 | 56 |
57 | 63 |
64 | 65 | {originalModeration[localized ? lang : ''] !== entry[moderation_field] ? App.i18n.get("Change to:") : App.i18n.get("Save as:")} {App.i18n.get(entry[moderation_field])} 66 | 67 |
68 | 73 |
74 |
75 | 76 | 79 |
80 | { schedule.type }
81 | { schedule.date } { schedule.time } @lang('Cancel') 82 |
83 |
84 | 85 | 86 |
87 | 88 | 319 | --------------------------------------------------------------------------------