From 53f669e25c552289e2fa7454f705cbdf50fc2edf Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Wed, 7 Dec 2022 14:54:28 +0100 Subject: [PATCH] new: [sync+meta_fields] Initial work on meta_field synchronisation and meta_template_directory - WiP The new directory allows to ingest meta_fields without knowing their associated meta_template. Improved the way data is re-arranged, how meta-templates are saved and a helper widget showing the difference local objects have with their remote counter-part --- .../20221130000000_MoreDataOnMetaFields.php | 160 ++++++++++++++++ src/Controller/BroodsController.php | 67 ++++--- .../Component/APIRearrangeComponent.php | 8 +- src/Controller/Component/CRUDComponent.php | 2 + .../Component/RestResponseComponent.php | 10 +- src/Model/Entity/AppModel.php | 45 +++-- src/Model/Entity/AuditLog.php | 2 +- src/Model/Entity/EncryptionKey.php | 2 +- src/Model/Entity/Individual.php | 7 +- .../Entity/MetaTemplateNameDirectory.php | 11 ++ src/Model/Entity/Organisation.php | 10 +- src/Model/Entity/User.php | 7 +- src/Model/Table/BroodsTable.php | 151 +++++++++++++++- src/Model/Table/MetaFieldsTable.php | 27 ++- .../Table/MetaTemplateNameDirectoryTable.php | 47 +++++ src/Model/Table/MetaTemplatesTable.php | 7 +- templates/Broods/preview_individuals.php | 21 ++- templates/Broods/preview_organisations.php | 23 ++- templates/Broods/preview_sharing_groups.php | 21 ++- .../IndexTable/Fields/brood_sync_status.php | 171 ++++++++++++++++++ 20 files changed, 724 insertions(+), 75 deletions(-) create mode 100644 config/Migrations/20221130000000_MoreDataOnMetaFields.php create mode 100644 src/Model/Entity/MetaTemplateNameDirectory.php create mode 100644 src/Model/Table/MetaTemplateNameDirectoryTable.php create mode 100644 templates/element/genericElements/IndexTable/Fields/brood_sync_status.php diff --git a/config/Migrations/20221130000000_MoreDataOnMetaFields.php b/config/Migrations/20221130000000_MoreDataOnMetaFields.php new file mode 100644 index 0000000..69598ad --- /dev/null +++ b/config/Migrations/20221130000000_MoreDataOnMetaFields.php @@ -0,0 +1,160 @@ +table('meta_fields'); + if (!$metaFieldTable->hasColumn('meta_template_directory_id')) { + $metaFieldTable + ->addColumn('meta_template_directory_id', 'integer', [ + 'default' => null, + 'null' => false, + 'signed' => false, + 'length' => 10 + ]) + ->addIndex('meta_template_directory_id') + ->update(); + } + + $exists = $this->hasTable('meta_template_name_directory'); + if (!$exists) { + $templateNameDirectoryTable = $this->table('meta_template_name_directory', [ + 'signed' => false, + 'collation' => 'utf8mb4_unicode_ci' + ]); + $templateNameDirectoryTable + ->addColumn('id', 'integer', [ + 'autoIncrement' => true, + 'limit' => 10, + 'signed' => false, + ]) + ->addPrimaryKey('id') + ->addColumn('name', 'string', [ + 'null' => false, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + ]) + ->addColumn('namespace', 'string', [ + 'null' => false, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + ]) + ->addColumn('version', 'string', [ + 'null' => false, + 'limit' => 191, + 'collation' => 'utf8mb4_unicode_ci', + 'encoding' => 'utf8mb4', + ]) + ->addColumn('uuid', 'uuid', [ + 'null' => false, + 'default' => null, + ]); + + $templateNameDirectoryTable + ->addIndex(['uuid', 'version'], ['unique' => true]) + ->addIndex('name') + ->addIndex('namespace'); + + $templateNameDirectoryTable->create(); + + $allTemplates = $this->getAllTemplates(); + $this->populateTemplateDirectoryTable($allTemplates); + + $metaTemplateTable = $this->table('meta_templates'); + $metaTemplateTable + ->addColumn('meta_template_directory_id', 'integer', [ + 'default' => null, + 'null' => false, + 'signed' => false, + 'length' => 10 + ]) + ->update(); + $this->assignTemplateDirectory($allTemplates); + $metaTemplateTable + ->addForeignKey('meta_template_directory_id', 'meta_template_name_directory', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']) + ->save(); + + $metaFieldTable + ->dropForeignKey('meta_template_id') + ->dropForeignKey('meta_template_field_id') + ->addForeignKey('meta_template_directory_id', 'meta_template_name_directory', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']) + ->save(); + } + } + + private function populateTemplateDirectoryTable(array $allTemplates): void + { + $builder = $this->getQueryBuilder() + ->insert(['uuid', 'name', 'namespace', 'version']) + ->into('meta_template_name_directory'); + + if (!empty($allTemplates)) { + foreach ($allTemplates as $template) { + $builder->values([ + 'uuid' => $template['uuid'], + 'name' => $template['name'], + 'namespace' => $template['namespace'], + 'version' => $template['version'], + ]); + } + $builder->execute(); + } + } + + private function assignTemplateDirectory(array $allTemplates): void + { + foreach ($allTemplates as $template) { + $directory_template = $this->getDirectoryTemplate($template['uuid'], $template['version'])[0]; + $this->getQueryBuilder() + ->update('meta_templates') + ->set('meta_template_directory_id', $directory_template['id']) + ->where(['meta_template_id' => $template['id']]) + ->execute(); + $this->getQueryBuilder() + ->update('meta_fields') + ->set('meta_template_directory_id', $directory_template['id']) + ->where(['id' => $template['id']]) + ->execute(); + } + } + + private function getAllTemplates(): array + { + return $this->getQueryBuilder() + ->select(['id', 'uuid', 'name', 'namespace', 'version']) + ->from('meta_templates') + ->execute()->fetchAll('assoc'); + } + + private function getDirectoryTemplate(string $uuid, string $version): array + { + return $this->getQueryBuilder() + ->select(['id', 'uuid', 'version']) + ->from('meta_template_name_directory') + ->where([ + 'uuid' => $uuid, + 'version' => $version, + ]) + ->execute()->fetchAll('assoc'); + } +} diff --git a/src/Controller/BroodsController.php b/src/Controller/BroodsController.php index b120f8c..9800154 100644 --- a/src/Controller/BroodsController.php +++ b/src/Controller/BroodsController.php @@ -14,18 +14,6 @@ class BroodsController extends AppController public $quickFilterFields = [['Broods.name' => true], 'Broods.uuid', ['Broods.description' => true]]; public $containFields = ['Organisations']; - protected $previewScopes = [ - 'organisations' => [ - 'quickFilterFields' => ['uuid', ['name' => true], ], - ], - 'individuals' => [ - 'quickFilterFields' => ['uuid', ['email' => true], ['first_name' => true], ['last_name' => true], ], - ], - 'sharingGroups' => [ - 'quickFilterFields' => ['uuid', ['name' => true], ], - ], - ]; - public function index() { $this->CRUD->index([ @@ -108,23 +96,24 @@ class BroodsController extends AppController public function previewIndex($id, $scope) { - $validScopes = array_keys($this->previewScopes); + $validScopes = array_keys($this->Broods->previewScopes); if (!in_array($scope, $validScopes)) { throw new MethodNotAllowedException(__('Invalid scope. Valid options are: {0}', implode(', ', $validScopes))); } $filter = $this->request->getQuery('quickFilter'); - $data = $this->Broods->queryIndex($id, $scope, $filter); + $data = $this->Broods->queryIndex($id, $scope, $filter, true); if (!is_array($data)) { $data = []; } if ($this->ParamHandler->isRest()) { return $this->RestResponse->viewData($data, 'json'); } else { + $data = $this->Broods->attachAllSyncStatus($data, $scope); $data = $this->CustomPagination->paginate($data); $optionFilters = ['quickFilter']; $CRUDParams = $this->ParamHandler->harvestParams($optionFilters); $CRUDOptions = [ - 'quickFilters' => $this->previewScopes[$scope]['quickFilterFields'], + 'quickFilters' => $this->Broods->previewScopes[$scope]['quickFilterFields'], ]; $this->CRUD->setQuickFilterForView($CRUDParams, $CRUDOptions); $this->set('data', $data); @@ -139,23 +128,43 @@ class BroodsController extends AppController public function downloadOrg($brood_id, $org_id) { - $result = $this->Broods->downloadOrg($brood_id, $org_id); - $success = __('Organisation fetched from remote.'); - $fail = __('Could not save the remote organisation'); - if ($this->ParamHandler->isRest()) { - if ($result) { - return $this->RestResponse->saveSuccessResponse('Brood', 'downloadOrg', $brood_id, 'json', $success); + if ($this->request->is('post')) { + $result = $this->Broods->downloadOrg($brood_id, $org_id); + $success = __('Organisation fetched from remote.'); + $fail = __('Could not save the remote organisation'); + if ($this->ParamHandler->isRest()) { + if ($result) { + return $this->RestResponse->saveSuccessResponse('Brood', 'downloadOrg', $brood_id, 'json', $success); + } else { + return $this->RestResponse->saveFailResponse('Brood', 'downloadOrg', $brood_id, $fail, 'json'); + } } else { - return $this->RestResponse->saveFailResponse('Brood', 'downloadOrg', $brood_id, $fail, 'json'); + if ($result) { + $this->Flash->success($success); + } else { + $this->Flash->error($fail); + } + $this->redirect($this->referer()); } - } else { - if ($result) { - $this->Flash->success($success); - } else { - $this->Flash->error($fail); - } - $this->redirect($this->referer()); } + if ($org_id === 'all') { + $question = __('All organisations from brood `{0}` will be downloaded. Continue?', h($brood_id)); + $title = __('Download all organisations from this brood'); + $actionName = __('Download all'); + } else { + $question = __('The organisations `{0}` from brood `{1}` will be downloaded. Continue?', h($org_id), h($brood_id)); + $title = __('Download organisation from this brood'); + $actionName = __('Download organisation'); + } + $this->set('title', $title); + $this->set('question', $question); + $this->set('modalOptions', [ + 'confirmButton' => [ + 'variant' => $org_id === 'all' ? 'warning' : 'primary', + 'text' => $actionName, + ], + ]); + $this->render('/genericTemplates/confirm'); } public function downloadIndividual($brood_id, $individual_id) diff --git a/src/Controller/Component/APIRearrangeComponent.php b/src/Controller/Component/APIRearrangeComponent.php index 561b271..0de9a3e 100644 --- a/src/Controller/Component/APIRearrangeComponent.php +++ b/src/Controller/Component/APIRearrangeComponent.php @@ -16,17 +16,17 @@ use Cake\Collection\Collection; class APIRearrangeComponent extends Component { - public function rearrangeForAPI(object $data) + public static function rearrangeForAPI(object $data, array $options = []) { if (is_subclass_of($data, 'Iterator')) { $newData = []; - $data->each(function ($value, $key) use (&$newData) { - $value->rearrangeForAPI(); + $data->each(function ($value, $key) use (&$newData, $options) { + $value->rearrangeForAPI($options); $newData[] = $value; }); return new Collection($newData); } else { - $data->rearrangeForAPI(); + $data->rearrangeForAPI($options); } return $data; } diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index b2a7ab5..407598f 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -432,6 +432,7 @@ class CRUDComponent extends Component 'field' => $rawMetaTemplateField->field, 'meta_template_id' => $rawMetaTemplateField->meta_template_id, 'meta_template_field_id' => $rawMetaTemplateField->id, + 'meta_template_directory_id' => $allMetaTemplates[$template_id]->meta_template_directory_id, 'parent_id' => $entity->id, 'uuid' => Text::uuid(), ]); @@ -462,6 +463,7 @@ class CRUDComponent extends Component 'field' => $rawMetaTemplateField->field, 'meta_template_id' => $rawMetaTemplateField->meta_template_id, 'meta_template_field_id' => $rawMetaTemplateField->id, + 'meta_template_directory_id' => $template->meta_template_directory_id, 'parent_id' => $entity->id, 'uuid' => Text::uuid(), ]); diff --git a/src/Controller/Component/RestResponseComponent.php b/src/Controller/Component/RestResponseComponent.php index db5072b..8da9128 100644 --- a/src/Controller/Component/RestResponseComponent.php +++ b/src/Controller/Component/RestResponseComponent.php @@ -283,6 +283,7 @@ class RestResponseComponent extends Component private $__scopedFieldsConstraint = array(); public function initialize(array $config): void { + parent::initialize($config); $this->__configureFieldConstraints(); $this->Controller = $this->getController(); } @@ -559,7 +560,14 @@ class RestResponseComponent extends Component $data['errors'] = $errors; } if (!$raw && is_object($data)) { - $data = $this->APIRearrange->rearrangeForAPI($data); + $rearrangeOptions = []; + if (!empty($this->Controller->getRequest()->getQuery('includeMetatemplate', false))) { + $rearrangeOptions['includeMetatemplate'] = true; + } + if (!empty($this->Controller->getRequest()->getQuery('includeFullMetaFields', false))) { + $rearrangeOptions['includeFullMetaFields'] = true; + } + $data = $this->APIRearrange->rearrangeForAPI($data, $rearrangeOptions); } return $this->__sendResponse($data, 200, $format, $raw, $download, $headers); } diff --git a/src/Model/Entity/AppModel.php b/src/Model/Entity/AppModel.php index 3e70518..2d4a553 100644 --- a/src/Model/Entity/AppModel.php +++ b/src/Model/Entity/AppModel.php @@ -40,28 +40,47 @@ class AppModel extends Entity return TableRegistry::get($this->getSource()); } - public function rearrangeForAPI(): void + public function rearrangeForAPI(array $options = []): void { } - public function rearrangeMetaFields(): void + public function rearrangeMetaFields(array $options = []): void { - $this->meta_fields = []; - foreach ($this->MetaTemplates as $template) { - foreach ($template['meta_template_fields'] as $field) { - if ($field['counter'] > 0) { - foreach ($field['metaFields'] as $metaField) { - if (!empty($this->meta_fields[$template['name']][$field['field']])) { - if (!is_array($this->meta_fields[$template['name']][$field['field']])) { - $this->meta_fields[$template['name']][$field['field']] = [$this->meta_fields[$template['name']][$field['field']]]; + if (!empty($options['includeFullMetaFields'])) { + $this->meta_fields = []; + foreach ($this->MetaTemplates as $template) { + foreach ($template['meta_template_fields'] as $field) { + if ($field['counter'] > 0) { + foreach ($field['metaFields'] as $metaField) { + if (!empty($this->meta_fields[$template['name']][$field['field']])) { + if (!is_array($this->meta_fields[$template['name']][$field['field']])) { + $this->meta_fields[$template['name']][$field['field']] = [$this->meta_fields[$template['name']][$field['field']]]; + } + $this->meta_fields[$template['name']][$field['field']][] = $metaField['value']; + } else { + $this->meta_fields[$template['name']][$field['field']] = $metaField['value']; } - $this->meta_fields[$template['name']][$field['field']][] = $metaField['value']; - } else { - $this->meta_fields[$template['name']][$field['field']] = $metaField['value']; } } } } + } elseif (!empty($this->meta_fields)) { + $templateDirectoryTable = TableRegistry::get('MetaTemplateNameDirectory'); + $templates = []; + foreach ($this->meta_fields as $i => $metafield) { + $templateDirectoryId = $metafield['meta_template_directory_id']; + if (empty($templates[$templateDirectoryId])) { + $templates[$templateDirectoryId] = $templateDirectoryTable->find()->where(['id' => $templateDirectoryId])->first(); + } + $this->meta_fields[$i]['template_uuid'] = $templates[$templateDirectoryId]['uuid']; + $this->meta_fields[$i]['template_version'] = $templates[$templateDirectoryId]['version']; + $this->meta_fields[$i]['template_name'] = $templates[$templateDirectoryId]['name']; + $this->meta_fields[$i]['template_namespace'] = $templates[$templateDirectoryId]['namespace']; + } + } + // if ((!isset($options['includeMetatemplate']) || empty($options['includeMetatemplate'])) && !empty($this->MetaTemplates)) { + if ((!isset($options['includeMetatemplate']) || empty($options['includeMetatemplate']))) { + unset($this->MetaTemplates); } } diff --git a/src/Model/Entity/AuditLog.php b/src/Model/Entity/AuditLog.php index 4f1a2fe..eaaac47 100644 --- a/src/Model/Entity/AuditLog.php +++ b/src/Model/Entity/AuditLog.php @@ -66,7 +66,7 @@ class AuditLog extends AppModel return $title; } - public function rearrangeForAPI(): void + public function rearrangeForAPI(array $options = []): void { if (!empty($this->user)) { $this->user = $this->user->toArray(); diff --git a/src/Model/Entity/EncryptionKey.php b/src/Model/Entity/EncryptionKey.php index a9b2f67..e735e15 100644 --- a/src/Model/Entity/EncryptionKey.php +++ b/src/Model/Entity/EncryptionKey.php @@ -8,7 +8,7 @@ use Cake\ORM\Entity; class EncryptionKey extends AppModel { - public function rearrangeForAPI(): void + public function rearrangeForAPI(array $options = []): void { $this->rearrangeSimplify(['organisation', 'individual']); } diff --git a/src/Model/Entity/Individual.php b/src/Model/Entity/Individual.php index 1afd30c..14863b5 100644 --- a/src/Model/Entity/Individual.php +++ b/src/Model/Entity/Individual.php @@ -42,7 +42,7 @@ class Individual extends AppModel return $emails; } - public function rearrangeForAPI(): void + public function rearrangeForAPI(array $options = []): void { if (!empty($this->tags)) { $this->tags = $this->rearrangeTags($this->tags); @@ -51,10 +51,7 @@ class Individual extends AppModel $this->alignments = $this->rearrangeAlignments($this->alignments); } if (!empty($this->meta_fields)) { - $this->rearrangeMetaFields(); - } - if (!empty($this->MetaTemplates)) { - unset($this->MetaTemplates); + $this->rearrangeMetaFields($options); } } } diff --git a/src/Model/Entity/MetaTemplateNameDirectory.php b/src/Model/Entity/MetaTemplateNameDirectory.php new file mode 100644 index 0000000..84c9696 --- /dev/null +++ b/src/Model/Entity/MetaTemplateNameDirectory.php @@ -0,0 +1,11 @@ + true ]; - public function rearrangeForAPI(): void + public function rearrangeForAPI(array $options = []): void { if (!empty($this->tags)) { $this->tags = $this->rearrangeTags($this->tags); @@ -25,11 +25,9 @@ class Organisation extends AppModel if (!empty($this->alignments)) { $this->alignments = $this->rearrangeAlignments($this->alignments); } - if (!empty($this->meta_fields)) { - $this->rearrangeMetaFields(); - } - if (!empty($this->MetaTemplates)) { - unset($this->MetaTemplates); + if (!empty($this->meta_fields) || !empty($this->MetaTemplates)) { + $this->rearrangeMetaFields($options); } } + // MetaTemplate object property is not unset!! } diff --git a/src/Model/Entity/User.php b/src/Model/Entity/User.php index 5f39720..31437d8 100644 --- a/src/Model/Entity/User.php +++ b/src/Model/Entity/User.php @@ -49,16 +49,13 @@ class User extends AppModel } } - public function rearrangeForAPI(): void + public function rearrangeForAPI(array $options = []): void { if (!empty($this->tags)) { $this->tags = $this->rearrangeTags($this->tags); } if (!empty($this->meta_fields)) { - $this->rearrangeMetaFields(); - } - if (!empty($this->MetaTemplates)) { - unset($this->MetaTemplates); + $this->rearrangeMetaFields($options); } if (!empty($this->user_settings_by_name)) { $this->rearrangeUserSettings(); diff --git a/src/Model/Table/BroodsTable.php b/src/Model/Table/BroodsTable.php index 0708c18..ad7c703 100644 --- a/src/Model/Table/BroodsTable.php +++ b/src/Model/Table/BroodsTable.php @@ -2,10 +2,15 @@ namespace App\Model\Table; +require_once APP . DS . 'Utility/Utils.php'; use App\Model\Table\AppTable; +use function App\Utility\Utils\array_diff_recursive; use Cake\ORM\Table; use Cake\Validation\Validator; use Cake\Core\Configure; +use Cake\Utility\Inflector; +use Cake\Utility\Hash; +use Cake\I18n\FrozenTime; use Cake\Http\Client; use Cake\Http\Client\Response; use Cake\Http\Exception\NotFoundException; @@ -15,6 +20,27 @@ use Cake\Error\Debugger; class BroodsTable extends AppTable { + + public $previewScopes = [ + 'organisations' => [ + 'quickFilterFields' => ['uuid', ['name' => true],], + 'contain' => ['MetaFields' => ['MetaTemplateNameDirectory'], 'Tags'], + 'compareFields' => ['name', 'url', 'nationality', 'sector', 'type', 'contacts', 'modified', 'tags', 'meta_fields',], + ], + 'individuals' => [ + 'quickFilterFields' => ['uuid', ['email' => true], ['first_name' => true], ['last_name' => true],], + 'contain' => ['MetaFields'], + 'compareFields' => ['email', 'first_name', 'last_name', 'position', 'modified', 'meta_fields', 'tags',], + ], + 'sharingGroups' => [ + 'quickFilterFields' => ['uuid', ['name' => true],], + 'contain' => ['SharingGroupOrgs', 'Organisations'], + 'compareFields' => ['name', 'releasability', 'description', 'organisation_id', 'user_id', 'active', 'local', 'modified', 'organisation', 'sharing_group_orgs',], + ], + ]; + + private $metaFieldCompareFields = ['modified', 'value']; + public function initialize(array $config): void { parent::initialize($config); @@ -119,13 +145,16 @@ class BroodsTable extends AppTable return $result; } - public function queryIndex($id, $scope, $filter) + public function queryIndex($id, $scope, $filter, $full = false) { $brood = $this->find()->where(['id' => $id])->first(); if (empty($brood)) { throw new NotFoundException(__('Brood not found')); } $filterQuery = empty($filter) ? '' : '?quickFilter=' . urlencode($filter); + if (!empty($full)) { + $filterQuery .= (empty($filterQuery) ? '?' : '&') . 'full=1'; + } $response = $this->HTTPClientGET(sprintf('/%s/index.json%s', $scope, $filterQuery), $brood); if ($response->isOk()) { return $response->getJson(); @@ -371,4 +400,124 @@ class BroodsTable extends AppTable $connector = $params['connector'][$params['remote_tool']['connector']]; $connector->remoteToolConnectionStatus($params, constant(get_class($connector) . '::' . $status)); } + + public function attachAllSyncStatus(array $data, string $scope): array + { + $options = $this->previewScopes[$scope]; + foreach ($data as $i => $entry) { + $data[$i] = $this->__attachSyncStatus($scope, $entry, $options); + } + return $data; + } + + private function __attachSyncStatus(string $scope, array $entry, array $options = []): array + { + $table = TableRegistry::getTableLocator()->get(Inflector::camelize($scope)); + $localEntry = $table + ->find() + ->where(['uuid' => $entry['uuid']]) + ->first(); + if (is_null($localEntry)) { + $entry['status'] = $this->__statusNotLocal(); + } else { + if (!empty($options['contain'])) { + $localEntry = $table->loadInto($localEntry, $options['contain']); + } + $localEntry = json_decode(json_encode($localEntry), true); + $entry['status'] = $this->__statusLocal($entry, $localEntry, $options); + } + + return $entry; + } + + private function __statusNotLocal(): array + { + return self::__getStatus(false); + } + + private function __statusLocal(array $remoteEntry, $localEntry, array $options = []): array + { + $isLocalNewer = (new FrozenTime($localEntry['modified']))->toUnixString() >= (new FrozenTime($remoteEntry['modified']))->toUnixString(); + $compareFields = $options['compareFields']; + $fieldDifference = []; + $fieldDifference = array_diff_recursive($remoteEntry, $localEntry); + // if (in_array('meta_fields', $options['compareFields']) && !empty($fieldDifference['meta_fields'])) { + // $fieldDifference['meta_fields'] = $this->_compareMetaFields($remoteEntry, $localEntry, $options); + // } + $fieldDifference = array_filter($fieldDifference, function($value, $field) use ($compareFields) { + return in_array($field, $compareFields); + }, ARRAY_FILTER_USE_BOTH); + foreach ($fieldDifference as $fieldName => $value) { + $fieldDifference[$fieldName] = [ + 'local' => $localEntry[$fieldName], + 'remote' => $value, + ]; + } + if (in_array('meta_fields', $options['compareFields']) && !empty($fieldDifference['meta_fields'])) { + $fieldDifference['meta_fields'] = $this->_compareMetaFields($remoteEntry, $localEntry, $options); + } + + return self::__getStatus(true, $isLocalNewer, $fieldDifference); + } + + private static function __getStatus($local=true, $updateToDate=false, array $data = []): array + { + $status = [ + 'local' => $local, + 'up_to_date' => $updateToDate, + 'data' => $data, + ]; + if ($status['local'] && $status['up_to_date']) { + $status['title'] = __('This entity is up-to-date'); + } else if ($status['local'] && !$status['up_to_date']) { + $status['title'] = __('This entity is known but differs with the remote'); + } else { + $status['title'] = __('This entity is not known locally'); + } + return $status; + } + + private function _compareMetaFields($remoteEntry, $localEntry): array + { + $compareFields = $this->metaFieldCompareFields; + $indexedRemoteMF = []; + $indexedLocalMF = []; + foreach ($remoteEntry['meta_fields'] as $metafields) { + $indexedRemoteMF[$metafields['uuid']] = array_intersect_key($metafields, array_flip($compareFields)); + } + foreach ($localEntry['meta_fields'] as $metafields) { + $indexedLocalMF[$metafields['uuid']] = array_intersect_key($metafields, array_flip($compareFields)); + } + $fieldDifference = []; + foreach ($remoteEntry['meta_fields'] as $remoteMetafield) { + $uuid = $remoteMetafield['uuid']; + $metafieldName = $remoteMetafield['field']; + // $metafieldName = sprintf('%s(v%s) :: %s', $remoteMetafield['template_name'], $remoteMetafield['template_version'], $remoteMetafield['field']); + if (empty($fieldDifference[$metafieldName])) { + $fieldDifference[$metafieldName] = [ + 'meta_template' => [ + 'name' => $remoteMetafield['template_name'], + 'version' => $remoteMetafield['template_version'], + 'uuid' => $remoteMetafield['template_uuid'] + ], + 'delta' => [], + ]; + } + if (empty($indexedLocalMF[$uuid])) { + $fieldDifference[$metafieldName]['delta'][] = [ + 'local' => null, + 'remote' => $indexedRemoteMF[$uuid], + ]; + } else { + $fieldDifferenceTmp = array_diff_recursive($indexedRemoteMF[$uuid], $indexedLocalMF[$uuid]); + if (!empty($fieldDifferenceTmp)) { + $fieldDifference[$metafieldName]['delta'][] = [ + 'local' => $indexedLocalMF[$uuid], + 'remote' => $indexedRemoteMF[$uuid], + ]; + } + } + } + return $fieldDifference; + } } diff --git a/src/Model/Table/MetaFieldsTable.php b/src/Model/Table/MetaFieldsTable.php index 8c5a6d0..1e55501 100644 --- a/src/Model/Table/MetaFieldsTable.php +++ b/src/Model/Table/MetaFieldsTable.php @@ -5,7 +5,9 @@ namespace App\Model\Table; use App\Model\Table\AppTable; use Cake\ORM\Table; use Cake\Validation\Validator; +use Cake\Event\EventInterface; use Cake\ORM\RulesChecker; +use ArrayObject; class MetaFieldsTable extends AppTable { @@ -22,6 +24,8 @@ class MetaFieldsTable extends AppTable $this->addBehavior('Timestamp'); $this->belongsTo('MetaTemplates'); $this->belongsTo('MetaTemplateFields'); + $this->belongsTo('MetaTemplateNameDirectory') + ->setForeignKey('meta_template_directory_id'); $this->setDisplayField('field'); } @@ -35,7 +39,10 @@ class MetaFieldsTable extends AppTable ->notEmptyString('value') ->notEmptyString('meta_template_id') ->notEmptyString('meta_template_field_id') - ->requirePresence(['scope', 'field', 'value', 'uuid', 'meta_template_id', 'meta_template_field_id'], 'create'); + // ->requirePresence(['scope', 'field', 'value', 'uuid', 'meta_template_id', 'meta_template_field_id'], 'create'); + // ->requirePresence(['scope', 'field', 'value', 'uuid',], 'create'); + ->notEmptyString('meta_template_directory_id') + ->requirePresence(['scope', 'field', 'value', 'uuid', 'meta_template_directory_id', ], 'create'); $validator->add('value', 'validMetaField', [ 'rule' => 'isValidMetaField', @@ -46,10 +53,28 @@ class MetaFieldsTable extends AppTable return $validator; } + public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options) + { + if (!isset($data['meta_template_directory_id'])) { + $data['meta_template_directory_id'] = $this->getTemplateDirectoryIdFromMetaTemplate($data['meta_template_id']); + } + } + + public function getTemplateDirectoryIdFromMetaTemplate($metaTemplateId): int + { + return $this->MetaTemplates->find() + ->select('meta_template_directory_id') + ->where(['id' => $metaTemplateId]) + ->first(); + } + public function isValidMetaField($value, array $context) { $metaFieldsTable = $context['providers']['table']; $entityData = $context['data']; + if (empty($entityData['meta_template_field_id'])) { + return true; + } $metaTemplateField = $metaFieldsTable->MetaTemplateFields->get($entityData['meta_template_field_id']); return $this->isValidMetaFieldForMetaTemplateField($value, $metaTemplateField); } diff --git a/src/Model/Table/MetaTemplateNameDirectoryTable.php b/src/Model/Table/MetaTemplateNameDirectoryTable.php new file mode 100644 index 0000000..f1a3dfe --- /dev/null +++ b/src/Model/Table/MetaTemplateNameDirectoryTable.php @@ -0,0 +1,47 @@ +hasMany( + 'MetaFields', + [ + 'foreignKey' => 'meta_template_directory_id', + ] + ); + $this->setDisplayField('name'); + } + + public function validationDefault(Validator $validator): Validator + { + $validator + ->notEmptyString('name') + ->notEmptyString('namespace') + ->notEmptyString('uuid') + ->notEmptyString('version') + ->requirePresence(['version', 'uuid', 'name', 'namespace'], 'create'); + return $validator; + } + + public function createFromMetaTemplate(MetaTemplate $metaTemplate): MetaTemplateNameDirectory + { + $metaTemplateDirectory = $this->newEntity([ + 'name' => $metaTemplate['name'], + 'namespace' => $metaTemplate['namespace'], + 'uuid' => $metaTemplate['uuid'], + 'version' => $metaTemplate['version'], + ]); + $this->save($metaTemplateDirectory); + return $metaTemplateDirectory; + } +} diff --git a/src/Model/Table/MetaTemplatesTable.php b/src/Model/Table/MetaTemplatesTable.php index 77df33d..76d82e9 100644 --- a/src/Model/Table/MetaTemplatesTable.php +++ b/src/Model/Table/MetaTemplatesTable.php @@ -42,6 +42,9 @@ class MetaTemplatesTable extends AppTable 'cascadeCallbacks' => true, ] ); + $this->hasOne('MetaTemplateNameDirectory') + ->setForeignKey('meta_template_directory_id'); + $this->setDisplayField('name'); } @@ -54,7 +57,7 @@ class MetaTemplatesTable extends AppTable ->notEmptyString('uuid') ->notEmptyString('version') ->notEmptyString('source') - ->requirePresence(['scope', 'source', 'version', 'uuid', 'name', 'namespace'], 'create'); + ->requirePresence(['scope', 'source', 'version', 'uuid', 'name', 'namespace', 'meta_template_directory_id'], 'create'); return $validator; } @@ -731,6 +734,8 @@ class MetaTemplatesTable extends AppTable $metaTemplate = $this->newEntity($template, [ 'associated' => ['MetaTemplateFields'] ]); + $metaTemplateDirectory = $this->MetaTemplateNameDirectory->createFromMetaTemplate($metaTemplate); + $metaTemplate->meta_template_directory_id = $metaTemplateDirectory->id; $tmp = $this->save($metaTemplate, [ 'associated' => ['MetaTemplateFields'] ]); diff --git a/templates/Broods/preview_individuals.php b/templates/Broods/preview_individuals.php index 73e2199..d0ae0e4 100644 --- a/templates/Broods/preview_individuals.php +++ b/templates/Broods/preview_individuals.php @@ -5,6 +5,16 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'top_bar' => [ 'pull' => 'right', 'children' => [ + [ + 'type' => 'simple', + 'children' => [ + 'data' => [ + 'type' => 'simple', + 'text' => __('Download All'), + 'popover_url' => sprintf('/broods/downloadIndividual/%s/all', h($brood_id)), + ] + ] + ], [ 'type' => 'search', 'button' => __('Search'), @@ -21,6 +31,13 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'sort' => 'id', 'data_path' => 'id', ], + [ + 'name' => __('Status'), + 'class' => 'short', + 'data_path' => 'status', + 'sort' => 'status', + 'element' => 'brood_sync_status', + ], [ 'name' => __('Email'), 'sort' => 'email', @@ -53,8 +70,8 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'pull' => 'right', 'actions' => [ [ - 'url' => '/broods/downloadIndividual/' . $brood_id, - 'url_params_data_paths' => ['id'], + 'open_modal' => '/broods/downloadIndividual/' . $brood_id . '/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', 'title' => __('Download'), 'icon' => 'download' ] diff --git a/templates/Broods/preview_organisations.php b/templates/Broods/preview_organisations.php index 40b09c3..2f082e4 100644 --- a/templates/Broods/preview_organisations.php +++ b/templates/Broods/preview_organisations.php @@ -3,8 +3,17 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'data' => [ 'data' => $data, 'top_bar' => [ - 'pull' => 'right', 'children' => [ + [ + 'type' => 'simple', + 'children' => [ + 'data' => [ + 'type' => 'simple', + 'text' => __('Download All'), + 'popover_url' => sprintf('/broods/downloadOrg/%s/all', h($brood_id)), + ] + ] + ], [ 'type' => 'search', 'button' => __('Search'), @@ -22,6 +31,14 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'class' => 'short', 'data_path' => 'id', ], + [ + 'name' => __('Status'), + 'class' => 'short', + 'data_path' => 'status', + 'display_field_data_path' => 'name', + 'sort' => 'status', + 'element' => 'brood_sync_status', + ], [ 'name' => __('Name'), 'class' => 'short', @@ -58,8 +75,8 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'pull' => 'right', 'actions' => [ [ - 'url' => '/broods/downloadOrg/' . $brood_id, - 'url_params_data_paths' => ['id'], + 'open_modal' => '/broods/downloadOrg/' . $brood_id . '/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', 'title' => __('Download'), 'icon' => 'download' ] diff --git a/templates/Broods/preview_sharing_groups.php b/templates/Broods/preview_sharing_groups.php index 5a4cc58..1dac07a 100644 --- a/templates/Broods/preview_sharing_groups.php +++ b/templates/Broods/preview_sharing_groups.php @@ -5,6 +5,16 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'top_bar' => [ 'pull' => 'right', 'children' => [ + [ + 'type' => 'simple', + 'children' => [ + 'data' => [ + 'type' => 'simple', + 'text' => __('Download All'), + 'popover_url' => sprintf('/broods/downloadSharingGroup/%s/all', h($brood_id)), + ] + ] + ], [ 'type' => 'search', 'button' => __('Search'), @@ -22,6 +32,13 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'class' => 'short', 'data_path' => 'id', ], + [ + 'name' => __('Status'), + 'class' => 'short', + 'data_path' => 'status', + 'sort' => 'status', + 'element' => 'brood_sync_status', + ], [ 'name' => __('Name'), 'class' => 'short', @@ -38,8 +55,8 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'pull' => 'right', 'actions' => [ [ - 'url' => '/broods/downloadSharingGroup/' . $brood_id, - 'url_params_data_paths' => ['id'], + 'open_modal' => '/broods/downloadSharingGroup/' . $brood_id . '/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', 'title' => __('Download'), 'icon' => 'download' ] diff --git a/templates/element/genericElements/IndexTable/Fields/brood_sync_status.php b/templates/element/genericElements/IndexTable/Fields/brood_sync_status.php new file mode 100644 index 0000000..018c585 --- /dev/null +++ b/templates/element/genericElements/IndexTable/Fields/brood_sync_status.php @@ -0,0 +1,171 @@ +Hash->extract($row, $field['data_path']); +$displayField = $this->Hash->get($row, $field['display_field_data_path']); + +if ($status['local'] && $status['up_to_date']) { + $variant = 'success'; + $text = __('Ok'); +} else if ($status['local'] && !$status['up_to_date']) { + $variant = 'warning'; + $text = __('Outdated'); +} else { + $variant = 'danger'; + $text = __('N/A'); +} + +echo $this->Bootstrap->badge([ + 'id' => $seed, + 'variant' => $variant, + 'text' => $text, + 'icon' => ($status['local'] && !$status['up_to_date']) ? 'question-circle' : false, + 'title' => $status['title'], + 'class' => [ + (($status['local'] && !$status['up_to_date']) ? 'cursor-pointer' : ''), + ], +]); +?> + + + + \ No newline at end of file