├── updates └── version.yaml ├── components ├── create │ └── default.htm ├── update │ └── default.htm ├── listitems │ └── default.htm ├── ListItems.php ├── Update.php └── Create.php ├── LICENSE ├── README.md └── Plugin.php /updates/version.yaml: -------------------------------------------------------------------------------- 1 | 1.0.1: First version of CRUD 2 | -------------------------------------------------------------------------------- /components/create/default.htm: -------------------------------------------------------------------------------- 1 |
3 | 4 | {{ form.formRender()|raw }} 5 | 6 |
7 | 8 |
9 |
-------------------------------------------------------------------------------- /components/update/default.htm: -------------------------------------------------------------------------------- 1 | 2 |
4 | 5 | {{ __SELF__.form.formRender()|raw }} 6 | 7 |
8 | 14 |
15 |
-------------------------------------------------------------------------------- /components/listitems/default.htm: -------------------------------------------------------------------------------- 1 | 3 | {% for item in __SELF__.items %} 4 | 5 | 8 | 20 | 21 | {% endfor %} 22 |
6 | {{ item[__SELF__.field] }} 7 | 9 | 11 | edit 12 | 13 | 19 |
23 | 24 | {% if __SELF__.createPageUrl is not empty %} 25 | New item 26 | {% endif %} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 inetis.ch 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | Sample OctoberCMS plugin to demonstrate creating CRUD features for existing backend models/controllers within the frontend. 4 | 5 | >**NOTE:** This plugin is NOT production ready. Additional security checks and verification steps MUST be added before implementing on a production site. 6 | 7 | ## Demo 8 | ![CRUD demo](https://user-images.githubusercontent.com/12028540/27440023-be556244-5769-11e7-8d04-1540b822d66c.gif) 9 | 10 | ## Usage 11 | This plugin demonstrates the following three components: 12 | 13 | ### crudList 14 | Displays a list of items and adds ability to delete items when clicking on the delete button. 15 | 16 | Property | Description | Example 17 | --- | :--- | --- 18 | Model | The full class name of the model you want to use (with namespace) | `\Dev\Plugin\Models\User` 19 | Field to display | The name of the property you want to display in the list | `title` 20 | Page Edit Item | The page that contains the crudUpdate component (used by the Edit buttons) | `update.htm` 21 | Page New Item | The page that contain the crudCreate component (used by the New item button) | `create.htm` 22 | 23 | ### crudUpdate 24 | Displays an edit form. 25 | 26 | Property | Description | Example 27 | --- | :--- | --- 28 | Model | The full class name of the model you want to use (with namespace) | `Dev\Plugin\Models\User` 29 | Controller | The full class name of the controller you want to use (with namespace) | `Dev\Plugin\Controllers\Users` 30 | Form context | The context you want to use (see [Backend forms > Field options > Context](https://octobercms.com/docs/backend/forms#form-field-options)) | `frontend` 31 | ID | The primary key of the item you want to edit (can be a URL parameter) | `{{ :id }}` 32 | Form config (optional) | Override the default form configuration file (`fields.yaml`) defined in the controller (`config_form.yaml`) (see [Backend forms > Defining form fields](https://octobercms.com/docs/backend/forms#form-fields) | `$/dev/plugin/models/user/fields.yaml` 33 | Success page | The Page to redirect the user after model has correctly saved | `update.htm` 34 | 35 | ### crudCreate 36 | Displays a create form 37 | 38 | Property | Description | Example 39 | --- | :--- | --- 40 | Model | The full class name of the model you want to use (with namespace) | `Dev\Plugin\Models\User` 41 | Controller | The full class name of the controller you want to use (with namespace) | `Dev\Plugin\Controllers\Users` 42 | Form context | The context you want to use (see [Backend forms > Field options > Context](https://octobercms.com/docs/backend/forms#form-field-options)) | `frontend` 43 | ID | Primary key of the item you want edit 44 | Form config (optional) | Override the default form configuration file (`fields.yaml`) defined in the controller (`config_form.yaml`) (see [Backend forms > Defining form fields](https://octobercms.com/docs/backend/forms#form-fields) | `$/dev/plugin/models/user/fields.yaml` 45 | Success page | The Page to redirect the user after model has correctly saved | `update.htm` 46 | 47 | ## Current Limitations 48 | - Backend widget are not supported (the DOM is generated but without any JS/CSS) - Any PRs are welcome 49 | - No access validation, anybody with access to the routes that these components live on can update your models without authorization or validation. 50 | 51 | ## Author 52 | inetis is a webdesign agency in Vufflens-la-Ville, Switzerland. We love coding and creating powerful apps and sites [see our website](https://inetis.ch). 53 | -------------------------------------------------------------------------------- /Plugin.php: -------------------------------------------------------------------------------- 1 | 'Frontend CRUD example plugin', 19 | 'description' => 'CRUD auto-generated from existing backend model controller', 20 | 'author' => 'inetis', 21 | 'icon' => 'icon-list-alt', 22 | 'homepage' => 'https://github.com/inetis-ch/oc-CRUD-plugin' 23 | ]; 24 | } 25 | 26 | /** 27 | * Registers any front-end components implemented in this plugin. 28 | * 29 | * @return array 30 | */ 31 | public function registerComponents() 32 | { 33 | return [ 34 | 'Inetis\Crud\Components\Create' => 'crudCreate', 35 | 'Inetis\Crud\Components\ListItems' => 'crudList', 36 | 'Inetis\Crud\Components\Update' => 'crudUpdate', 37 | ]; 38 | } 39 | 40 | /** 41 | * Register backend widgets 42 | * 43 | * @return array 44 | */ 45 | public function registerFormWidgets() 46 | { 47 | return [ 48 | 'Backend\FormWidgets\CodeEditor' => [ 49 | 'label' => 'Code editor', 50 | 'code' => 'codeeditor' 51 | ], 52 | 53 | 'Backend\FormWidgets\RichEditor' => [ 54 | 'label' => 'Rich editor', 55 | 'code' => 'richeditor' 56 | ], 57 | 58 | 'Backend\FormWidgets\MarkdownEditor' => [ 59 | 'label' => 'Markdown editor', 60 | 'code' => 'markdown' 61 | ], 62 | 63 | 'Backend\FormWidgets\FileUpload' => [ 64 | 'label' => 'File uploader', 65 | 'code' => 'fileupload' 66 | ], 67 | 68 | 'Backend\FormWidgets\Relation' => [ 69 | 'label' => 'Relationship', 70 | 'code' => 'relation' 71 | ], 72 | 73 | 'Backend\FormWidgets\DatePicker' => [ 74 | 'label' => 'Date picker', 75 | 'code' => 'datepicker' 76 | ], 77 | 78 | 'Backend\FormWidgets\TimePicker' => [ 79 | 'label' => 'Time picker', 80 | 'code' => 'timepicker' 81 | ], 82 | 83 | 'Backend\FormWidgets\ColorPicker' => [ 84 | 'label' => 'Color picker', 85 | 'code' => 'colorpicker' 86 | ], 87 | 88 | 'Backend\FormWidgets\DataTable' => [ 89 | 'label' => 'Data Table', 90 | 'code' => 'datatable' 91 | ], 92 | 93 | 'Backend\FormWidgets\RecordFinder' => [ 94 | 'label' => 'Record Finder', 95 | 'code' => 'recordfinder' 96 | ], 97 | 98 | 'Backend\FormWidgets\Repeater' => [ 99 | 'label' => 'Repeater', 100 | 'code' => 'repeater' 101 | ], 102 | 103 | 'Backend\FormWidgets\TagList' => [ 104 | 'label' => 'Tag List', 105 | 'code' => 'taglist' 106 | ] 107 | ]; 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /components/ListItems.php: -------------------------------------------------------------------------------- 1 | 'List items', 21 | 'description' => 'List all items of a model' 22 | ]; 23 | } 24 | 25 | public function defineProperties() 26 | { 27 | return [ 28 | 'modelClass' => [ 29 | 'title' => 'Model', 30 | 'description' => 'Full model name (with namespace)', 31 | 'type' => 'string', 32 | 'required' => true 33 | ], 34 | 'field' => [ 35 | 'title' => 'Field to display', 36 | 'type' => 'string', 37 | 'required' => true 38 | ], 39 | 'updatePage' => [ 40 | 'title' => "Page Edit item", 41 | 'description' => "Form for edit an item", 42 | 'type' => 'dropdown', 43 | 'required' => true 44 | ], 45 | 'createPage' => [ 46 | 'title' => "Page New item", 47 | 'description' => "Form for create a new item", 48 | 'type' => 'dropdown', 49 | 'required' => true 50 | ], 51 | ]; 52 | } 53 | 54 | /** 55 | * Dropdown for choose edit page 56 | * 57 | * @return mixed 58 | */ 59 | public function getUpdatePageOptions() 60 | { 61 | return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); 62 | } 63 | 64 | /** 65 | * Dropdown for choose create page 66 | * 67 | * @return mixed 68 | */ 69 | public function getCreatePageOptions() 70 | { 71 | return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); 72 | } 73 | 74 | /** 75 | * Display the list 76 | */ 77 | public function onRun() 78 | { 79 | $this->fillParameters(); 80 | 81 | $this->createPageUrl = $this->controller->pageUrl($this->createPage); 82 | $this->listItems(); 83 | } 84 | 85 | /** 86 | * AJAX - delete an item 87 | * 88 | * @return array 89 | */ 90 | public function onDelete() 91 | { 92 | $this->fillParameters(); 93 | $modelClass = $this->modelClass; 94 | 95 | $model = $modelClass::findOrFail(post('id')); 96 | $model->delete(); 97 | 98 | if (post('list_id')) { 99 | return [ 100 | '#' . post('list_id') => $this->listItems() 101 | ]; 102 | } 103 | } 104 | 105 | 106 | /** 107 | * Trigger exception if a required property is not set 108 | * 109 | * @return array Properties indexed by name 110 | * @throws SystemException 111 | */ 112 | private function fillParameters() 113 | { 114 | $properties = $this->defineProperties(); 115 | foreach ($data = $this->getProperties() as $key => $value) { 116 | if (!key_exists($key, $properties)) { 117 | continue; 118 | } 119 | 120 | if (empty($value) && $properties[$key]['required']) { 121 | throw new SystemException("Property $key ({$properties[$key]['title']}) can't be empty"); 122 | } 123 | 124 | if (property_exists($this, $key)) { 125 | $this->{$key} = $value; 126 | } 127 | } 128 | } 129 | 130 | /** 131 | * Generate list view 132 | * 133 | * @return string 134 | */ 135 | protected function listItems() 136 | { 137 | $modelClass = $this->modelClass; 138 | 139 | $this->items = $modelClass::all(); 140 | $this->items->each(function ($item) { 141 | $item->crudUpdatePage = $this->controller->pageUrl($this->updatePage, ['id' => $item->id]); 142 | }); 143 | 144 | return $this->renderPartial('@default'); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /components/Update.php: -------------------------------------------------------------------------------- 1 | 'Update existing item', 26 | 'description' => 'Display form for update an existing item' 27 | ]; 28 | } 29 | 30 | public function defineProperties() 31 | { 32 | return [ 33 | 'modelClass' => [ 34 | 'title' => 'Model', 35 | 'description' => 'Full model name (with namespace)', 36 | 'type' => 'string', 37 | 'required' => true, 38 | ], 39 | 'controllerClass' => [ 40 | 'title' => 'Controller', 41 | 'description' => 'Full controller name (with namespace)', 42 | 'type' => 'string', 43 | 'required' => true, 44 | ], 45 | 'formControllerContext' => [ 46 | 'title' => 'Form context', 47 | 'description' => '', 48 | 'type' => 'string', 49 | 'default' => 'frontend', 50 | 'required' => true, 51 | ], 52 | 'itemId' => [ 53 | 'title' => 'ID', 54 | 'type' => 'string', 55 | 'default' => '{{ :id }}', 56 | 'required' => true, 57 | ], 58 | 'formConfigFile' => [ 59 | 'title' => 'Form config', 60 | 'description' => 'Path to model fields config (by default use the one defined in config_form.yaml) - ex: $/dev/plugin/models/user/fields.yaml', 61 | 'type' => 'string', 62 | 'required' => false, 63 | ], 64 | 'successPage' => [ 65 | 'title' => 'Success page', 66 | 'description' => 'Page where redirect user when item was successfully created', 67 | 'type' => 'dropdown', 68 | 'required' => false 69 | ], 70 | ]; 71 | } 72 | 73 | /** 74 | * Dropdown for choose edit page 75 | * 76 | * @return mixed 77 | */ 78 | public function getSuccessPageOptions() 79 | { 80 | return ['' => 'Nothing'] + Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); 81 | } 82 | 83 | public function onRun() 84 | { 85 | $this->fillParameters(); 86 | $this->validateModel(); 87 | 88 | $formController = new FormController(new $this->controllerClass); 89 | 90 | if (!empty($this->formConfigFile)) { 91 | $formController->setConfig(array_merge( 92 | (array)$formController->getConfig(), 93 | ['form' => $this->formConfigFile] 94 | )); 95 | } 96 | 97 | $formController->update($this->itemId, $this->formControllerContext); 98 | 99 | $this->form = $formController; 100 | } 101 | 102 | /** 103 | * AJAX - Model edit 104 | * 105 | * @return \Illuminate\Http\RedirectResponse 106 | */ 107 | public function onUpdate() 108 | { 109 | $this->fillParameters(); 110 | $modelClass = $this->modelClass; 111 | $arrayName = class_basename($modelClass); 112 | 113 | $model = $modelClass::findOrFail($this->itemId); 114 | 115 | foreach (post($arrayName) as $property => $value) { 116 | $model->{$property} = $value; 117 | } 118 | $model->save(); 119 | 120 | Flash::success('Item correctly edited'); 121 | 122 | if (!$this->successPage) { 123 | return; 124 | } 125 | 126 | return Redirect::to($this->controller->pageUrl($this->successPage)); 127 | } 128 | 129 | /** 130 | * Trigger exception if a required property is not set 131 | * 132 | * @return array Properties indexed by name 133 | * @throws SystemException 134 | */ 135 | private function fillParameters() 136 | { 137 | foreach ($data = $this->getProperties() as $key => $value) { 138 | if (empty($value) && $this->defineProperties()[$key]['required']) { 139 | throw new SystemException('Property ' . $key . ' is can\'t be empty'); 140 | } 141 | 142 | if (property_exists($this, $key)) { 143 | $this->{$key} = $value; 144 | } 145 | } 146 | } 147 | 148 | /** 149 | * Trigger exception if model class not exist or if item not exist 150 | * 151 | * @param string $modelName 152 | * @param string $id 153 | * 154 | * @throws SystemException|ModelNotFoundException 155 | */ 156 | private function validateModel($modelName = null, $id = null) 157 | { 158 | $modelName = $modelName ?: $this->modelClass; 159 | $id = $id ?: $this->itemId; 160 | 161 | if (!class_exists($modelName)) { 162 | throw new SystemException('Model class ' . $modelName . ' not exist'); 163 | } 164 | 165 | $modelName::findOrFail($id); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /components/Create.php: -------------------------------------------------------------------------------- 1 | 'Create new item', 25 | 'description' => 'Display form for create a new item' 26 | ]; 27 | } 28 | 29 | public function defineProperties() 30 | { 31 | return [ 32 | 'modelClass' => [ 33 | 'title' => 'Model', 34 | 'description' => 'Full model name (with namespace)', 35 | 'type' => 'string', 36 | 'required' => true, 37 | ], 38 | 'controllerClass' => [ 39 | 'title' => 'Controller', 40 | 'description' => 'Full controller name (with namespace)', 41 | 'type' => 'string', 42 | 'required' => true, 43 | ], 44 | 'formControllerContext' => [ 45 | 'title' => 'Form context', 46 | 'description' => '', 47 | 'type' => 'string', 48 | 'default' => 'frontend', 49 | 'required' => true, 50 | ], 51 | 'formConfigFile' => [ 52 | 'title' => 'Form config', 53 | 'description' => 'Path to model fields config (by default use the one defined in config_form.yaml) - ex: $/dev/plugin/models/user/fields.yaml', 54 | 'type' => 'string', 55 | 'required' => false, 56 | ], 57 | 'successPage' => [ 58 | 'title' => 'Success page', 59 | 'description' => 'Page where redirect user when item was successfully created', 60 | 'type' => 'dropdown', 61 | 'required' => false 62 | ], 63 | ]; 64 | } 65 | 66 | /** 67 | * Dropdown for choose edit page 68 | * 69 | * @return mixed 70 | */ 71 | public function getSuccessPageOptions() 72 | { 73 | return ['' => 'Nothing'] + Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); 74 | } 75 | 76 | /** 77 | * Display form 78 | */ 79 | public function onRun() 80 | { 81 | $this->fillParameters(); 82 | $this->validateModel(); 83 | 84 | $formController = new FormController(new $this->controllerClass); 85 | 86 | if (!empty($this->formConfigFile)) { 87 | $formController->setConfig(array_merge( 88 | (array)$formController->getConfig(), 89 | ['form' => $this->formConfigFile] 90 | )); 91 | } 92 | 93 | $formController->create($this->formControllerContext); 94 | 95 | $this->page['form'] = $formController; 96 | } 97 | 98 | /** 99 | * AJAX - Model create 100 | * 101 | * @return \Illuminate\Http\RedirectResponse 102 | */ 103 | public function onSave() 104 | { 105 | $this->fillParameters(); 106 | $modelClass = $this->modelClass; 107 | $arrayName = class_basename($modelClass); 108 | 109 | $model = new $modelClass; 110 | foreach (post($arrayName) as $property => $value) { 111 | $model->{$property} = $value; 112 | } 113 | 114 | $model->save(); 115 | 116 | Flash::success('Item correctly created'); 117 | 118 | if ($this->successPage) { 119 | return Redirect::to($this->controller->pageUrl($this->successPage)); 120 | } 121 | } 122 | 123 | 124 | /** 125 | * Trigger exception if a required property is not set 126 | * 127 | * @return array Properties indexed by name 128 | * @throws SystemException 129 | */ 130 | private function fillParameters() 131 | { 132 | foreach ($data = $this->getAllProperties() as $key => $value) { 133 | if (empty($value) && $this->defineProperties()[$key]['required']) { 134 | throw new SystemException('Property ' . $key . ' can\'t be empty'); 135 | } 136 | 137 | if (property_exists($this, $key)) { 138 | $this->{$key} = $value; 139 | } 140 | } 141 | } 142 | 143 | /** 144 | * Get all properties (include not defined properties) 145 | * 146 | * @return array 147 | */ 148 | private function getAllProperties() 149 | { 150 | $properties = $this->getProperties(); 151 | 152 | foreach ($this->defineProperties() as $key => $property) { 153 | if (isset($properties[$key])) { 154 | continue; 155 | } 156 | 157 | if (!isset($property['default']) || starts_with($property['default'], '{{')) { 158 | $properties[$key] = null; 159 | continue; 160 | } 161 | 162 | $properties[$key] = $property['default']; 163 | } 164 | 165 | return $properties; 166 | } 167 | 168 | /** 169 | * Trigger exception if model class not exist or if item not exist 170 | * 171 | * @param string $modelName 172 | * 173 | * @throws SystemException|ModelNotFoundException 174 | */ 175 | private function validateModel($modelName = null) 176 | { 177 | $modelName = $modelName ?: $this->modelClass; 178 | 179 | if (!class_exists($modelName)) { 180 | throw new SystemException('Model class "' . $modelName . '" not exist'); 181 | } 182 | } 183 | 184 | } 185 | --------------------------------------------------------------------------------