diff --git a/src/Command/UpdaterCommand.php b/src/Command/UpdaterCommand.php new file mode 100644 index 0000000..9bf0bee --- /dev/null +++ b/src/Command/UpdaterCommand.php @@ -0,0 +1,133 @@ + 'metaTemplateV2', + ]; + + protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser + { + $parser->setDescription('Execute updates.'); + $parser->addArgument('updateName', [ + 'help' => 'The name of the update to execute', + 'required' => false, + 'choices' => array_keys($this->availableUpdates) + ]); + return $parser; + } + + public function execute(Arguments $args, ConsoleIo $io) + { + $this->io = $io; + $targetUpdateName = $args->getArgument('updateName'); + + if (!in_array($targetUpdateName, array_keys($this->availableUpdates))) { + $io->out('Available updates:'); + $io->helper('Table')->output($this->listAvailableUpdates()); + die(1); + } + + $selection = $io->askChoice("Do you wish to apply update `{$targetUpdateName}`?", ['Y', 'N'], 'N'); + if ($selection == 'Y') { + $updateFunction = $this->availableUpdates[$targetUpdateName]; + $updateResult = $this->{$updateFunction}(); + } else { + $io->out('Update canceled'); + } + } + + private function listAvailableUpdates() + { + $list = [['Update name']]; + foreach ($this->availableUpdates as $updateName => $f) { + $list[] = [$updateName]; + } + return $list; + } + + private function metaTemplateV2() + { + + $db = ConnectionManager::get('default'); + try { + $db->query("ALTER TABLE `meta_fields` ADD `meta_template_id` int(10) unsigned NOT NULL;"); + } catch (\Exception $e) { + $this->io->out('Caught exception: '. $e->getMessage()); + } + try { + $db->query("ALTER TABLE `meta_fields` ADD `meta_template_field_id` int(10) unsigned NOT NULL;"); + } catch (\Exception $e) { + $this->io->out('Caught exception: '. $e->getMessage()); + } + try { + $db->query("ALTER TABLE `meta_templates` ADD `is_default` tinyint(1) NOT NULL DEFAULT 0;"); + } catch (\Exception $e) { + $this->io->out('Caught exception: '. $e->getMessage()); + } + try { + $db->query("ALTER TABLE `meta_fields` ADD INDEX `meta_template_id` (`meta_template_id`);"); + } catch (\Exception $e) { + $this->io->out('Caught exception: '. $e->getMessage()); + } + try { + $db->query("ALTER TABLE `meta_fields` ADD INDEX `meta_template_field_id` (`meta_template_field_id`);"); + } catch (\Exception $e) { + $this->io->out('Caught exception: '. $e->getMessage()); + } + + // $schemaMetaFields = new TableSchema('meta_fields'); + // $schemaMetaTemplates = new TableSchema('meta_templates'); + + // $schemaMetaFields->addColumn('meta_template_id', [ + // 'type' => 'integer', + // 'length' => 10, + // 'unsigned' => true, + // 'null' => false + // ]) + // ->addColumn('meta_template_field_id', [ + // 'type' => 'integer', + // 'length' => 10, + // 'unsigned' => true, + // 'null' => false + // ]) + // ->addIndex('meta_template_id', [ + // 'columns' => ['meta_template_id'], + // 'type' => 'index' + // ]) + // ->addIndex('meta_template_field_id', [ + // 'columns' => ['meta_template_field_id'], + // 'type' => 'index' + // ]); + + + // $schemaMetaTemplates->addColumn('is_default', [ + // 'type' => 'tinyint', + // 'length' => 1, + // 'null' => false, + // 'default' => 1 + // ]); + + // $queries = $schemaMetaFields->createSql($db); + + // $collection = $db->getSchemaCollection(); + // $tableSchema = $collection->describe('meta_fields'); + // $tableSchema->addColumn('foobar', [ + // 'type' => 'integer', + // 'length' => 10, + // 'unsigned' => true, + // 'null' => false + // ]); + return true; + } +} \ No newline at end of file diff --git a/src/Command/UserCommand.php b/src/Command/UserCommand.php index e8dc21c..8efd6d4 100644 --- a/src/Command/UserCommand.php +++ b/src/Command/UserCommand.php @@ -18,6 +18,7 @@ class UserCommand extends Command ['', 'Cerebrate users'], ['1', 'List users'], ['2', 'Reset password for a user'], + ['3', 'Enable/Disable a user'], ['0', 'Exit'] ]; $io->helper('Table')->output($menu); @@ -49,6 +50,23 @@ class UserCommand extends Command } } break; + case '3': + $user = $io->ask(__('Which user do you want to enable/disable?')); + $user = $this->selectUser($user); + if (empty($user)) { + $io->out('Invalid user.'); + } else { + $confirm = $io->askChoice(__('Do you want to {0} the user {1}', $user->disabled ? __('enable') : __('disable'), $user->username), ['Y', 'N'], 'N'); + if ($confirm) { + $user = $this->toggleDisable($user); + if ($user) { + $io->out(__('User {0}', !$user->disabled ? __('enabled') : __('disabled'))); + } else { + $io->out('Could not save the disabled flag.'); + } + } + } + break; case '0': $exit = true; break; @@ -93,4 +111,10 @@ class UserCommand extends Command $user->password = $password; return $this->Users->save($user); } + + private function toggleDisable($user) + { + $user->disabled = !$user->disabled; + return $this->Users->save($user); + } } diff --git a/src/Controller/AlignmentsController.php b/src/Controller/AlignmentsController.php index b91db4c..a8dd626 100644 --- a/src/Controller/AlignmentsController.php +++ b/src/Controller/AlignmentsController.php @@ -36,10 +36,8 @@ class AlignmentsController extends AppController throw new NotFoundException(__('Invalid alignment.')); } $individual = $this->Alignments->get($id); - if ($this->ParamHandler->isRest()) { + if ($this->ParamHandler->isRest() || $this->ParamHandler->isAjax()) { return $this->RestResponse->viewData($individual, 'json'); - } else { - } $this->set('metaGroup', 'ContactDB'); $this->set('alignment', $individual); @@ -50,12 +48,11 @@ class AlignmentsController extends AppController if (empty($id)) { throw new NotFoundException(__('Invalid alignment.')); } - $individual = $this->Alignments->get($id); + $alignment = $this->Alignments->get($id); if ($this->request->is('post') || $this->request->is('delete')) { - if ($this->Alignments->delete($individual)) { - $message = __('Individual deleted.'); - if ($this->ParamHandler->isRest()) { - $individual = $this->Alignments->get($id); + if ($this->Alignments->delete($alignment)) { + $message = __('Alignments deleted.'); + if ($this->ParamHandler->isRest() || $this->ParamHandler->isAjax()) { return $this->RestResponse->saveSuccessResponse('Alignments', 'delete', $id, 'json', $message); } else { $this->Flash->success($message); @@ -65,8 +62,8 @@ class AlignmentsController extends AppController } $this->set('metaGroup', 'ContactDB'); $this->set('scope', 'alignments'); - $this->set('id', $individual['id']); - $this->set('alignment', $individual); + $this->set('id', $alignment['id']); + $this->set('alignment', $alignment); $this->viewBuilder()->setLayout('ajax'); $this->render('/genericTemplates/delete'); } @@ -86,18 +83,20 @@ class AlignmentsController extends AppController } else { $alignment['organisation_id'] = $source_id; } - if ($this->Alignments->save($alignment)) { + $alignment = $this->Alignments->save($alignment); + if ($alignment) { $message = __('Alignment added.'); if ($this->ParamHandler->isRest()) { - $alignment = $this->Alignments->get($this->Alignments->id); return $this->RestResponse->viewData($alignment, 'json'); + } else if($this->ParamHandler->isAjax()) { + return $this->RestResponse->ajaxSuccessResponse('Alignment', 'add', $alignment, $message); } else { $this->Flash->success($message); $this->redirect($this->referer()); } } else { $message = __('Alignment could not be added.'); - if ($this->ParamHandler->isRest()) { + if ($this->ParamHandler->isRest() || $this->ParamHandler->isAjax()) { return $this->RestResponse->saveFailResponse('Individuals', 'addAlignment', false, $message); } else { $this->Flash->error($message); diff --git a/src/Controller/AuthKeysController.php b/src/Controller/AuthKeysController.php index 0de9fed..ca562b0 100644 --- a/src/Controller/AuthKeysController.php +++ b/src/Controller/AuthKeysController.php @@ -17,13 +17,14 @@ class AuthKeysController extends AppController public function index() { $this->CRUD->index([ - 'filters' => ['users.username', 'authkey', 'comment', 'users.id'], + 'filters' => ['Users.username', 'authkey', 'comment', 'Users.id'], 'quickFilters' => ['authkey', 'comment'], 'contain' => ['Users'], 'exclude_fields' => ['authkey'] ]); - if ($this->ParamHandler->isRest()) { - return $this->restResponsePayload; + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; } $this->set('metaGroup', $this->isAdmin ? 'Administration' : 'Cerebrate'); } @@ -31,8 +32,9 @@ class AuthKeysController extends AppController public function delete($id) { $this->CRUD->delete($id); - if ($this->ParamHandler->isRest()) { - return $this->restResponsePayload; + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; } $this->set('metaGroup', $this->isAdmin ? 'Administration' : 'Cerebrate'); } @@ -43,8 +45,11 @@ class AuthKeysController extends AppController $this->CRUD->add([ 'displayOnSuccess' => 'authkey_display' ]); - if ($this->ParamHandler->isRest()) { - return $this->restResponsePayload; + $responsePayload = $this->CRUD->getResponsePayload([ + 'displayOnSuccess' => 'authkey_display' + ]); + if (!empty($responsePayload)) { + return $responsePayload; } $this->loadModel('Users'); $dropdownData = [ diff --git a/src/Controller/BroodsController.php b/src/Controller/BroodsController.php index cbcb51c..7796324 100644 --- a/src/Controller/BroodsController.php +++ b/src/Controller/BroodsController.php @@ -12,12 +12,18 @@ class BroodsController extends AppController public function index() { $this->CRUD->index([ - 'filters' => ['name', 'uuid', 'url', 'description', 'Organisations.id', 'trusted', 'pull', 'authkey'], - 'quickFilters' => ['name', 'uuid', 'description'], + 'filters' => ['Broods.name', 'Broods.uuid', 'Broods.url', 'Broods.description', 'Organisations.id', 'Broods.trusted', 'pull', 'authkey'], + 'quickFilters' => [['Broods.name' => true], 'Broods.uuid', ['Broods.description' => true]], + 'contextFilters' => [ + 'fields' => [ + 'pull', + ] + ], 'contain' => ['Organisations'] ]); - if ($this->ParamHandler->isRest()) { - return $this->restResponsePayload; + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; } $this->set('metaGroup', 'Sync'); } @@ -25,8 +31,9 @@ class BroodsController extends AppController public function add() { $this->CRUD->add(); - if ($this->ParamHandler->isRest()) { - return $this->restResponsePayload; + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; } $this->set('metaGroup', 'Sync'); $this->loadModel('Organisations'); @@ -41,8 +48,9 @@ class BroodsController extends AppController public function view($id) { $this->CRUD->view($id, ['contain' => ['Organisations']]); - if ($this->ParamHandler->isRest()) { - return $this->restResponsePayload; + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; } $this->set('metaGroup', 'Sync'); } @@ -50,18 +58,27 @@ class BroodsController extends AppController public function edit($id) { $this->CRUD->edit($id); - if ($this->ParamHandler->isRest()) { - return $this->restResponsePayload; + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; } $this->set('metaGroup', 'Sync'); + $this->loadModel('Organisations'); + $dropdownData = [ + 'organisation' => $this->Organisations->find('list', [ + 'sort' => ['name' => 'asc'] + ]) + ]; + $this->set(compact('dropdownData')); $this->render('add'); } public function delete($id) { $this->CRUD->delete($id); - if ($this->ParamHandler->isRest()) { - return $this->restResponsePayload; + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; } $this->set('metaGroup', 'Sync'); } diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 8e69794..93419f0 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -6,6 +6,7 @@ use Cake\Controller\Component; use Cake\Error\Debugger; use Cake\Utility\Hash; use Cake\Utility\Inflector; +use Cake\View\ViewBuilder; class CRUDComponent extends Component { @@ -16,7 +17,7 @@ class CRUDComponent extends Component $this->Table = $config['table']; $this->request = $config['request']; $this->TableAlias = $this->Table->getAlias(); - $this->ObjectAlias = \Cake\Utility\Inflector::singularize($this->TableAlias); + $this->ObjectAlias = Inflector::singularize($this->TableAlias); $this->MetaFields = $config['MetaFields']; $this->MetaTemplates = $config['MetaTemplates']; } @@ -42,28 +43,43 @@ class CRUDComponent extends Component } else { $this->Controller->loadComponent('Paginator'); $data = $this->Controller->Paginator->paginate($query); + if (!empty($options['contextFilters'])) { + $this->setFilteringContext($options['contextFilters'], $params); + } $this->Controller->set('data', $data); } } + + /** + * getResponsePayload Returns the adaquate response payload based on the request context + * + * @return false or Array + */ + public function getResponsePayload() + { + if ($this->Controller->ParamHandler->isRest()) { + return $this->Controller->restResponsePayload; + } else if ($this->Controller->ParamHandler->isAjax() && $this->request->is(['post', 'put'])) { + return $this->Controller->ajaxResponsePayload; + } + return false; + } private function getMetaTemplates() { - $metaFields = []; + $metaTemplates = []; if (!empty($this->Table->metaFields)) { $metaQuery = $this->MetaTemplates->find(); - $metaQuery->where([ - 'scope' => $this->Table->metaFields, - 'enabled' => 1 - ]); + $metaQuery + ->order(['is_default' => 'DESC']) + ->where([ + 'scope' => $this->Table->metaFields, + 'enabled' => 1 + ]); $metaQuery->contain(['MetaTemplateFields']); $metaTemplates = $metaQuery->all(); - foreach ($metaTemplates as $metaTemplate) { - foreach ($metaTemplate->meta_template_fields as $field) { - $metaFields[$field['field']] = $field; - } - } } - $this->Controller->set('metaFields', $metaFields); + $this->Controller->set('metaTemplates', $metaTemplates); return true; } @@ -86,21 +102,23 @@ class CRUDComponent extends Component $patchEntityParams['fields'] = $params['fields']; } $data = $this->Table->patchEntity($data, $input, $patchEntityParams); - if ($this->Table->save($data)) { + $savedData = $this->Table->save($data); + if ($savedData !== false) { $message = __('{0} added.', $this->ObjectAlias); if (!empty($input['metaFields'])) { $this->saveMetaFields($data->id, $input); } if ($this->Controller->ParamHandler->isRest()) { - $this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json'); + $this->Controller->restResponsePayload = $this->RestResponse->viewData($savedData, 'json'); + } else if ($this->Controller->ParamHandler->isAjax()) { + if (!empty($params['displayOnSuccess'])) { + $displayOnSuccess = $this->renderViewInVariable($params['displayOnSuccess'], ['entity' => $data]); + $this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'add', $savedData, $message, ['displayOnSuccess' => $displayOnSuccess]); + } else { + $this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'add', $savedData, $message); + } } else { $this->Controller->Flash->success($message); - if (!empty($params['displayOnSuccess'])) { - $this->Controller->set('entity', $data); - $this->Controller->set('referer', $this->Controller->referer()); - $this->Controller->render($params['displayOnSuccess']); - return; - } if (empty($params['redirect'])) { $this->Controller->redirect(['action' => 'view', $data->id]); } else { @@ -108,6 +126,7 @@ class CRUDComponent extends Component } } } else { + $this->Controller->isFailResponse = true; $validationMessage = $this->prepareValidationError($data); $message = __( '{0} could not be added.{1}', @@ -115,7 +134,8 @@ class CRUDComponent extends Component empty($validationMessage) ? '' : ' ' . __('Reason:{0}', $validationMessage) ); if ($this->Controller->ParamHandler->isRest()) { - + } else if ($this->Controller->ParamHandler->isAjax()) { + $this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxFailResponse($this->ObjectAlias, 'add', $data, $message, $validationMessage); } else { $this->Controller->Flash->error($message); } @@ -141,7 +161,7 @@ class CRUDComponent extends Component private function saveMetaFields($id, $input) { - $this->Table->saveMetaFields($id, $input); + $this->Table->saveMetaFields($id, $input, $this->Table); } private function __massageInput($params) @@ -153,9 +173,10 @@ class CRUDComponent extends Component } } if (!empty($params['removeEmpty'])) { - foreach ($params['removeEmpty'] as $removeEmptyField) - if (isset($input[$removeEmptyField])) { - unset($input[$removeEmptyField]); + foreach ($params['removeEmpty'] as $removeEmptyField) { + if (empty($input[$removeEmptyField])) { + unset($input[$removeEmptyField]); + } } } return $input; @@ -181,14 +202,17 @@ class CRUDComponent extends Component $patchEntityParams['fields'] = $params['fields']; } $data = $this->Table->patchEntity($data, $input, $patchEntityParams); - if ($this->Table->save($data)) { - $message = __('{0} updated.', $this->ObjectAlias); + $savedData = $this->Table->save($data); + if ($savedData !== false) { + $message = __('{0} `{1}` updated.', $this->ObjectAlias, $savedData->{$this->Table->getDisplayField()}); if (!empty($input['metaFields'])) { - $this->MetaFields->deleteAll(['scope' => $this->Table->metaFields, 'parent_id' => $data->id]); - $this->saveMetaFields($data->id, $input); + $this->MetaFields->deleteAll(['scope' => $this->Table->metaFields, 'parent_id' => $savedData->id]); + $this->saveMetaFields($savedData->id, $input); } if ($this->Controller->ParamHandler->isRest()) { - $this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json'); + $this->Controller->restResponsePayload = $this->RestResponse->viewData($savedData, 'json'); + } else if ($this->Controller->ParamHandler->isAjax()) { + $this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'edit', $savedData, $message); } else { $this->Controller->Flash->success($message); if (empty($params['redirect'])) { @@ -200,12 +224,12 @@ class CRUDComponent extends Component } else { $validationMessage = $this->prepareValidationError($data); $message = __( - '{0} could not be modified.{1}', - $this->ObjectAlias, - empty($validationMessage) ? '' : ' ' . __('Reason:{0}', $validationMessage) + __('{0} could not be modified.'), + $this->ObjectAlias ); if ($this->Controller->ParamHandler->isRest()) { - + } else if ($this->Controller->ParamHandler->isAjax()) { + $this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxFailResponse($this->ObjectAlias, 'edit', $data, $message, $data->getErrors()); } else { $this->Controller->Flash->error($message); } @@ -214,17 +238,37 @@ class CRUDComponent extends Component $this->Controller->set('entity', $data); } + public function attachMetaData($id, $data) + { + if (empty($this->Table->metaFields)) { + return $data; + } + $query = $this->MetaFields->MetaTemplates->find(); + $metaFields = $this->Table->metaFields; + $query->contain('MetaTemplateFields', function ($q) use ($id, $metaFields) { + return $q->innerJoinWith('MetaFields') + ->where(['MetaFields.scope' => $metaFields, 'MetaFields.parent_id' => $id]); + }); + $query->innerJoinWith('MetaTemplateFields', function ($q) { + return $q->contain('MetaFields')->innerJoinWith('MetaFields'); + }); + $query->group(['MetaTemplates.id'])->order(['MetaTemplates.is_default' => 'DESC']); + $metaTemplates = $query->all(); + $data['metaTemplates'] = $metaTemplates; + return $data; + } + public function getMetaFields($id, $data) { if (empty($this->Table->metaFields)) { return $data; } $query = $this->MetaFields->find(); - $query->where(['scope' => $this->Table->metaFields, 'parent_id' => $id]); + $query->where(['MetaFields.scope' => $this->Table->metaFields, 'MetaFields.parent_id' => $id]); $metaFields = $query->all(); $data['metaFields'] = []; foreach($metaFields as $metaField) { - $data['metaFields'][$metaField->field] = $metaField->value; + $data['metaFields'][$metaField->meta_template_id][$metaField->field] = $metaField->value; } return $data; } @@ -236,7 +280,7 @@ class CRUDComponent extends Component } $data = $this->Table->get($id, $params); - $data = $this->getMetaFields($id, $data); + $data = $this->attachMetaData($id, $data); if ($this->Controller->ParamHandler->isRest()) { $this->Controller->restResponsePayload = $this->Controller->RestResponse->viewData($data, 'json'); } @@ -253,8 +297,9 @@ class CRUDComponent extends Component if ($this->Table->delete($data)) { $message = __('{0} deleted.', $this->ObjectAlias); if ($this->Controller->ParamHandler->isRest()) { - $data = $this->Table->get($id); - $this->Controller->restResponsePayload = $this->RestResponse->saveSuccessResponse($this->TableAlias, 'delete', $id, 'json', $message); + $this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json'); + } else if ($this->Controller->ParamHandler->isAjax()) { + $this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'delete', $data, $message); } else { $this->Controller->Flash->success($message); $this->Controller->redirect($this->Controller->referer()); @@ -295,11 +340,22 @@ class CRUDComponent extends Component protected function setQuickFilters(array $params, \Cake\ORM\Query $query, array $quickFilterFields): \Cake\ORM\Query { $queryConditions = []; + $this->Controller->set('quickFilter', empty($quickFilterFields) ? [] : $quickFilterFields); if (!empty($params['quickFilter']) && !empty($quickFilterFields)) { + $this->Controller->set('quickFilterValue', $params['quickFilter']); foreach ($quickFilterFields as $filterField) { - $queryConditions[$filterField] = $params['quickFilter']; + $likeCondition = false; + if (is_array($filterField)) { + $likeCondition = reset($filterField); + $filterFieldName = array_key_first($filterField); + $queryConditions[$filterFieldName . ' LIKE'] = '%' . $params['quickFilter'] .'%'; + } else { + $queryConditions[$filterField] = $params['quickFilter']; + } } $query->where(['OR' => $queryConditions]); + } else { + $this->Controller->set('quickFilterValue', ''); } return $query; } @@ -313,10 +369,14 @@ class CRUDComponent extends Component if ($filter === 'quickFilter') { continue; } - if (strlen(trim($filterValue, '%')) === strlen($filterValue)) { - $query->where([$filter => $filterValue]); + if (is_array($filterValue)) { + $query->where([($filter . ' IN') => $filterValue]); } else { - $query->like([$filter => $filterValue]); + if (strlen(trim($filterValue, '%')) === strlen($filterValue)) { + $query->where([$filter => $filterValue]); + } else { + $query->like([$filter => $filterValue]); + } } } } @@ -335,6 +395,95 @@ class CRUDComponent extends Component return $query; } + protected function setFilteringContext($contextFilters, $params) + { + $filteringContexts = []; + if (!isset($contextFilters['allow_all']) || $contextFilters['allow_all']) { + $filteringContexts[] = ['label' => __('All')]; + } + if (!empty($contextFilters['fields'])) { + foreach ($contextFilters['fields'] as $field) { + $contextsFromField = $this->getFilteringContextFromField($field); + foreach ($contextsFromField as $contextFromField) { + if (is_bool($contextFromField)) { + $contextFromFieldText = sprintf('%s: %s', $field, $contextFromField ? 'true' : 'false'); + } else { + $contextFromFieldText = $contextFromField; + } + $filteringContexts[] = [ + 'label' => Inflector::humanize($contextFromFieldText), + 'filterCondition' => [ + $field => $contextFromField + ] + ]; + } + } + } + if (!empty($contextFilters['custom'])) { + $filteringContexts = array_merge($filteringContexts, $contextFilters['custom']); + } + $this->Controller->set('filteringContexts', $filteringContexts); + } + + public function toggle(int $id, string $fieldName = 'enabled', array $params = []): void + { + if (empty($id)) { + throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias)); + } + + $data = $this->Table->get($id, $params); + if ($this->request->is(['post', 'put'])) { + if (isset($params['force_state'])) { + $data->{$fieldName} = $params['force_state']; + } else { + $data->{$fieldName} = !$data->{$fieldName}; + } + $savedData = $this->Table->save($data); + if ($savedData !== false) { + $message = __('{0} field {1}. (ID: {2} {3})', + $fieldName, + $data->{$fieldName} ? __('enabled') : __('disabled'), + Inflector::humanize($this->ObjectAlias), + $data->id + ); + if ($this->Controller->ParamHandler->isRest()) { + $this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json'); + } else if ($this->Controller->ParamHandler->isAjax()) { + $this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'toggle', $savedData, $message); + } else { + $this->Controller->Flash->success($message); + if (empty($params['redirect'])) { + $this->Controller->redirect(['action' => 'view', $id]); + } else { + $this->Controller->redirect($params['redirect']); + } + } + } else { + $validationMessage = $this->prepareValidationError($data); + $message = __( + '{0} could not be modified.{1}', + $this->ObjectAlias, + empty($validationMessage) ? '' : ' ' . __('Reason:{0}', $validationMessage) + ); + if ($this->Controller->ParamHandler->isRest()) { + } else if ($this->Controller->ParamHandler->isAjax()) { + $this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxFailResponse($this->ObjectAlias, 'toggle', $message, $validationMessage); + } else { + $this->Controller->Flash->error($message); + if (empty($params['redirect'])) { + $this->Controller->redirect(['action' => 'view', $id]); + } else { + $this->Controller->redirect($params['redirect']); + } + } + } + } + $this->Controller->set('entity', $data); + $this->Controller->set('fieldName', $fieldName); + $this->Controller->viewBuilder()->setLayout('ajax'); + $this->Controller->render('/genericTemplates/toggle'); + } + public function toggleEnabled(int $id, array $path, string $fieldName = 'enabled'): bool { if (empty($id)) { @@ -356,4 +505,46 @@ class CRUDComponent extends Component } } } + + private function getFilteringContextFromField($field) + { + $exploded = explode('.', $field); + if (count($exploded) > 1) { + $model = $exploded[0]; + $subField = $exploded[1]; + $association = $this->Table->associations()->get($model); + $associationType = $association->type(); + if ($associationType == 'oneToMany') { + $fieldToExtract = $subField; + $associatedTable = $association->getTarget(); + $query = $associatedTable->find()->rightJoin( + [$this->Table->getAlias() => $this->Table->getTable()], + [sprintf('%s.id = %s.%s', $this->Table->getAlias(), $associatedTable->getAlias(), $association->getForeignKey())] + ) + ->where([ + ["${field} IS NOT" => NULL] + ]); + } else if ($associationType == 'manyToOne') { + $fieldToExtract = sprintf('%s.%s', Inflector::singularize(strtolower($model)), $subField); + $query = $this->Table->find()->contain($model); + } else { + throw new Exception("Association ${associationType} not supported in CRUD Component"); + } + } else { + $fieldToExtract = $field; + $query = $this->Table->find(); + } + return $query->select([$field]) + ->distinct() + ->extract($fieldToExtract) + ->toList(); + } + + private function renderViewInVariable($templateRelativeName, $data) + { + $builder = new ViewBuilder(); + $builder->disableAutoLayout()->setTemplate("{$this->TableAlias}/{$templateRelativeName}"); + $view = $builder->build($data); + return $view->render(); + } } diff --git a/src/Controller/Component/ParamHandlerComponent.php b/src/Controller/Component/ParamHandlerComponent.php index 77d1177..0ef678f 100644 --- a/src/Controller/Component/ParamHandlerComponent.php +++ b/src/Controller/Component/ParamHandlerComponent.php @@ -61,4 +61,13 @@ class ParamHandlerComponent extends Component { return (json_decode($data) != null) ? true : false; } + + public function isAjax() + { + if ($this->isAjax !== null) { + return $this->isAjax; + } + $this->isAjax = $this->request->is('ajax'); + return $this->isAjax; + } } diff --git a/src/Controller/Component/RestResponseComponent.php b/src/Controller/Component/RestResponseComponent.php index d433358..ef9bf53 100644 --- a/src/Controller/Component/RestResponseComponent.php +++ b/src/Controller/Component/RestResponseComponent.php @@ -4,6 +4,7 @@ namespace App\Controller\Component; use Cake\Controller\Component; use Cake\Core\Configure; +use Cake\Utility\Inflector; class RestResponseComponent extends Component { @@ -419,6 +420,33 @@ class RestResponseComponent extends Component return $this->__sendResponse($response, 200, $format); } + public function ajaxSuccessResponse($ObjectAlias, $action, $entity, $message, $additionalData=[]) + { + $action = $this->__dissectAdminRouting($action); + $response = [ + 'success' => true, + 'message' => $message, + 'data' => $entity->toArray(), + 'url' => $this->__generateURL($action, $ObjectAlias, $entity->id) + ]; + if (!empty($additionalData)) { + $response['additionalData'] = $additionalData; + } + return $this->viewData($response); + } + + public function ajaxFailResponse($ObjectAlias, $action, $entity, $message, $errors = []) + { + $action = $this->__dissectAdminRouting($action); + $response = [ + 'success' => false, + 'message' => $message, + 'errors' => $errors, + 'url' => $this->__generateURL($action, $ObjectAlias, $entity->id) + ]; + return $this->viewData($response); + } + private function __sendResponse($response, $code, $format = false, $raw = false, $download = false, $headers = array()) { if (strtolower($format) === 'application/xml' || strtolower($format) === 'xml') { diff --git a/src/Controller/EncryptionKeysController.php b/src/Controller/EncryptionKeysController.php index df85c81..afcc611 100644 --- a/src/Controller/EncryptionKeysController.php +++ b/src/Controller/EncryptionKeysController.php @@ -19,6 +19,11 @@ class EncryptionKeysController extends AppController $this->CRUD->index([ 'quickFilters' => ['encryption_key'], 'filters' => ['owner_type', 'organisation_id', 'individual_id', 'encryption_key'], + 'contextFilters' => [ + 'fields' => [ + 'type' + ] + ], 'contain' => ['Individuals', 'Organisations'] ]); if ($this->ParamHandler->isRest()) { diff --git a/src/Controller/IndividualsController.php b/src/Controller/IndividualsController.php index 2e7ba75..e3445eb 100644 --- a/src/Controller/IndividualsController.php +++ b/src/Controller/IndividualsController.php @@ -15,12 +15,18 @@ class IndividualsController extends AppController public function index() { $this->CRUD->index([ - 'filters' => ['uuid', 'email', 'first_name', 'last_name', 'position', 'Organisations.id'], + 'filters' => ['uuid', 'email', 'first_name', 'last_name', 'position', 'Organisations.id', 'Alignments.type'], 'quickFilters' => ['uuid', 'email', 'first_name', 'last_name', 'position'], + 'contextFilters' => [ + 'fields' => [ + 'Alignments.type' + ] + ], 'contain' => ['Alignments' => 'Organisations'] ]); - if ($this->ParamHandler->isRest()) { - return $this->restResponsePayload; + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; } $this->set('alignmentScope', 'individuals'); $this->set('metaGroup', 'ContactDB'); @@ -29,8 +35,9 @@ class IndividualsController extends AppController public function add() { $this->CRUD->add(); - if ($this->ParamHandler->isRest()) { - return $this->restResponsePayload; + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; } $this->set('metaGroup', 'ContactDB'); } @@ -38,8 +45,9 @@ class IndividualsController extends AppController public function view($id) { $this->CRUD->view($id, ['contain' => ['Alignments' => 'Organisations']]); - if ($this->ParamHandler->isRest()) { - return $this->restResponsePayload; + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; } $this->set('metaGroup', 'ContactDB'); } @@ -47,8 +55,9 @@ class IndividualsController extends AppController public function edit($id) { $this->CRUD->edit($id); - if ($this->ParamHandler->isRest()) { - return $this->restResponsePayload; + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; } $this->set('metaGroup', 'ContactDB'); $this->render('add'); @@ -57,8 +66,9 @@ class IndividualsController extends AppController public function delete($id) { $this->CRUD->delete($id); - if ($this->ParamHandler->isRest()) { - return $this->restResponsePayload; + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; } $this->set('metaGroup', 'ContactDB'); } diff --git a/src/Controller/MetaTemplatesController.php b/src/Controller/MetaTemplatesController.php index ac3df97..5255768 100644 --- a/src/Controller/MetaTemplatesController.php +++ b/src/Controller/MetaTemplatesController.php @@ -9,6 +9,7 @@ use \Cake\Database\Expression\QueryExpression; class MetaTemplatesController extends AppController { + public function update() { if ($this->request->is('post')) { @@ -33,13 +34,27 @@ class MetaTemplatesController extends AppController public function index() { $this->CRUD->index([ - 'filters' => ['name', 'uuid', 'scope'], + 'filters' => ['name', 'uuid', 'scope', 'namespace'], 'quickFilters' => ['name', 'uuid', 'scope'], + 'contextFilters' => [ + 'fields' => ['scope'], + 'custom' => [ + [ + 'label' => __('Contact DB'), + 'filterCondition' => ['scope' => ['individual', 'organisation']] + ], + [ + 'label' => __('Namespace CNW'), + 'filterCondition' => ['namespace' => 'cnw'] + ], + ] + ], 'contain' => ['MetaTemplateFields'] ]); if ($this->ParamHandler->isRest()) { return $this->restResponsePayload; } + $this->set('defaultTemplatePerScope', $this->MetaTemplates->getDefaultTemplatePerScope()); $this->set('alignmentScope', 'individuals'); $this->set('metaGroup', 'Administration'); } @@ -55,25 +70,19 @@ class MetaTemplatesController extends AppController $this->set('metaGroup', 'Administration'); } - public function toggle($id) + public function toggle($id, $fieldName = 'enabled') { - $template = $this->MetaTemplates->getTemplate($id); - $template['enabled'] = $template['enabled'] ? 0 : 1; - $result = $this->MetaTemplates->save($template); - if ($template['enabled']) { - $message = $result ? __('Template enabled.') : __('Could not enable template'); + if ($this->request->is('POST') && $fieldName == 'is_default') { + $template = $this->MetaTemplates->get($id); + $this->MetaTemplates->removeDefaultFlag($template->scope); + $this->CRUD->toggle($id, $fieldName, ['force_state' => !$template->is_default]); } else { - $message = $result ? __('Template disabled.') : __('Could not disable template'); + $this->CRUD->toggle($id, $fieldName); } if ($this->ParamHandler->isRest()) { - if ($result) { - return $this->RestResponse->saveSuccessResponse('MetaTemplates', 'toggle', $id, 'json', $message); - } else { - return $this->RestResponse->saveFailResponse('MetaTemplates', 'toggle', $id, 'json', $message); - } - } else { - if ($this->Flash->{$result ? 'success' : 'error'}($message)); - $this->redirect($this->referer()); + return $this->restResponsePayload; + } else if($this->ParamHandler->isAjax() && $this->request->is(['post', 'put'])) { + return $this->ajaxResponsePayload; } } } diff --git a/src/Controller/OrganisationsController.php b/src/Controller/OrganisationsController.php index 9e6162c..9a599c3 100644 --- a/src/Controller/OrganisationsController.php +++ b/src/Controller/OrganisationsController.php @@ -19,8 +19,9 @@ class OrganisationsController extends AppController 'quickFilters' => ['name', 'uuid', 'nationality', 'sector', 'type', 'url'], 'contain' => ['Alignments' => 'Individuals'] ]); - if ($this->ParamHandler->isRest()) { - return $this->restResponsePayload; + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; } $this->set('alignmentScope', 'individuals'); $this->set('metaGroup', 'ContactDB'); @@ -29,8 +30,9 @@ class OrganisationsController extends AppController public function add() { $this->CRUD->add(); - if ($this->ParamHandler->isRest()) { - return $this->restResponsePayload; + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; } $this->set('metaGroup', 'ContactDB'); } @@ -38,8 +40,9 @@ class OrganisationsController extends AppController public function view($id) { $this->CRUD->view($id, ['contain' => ['Alignments' => 'Individuals']]); - if ($this->ParamHandler->isRest()) { - return $this->restResponsePayload; + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; } $this->set('metaGroup', 'ContactDB'); } @@ -47,8 +50,9 @@ class OrganisationsController extends AppController public function edit($id) { $this->CRUD->edit($id); - if ($this->ParamHandler->isRest()) { - return $this->restResponsePayload; + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; } $this->set('metaGroup', 'ContactDB'); $this->render('add'); @@ -57,8 +61,9 @@ class OrganisationsController extends AppController public function delete($id) { $this->CRUD->delete($id); - if ($this->ParamHandler->isRest()) { - return $this->restResponsePayload; + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; } $this->set('metaGroup', 'ContactDB'); } diff --git a/src/Controller/SharingGroupsController.php b/src/Controller/SharingGroupsController.php index a37701c..75916ba 100644 --- a/src/Controller/SharingGroupsController.php +++ b/src/Controller/SharingGroupsController.php @@ -2,6 +2,7 @@ namespace App\Controller; use App\Controller\AppController; +use Cake\Utility\Inflector; use Cake\Utility\Hash; use Cake\Utility\Text; use \Cake\Database\Expression\QueryExpression; @@ -31,8 +32,9 @@ class SharingGroupsController extends AppController $dropdownData = [ 'organisation' => $this->getAvailableOrgForSg($this->ACL->getUser()) ]; - if ($this->ParamHandler->isRest()) { - return $this->restResponsePayload; + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; } $this->set(compact('dropdownData')); $this->set('metaGroup', 'Trust Circles'); @@ -52,8 +54,9 @@ class SharingGroupsController extends AppController public function edit($id = false) { $this->CRUD->edit($id); - if ($this->ParamHandler->isRest()) { - return $this->restResponsePayload; + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; } $dropdownData = [ 'organisation' => $this->getAvailableOrgForSg($this->ACL->getUser()) @@ -66,8 +69,9 @@ class SharingGroupsController extends AppController public function delete($id) { $this->CRUD->delete($id); - if ($this->ParamHandler->isRest()) { - return $this->restResponsePayload; + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; } $this->set('metaGroup', 'Trust Circles'); } @@ -110,11 +114,14 @@ class SharingGroupsController extends AppController } else { $message = __('Organisation(s) could not be added to the sharing group.'); } - if ($this->ParamHandler->isRest()) { + if ($this->ParamHandler->isRest() || $this->ParamHandler->isAjax()) { if ($result) { - $this->RestResponse->saveSuccessResponse('SharingGroups', 'addOrg', $id, 'json', $message); + $savedData = $this->SharingGroups->get($id, [ + 'contain' => 'SharingGroupOrgs' + ]); + return $this->RestResponse->ajaxSuccessResponse(Inflector::singularize($this->SharingGroups->getAlias()), 'addOrg', $savedData, $message); } else { - $this->RestResponse->saveFailResponse('SharingGroups', 'addOrg', $id, $message, 'json'); + return $this->RestResponse->ajaxFailResponse(Inflector::singularize($this->SharingGroups->getAlias()), 'addOrg', $sharingGroup, $message);; } } else { if ($result) { @@ -128,9 +135,45 @@ class SharingGroupsController extends AppController $this->set(compact('dropdownData')); } - public function removeOrg($id) + public function removeOrg($id, $org_id) { - + $sharingGroup = $this->SharingGroups->get($id, [ + 'contain' => 'SharingGroupOrgs' + ]); + if ($this->request->is('post')) { + $org = $this->SharingGroups->SharingGroupOrgs->get($org_id); + $result = (bool)$this->SharingGroups->SharingGroupOrgs->unlink($sharingGroup, [$org]); + if ($result) { + $message = __('Organisation(s) removed from the sharing group.'); + } else { + $message = __('Organisation(s) could not be removed to the sharing group.'); + } + if ($this->ParamHandler->isRest() || $this->ParamHandler->isAjax()) { + if ($result) { + $savedData = $this->SharingGroups->get($id, [ + 'contain' => 'SharingGroupOrgs' + ]); + return $this->RestResponse->ajaxSuccessResponse(Inflector::singularize($this->SharingGroups->getAlias()), 'removeOrg', $savedData, $message); + } else { + return $this->RestResponse->ajaxFailResponse(Inflector::singularize($this->SharingGroups->getAlias()), 'removeOrg', $sharingGroup, $message); + ; + } + } else { + if ($result) { + $this->Flash->success($message); + } else { + $this->Flash->error($message); + } + $this->redirect(['action' => 'view', $id]); + } + } + $this->set('scope', 'sharing_groups'); + $this->set('id', $org_id); + $this->set('sharingGroup', $sharingGroup); + $this->set('deletionText', __('Are you sure you want to remove Organisation #{0} from Sharing group #{1}?', $org_id, $sharingGroup['id'])); + $this->set('postLinkParameters', ['action' => 'removeOrg', $id, $org_id]); + $this->viewBuilder()->setLayout('ajax'); + $this->render('/genericTemplates/delete'); } public function listOrgs($id) diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php index d1bbcbd..db11453 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -23,8 +23,9 @@ class UsersController extends AppController public function add() { $this->CRUD->add(); - if ($this->ParamHandler->isRest()) { - return $this->restResponsePayload; + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; } $dropdownData = [ 'role' => $this->Users->Roles->find('list', [ @@ -74,8 +75,9 @@ class UsersController extends AppController $params['fields'][] = 'role_id'; } $this->CRUD->edit($id, $params); - if ($this->ParamHandler->isRest()) { - return $this->restResponsePayload; + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; } $dropdownData = [ 'role' => $this->Users->Roles->find('list', [ @@ -90,11 +92,21 @@ class UsersController extends AppController $this->render('add'); } + public function toggle($id, $fieldName = 'disabled') + { + $this->CRUD->toggle($id, $fieldName); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } + } + public function delete($id) { $this->CRUD->delete($id); - if ($this->ParamHandler->isRest()) { - return $this->restResponsePayload; + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; } $this->set('metaGroup', $this->isAdmin ? 'Administration' : 'Cerebrate'); } diff --git a/src/Model/Entity/User.php b/src/Model/Entity/User.php index e6a6796..fe31e50 100644 --- a/src/Model/Entity/User.php +++ b/src/Model/Entity/User.php @@ -8,7 +8,7 @@ use Authentication\PasswordHasher\DefaultPasswordHasher; class User extends AppModel { - protected $_hidden = ['password']; + protected $_hidden = ['password', 'confirm_password']; protected function _setPassword(string $password) : ?string { if (strlen($password) > 0) { diff --git a/src/Model/Table/AppTable.php b/src/Model/Table/AppTable.php index 2ef9fc2..4164456 100644 --- a/src/Model/Table/AppTable.php +++ b/src/Model/Table/AppTable.php @@ -17,18 +17,31 @@ class AppTable extends Table public function saveMetaFields($id, $input) { $this->MetaFields = TableRegistry::getTableLocator()->get('MetaFields'); - foreach ($input['metaFields'] as $metaField => $values) { - if (!is_array($values)) { - $values = [$values]; + $this->MetaTemplates = TableRegistry::getTableLocator()->get('MetaTemplates'); + foreach ($input['metaFields'] as $templateID => $metaFields) { + $metaTemplates = $this->MetaTemplates->find()->where([ + 'id' => $templateID, + 'enabled' => 1 + ])->contain(['MetaTemplateFields'])->first(); + $fieldNameToId = []; + foreach ($metaTemplates->meta_template_fields as $i => $metaTemplateField) { + $fieldNameToId[$metaTemplateField->field] = $metaTemplateField->id; } - foreach ($values as $value) { - if ($value !== '') { - $temp = $this->MetaFields->newEmptyEntity(); - $temp->field = $metaField; - $temp->value = $value; - $temp->scope = $this->metaFields; - $temp->parent_id = $id; - $this->MetaFields->save($temp); + foreach ($metaFields as $metaField => $values) { + if (!is_array($values)) { + $values = [$values]; + } + foreach ($values as $value) { + if ($value !== '') { + $temp = $this->MetaFields->newEmptyEntity(); + $temp->field = $metaField; + $temp->value = $value; + $temp->scope = $this->metaFields; + $temp->parent_id = $id; + $temp->meta_template_id = $templateID; + $temp->meta_template_field_id = $fieldNameToId[$metaField]; + $res = $this->MetaFields->save($temp); + } } } } diff --git a/src/Model/Table/BroodsTable.php b/src/Model/Table/BroodsTable.php index fcbacce..0f43c43 100644 --- a/src/Model/Table/BroodsTable.php +++ b/src/Model/Table/BroodsTable.php @@ -44,6 +44,10 @@ class BroodsTable extends AppTable 'error' => __('Authentication failure'), 'reason' => __('Invalid user credentials.') ], + 404 => [ + 'error' => __('Not found'), + 'reason' => __('Incorrect URL or proxy error') + ], 405 => [ 'error' => __('Insufficient privileges'), 'reason' => __('The remote user account doesn\'t have the required privileges to synchronise.') diff --git a/src/Model/Table/MetaFieldsTable.php b/src/Model/Table/MetaFieldsTable.php index e0abcab..d3cca97 100644 --- a/src/Model/Table/MetaFieldsTable.php +++ b/src/Model/Table/MetaFieldsTable.php @@ -13,6 +13,8 @@ class MetaFieldsTable extends AppTable parent::initialize($config); $this->addBehavior('UUID'); $this->setDisplayField('field'); + $this->belongsTo('MetaTemplates'); + $this->belongsTo('MetaTemplateFields'); } public function validationDefault(Validator $validator): Validator @@ -22,7 +24,9 @@ class MetaFieldsTable extends AppTable ->notEmptyString('field') ->notEmptyString('uuid') ->notEmptyString('value') - ->requirePresence(['scope', 'field', 'value', 'uuid'], 'create'); + ->notEmptyString('meta_template_id') + ->notEmptyString('meta_template_field_id') + ->requirePresence(['scope', 'field', 'value', 'uuid', 'meta_template_id', 'meta_template_field_id'], 'create'); return $validator; } } diff --git a/src/Model/Table/MetaTemplateFieldsTable.php b/src/Model/Table/MetaTemplateFieldsTable.php index 4237be4..453690a 100644 --- a/src/Model/Table/MetaTemplateFieldsTable.php +++ b/src/Model/Table/MetaTemplateFieldsTable.php @@ -14,6 +14,7 @@ class MetaTemplateFieldsTable extends AppTable $this->BelongsTo( 'MetaTemplates' ); + $this->hasMany('MetaFields'); $this->setDisplayField('field'); } diff --git a/src/Model/Table/MetaTemplatesTable.php b/src/Model/Table/MetaTemplatesTable.php index 9fce152..1097950 100644 --- a/src/Model/Table/MetaTemplatesTable.php +++ b/src/Model/Table/MetaTemplatesTable.php @@ -68,6 +68,28 @@ class MetaTemplatesTable extends AppTable return $template; } + public function getDefaultTemplatePerScope(String $scope = '') + { + $query = $this->find('list', [ + 'keyField' => 'scope', + 'valueField' => function ($template) { + return $template; + } + ])->where(['is_default' => true]); + if (!empty($scope)) { + $query->where(['scope' => $scope]); + } + return $query->all()->toArray(); + } + + public function removeDefaultFlag(String $scope) + { + $this->updateAll( + ['is_default' => false], + ['scope' => $scope] + ); + } + public function loadMetaFile(String $filePath) { if (file_exists($filePath)) { diff --git a/src/Model/Table/UsersTable.php b/src/Model/Table/UsersTable.php index d294914..09a142e 100644 --- a/src/Model/Table/UsersTable.php +++ b/src/Model/Table/UsersTable.php @@ -56,7 +56,9 @@ class UsersTable extends AppTable }, 'message' => __('Password confirmation missing or not matching the password.') ] - ]); + ]) + ->requirePresence(['username'], 'create') + ->notEmptyString('username', 'Please fill this field'); return $validator; } diff --git a/src/View/Helper/BootstrapHelper.php b/src/View/Helper/BootstrapHelper.php new file mode 100644 index 0000000..47fe34f --- /dev/null +++ b/src/View/Helper/BootstrapHelper.php @@ -0,0 +1,313 @@ +Bootstrap->Tabs([ + * 'pills' => true, + * 'card' => true, + * 'data' => [ + * 'navs' => [ + * 'tab1', + * ['text' => 'tab2', 'active' => true], + * ['html' => 'tab3', 'disabled' => true], + * ], + * 'content' => [ + * 'body1', + * 'body2', + * '~body3~' + * ] + * ] + * ]); + */ + +namespace App\View\Helper; + +use Cake\View\Helper; +use Cake\Utility\Security; +use InvalidArgumentException; + +class BootstrapHelper extends Helper +{ + public function tabs($options) + { + $bsTabs = new BootstrapTabs($options); + return $bsTabs->tabs(); + } +} + +class BootstrapTabs extends Helper +{ + private $defaultOptions = [ + 'fill' => false, + 'justify' => false, + 'pills' => false, + 'vertical' => false, + 'vertical-size' => 3, + 'card' => false, + 'header-variant' => 'light', + 'body-variant' => '', + 'nav-class' => [], + 'nav-item-class' => [], + 'content-class' => [], + 'data' => [ + 'navs' => [], + 'content' => [], + ], + ]; + + private $allowedOptionValues = [ + 'justify' => [false, 'center', 'end'], + 'body-variant' => ['primary', 'success', 'danger', 'warning', 'info', 'light', 'dark', 'white', 'transparent', ''], + 'header-variant' => ['primary', 'success', 'danger', 'warning', 'info', 'light', 'dark', 'white', 'transparent'], + ]; + + private $options = null; + private $bsClasses = null; + + function __construct($options) { + $this->processOptions($options); + } + + public function tabs() + { + return $this->genTabs(); + } + + private function processOptions($options) + { + $this->options = array_merge($this->defaultOptions, $options); + $this->data = $this->options['data']; + $this->checkOptionValidity(); + $this->bsClasses = [ + 'nav' => [], + 'nav-item' => $this->options['nav-item-class'], + + ]; + + if (!empty($this->options['justify'])) { + $this->bsClasses['nav'][] = 'justify-content-' . $this->options['justify']; + } + + if ($this->options['vertical'] && !isset($options['pills']) && !isset($options['card'])) { + $this->options['pills'] = true; + $this->options['card'] = true; + } + + if ($this->options['pills']) { + $this->bsClasses['nav'][] = 'nav-pills'; + if ($this->options['vertical']) { + $this->bsClasses['nav'][] = 'flex-column'; + } + if ($this->options['card']) { + $this->bsClasses['nav'][] = 'card-header-pills'; + } + } else { + $this->bsClasses['nav'][] = 'nav-tabs'; + if ($this->options['card']) { + $this->bsClasses['nav'][] = 'card-header-tabs'; + } + } + + if ($this->options['fill']) { + $this->bsClasses['nav'][] = 'nav-fill'; + } + if ($this->options['justify']) { + $this->bsClasses['nav'][] = 'nav-justify'; + } + + $activeTab = 0; + foreach ($this->data['navs'] as $i => $nav) { + if (!is_array($nav)) { + $this->data['navs'][$i] = ['text' => $nav]; + } + if (!isset($this->data['navs'][$i]['id'])) { + $this->data['navs'][$i]['id'] = 't-' . Security::randomString(8); + } + if (!empty($nav['active'])) { + $activeTab = $i; + } + } + $this->data['navs'][$activeTab]['active'] = true; + + $this->options['vertical-size'] = $this->options['vertical-size'] < 0 || $this->options['vertical-size'] > 11 ? 3 : $this->options['vertical-size']; + + $this->options['header-text-variant'] = $this->options['header-variant'] == 'light' ? 'body' : 'white'; + $this->options['header-border-variant'] = $this->options['header-variant'] == 'light' ? '' : $this->options['header-variant']; + $this->options['body-text-variant'] = $this->options['body-variant'] == '' ? 'body' : 'white'; + + if (!is_array($this->options['nav-class'])) { + $this->options['nav-class'] = [$this->options['nav-class']]; + } + if (!is_array($this->options['content-class'])) { + $this->options['content-class'] = [$this->options['content-class']]; + } + } + + private function checkOptionValidity() + { + foreach ($this->allowedOptionValues as $option => $values) { + if (!isset($this->options[$option])) { + throw new InvalidArgumentException(__('Option `{0}` should have a value', $option)); + } + if (!in_array($this->options[$option], $values)) { + throw new InvalidArgumentException(__('Option `{0}` is not a valid option for `{1}`. Accepted values: {2}', json_encode($this->options[$option]), $option, json_encode($values))); + } + } + if (empty($this->data['navs'])) { + throw new InvalidArgumentException(__('No navigation data provided')); + } + } + + private function genTabs() + { + $html = ''; + if ($this->options['vertical']) { + $html .= $this->genVerticalTabs(); + } else { + $html .= $this->genHorizontalTabs(); + } + return $html; + } + + private function genHorizontalTabs() + { + $html = ''; + if ($this->options['card']) { + $html .= $this->genNode('div', ['class' => array_merge(['card'], ["border-{$this->options['header-border-variant']}"])]); + $html .= $this->genNode('div', ['class' => array_merge(['card-header'], ["bg-{$this->options['header-variant']}", "text-{$this->options['header-text-variant']}"])]); + } + $html .= $this->genNav(); + if ($this->options['card']) { + $html .= ''; + $html .= $this->genNode('div', ['class' => array_merge(['card-body'], ["bg-{$this->options['body-variant']}", "text-{$this->options['body-text-variant']}"])]); + } + $html .= $this->genContent(); + if ($this->options['card']) { + $html .= ''; + $html .= ''; + } + return $html; + } + + private function genVerticalTabs() + { + $html = $this->genNode('div', ['class' => array_merge(['row', ($this->options['card'] ? 'card flex-row' : '')], ["border-{$this->options['header-border-variant']}"])]); + $html .= $this->genNode('div', ['class' => array_merge(['col-' . $this->options['vertical-size'], ($this->options['card'] ? 'card-header border-right' : '')], ["bg-{$this->options['header-variant']}", "text-{$this->options['header-text-variant']}", "border-{$this->options['header-border-variant']}"])]); + $html .= $this->genNav(); + $html .= ''; + $html .= $this->genNode('div', ['class' => array_merge(['col-' . (12 - $this->options['vertical-size']), ($this->options['card'] ? 'card-body2' : '')], ["bg-{$this->options['body-variant']}", "text-{$this->options['body-text-variant']}"])]); + $html .= $this->genContent(); + $html .= ''; + $html .= ''; + return $html; + } + + private function genNav() + { + $html = $this->genNode('ul', [ + 'class' => array_merge(['nav'], $this->bsClasses['nav'], $this->options['nav-class']), + 'role' => 'tablist', + ]); + foreach ($this->data['navs'] as $navItem) { + $html .= $this->genNavItem($navItem); + } + $html .= ''; + return $html; + } + + private function genNavItem($navItem) + { + $html = $this->genNode('li', [ + 'class' => array_merge(['nav-item'], $this->bsClasses['nav-item'], $this->options['nav-item-class']), + 'role' => 'presentation', + ]); + $html .= $this->genNode('a', [ + 'class' => array_merge( + ['nav-link'], + [!empty($navItem['active']) ? 'active' : ''], + [!empty($navItem['disabled']) ? 'disabled' : ''] + ), + 'data-toggle' => $this->options['pills'] ? 'pill' : 'tab', + 'id' => $navItem['id'] . '-tab', + 'href' => '#' . $navItem['id'], + 'aria-controls' => $navItem['id'], + 'aria-selected' => !empty($navItem['active']), + 'role' => 'tab', + ]); + if (!empty($navItem['html'])) { + $html .= $navItem['html']; + } else { + $html .= h($navItem['text']); + } + $html .= ''; + return $html; + } + + private function genContent() + { + $html = $this->genNode('div', [ + 'class' => array_merge(['tab-content'], $this->options['content-class']), + ]); + foreach ($this->data['content'] as $i => $content) { + $navItem = $this->data['navs'][$i]; + $html .= $this->genContentItem($navItem, $content); + } + $html .= ''; + return $html; + } + + private function genContentItem($navItem, $content) + { + $html = $this->genNode('div', [ + 'class' => array_merge(['tab-pane', 'fade'], [!empty($navItem['active']) ? 'show active' : '']), + 'role' => 'tabpanel', + 'id' => $navItem['id'], + 'aria-labelledby' => $navItem['id'] . '-tab' + ]); + $html .= $content; + $html .= ''; + return $html; + } + + private function genNode($node, $params) + { + return sprintf('<%s %s>', $node, $this->genHTMLParams($params)); + } + + private function genHTMLParams($params) + { + $html = ''; + foreach ($params as $k => $v) { + $html .= $this->genHTMLParam($k, $v) . ' '; + } + return $html; + } + + private function genHTMLParam($paramName, $values) + { + if (!is_array($values)) { + $values = [$values]; + } + return sprintf('%s="%s"', $paramName, implode(' ', $values)); + } +} + diff --git a/src/View/Helper/DataFromPathHelper.php b/src/View/Helper/DataFromPathHelper.php new file mode 100644 index 0000000..16a4209 --- /dev/null +++ b/src/View/Helper/DataFromPathHelper.php @@ -0,0 +1,97 @@ + true, // Should the variables to be injected into the string be sanitized. (ignored with the function) + 'highlight' => false, // Should the extracted data be highlighted + ]; + + /** + * buildStringFromDataPath Inject data into a string at the correct place + * + * @param String $str The string that will have its arguments replaced by their value + * @param mixed $data The data from which the value of datapath arguement will be taken + * @param array $strArgs The arguments to be injected into the string. + * - Each argument can be of mixed type: + * - String: A cakephp's Hash datapath used to extract the data + * - array: can contain a key of either + * - `datapath`: A cakephp's Hash datapath used to extract the data + * - `raw`: A raw string to be injecte as-is + * - `function`: A function to be executed with its $strArgs being passed + * @param array $options Allows to configure the behavior of the function + * @return String The string with its arguments replaced by their value + */ + public function buildStringFromDataPath(String $str, $data=[], array $strArgs=[], array $options=[]) + { + $options = array_merge($this->defaultOptions, $options); + if (!empty($strArgs)) { + $extractedVars = []; + foreach ($strArgs as $i => $strArg) { + $varValue = ''; + if (is_array($strArg)) { + $varValue = ''; + if (!empty($strArg['datapath'])) { + $varValue = Hash::get($data, $strArg['datapath']); + } else if (!empty($strArg['raw'])) { + $varValue = $strArg['raw']; + } else if (!empty($strArg['function'])) { + $varValue = $strArg['function']($data, $strArg); + } + } else { + $varValue = Hash::get($data, $strArg); + } + if (empty($strArg['function'])) { + $varValue = $options['sanitize'] ? h($varValue) : $varValue; + } + $extractedVars[] = $varValue; + } + foreach ($extractedVars as $i => $value) { + $value = $options['highlight'] ? "${value}" : $value; + $str = str_replace( + "{{{$i}}}", + $value, + $str + ); + } + } + return $str; + } + + /** + * buildStringsInArray Apply buildStringFromDataPath for all strings in the provided array + * + * @param array $stringArray The array containing the strings that will have their arguments replaced by their value + * @param mixed $data The data from which the value of datapath arguement will be taken + * @param array $stringArrayArgs The arguments to be injected into each strings. + * - Each argument can be of mixed type: + * - String: A cakephp's Hash datapath used to extract the data + * - array: can contain a key of either + * - `datapath`: A cakephp's Hash datapath used to extract the data + * - `raw`: A raw string to be injecte as-is + * - `function`: A function to be executed with its $strArgs being passed + * @param array $options Allows to configure the behavior of the function + * @return array The array containing the strings with their arguments replaced by their value + */ + public function buildStringsInArray(array $stringArray, $data=[], array $stringArrayArgs, array $options=[]) + { + foreach ($stringArrayArgs as $stringName => $argsPath) { + $theString = Hash::get($stringArray, $stringName); + if (!is_null($theString)) { + $argsPath = !is_array($argsPath) ? [$argsPath] : $argsPath; + if (!empty($argsPath['function'])) { + $newString = $argsPath['function']($data, $argsPath); + } else { + $newString = $this->buildStringFromDataPath($theString, $data, $argsPath, $options); + } + $stringArray = Hash::insert($stringArray, $stringName, $newString); + } + } + return $stringArray; + } +} diff --git a/src/View/Helper/HashHelper.php b/src/View/Helper/HashHelper.php index 5a2cbef..c926a69 100644 --- a/src/View/Helper/HashHelper.php +++ b/src/View/Helper/HashHelper.php @@ -11,4 +11,9 @@ class HashHelper extends Helper { return Hash::extract($target, $extraction_string); } + + public function get($target, $extraction_string) + { + return Hash::get($target, $extraction_string); + } } diff --git a/templates/AuthKeys/authkey_display.php b/templates/AuthKeys/authkey_display.php index e131596..2072d17 100644 --- a/templates/AuthKeys/authkey_display.php +++ b/templates/AuthKeys/authkey_display.php @@ -1,5 +1,13 @@ -

-

-

-

%s', __('Authkey'), h($entity->authkey_raw)) ?>

- +element('genericElements/genericModal', [ + 'title' => __('Authkey created'), + 'body' => sprintf( + '

%s

%s

%s

', + __('Please make sure that you note down the authkey below, this is the only time the authkey is shown in plain text, so make sure you save it. If you lose the key, simply remove the entry and generate a new one.'), + __('Cerebrate will use the first and the last 4 digit for identification purposes.'), + sprintf('%s: %s', __('Authkey'), h($entity->authkey_raw)) + ), + 'actionButton' => sprintf('%s', __('I have noted down my key, take me back now')), + 'noCancel' => true, + 'staticBackdrop' => true, +]); diff --git a/templates/AuthKeys/index.php b/templates/AuthKeys/index.php index c11d4e4..b5fbc35 100644 --- a/templates/AuthKeys/index.php +++ b/templates/AuthKeys/index.php @@ -3,7 +3,6 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'data' => [ 'data' => $data, 'top_bar' => [ - 'pull' => 'right', 'children' => [ [ 'type' => 'simple', @@ -11,7 +10,6 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'data' => [ 'type' => 'simple', 'text' => __('Add authentication key'), - 'class' => 'btn btn-primary', 'popover_url' => '/authKeys/add' ] ] @@ -65,8 +63,8 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'pull' => 'right', 'actions' => [ [ - 'onclick' => 'populateAndLoadModal(\'/authKeys/delete/[onclick_params_data_path]\');', - 'onclick_params_data_path' => 'id', + 'open_modal' => '/authKeys/delete/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', 'icon' => 'trash' ] ] diff --git a/templates/Broods/index.php b/templates/Broods/index.php index 9a3e103..8e873d4 100644 --- a/templates/Broods/index.php +++ b/templates/Broods/index.php @@ -3,7 +3,6 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'data' => [ 'data' => $data, 'top_bar' => [ - 'pull' => 'right', 'children' => [ [ 'type' => 'simple', @@ -11,11 +10,14 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'data' => [ 'type' => 'simple', 'text' => __('Add brood'), - 'class' => 'btn btn-primary', 'popover_url' => '/broods/add' ] ] ], + [ + 'type' => 'context_filters', + 'context_filters' => $filteringContexts + ], [ 'type' => 'search', 'button' => __('Filter'), @@ -67,15 +69,15 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'icon' => 'eye' ], [ - 'onclick' => 'populateAndLoadModal(\'/broods/edit/[onclick_params_data_path]\');', - 'onclick_params_data_path' => 'id', + 'open_modal' => '/broods/edit/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', 'icon' => 'edit' ], [ - 'onclick' => 'populateAndLoadModal(\'/broods/delete/[onclick_params_data_path]\');', - 'onclick_params_data_path' => 'id', + 'open_modal' => '/broods/delete/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', 'icon' => 'trash' - ] + ], ] ] ]); diff --git a/templates/EncryptionKeys/index.php b/templates/EncryptionKeys/index.php index 7e2dc28..fed2abd 100644 --- a/templates/EncryptionKeys/index.php +++ b/templates/EncryptionKeys/index.php @@ -6,7 +6,6 @@ echo $this->element('genericElements/IndexTable/index_table', [ }, 'data' => $data, 'top_bar' => [ - 'pull' => 'right', 'children' => [ [ 'type' => 'simple', @@ -19,6 +18,10 @@ echo $this->element('genericElements/IndexTable/index_table', [ ] ] ], + [ + 'type' => 'context_filters', + 'context_filters' => $filteringContexts + ], [ 'type' => 'search', 'button' => __('Filter'), @@ -61,20 +64,20 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'pull' => 'right', 'actions' => [ [ - 'onclick' => 'populateAndLoadModal(\'/encryptionKeys/view/[onclick_params_data_path]\');', - 'onclick_params_data_path' => 'id', + 'url' => '/encryptionKeys/view', + 'url_params_data_paths' => ['id'], 'icon' => 'eye' ], [ - 'onclick' => 'populateAndLoadModal(\'/encryptionKeys/edit/[onclick_params_data_path]\');', - 'onclick_params_data_path' => 'id', + 'open_modal' => '/encryptionKeys/edit/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', 'icon' => 'edit' ], [ - 'onclick' => 'populateAndLoadModal(\'/encryptionKeys/delete/[onclick_params_data_path]\');', - 'onclick_params_data_path' => 'id', + 'open_modal' => '/encryptionKeys/delete/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', 'icon' => 'trash' - ] + ], ] ] ]); diff --git a/templates/Individuals/add.php b/templates/Individuals/add.php index 10f67b5..1941ad3 100644 --- a/templates/Individuals/add.php +++ b/templates/Individuals/add.php @@ -22,7 +22,7 @@ 'field' => 'position' ) ), - 'metaFields' => empty($metaFields) ? [] : $metaFields, + 'metaTemplates' => empty($metaTemplates) ? [] : $metaTemplates, 'submit' => array( 'action' => $this->request->getParam('action') ) diff --git a/templates/Individuals/index.php b/templates/Individuals/index.php index 249314c..91520e1 100644 --- a/templates/Individuals/index.php +++ b/templates/Individuals/index.php @@ -3,7 +3,6 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'data' => [ 'data' => $data, 'top_bar' => [ - 'pull' => 'right', 'children' => [ [ 'type' => 'simple', @@ -11,11 +10,14 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'data' => [ 'type' => 'simple', 'text' => __('Add individual'), - 'class' => 'btn btn-primary', 'popover_url' => '/individuals/add' ] ] ], + [ + 'type' => 'context_filters', + 'context_filters' => $filteringContexts + ], [ 'type' => 'search', 'button' => __('Filter'), @@ -69,15 +71,15 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'icon' => 'eye' ], [ - 'onclick' => 'populateAndLoadModal(\'/individuals/edit/[onclick_params_data_path]\');', - 'onclick_params_data_path' => 'id', + 'open_modal' => '/individuals/edit/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', 'icon' => 'edit' ], [ - 'onclick' => 'populateAndLoadModal(\'/individuals/delete/[onclick_params_data_path]\');', - 'onclick_params_data_path' => 'id', + 'open_modal' => '/individuals/delete/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', 'icon' => 'trash' - ] + ], ] ] ]); diff --git a/templates/MetaTemplateFields/index.php b/templates/MetaTemplateFields/index.php index 7e88292..6cadf15 100644 --- a/templates/MetaTemplateFields/index.php +++ b/templates/MetaTemplateFields/index.php @@ -3,7 +3,6 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'data' => [ 'data' => $data, 'top_bar' => [ - 'pull' => 'right', 'children' => [ [ 'type' => 'search', diff --git a/templates/MetaTemplates/index.php b/templates/MetaTemplates/index.php index c6987d7..882ca4e 100644 --- a/templates/MetaTemplates/index.php +++ b/templates/MetaTemplates/index.php @@ -3,8 +3,11 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'data' => [ 'data' => $data, 'top_bar' => [ - 'pull' => 'right', 'children' => [ + [ + 'type' => 'context_filters', + 'context_filters' => $filteringContexts + ], [ 'type' => 'search', 'button' => __('Filter'), @@ -24,7 +27,92 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'name' => 'Enabled', 'sort' => 'enabled', 'data_path' => 'enabled', - 'element' => 'boolean' + 'element' => 'toggle', + 'url' => '/metaTemplates/toggle/{{0}}', + 'url_params_vars' => ['id'], + 'toggle_data' => [ + 'editRequirement' => [ + 'function' => function($row, $options) { + return true; + }, + ], + 'skip_full_reload' => true + ] + ], + [ + 'name' => 'Default', + 'sort' => 'is_default', + 'data_path' => 'is_default', + 'element' => 'toggle', + 'url' => '/metaTemplates/toggle/{{0}}/{{1}}', + 'url_params_vars' => [['datapath' => 'id'], ['raw' => 'is_default']], + 'toggle_data' => [ + 'editRequirement' => [ + 'function' => function($row, $options) { + return true; + } + ], + 'confirm' => [ + 'enable' => [ + 'titleHtml' => __('Make {{0}} the default template?'), + 'bodyHtml' => $this->Html->nestedList([ + __('Only one template per scope can be set as the default template'), + '{{0}}', + ]), + 'type' => '{{0}}', + 'confirmText' => __('Yes, set as default'), + 'arguments' => [ + 'titleHtml' => ['name'], + 'bodyHtml' => [ + [ + 'function' => function($row, $data) { + $conflictingTemplate = getConflictingTemplate($row, $data); + if (!empty($conflictingTemplate)) { + return sprintf( + "%s %s.
+ ", + __('Conflict with:'), + $this->Html->link( + h($conflictingTemplate->name), + '/metaTemplates/view/' . h($conflictingTemplate->id), + ['target' => '_blank'] + ), + __('By proceeding'), + h($conflictingTemplate->name), + __('will not be the default anymore') + ); + } + return __('Current scope: {0}', h($row->scope)); + }, + 'data' => [ + 'defaultTemplatePerScope' => $defaultTemplatePerScope + ] + ] + ], + 'type' => [ + 'function' => function($row, $data) { + $conflictingTemplate = getConflictingTemplate($row, $data); + if (!empty($conflictingTemplate)) { + return 'confirm-danger'; + } + return 'confirm-warning'; + }, + 'data' => [ + 'defaultTemplatePerScope' => $defaultTemplatePerScope + ] + ] + ] + ], + 'disable' => [ + 'titleHtml' => __('Remove {{0}} as the default template?'), + 'type' => 'confirm-warning', + 'confirmText' => __('Yes, do not set as default'), + 'arguments' => [ + 'titleHtml' => ['name'], + ] + ] + ] + ] ], [ 'name' => __('Scope'), @@ -56,30 +144,17 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'url_params_data_paths' => ['id'], 'icon' => 'eye' ], - [ - 'url' => '/metaTemplates/toggle', - 'url_params_data_paths' => ['id'], - 'title' => __('Enable template'), - 'icon' => 'plus', - 'complex_requirement' => [ - 'function' => function($row, $options) { - return !(bool)$row['enabled']; - } - ] - ], - [ - 'url' => '/metaTemplates/toggle', - 'url_params_data_paths' => ['id'], - 'title' => __('DIsable template'), - 'icon' => 'minus', - 'complex_requirement' => [ - 'function' => function($row, $options) { - return (bool)$row['enabled']; - } - ] - ] - ] ] ]); + +function getConflictingTemplate($row, $data) { + if (!empty($data['data']['defaultTemplatePerScope'][$row->scope])) { + $conflictingTemplate = $data['data']['defaultTemplatePerScope'][$row->scope]; + if (!empty($conflictingTemplate)) { + return $conflictingTemplate; + } + } + return []; +} ?> diff --git a/templates/MetaTemplates/view.php b/templates/MetaTemplates/view.php index d8a282e..902949b 100644 --- a/templates/MetaTemplates/view.php +++ b/templates/MetaTemplates/view.php @@ -20,6 +20,16 @@ echo $this->element( 'key' => __('Description'), 'path' => 'description' ], + [ + 'key' => __('Enabled'), + 'path' => 'enabled', + 'type' => 'boolean' + ], + [ + 'key' => __('is_default'), + 'path' => 'is_default', + 'type' => 'boolean' + ], [ 'key' => __('Version'), 'path' => 'version' diff --git a/templates/Open/Individuals/index.php b/templates/Open/Individuals/index.php index 30ee3f5..411f331 100644 --- a/templates/Open/Individuals/index.php +++ b/templates/Open/Individuals/index.php @@ -56,16 +56,6 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'url' => '/individuals/view', 'url_params_data_paths' => ['id'], 'icon' => 'eye' - ], - [ - 'onclick' => 'populateAndLoadModal(\'/individuals/edit/[onclick_params_data_path]\');', - 'onclick_params_data_path' => 'id', - 'icon' => 'edit' - ], - [ - 'onclick' => 'populateAndLoadModal(\'/individuals/delete/[onclick_params_data_path]\');', - 'onclick_params_data_path' => 'id', - 'icon' => 'trash' ] ] ] diff --git a/templates/Open/Organisations/index.php b/templates/Open/Organisations/index.php index 794f929..e74a5f2 100644 --- a/templates/Open/Organisations/index.php +++ b/templates/Open/Organisations/index.php @@ -77,16 +77,6 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'url' => '/organisations/view', 'url_params_data_paths' => ['id'], 'icon' => 'eye' - ], - [ - 'onclick' => 'populateAndLoadModal(\'/organisations/edit/[onclick_params_data_path]\');', - 'onclick_params_data_path' => 'id', - 'icon' => 'edit' - ], - [ - 'onclick' => 'populateAndLoadModal(\'/organisations/delete/[onclick_params_data_path]\');', - 'onclick_params_data_path' => 'id', - 'icon' => 'trash' ] ] ] diff --git a/templates/Organisations/add.php b/templates/Organisations/add.php index 37fefe7..987809e 100644 --- a/templates/Organisations/add.php +++ b/templates/Organisations/add.php @@ -29,7 +29,7 @@ 'field' => 'type' ) ), - 'metaFields' => empty($metaFields) ? [] : $metaFields, + 'metaTemplates' => empty($metaTemplates) ? [] : $metaTemplates, 'submit' => array( 'action' => $this->request->getParam('action') ) diff --git a/templates/Organisations/index.php b/templates/Organisations/index.php index 3870899..5358dd1 100644 --- a/templates/Organisations/index.php +++ b/templates/Organisations/index.php @@ -3,7 +3,6 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'data' => [ 'data' => $data, 'top_bar' => [ - 'pull' => 'right', 'children' => [ [ 'type' => 'simple', @@ -79,15 +78,15 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'icon' => 'eye' ], [ - 'onclick' => 'populateAndLoadModal(\'/organisations/edit/[onclick_params_data_path]\');', - 'onclick_params_data_path' => 'id', + 'open_modal' => '/organisations/edit/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', 'icon' => 'edit' ], [ - 'onclick' => 'populateAndLoadModal(\'/organisations/delete/[onclick_params_data_path]\');', - 'onclick_params_data_path' => 'id', + 'open_modal' => '/organisations/delete/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', 'icon' => 'trash' - ] + ], ] ] ]); diff --git a/templates/Organisations/view.php b/templates/Organisations/view.php index d1986ec..323571e 100644 --- a/templates/Organisations/view.php +++ b/templates/Organisations/view.php @@ -44,7 +44,7 @@ echo $this->element( 'scope' => 'organisations' ] ], - 'metaFields' => empty($metaFields) ? [] : $metaFields, + 'metaTemplates' => empty($metaFields) ? [] : $metaFields, 'children' => [] ] ); diff --git a/templates/Roles/index.php b/templates/Roles/index.php index 40c0039..e3fec85 100644 --- a/templates/Roles/index.php +++ b/templates/Roles/index.php @@ -3,7 +3,6 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'data' => [ 'data' => $data, 'top_bar' => [ - 'pull' => 'right', 'children' => [ [ 'type' => 'simple', @@ -65,15 +64,15 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'icon' => 'eye' ], [ - 'onclick' => 'populateAndLoadModal(\'/roles/edit/[onclick_params_data_path]\');', - 'onclick_params_data_path' => 'id', - 'icon' => 'edit', + 'open_modal' => '/roles/edit/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', + 'icon' => 'edit' ], [ - 'onclick' => 'populateAndLoadModal(\'/roles/delete/[onclick_params_data_path]\');', - 'onclick_params_data_path' => 'id', + 'open_modal' => '/roles/delete/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', 'icon' => 'trash' - ] + ], ] ] ]); diff --git a/templates/SharingGroups/index.php b/templates/SharingGroups/index.php index f862aff..6076c6c 100644 --- a/templates/SharingGroups/index.php +++ b/templates/SharingGroups/index.php @@ -3,7 +3,6 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'data' => [ 'data' => $data, 'top_bar' => [ - 'pull' => 'right', 'children' => [ [ 'type' => 'simple', @@ -11,7 +10,6 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'data' => [ 'type' => 'simple', 'text' => __('Add sharing group'), - 'class' => 'btn btn-primary', 'popover_url' => '/SharingGroups/add' ] ] @@ -61,15 +59,15 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'icon' => 'eye' ], [ - 'onclick' => 'populateAndLoadModal(\'/sharingGroups/edit/[onclick_params_data_path]\');', - 'onclick_params_data_path' => 'id', + 'open_modal' => '/sharingGroups/edit/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', 'icon' => 'edit' ], [ - 'onclick' => 'populateAndLoadModal(\'/sharingGroups/delete/[onclick_params_data_path]\');', - 'onclick_params_data_path' => 'id', + 'open_modal' => '/sharingGroups/delete/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', 'icon' => 'trash' - ] + ], ] ] ]); diff --git a/templates/SharingGroups/list_orgs.php b/templates/SharingGroups/list_orgs.php index 0862276..1c0658f 100644 --- a/templates/SharingGroups/list_orgs.php +++ b/templates/SharingGroups/list_orgs.php @@ -4,7 +4,6 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'data' => $sharing_group_orgs, 'skip_pagination' => 1, 'top_bar' => [ - 'pull' => 'right', 'children' => [ [ 'type' => 'simple', @@ -12,8 +11,8 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'data' => [ 'type' => 'simple', 'text' => __('Add member'), - 'class' => 'btn btn-primary', - 'popover_url' => '/sharingGroups/addOrg/' . h($sharing_group_id) + 'popover_url' => '/sharingGroups/addOrg/' . h($sharing_group_id), + 'reload_url' => '/sharingGroups/listOrgs/' . h($sharing_group_id) ] ] ], @@ -53,10 +52,11 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'icon' => 'eye' ], [ - 'onclick' => 'populateAndLoadModal(\'/sharingGroups/removeOrg/' . h($sharing_group_id) . '/[onclick_params_data_path]\');', - 'onclick_params_data_path' => 'id', + 'open_modal' => '/sharingGroups/removeOrg/' . h($sharing_group_id) . '/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', + 'reload_url' => '/sharingGroups/listOrgs/' . h($sharing_group_id), 'icon' => 'trash' - ] + ], ] ] ]); diff --git a/templates/Users/index.php b/templates/Users/index.php index e73e860..9da12ba 100644 --- a/templates/Users/index.php +++ b/templates/Users/index.php @@ -3,7 +3,6 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'data' => [ 'data' => $data, 'top_bar' => [ - 'pull' => 'right', 'children' => [ [ 'type' => 'simple', @@ -31,6 +30,22 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'sort' => 'id', 'data_path' => 'id', ], + [ + 'name' => __('Disabled'), + 'sort' => 'disabled', + 'data_path' => 'disabled', + 'element' => 'toggle', + 'url' => '/users/toggle/{{0}}', + 'url_params_vars' => ['id'], + 'toggle_data' => [ + 'editRequirement' => [ + 'function' => function($row, $options) { + return true; + }, + ], + 'skip_full_reload' => true + ] + ], [ 'name' => __('Username'), 'sort' => 'username', @@ -71,15 +86,15 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'icon' => 'eye' ], [ - 'onclick' => 'populateAndLoadModal(\'/users/edit/[onclick_params_data_path]\');', - 'onclick_params_data_path' => 'id', + 'open_modal' => '/users/edit/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', 'icon' => 'edit' ], [ - 'onclick' => 'populateAndLoadModal(\'/users/delete/[onclick_params_data_path]\');', - 'onclick_params_data_path' => 'id', + 'open_modal' => '/users/delete/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', 'icon' => 'trash' - ] + ], ] ] ]); diff --git a/templates/element/genericElements/Form/genericForm.php b/templates/element/genericElements/Form/genericForm.php index ed73cf1..6e642ff 100644 --- a/templates/element/genericElements/Form/genericForm.php +++ b/templates/element/genericElements/Form/genericForm.php @@ -12,6 +12,7 @@ - use these to define dynamic form fields, or anything that will feed into the regular fields via JS population * - submit: The submit button itself. By default it will simply submit to the form as defined via the 'model' field */ + $this->Form->setConfig('errorClass', 'is-invalid'); $modelForForm = empty($data['model']) ? h(\Cake\Utility\Inflector::singularize(\Cake\Utility\Inflector::classify($this->request->getParam('controller')))) : h($data['model']); @@ -35,11 +36,14 @@ 'select' => '', 'checkbox' => '', 'checkboxFormGroup' => '{{label}}', - 'formGroup' => '
{{label}}
{{input}}
', + 'formGroup' => '
{{label}}
{{input}}{{error}}
', 'nestingLabel' => '{{hidden}}
{{text}}
{{input}}
', 'option' => '', 'optgroup' => '{{content}}', - 'select' => '' + 'select' => '', + 'error' => '
{{content}}
', + 'errorList' => '', + 'errorItem' => '
  • {{text}}
  • ', ]; if (!empty($data['fields'])) { foreach ($data['fields'] as $fieldData) { @@ -49,6 +53,7 @@ } } // we reset the template each iteration as individual fields might override the defaults. + $this->Form->setConfig($default_template); $this->Form->setTemplates($default_template); if (isset($fieldData['requirements']) && !$fieldData['requirements']) { continue; @@ -62,18 +67,13 @@ ); } } - $metaFieldString = ''; - if (!empty($data['metaFields'])) { - foreach ($data['metaFields'] as $metaField) { - $metaField['label'] = \Cake\Utility\Inflector::humanize($metaField['field']); - $metaField['field'] = 'metaFields.' . $metaField['field']; - $metaFieldString .= $this->element( - 'genericElements/Form/fieldScaffold', [ - 'fieldData' => $metaField->toArray(), - 'form' => $this->Form - ] - ); - } + if (!empty($data['metaTemplates']) && $data['metaTemplates']->count() > 0) { + $metaTemplateString = $this->element( + 'genericElements/Form/metaTemplateScaffold', [ + 'metaTemplatesData' => $data['metaTemplates'], + 'form' => $this->Form, + ] + ); } $submitButtonData = ['model' => $modelForForm, 'formRandomValue' => $formRandomValue]; if (!empty($data['submit'])) { @@ -104,9 +104,9 @@ $ajaxFlashMessage, $formCreate, $fieldsString, - empty($metaFieldString) ? '' : $this->element( + empty($metaTemplateString) ? '' : $this->element( 'genericElements/accordion_scaffold', [ - 'body' => $metaFieldString, + 'body' => $metaTemplateString, 'title' => 'Meta fields' ] ), @@ -127,10 +127,11 @@ $data['description'] ), $fieldsString, - empty($metaFieldString) ? '' : $this->element( + empty($metaTemplateString) ? '' : $this->element( 'genericElements/accordion_scaffold', [ - 'body' => $metaFieldString, - 'title' => 'Meta fields' + 'body' => $metaTemplateString, + 'title' => 'Meta fields', + 'class' => 'mb-2' ] ), $this->element('genericElements/Form/submitButton', $submitButtonData), diff --git a/templates/element/genericElements/Form/metaTemplateScaffold.php b/templates/element/genericElements/Form/metaTemplateScaffold.php new file mode 100644 index 0000000..614dc2a --- /dev/null +++ b/templates/element/genericElements/Form/metaTemplateScaffold.php @@ -0,0 +1,32 @@ + $metaTemplate) { + if ($metaTemplate->is_default) { + $tabData['navs'][$i] = [ + 'html' => $this->element('/genericElements/MetaTemplates/metaTemplateNav', ['metaTemplate' => $metaTemplate]) + ]; + } else { + $tabData['navs'][$i] = [ + 'text' => $metaTemplate->name + ]; + } + $fieldsHtml = ''; + foreach ($metaTemplate->meta_template_fields as $metaField) { + $metaField->label = Inflector::humanize($metaField->field); + $metaField->field = sprintf('%s.%s.%s', 'metaFields', $metaField->meta_template_id, $metaField->field); + $fieldsHtml .= $this->element( + 'genericElements/Form/fieldScaffold', [ + 'fieldData' => $metaField->toArray(), + 'form' => $this->Form + ] + ); + } + $tabData['content'][$i] = $fieldsHtml; +} +echo $this->Bootstrap->Tabs([ + 'pills' => true, + 'data' => $tabData, + 'nav-class' => ['pb-1'] +]); \ No newline at end of file diff --git a/templates/element/genericElements/Form/submitButton.php b/templates/element/genericElements/Form/submitButton.php index 3656e70..9a2a82c 100644 --- a/templates/element/genericElements/Form/submitButton.php +++ b/templates/element/genericElements/Form/submitButton.php @@ -3,8 +3,8 @@ echo sprintf( '%s', sprintf( - '', - "$('#form-" . h($formRandomValue) . "').submit()", + '', + '#form-' . h($formRandomValue), __('Submit') ) ); diff --git a/templates/element/genericElements/IndexTable/Fields/actions.php b/templates/element/genericElements/IndexTable/Fields/actions.php index 1a58422..fd03628 100644 --- a/templates/element/genericElements/IndexTable/Fields/actions.php +++ b/templates/element/genericElements/IndexTable/Fields/actions.php @@ -80,6 +80,14 @@ $action['onclick'] ); + } else if (!empty($action['open_modal']) && !empty($action['modal_params_data_path'])) { + $modal_url = str_replace( + '[onclick_params_data_path]', + h(Cake\Utility\Hash::extract($row, $action['modal_params_data_path'])[0]), + $action['open_modal'] + ); + $reload_url = !empty($action['reload_url']) ? $action['reload_url'] : $this->Url->build(['action' => 'index']); + $action['onclick'] = sprintf('UI.openModalFromURL(\'%s\', \'%s\', \'%s\')', $modal_url, $reload_url, $tableRandomValue); } echo sprintf( ' ', diff --git a/templates/element/genericElements/IndexTable/Fields/alignments.php b/templates/element/genericElements/IndexTable/Fields/alignments.php index 91b3719..611f990 100644 --- a/templates/element/genericElements/IndexTable/Fields/alignments.php +++ b/templates/element/genericElements/IndexTable/Fields/alignments.php @@ -14,10 +14,10 @@ if ($field['scope'] === 'individuals') { h($alignment['organisation']['name']) ), !$canRemove ? '' : sprintf( - "populateAndLoadModal(%s);", + "UI.openModalFromURL(%s);", sprintf( "'/alignments/delete/%s'", - $alignment['id'] + h($alignment['id']) ) ) ); @@ -34,10 +34,10 @@ if ($field['scope'] === 'individuals') { h($alignment['individual']['email']) ), !$canRemove ? '' : sprintf( - "populateAndLoadModal(%s);", + "UI.openModalFromURL(%s);", sprintf( "'/alignments/delete/%s'", - $alignment['id'] + h($alignment['id']) ) ) ); diff --git a/templates/element/genericElements/IndexTable/Fields/toggle.php b/templates/element/genericElements/IndexTable/Fields/toggle.php index cd17d9d..4064c8f 100644 --- a/templates/element/genericElements/IndexTable/Fields/toggle.php +++ b/templates/element/genericElements/IndexTable/Fields/toggle.php @@ -4,56 +4,96 @@ * On click, issues a GET to a given endpoint, retrieving a form with the * value flipped, which is immediately POSTed. * to fetch it. + * Options: + * - url: The URL on which to perform the POST + * - url_params_vars: Variables to be injected into the URL using the DataFromPath helper + * - toggle_data.skip_full_reload: If true, the index will not be reloaded and the checkbox will be flipped on success + * - toggle_data.editRequirement.function: A function to be called to assess if the checkbox can be toggled + * - toggle_data.editRequirement.options: Option that will be passed to the function + * - toggle_data.editRequirement.options.datapath: If provided, entries will have their datapath values converted into their extracted value + * - toggle_data.confirm.[enable/disable].title: + * - toggle_data.confirm.[enable/disable].titleHtml: + * - toggle_data.confirm.[enable/disable].body: + * - toggle_data.confirm.[enable/disable].bodyHtml: + * - toggle_data.confirm.[enable/disable].type: * */ - $data = $this->Hash->extract($row, $field['data_path']); + $data = $this->Hash->get($row, $field['data_path']); $seed = rand(); $checkboxId = 'GenericToggle-' . $seed; $tempboxId = 'TempBox-' . $seed; + + $requirementMet = false; + if (isset($field['toggle_data']['editRequirement'])) { + if (isset($field['toggle_data']['editRequirement']['options']['datapath'])) { + foreach ($field['toggle_data']['editRequirement']['options']['datapath'] as $name => $path) { + $field['toggle_data']['editRequirement']['options']['datapath'][$name] = empty($this->Hash->extract($row, $path)[0]) ? null : $this->Hash->extract($row, $path)[0]; + } + } + $options = isset($field['toggle_data']['editRequirement']['options']) ? $field['toggle_data']['editRequirement']['options'] : array(); + $requirementMet = $field['toggle_data']['editRequirement']['function']($row, $options); + } + echo sprintf( - '