PageRenderTime 49ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/framework/classes/controller/admin/crud.ctrl.php

https://github.com/jay3/core
PHP | 660 lines | 455 code | 86 blank | 119 comment | 64 complexity | cbba46400dc9a0e9bb46ecca840ee82a MD5 | raw file
Possible License(s): LGPL-2.1
  1. <?php
  2. /**
  3. * NOVIUS OS - Web OS for digital communication
  4. *
  5. * @copyright 2011 Novius
  6. * @license GNU Affero General Public License v3 or (at your option) any later version
  7. * http://www.gnu.org/licenses/agpl-3.0.html
  8. * @link http://www.novius-os.org
  9. */
  10. namespace Nos;
  11. class Controller_Admin_Crud extends Controller_Admin_Application
  12. {
  13. protected $config = array(
  14. 'model' => '',
  15. 'environment_relation' => null,
  16. 'tab' => array(
  17. 'iconUrl' => '',
  18. 'labels' => array(
  19. 'update' => null,
  20. 'insert' => 'New item',
  21. 'blankSlate' => 'Translate an item',
  22. ),
  23. ),
  24. 'actions' => array(),
  25. 'layout' => array(),
  26. 'fields' => array(),
  27. 'require_js' => array(),
  28. 'views' => array(
  29. 'form' => 'nos::crud/form',
  30. 'delete' => 'nos::crud/delete_popup',
  31. ),
  32. );
  33. protected $pk = '';
  34. protected $behaviours = array();
  35. protected $item = null;
  36. protected $clone = null;
  37. protected $is_new = false;
  38. protected $item_from = null;
  39. protected $item_environment = null;
  40. public function & __get($property)
  41. {
  42. return $this->{$property};
  43. }
  44. public function before()
  45. {
  46. parent::before();
  47. $this->config_build();
  48. }
  49. public function prepare_i18n()
  50. {
  51. parent::prepare_i18n();
  52. \Nos\I18n::current_dictionary(array('nos::application', 'nos::common'));
  53. }
  54. /**
  55. * Set properties from the config
  56. */
  57. protected function config_build()
  58. {
  59. $model = $this->config['model'];
  60. if (!empty($this->config['environment_relation'])) {
  61. $this->config['environment_relation'] = $model::relations($this->config['environment_relation']);
  62. if (!is_a($this->config['environment_relation'], 'Orm\\BelongsTo')) {
  63. $this->config['environment_relation'] = null;
  64. }
  65. }
  66. // Convert simplified layout syntax into the full syntax
  67. foreach (array('layout', 'layout_insert', 'layout_update') as $layout_name) {
  68. if (!empty($this->config[$layout_name])) {
  69. $layout = $this->config[$layout_name];
  70. $view = current($layout);
  71. if (!is_array($view) || empty($view['view'])) {
  72. $this->config[$layout_name] = array(
  73. array(
  74. 'view' => 'nos::form/layout_standard',
  75. 'params' => $layout,
  76. ),
  77. );
  78. }
  79. }
  80. }
  81. $this->behaviours = array(
  82. 'contextable' => $model::behaviours('Nos\Orm_Behaviour_Contextable', false),
  83. 'twinnable' => $model::behaviours('Nos\Orm_Behaviour_Twinnable', false),
  84. 'sharable' => $model::behaviours('Nos\Orm_Behaviour_Sharable', false),
  85. 'tree' => $model::behaviours('Nos\Orm_Behaviour_Tree', false),
  86. 'url' => $model::behaviours('Nos\Orm_Behaviour_Urlenhancer', false),
  87. );
  88. if (!$this->behaviours['contextable'] && $this->behaviours['twinnable']) {
  89. $this->behaviours['contextable'] = $this->behaviours['twinnable'];
  90. }
  91. $this->pk = \Arr::get($model::primary_key(), 0);
  92. if (empty($this->config['require_js'])) {
  93. $this->config['require_js'] = array();
  94. }
  95. $common_config = \Nos\Config_Common::load($model, array());
  96. $i18n_default = \Config::load('nos::i18n_common', true);
  97. $this->config['i18n'] = array_merge($i18n_default, \Arr::get($common_config, 'i18n', array()));
  98. $model::eventStatic('crudConfig', array(&$this->config, $this));
  99. foreach (array('insert', 'update') as $layout_suffix) {
  100. if (empty($this->config['views'][$layout_suffix])) {
  101. $this->config['views'][$layout_suffix] = $this->config['views']['form'];
  102. }
  103. if (empty($this->config['layout_'.$layout_suffix]) && !empty($this->config['layout'])) {
  104. $this->config['layout_'.$layout_suffix] = $this->config['layout'];
  105. }
  106. }
  107. }
  108. /**
  109. * Generic method to get an instance wether it's been already created or not
  110. * @param type $id : the id of the instance you want to edit (or create from)
  111. * @return type : instance of the model
  112. */
  113. protected function crud_item($id)
  114. {
  115. $model = $this->config['model'];
  116. return $id === null ? $model::forge() : $model::find($id);
  117. }
  118. /**
  119. * Set params used in view
  120. * WARNING : As views can forge other views, it is necessary to add view_params in view_params...
  121. * --> every time view_params is changed, $view_params['view_params'] = &$view_params; must be written.
  122. * @return Array : params for views and the array itself
  123. */
  124. protected function view_params()
  125. {
  126. $self = $this;
  127. $view_params = array(
  128. 'crud' => array(
  129. 'model' => $this->config['model'],
  130. 'behaviours' => $this->behaviours,
  131. 'pk' => $this->pk,
  132. 'environment' => $this->item_environment,
  133. 'config' => $this->config,
  134. 'url_form' => $this->config['controller_url'].'/form',
  135. 'url_insert_update' => $this->config['controller_url'].'/insert_update'.($this->is_new ? '' : '/'.$this->item->{$this->pk}),
  136. 'url_actions' => $this->config['controller_url'].'/json_actions'.($this->is_new ? '' : '/'.$this->item->{$this->pk}),
  137. 'is_new' => $this->is_new,
  138. 'actions' => array_values($this->get_actions()),
  139. 'dataset' => \Nos\Controller::dataset_item($this->item),
  140. 'tab_params' => $this->get_tab_params()
  141. ),
  142. 'item' => $this->item,
  143. );
  144. if ($this->behaviours['contextable']) {
  145. $view_params['crud']['context'] = $this->item->{$this->behaviours['contextable']['context_property']};
  146. }
  147. $view_params['view_params'] = &$view_params;
  148. return $view_params;
  149. }
  150. /**
  151. * Called before displaying the form to
  152. * - call init_item
  153. * - check permission
  154. * - build fields
  155. * @param type $id
  156. * @return View
  157. */
  158. public function action_form($id = null)
  159. {
  160. $this->item = $this->crud_item($id);
  161. $this->clone = clone $this->item;
  162. $this->is_new = $this->item->is_new();
  163. if ($this->is_new) {
  164. $this->init_item();
  165. }
  166. $this->checkPermission($this->is_new ? 'add' : 'edit');
  167. $fields = $this->fields($this->config['fields']);
  168. $fieldset = \Fieldset::build_from_config($fields, $this->item, $this->build_from_config());
  169. $fieldset = $this->fieldset($fieldset);
  170. if (isset($this->config['css'])) {
  171. $fieldset->prepend(render('nos::admin/load_css', array('css_files' => $this->config['css'])));
  172. }
  173. $view_params = $this->view_params();
  174. $view_params['fieldset'] = $fieldset;
  175. // We can't do this form inside the view_params() method, because additional vars (added
  176. // after the reference was created) won't be available from the reference
  177. $view_params['view_params'] = &$view_params;
  178. return \View::forge($this->config['views'][$this->is_new ? 'insert' : 'update'], $view_params, false);
  179. }
  180. /**
  181. * init_item() is used to pre-configure an new object.
  182. */
  183. protected function init_item()
  184. {
  185. $create_from_id = \Input::get('create_from_id', \Input::post('create_from_id', 0));
  186. $common_id = \Input::get('common_id', null);
  187. $environment_id = \Input::get('environment_id', null);
  188. if (!empty($create_from_id)) {
  189. $this->item_from = $this->crud_item($create_from_id);
  190. $this->item = clone $this->item_from;
  191. } elseif (!empty($common_id) && $this->behaviours['twinnable']) {
  192. $this->item->{$this->behaviours['twinnable']['common_id_property']} = $common_id;
  193. } elseif (!empty($environment_id) && !empty($this->config['environment_relation'])) {
  194. $model_environment = $this->config['environment_relation']->model_to;
  195. $this->item_environment = $model_environment::find($environment_id);
  196. $this->item->{$this->config['environment_relation']->key_from[0]} = $this->item_environment->{$this->config['environment_relation']->key_to[0]};
  197. }
  198. if ($this->behaviours['contextable']) {
  199. $context = \Input::get('context', false);
  200. $allowed_contexts = \Nos\User\Permission::contexts();
  201. $this->item->{$this->behaviours['contextable']['context_property']} = $context && isset($allowed_contexts[$context]) ? $context : key($allowed_contexts);
  202. }
  203. if ($this->behaviours['twinnable'] && $this->behaviours['tree']) {
  204. // New page: no parent
  205. // Translation: we have a common_id and can determine the parent
  206. if (!empty($this->item->{$this->behaviours['twinnable']['common_id_property']})) {
  207. $model = $this->config['model'];
  208. $common_id_property = $this->behaviours['twinnable']['common_id_property'];
  209. $item_context_common = $model::find('first', array(
  210. 'where' => array(
  211. array($common_id_property, $this->item->{$common_id_property}),
  212. ),
  213. ));
  214. $item_parent = $item_context_common->get_parent();
  215. // Fetch in the appropriate context
  216. if (!empty($item_parent)) {
  217. $item_parent = $item_parent->find_context($this->item->{$this->behaviours['twinnable']['context_property']});
  218. }
  219. // Set manually, because set_parent doesn't handle new items
  220. if (!empty($item_parent)) {
  221. $this->item->{$this->item->parent_relation()->key_from[0]} = $item_parent->{$this->pk};
  222. }
  223. }
  224. }
  225. }
  226. /**
  227. * If necessary, add specific fields to those already specified through config.
  228. * @return Array : merged fields;
  229. */
  230. protected function fields($fields)
  231. {
  232. if (!empty($this->item_from)) {
  233. $fields['create_from_id'] = array(
  234. 'form' => array(
  235. 'type' => 'hidden',
  236. 'value' => \Input::get('create_from_id', \Input::post('create_from_id', 0)),
  237. ),
  238. );
  239. }
  240. $model = $this->config['model'];
  241. $model::eventStatic('crudFields', array(&$fields, $this));
  242. return $fields;
  243. }
  244. /**
  245. * Set and apply validation, populate fieldset and modify template to show errors from validation
  246. * @param type Fieldset
  247. * @return type Fieldset
  248. */
  249. protected function fieldset($fieldset)
  250. {
  251. $fieldset->js_validation($this->config['require_js']);
  252. $fieldset->populate_with_instance($this->item);
  253. $fieldset->form()->set_config('field_template', '<tr><th class="{error_class}">{label}{required}</th><td class="{error_class}">{field} {error_msg}</td></tr>');
  254. return $fieldset;
  255. }
  256. /**
  257. * Default config for building the fieldset with \Fieldset::build_from_config.
  258. * @return Array : config
  259. */
  260. protected function build_from_config()
  261. {
  262. return array(
  263. 'before_save' => array($this, 'before_save'),
  264. 'success' => array($this, 'save'),
  265. );
  266. }
  267. /**
  268. * Default method 'save' called when building fieldset :
  269. * Create the dispatched event.
  270. * @return Array : config needed in the dispatched event.
  271. */
  272. public function save($item, $data)
  273. {
  274. $dispatchEvent = array(
  275. 'name' => $this->config['model'],
  276. 'action' => $this->is_new ? 'insert' : 'update',
  277. 'id' => (int) $item->{$this->pk},
  278. );
  279. if ($this->behaviours['contextable']) {
  280. $dispatchEvent['context'] = $item->{$this->behaviours['contextable']['context_property']};
  281. }
  282. if ($this->behaviours['twinnable']) {
  283. $dispatchEvent['context_common_id'] = (int) $item->{$this->behaviours['twinnable']['common_id_property']};
  284. }
  285. $return = array(
  286. 'notify' => $this->is_new ? $this->config['i18n']['notification item added'] : $this->config['i18n']['notification item saved'],
  287. 'closeDialog' => true,
  288. 'dispatchEvent' => array($dispatchEvent),
  289. );
  290. if ($this->is_new) {
  291. $return['replaceTab'] = $this->config['controller_url'].'/insert_update/'.$item->{$this->pk};
  292. } else {
  293. $return['action'] = array(
  294. 'action' => 'nosTabs',
  295. 'method' => 'update',
  296. 'tab' => $this->get_tab_params(),
  297. );
  298. $return['dataset'] = \Nos\Controller::dataset_item($this->item);
  299. }
  300. return $return;
  301. }
  302. /**
  303. * Default method 'before_save' called when building fieldset.
  304. */
  305. public function before_save($item, $data)
  306. {
  307. $this->checkPermission($this->is_new ? 'add' : 'edit');
  308. if ($this->behaviours['twinnable'] && $this->is_new) {
  309. $item_context = $this->item->get_context();
  310. $existing = $this->item->find_context($item_context);
  311. if (!empty($existing)) {
  312. $message = strtr(
  313. __('This item already exists in {{context}}. Therefore your item cannot be added.'),
  314. array(
  315. '{{context}}' => Tools_Context::contextLabel($item_context),
  316. )
  317. );
  318. $this->send_error(new \Exception($message));
  319. }
  320. }
  321. if ($this->behaviours['tree']) {
  322. // This doesn't work for now, because Fuel prevent relation from being fetch on new objects
  323. // https://github.com/fuel/orm/issues/171
  324. //$item = $item->get_parent();
  325. // Instead, retrieve the object manually
  326. // Model::find(null) returns an Orm\Query. We don't want that.
  327. $parent = empty($item->{$item->parent_relation()->key_from[0]}) ? null : $item::find($item->{$item->parent_relation()->key_from[0]});
  328. // Event 'change_parent' will set the appropriate context
  329. $item->set_parent($parent);
  330. }
  331. }
  332. /**
  333. * Determine wether the item is udpated or added and if it's creating from a different language
  334. * @param type $id of the item
  335. * @return View resulting from the call of a method (either action_form or blank_slate)
  336. */
  337. public function action_insert_update($id = null)
  338. {
  339. // insert_update : add a new item
  340. // insert_update?context=fr_FR : add a new item in the specific context
  341. // insert_update/ID : edit an existing item
  342. // insert_update/ID?context=fr_FR : translate an existing item (can be forbidden if the parent doesn't exists in that context)
  343. $this->item = $this->crud_item($id);
  344. if (empty($this->item)) {
  345. return $this->send_error(new \Exception($this->config['i18n']['notification item not found']));
  346. }
  347. $this->is_new = $this->item->is_new();
  348. if ($this->is_new || !$this->behaviours['twinnable']) {
  349. return $this->action_form($id);
  350. }
  351. if ($this->behaviours['twinnable']) {
  352. $selected_context = \Input::get('context', $this->is_new ? null : $this->item->get_context());
  353. foreach ($this->item->get_all_context() as $context_id => $context) {
  354. if ($selected_context == $context) {
  355. return $this->action_form($context_id);
  356. }
  357. }
  358. return $this->blank_slate($id, $selected_context);
  359. }
  360. }
  361. /**
  362. * Display a blank slate to create a new item from an another one in a different language
  363. * @param type $id : orignal item's id
  364. * @param type $context : chosen context
  365. * @return type View : blank_slate
  366. */
  367. public function blank_slate($id, $context)
  368. {
  369. $this->item = $this->crud_item($id);
  370. $this->is_new = true;
  371. if (empty($context)) {
  372. $context = \Input::get('context', key(Tools_Context::contexts()));
  373. }
  374. $view_params = array_merge(
  375. $this->view_params(),
  376. array(
  377. 'context' => $context,
  378. 'common_id' => $id,
  379. )
  380. );
  381. $view_params['crud']['tab_params']['url'] .= '?context='.$context;
  382. $view_params['crud']['tab_params']['label'] = $this->config['tab']['labels']['blankSlate'];
  383. // We can't do this form inside the view_params() method, because additional vars (added
  384. // after the reference was created) won't be available from the reference
  385. $view_params['view_params'] = &$view_params;
  386. return \View::forge('nos::crud/blank_slate', $view_params, false);
  387. }
  388. /**
  389. * Return possible actions from the config and transform them into json to display them
  390. * @param type $id : id of the item on which the actions call be applied
  391. * @return type : json
  392. */
  393. public function action_json_actions($id = null)
  394. {
  395. $this->item = $this->crud_item($id);
  396. if (empty($this->item)) {
  397. return $this->send_error(new \Exception($this->config['i18n']['notification item deleted']));
  398. }
  399. $this->is_new = $this->item->is_new();
  400. \Response::json(array_values($this->get_actions()));
  401. }
  402. /**
  403. * Return the config for setting the url of the novius-os tab
  404. * @return Array
  405. */
  406. protected function get_tab_params()
  407. {
  408. list($application_name) = \Config::configFile(get_called_class());
  409. $labelUpdate = $this->config['tab']['labels']['update'];
  410. $url = $this->config['controller_url'].'/insert_update'.(empty($this->item->id) ? '' : '/'.$this->item->id);
  411. if ($this->is_new) {
  412. $params = array();
  413. foreach (array('create_from_id', 'common_id', 'environment_id') as $key) {
  414. $value = \Input::get($key, false);
  415. if ($value !== false) {
  416. $params[$key] = $value;
  417. }
  418. }
  419. // Don't add context in blank slate case
  420. if ($this->behaviours['contextable'] && empty($this->item->id)) {
  421. $params['context'] = $this->item->get_context();
  422. }
  423. if (count($params)) {
  424. $url .= '?'.http_build_query($params);
  425. }
  426. }
  427. $tabInfos = array(
  428. 'iconUrl' => empty($this->config['tab']['iconUrl']) ? \Config::icon($application_name, 16) : $this->config['tab']['iconUrl'],
  429. 'label' => $this->is_new ? $this->config['tab']['labels']['insert'] : (is_callable($labelUpdate) ? $labelUpdate($this->item) : (empty($labelUpdate) ? $this->item->title_item() : $this->item->{$labelUpdate})),
  430. 'url' => $url,
  431. );
  432. return $tabInfos;
  433. }
  434. /**
  435. * Get possible actions in the appdesk from the config
  436. * @return array
  437. */
  438. protected function get_actions($all_targets = false)
  439. {
  440. $actions = array_merge(
  441. \Config::actions(array(
  442. 'models' => array(get_class($this->item)),
  443. 'target' => 'toolbar-edit',
  444. 'class' => get_called_class(),
  445. 'item' => $this->item,
  446. 'all_targets' => $all_targets,
  447. )),
  448. \Nos\Config_Common::prefixActions($this->config['actions'], $this->config['model'])
  449. );
  450. return $actions;
  451. }
  452. /**
  453. * @deprecated
  454. */
  455. protected function check_permission($action_name)
  456. {
  457. \Log::deprecated('->check_permission($action_name) is deprecated, use ->checkPermission($action_name) instead.', 'Chiba.1');
  458. return $this->checkPermission($action_name);
  459. }
  460. /**
  461. * Check if it's possible to delete an item, i.e. if it's not a new one.
  462. * @param string $action
  463. * @throws \Exception
  464. */
  465. protected function checkPermission($action_name)
  466. {
  467. $action_name = \Nos\Config_Common::prefixActionName($action_name, $this->config['model']);
  468. $actions = $this->get_actions(true);
  469. if (!isset($actions[$action_name])) {
  470. logger(\Fuel::L_WARNING, '\Nos\Controller_Admin_Crud->check_permission($action_name = '.$action_name.'). The action name was not found in the common configuration file, please double check for typo.');
  471. return true;
  472. }
  473. $action = $actions[$action_name];
  474. $disabled = isset($action['disabled']) ? \Config::getActionDisabledState($action['disabled'], $this->item) : false;
  475. if ($disabled !== false) {
  476. if (!is_string($disabled)) {
  477. $disabled = $this->config['i18n']['action not allowed'];
  478. }
  479. $this->send_error(new \Exception($disabled));
  480. }
  481. }
  482. /**
  483. * Display a popup to confirm deletion
  484. * @param type $id : the id of item which will be display
  485. * @return type View : the popup
  486. */
  487. public function action_delete($id = null)
  488. {
  489. try {
  490. if (\Input::method() === 'POST') {
  491. $this->delete_confirm();
  492. } else {
  493. $this->item = $this->crud_item($id);
  494. $this->is_new = $this->item->is_new();
  495. $this->checkPermission('delete');
  496. return \View::forge('nos::crud/delete_popup_layout', $this->view_params(), false);
  497. }
  498. } catch (\Exception $e) {
  499. $this->send_error($e);
  500. }
  501. }
  502. /**
  503. * Perform deletion (and pay attention to children and items existing in other languages)
  504. */
  505. public function delete_confirm()
  506. {
  507. $id = \Input::post('id', 0);
  508. if (empty($id) && \Fuel::$env === \Fuel::DEVELOPMENT) {
  509. $id = \Input::get('id');
  510. }
  511. $this->item = $this->crud_item($id);
  512. $this->is_new = $this->item->is_new();
  513. $this->checkPermission('delete');
  514. $dispatchEvent = array(
  515. 'name' => $this->config['model'],
  516. 'action' => 'delete',
  517. 'id' => (int) $id,
  518. );
  519. $json_delete = $this->delete();
  520. if ($this->behaviours['twinnable']) {
  521. $dispatchEvent['context_common_id'] = $this->item->{$this->behaviours['twinnable']['common_id_property']};
  522. $dispatchEvent['id'] = array();
  523. $dispatchEvent['context'] = array();
  524. // Filter allowed contexts
  525. $contexts = array_intersect(array_keys(\Nos\User\Permission::contexts()), \Input::post('contexts', array()));
  526. $contexts_item = $this->item->get_all_context();
  527. $count_1 = count($contexts);
  528. $count_2 = count($contexts_item);
  529. $count_3 = count(array_intersect($contexts, $contexts_item));
  530. $delete_all_contexts = ($count_1 == $count_3 && $count_2 == $count_3);
  531. // Children will be deleted recursively (with the 'after_delete' event from the Tree behaviour)
  532. foreach ($this->item->find_context($contexts) as $item_context) {
  533. $dispatchEvent['id'][] = (int) $item_context->{$this->pk};
  534. $dispatchEvent['context'][] = $item_context->{$this->behaviours['twinnable']['context_property']};
  535. if ($this->behaviours['tree']) {
  536. foreach ($item_context->get_ids_children(false) as $item_id) {
  537. $dispatchEvent['id'][] = (int) $item_id;
  538. }
  539. }
  540. // Delete only selected contexts
  541. if (!$delete_all_contexts) {
  542. // Reassigns common_id if this item is the main context (with the 'after_delete' event from the Twinnable behaviour)
  543. $item_context->delete();
  544. }
  545. }
  546. // Optimised operation for deleting all contexts
  547. if ($delete_all_contexts) {
  548. $this->item->delete_all_context();
  549. }
  550. } else {
  551. if ($this->behaviours['contextable']) {
  552. $dispatchEvent['context'] = $this->item{$this->behaviours['contextable']['context_property']};
  553. }
  554. if ($this->behaviours['tree']) {
  555. $dispatchEvent['id'] = array($this->item->{$this->pk});
  556. foreach ($this->item->get_ids_children(false) as $item_id) {
  557. $dispatchEvent['id'][] = (int) $item_id;
  558. }
  559. }
  560. $this->item->delete();
  561. }
  562. $json = array(
  563. 'notify' => $this->config['i18n']['notification item deleted'],
  564. 'dispatchEvent' => array($dispatchEvent),
  565. );
  566. if (is_array($json_delete)) {
  567. $json = \Arr::merge($json, $json_delete);
  568. }
  569. $this->response($json);
  570. }
  571. public function delete()
  572. {
  573. }
  574. }