new: [meta-template] Improvement of the update system

- Changed default update strategy from `create_new` to `update_existing`
- Added mechanism to automatically migrate meta-fields to newest template
- Improved validation and conflict detection strategies
- Fixed various UI bugs and improved QoL
develop-unstable
Sami Mokaddem 2023-02-14 14:42:35 +01:00
parent c0636b89ab
commit 20eebd097d
No known key found for this signature in database
GPG Key ID: 164C473F627A06FA
10 changed files with 459 additions and 106 deletions

View File

@ -91,7 +91,7 @@ class MetaTemplatesNavigation extends BaseNavigation
'url' => '/metaTemplates/prune_outdated_template', 'url' => '/metaTemplates/prune_outdated_template',
]); ]);
if (empty($this->viewVars['updateableTemplates']['up-to-date'])) { if (empty($this->viewVars['templateStatus']['up-to-date'])) {
$this->bcf->addAction('MetaTemplates', 'view', 'MetaTemplates', 'update', [ $this->bcf->addAction('MetaTemplates', 'view', 'MetaTemplates', 'update', [
'label' => __('Update template'), 'label' => __('Update template'),
'url' => '/metaTemplates/update/{{id}}', 'url' => '/metaTemplates/update/{{id}}',

View File

@ -244,6 +244,55 @@ class MetaTemplatesController extends AppController
$this->set('movedMetaTemplateFields', $movedMetaTemplateFields); $this->set('movedMetaTemplateFields', $movedMetaTemplateFields);
} }
public function migrateMetafieldsToNewestTemplate(int $template_id)
{
$oldMetaTemplate = $this->MetaTemplates->find()->where([
'id' => $template_id
])->contain(['MetaTemplateFields'])->first();
if (empty($oldMetaTemplate)) {
throw new NotFoundException(__('Invalid {0} {1}.', $this->MetaTemplates->getAlias(), $template_id));
}
$newestMetaTemplate = $this->MetaTemplates->getNewestVersion($oldMetaTemplate, true);
if ($oldMetaTemplate->id == $newestMetaTemplate->id) {
throw new NotFoundException(__('Invalid {0} {1}. Template already the newest version', $this->MetaTemplates->getAlias(), $template_id));
}
if ($this->request->is('post')) {
$result = $this->MetaTemplates->migrateMetafieldsToNewestTemplate($oldMetaTemplate, $newestMetaTemplate);
if ($this->ParamHandler->isRest()) {
return $this->RestResponse->viewData($result, 'json');
} else {
if ($result['success']) {
$message = __('{0} entities updated. {1} entities could not be automatically migrated.', $result['migrated_count'], $result['conflicting_entities']);
} else {
$message = __('{0} entities updated. {1} entities could not be automatically migrated. {2} entities could not be updated due to errors', $result['migrated_count'], $result['conflicting_entities'], $result['migration_errors']);
}
$this->CRUD->setResponseForController('update', $result['success'], $message, $result, $result['migration_errors'], ['redirect' => $this->referer()]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
} else {
$entities = $this->MetaTemplates->getEntitiesHavingMetaFieldsFromTemplate($oldMetaTemplate->id, null);
$conflictingEntities = [];
foreach ($entities as $entity) {
$conflicts = $this->MetaTemplates->getMetaFieldsConflictsUnderTemplate($entity->meta_fields, $newestMetaTemplate);
if (!empty($conflicts)) {
$conflictingEntities[] = $entity;
}
}
if (!$this->ParamHandler->isRest()) {
$this->set('oldMetaTemplate', $oldMetaTemplate);
$this->set('newestMetaTemplate', $newestMetaTemplate);
$this->set('conflictingEntities', $conflictingEntities);
$this->set('entityCount', count($entities));
}
}
}
public function index() public function index()
{ {
$templatesUpdateStatus = $this->MetaTemplates->getUpdateStatusForTemplates(); $templatesUpdateStatus = $this->MetaTemplates->getUpdateStatusForTemplates();

View File

@ -39,8 +39,6 @@ class MetaFieldsTable extends AppTable
->notEmptyString('value') ->notEmptyString('value')
->notEmptyString('meta_template_id') ->notEmptyString('meta_template_id')
->notEmptyString('meta_template_field_id') ->notEmptyString('meta_template_field_id')
// ->requirePresence(['scope', 'field', 'value', 'uuid', 'meta_template_id', 'meta_template_field_id'], 'create');
// ->requirePresence(['scope', 'field', 'value', 'uuid',], 'create');
->notEmptyString('meta_template_directory_id') ->notEmptyString('meta_template_directory_id')
->requirePresence(['scope', 'field', 'value', 'uuid', 'meta_template_directory_id', ], 'create'); ->requirePresence(['scope', 'field', 'value', 'uuid', 'meta_template_directory_id', ], 'create');
@ -53,10 +51,9 @@ class MetaFieldsTable extends AppTable
return $validator; return $validator;
} }
public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options) public function afterMarshal(EventInterface $event, \App\Model\Entity\MetaField $entity, ArrayObject $data, ArrayObject $options) {
{ if (!isset($entity->meta_template_directory_id)) {
if (!isset($data['meta_template_directory_id'])) { $entity->set('meta_template_directory_id', $this->getTemplateDirectoryIdFromMetaTemplate($entity->meta_template_id));
$data['meta_template_directory_id'] = $this->getTemplateDirectoryIdFromMetaTemplate($data['meta_template_id']);
} }
} }
@ -89,6 +86,9 @@ class MetaFieldsTable extends AppTable
if (!empty($metaTemplateField['regex'])) { if (!empty($metaTemplateField['regex'])) {
return $this->isValidRegex($value, $metaTemplateField); return $this->isValidRegex($value, $metaTemplateField);
} }
if (!empty($metaTemplateField['values_list'])) {
return $this->isValidValuesList($value, $metaTemplateField);
}
return true; return true;
} }
@ -118,4 +118,11 @@ class MetaFieldsTable extends AppTable
} }
return true; return true;
} }
public function isValidValuesList($value, $metaTemplateField)
{
$valuesList = $metaTemplateField['values_list'];
return in_array($value, $valuesList);
}
} }

View File

@ -24,8 +24,12 @@ class MetaTemplatesTable extends AppTable
public const UPDATE_STRATEGY_KEEP_BOTH = 'keep_both'; public const UPDATE_STRATEGY_KEEP_BOTH = 'keep_both';
public const UPDATE_STRATEGY_DELETE = 'delete_all'; public const UPDATE_STRATEGY_DELETE = 'delete_all';
public const DEFAULT_STRATEGY = MetaTemplatesTable::UPDATE_STRATEGY_CREATE_NEW; public const DEFAULT_STRATEGY = MetaTemplatesTable::UPDATE_STRATEGY_UPDATE_EXISTING;
public const ALLOWED_STRATEGIES = [MetaTemplatesTable::UPDATE_STRATEGY_CREATE_NEW]; public const ALLOWED_STRATEGIES = [
MetaTemplatesTable::UPDATE_STRATEGY_CREATE_NEW,
MetaTemplatesTable::UPDATE_STRATEGY_UPDATE_EXISTING,
MetaTemplatesTable::UPDATE_STRATEGY_KEEP_BOTH,
];
private $templatesOnDisk = null; private $templatesOnDisk = null;
@ -73,6 +77,41 @@ class MetaTemplatesTable extends AppTable
* @return array The update result containing potential errors and the successes * @return array The update result containing potential errors and the successes
*/ */
public function updateAllTemplates(): array public function updateAllTemplates(): array
{
// Create new template found on the disk
$newTemplateResult = $this->createAllNewTemplates();
$files_processed = $newTemplateResult['files_processed'];
$updatesErrors = $newTemplateResult['update_errors'];
$templatesUpdateStatus = $this->getUpdateStatusForTemplates();
// Update all existing templates
foreach ($templatesUpdateStatus as $uuid => $templateUpdateStatus) {
if (!empty($templateUpdateStatus['existing_template'])) {
$metaTemplate = $templateUpdateStatus['existing_template'];
$result = $this->update($metaTemplate, null);
if ($result['success']) {
$files_processed[] = $metaTemplate->uuid;
}
if (!empty($result['errors'])) {
$updatesErrors[] = $result['errors'];
}
}
}
$results = [
'update_errors' => $updatesErrors,
'files_processed' => $files_processed,
'success' => !empty($files_processed),
];
return $results;
}
/**
* Load the templates stored on the disk update and create them in the database without touching at the existing ones
*
* @return array The update result containing potential errors and the successes
*/
public function createAllNewTemplates(): array
{ {
$updatesErrors = []; $updatesErrors = [];
$files_processed = []; $files_processed = [];
@ -106,7 +145,7 @@ class MetaTemplatesTable extends AppTable
* @param string|null $strategy The strategy to be used when updating templates with conflicts * @param string|null $strategy The strategy to be used when updating templates with conflicts
* @return array The update result containing potential errors and the successes * @return array The update result containing potential errors and the successes
*/ */
public function update($metaTemplate, $strategy = null): array public function update(\App\Model\Entity\MetaTemplate $metaTemplate, $strategy = null): array
{ {
$files_processed = []; $files_processed = [];
$updatesErrors = []; $updatesErrors = [];
@ -114,6 +153,25 @@ class MetaTemplatesTable extends AppTable
$templateStatus = $this->getStatusForMetaTemplate($templateOnDisk, $metaTemplate); $templateStatus = $this->getStatusForMetaTemplate($templateOnDisk, $metaTemplate);
$updateStatus = $this->computeFullUpdateStatusForMetaTemplate($templateStatus, $metaTemplate); $updateStatus = $this->computeFullUpdateStatusForMetaTemplate($templateStatus, $metaTemplate);
$errors = []; $errors = [];
$result = $this->doUpdate($updateStatus, $templateOnDisk, $metaTemplate, $strategy);
if ($result['success']) {
$files_processed[] = $templateOnDisk['uuid'];
}
if (!empty($result['errors'])) {
$updatesErrors[] = $errors;
}
$results = [
'update_errors' => $updatesErrors,
'files_processed' => $files_processed,
'success' => !empty($files_processed),
];
return $results;
}
private function doUpdate(array $updateStatus, array $templateOnDisk, \App\Model\Entity\MetaTemplate $metaTemplate, string $strategy = null): array
{
$errors = [];
$success = false; $success = false;
if ($updateStatus['up-to-date']) { if ($updateStatus['up-to-date']) {
$errors['message'] = __('Meta-template already up-to-date'); $errors['message'] = __('Meta-template already up-to-date');
@ -126,22 +184,17 @@ class MetaTemplatesTable extends AppTable
$errors['message'] = __('Cannot update meta-template, update strategy not allowed'); $errors['message'] = __('Cannot update meta-template, update strategy not allowed');
} else if (!$updateStatus['up-to-date']) { } else if (!$updateStatus['up-to-date']) {
$strategy = is_null($strategy) ? MetaTemplatesTable::DEFAULT_STRATEGY : $strategy; $strategy = is_null($strategy) ? MetaTemplatesTable::DEFAULT_STRATEGY : $strategy;
if ($strategy == MetaTemplatesTable::UPDATE_STRATEGY_UPDATE_EXISTING && !$updateStatus['automatically-updateable']) {
$strategy = MetaTemplatesTable::UPDATE_STRATEGY_KEEP_BOTH;
}
$success = $this->updateMetaTemplateWithStrategyRouter($metaTemplate, $templateOnDisk, $strategy, $errors); $success = $this->updateMetaTemplateWithStrategyRouter($metaTemplate, $templateOnDisk, $strategy, $errors);
} else { } else {
$errors['message'] = __('Could not update. Something went wrong.'); $errors['message'] = __('Could not update. Something went wrong.');
} }
if ($success) { return [
$files_processed[] = $templateOnDisk['uuid']; 'success' => $success,
} 'errors' => $errors,
if (!empty($errors)) {
$updatesErrors[] = $errors;
}
$results = [
'update_errors' => $updatesErrors,
'files_processed' => $files_processed,
'success' => !empty($files_processed),
]; ];
return $results;
} }
/** /**
@ -764,13 +817,27 @@ class MetaTemplatesTable extends AppTable
$errors[] = new UpdateError(false, $metaTemplate); $errors[] = new UpdateError(false, $metaTemplate);
return false; return false;
} }
$metaTemplate = $this->patchEntity($metaTemplate, $template, [ $metaTemplate = $this->patchEntity($metaTemplate, $template);
'associated' => ['MetaTemplateFields'] foreach ($template['metaFields'] as $newMetaField) {
]); $newMetaField['__patched'] = true;
foreach($metaTemplate->meta_template_fields as $i => $oldMetaField) {
if ($oldMetaField->field == $newMetaField['field']) {
$metaTemplate->meta_template_fields[$i] = $this->MetaTemplateFields->patchEntity($oldMetaField, $newMetaField);
continue 2;
}
}
$metaTemplate->meta_template_fields[] = $this->MetaTemplateFields->newEntity($newMetaField);
}
$metaTemplate->setDirty('meta_template_fields', true);
$metaTemplate = $this->save($metaTemplate, [ $metaTemplate = $this->save($metaTemplate, [
'associated' => ['MetaTemplateFields'] 'associated' => ['MetaTemplateFields']
]); ]);
if (!empty($metaTemplate)) { foreach ($metaTemplate->meta_template_fields as $savedMetafield) {
if (empty($savedMetafield['__patched'])) {
$this->MetaTemplateFields->delete($savedMetafield);
}
}
if (empty($metaTemplate)) {
$errors[] = new UpdateError(false, __('Could not save the template.'), $metaTemplate->getErrors()); $errors[] = new UpdateError(false, __('Could not save the template.'), $metaTemplate->getErrors());
return false; return false;
} }
@ -797,8 +864,8 @@ class MetaTemplatesTable extends AppTable
} }
if ($strategy == MetaTemplatesTable::UPDATE_STRATEGY_KEEP_BOTH) { if ($strategy == MetaTemplatesTable::UPDATE_STRATEGY_KEEP_BOTH) {
$result = $this->executeStrategyKeep($template, $metaTemplate); $result = $this->executeStrategyKeep($template, $metaTemplate);
} else if ($strategy == MetaTemplatesTable::UPDATE_STRATEGY_DELETE) { } else if ($strategy == MetaTemplatesTable::UPDATE_STRATEGY_UPDATE_EXISTING) {
$result = $this->executeStrategyDeleteAll($template, $metaTemplate); $result = $this->updateMetaTemplate($metaTemplate, $template);
} else if ($strategy == MetaTemplatesTable::UPDATE_STRATEGY_CREATE_NEW) { } else if ($strategy == MetaTemplatesTable::UPDATE_STRATEGY_CREATE_NEW) {
$result = $this->executeStrategyCreateNew($template, $metaTemplate); $result = $this->executeStrategyCreateNew($template, $metaTemplate);
} else { } else {
@ -830,6 +897,7 @@ class MetaTemplatesTable extends AppTable
$errors[] = new UpdateError(false, __('Could not save the template. A template with this UUID and version already exists'), ['A template with UUID and version already exists']); $errors[] = new UpdateError(false, __('Could not save the template. A template with this UUID and version already exists'), ['A template with UUID and version already exists']);
} }
$conflicts = $this->getMetaTemplateConflictsForMetaTemplate($metaTemplate, $template); $conflicts = $this->getMetaTemplateConflictsForMetaTemplate($metaTemplate, $template);
$conflictingEntities = Hash::combine($conflicts, '{s}.conflictingEntities.{n}.parent_id', '{s}.conflictingEntities.{n}.parent_id');
$blockingConflict = Hash::extract($conflicts, '{s}.conflicts'); $blockingConflict = Hash::extract($conflicts, '{s}.conflicts');
$errors = []; $errors = [];
if (empty($blockingConflict)) { // No conflict, everything can be updated without special care if (empty($blockingConflict)) { // No conflict, everything can be updated without special care
@ -838,7 +906,6 @@ class MetaTemplatesTable extends AppTable
} }
$entities = $this->getEntitiesHavingMetaFieldsFromTemplate($metaTemplate->id, null); $entities = $this->getEntitiesHavingMetaFieldsFromTemplate($metaTemplate->id, null);
$conflictingEntities = [];
foreach ($entities as $entity) { foreach ($entities as $entity) {
$conflicts = $this->getMetaFieldsConflictsUnderTemplate($entity['meta_fields'], $template); $conflicts = $this->getMetaFieldsConflictsUnderTemplate($entity['meta_fields'], $template);
if (!empty($conflicts)) { if (!empty($conflicts)) {
@ -849,8 +916,9 @@ class MetaTemplatesTable extends AppTable
$this->updateMetaTemplate($metaTemplate, $template, $errors); $this->updateMetaTemplate($metaTemplate, $template, $errors);
return !empty($errors) ? $errors[0] : true; return !empty($errors) ? $errors[0] : true;
} }
$template['is_default'] = $metaTemplate['is_default']; $template['is_default'] = $metaTemplate->is_default;
$template['enabled'] = $metaTemplate['enabled']; $template['enabled'] = $metaTemplate->enabled;
$metaTemplate->set('enabled', false);
if ($metaTemplate->is_default) { if ($metaTemplate->is_default) {
$metaTemplate->set('is_default', false); $metaTemplate->set('is_default', false);
$this->save($metaTemplate); $this->save($metaTemplate);
@ -858,13 +926,9 @@ class MetaTemplatesTable extends AppTable
$savedMetaTemplate = null; $savedMetaTemplate = null;
$this->saveNewMetaTemplate($template, $errors, $savedMetaTemplate); $this->saveNewMetaTemplate($template, $errors, $savedMetaTemplate);
if (!empty($savedMetaTemplate)) { if (!empty($savedMetaTemplate)) {
$savedMetaTemplateFieldByName = Hash::combine($savedMetaTemplate['meta_template_fields'], '{n}.field', '{n}');
foreach ($entities as $entity) { foreach ($entities as $entity) {
if (empty($conflictingEntities[$entity->id])) { // conflicting entities remain untouched if (empty($conflictingEntities[$entity->id])) { // conflicting entities remain untouched
foreach ($entity['meta_fields'] as $metaField) { $this->supersedeMetaFieldsWithMetaTemplateField($entity['meta_fields'], $savedMetaTemplate);
$savedMetaTemplateField = $savedMetaTemplateFieldByName[$metaField->field];
$this->supersedeMetaFieldWithMetaTemplateField($metaField, $savedMetaTemplateField);
}
} }
} }
} else { } else {
@ -873,6 +937,44 @@ class MetaTemplatesTable extends AppTable
return true; return true;
} }
public function migrateMetafieldsToNewestTemplate(\App\Model\Entity\MetaTemplate $oldMetaTemplate, \App\Model\Entity\MetaTemplate $newestMetaTemplate): array
{
$result = [
'success' => true,
'migrated_count' => 0,
'conflicting_entities' => 0,
'migration_errors' => 0,
];
$entities = $this->getEntitiesHavingMetaFieldsFromTemplate($oldMetaTemplate->id, null);
if (empty($entities)) {
return $result;
}
$successfullyMigratedEntities = 0;
$migrationErrors = 0;
$conflictingEntities = [];
foreach ($entities as $entity) {
$conflicts = $this->getMetaFieldsConflictsUnderTemplate($entity->meta_fields, $newestMetaTemplate);
if (!empty($conflicts)) {
$conflictingEntities[] = $entity->id;
} else {
$success = $this->supersedeMetaFieldsWithMetaTemplateField($entity->meta_fields, $newestMetaTemplate);
if ($success) {
$successfullyMigratedEntities += 1;
} else {
$migrationErrors += 1;
}
}
}
$result['success'] = $migrationErrors == 0;
$result['migrated_count'] = $successfullyMigratedEntities;
$result['conflicting_entities'] = count($conflictingEntities);
$result['migration_errors'] = $migrationErrors;
return $result;
}
/** /**
* Execute the `delete_all` update strategy by updating the meta-template and deleting all conflicting meta-fields. * Execute the `delete_all` update strategy by updating the meta-template and deleting all conflicting meta-fields.
* Strategy: * Strategy:
@ -941,16 +1043,20 @@ class MetaTemplatesTable extends AppTable
/** /**
* Supersede a meta-fields's meta-template-field with the provided one. * Supersede a meta-fields's meta-template-field with the provided one.
* *
* @param \App\Model\Entity\MetaField $metaField * @param array $metaFields
* @param \App\Model\Entity\MetaTemplateField $savedMetaTemplateField * @param \App\Model\Entity\MetaTemplateField $savedMetaTemplateField
* @return bool True if the replacement was a success, False otherwise * @return bool True if the replacement was a success, False otherwise
*/ */
public function supersedeMetaFieldWithMetaTemplateField(\App\Model\Entity\MetaField $metaField, \App\Model\Entity\MetaTemplateField $savedMetaTemplateField): bool public function supersedeMetaFieldsWithMetaTemplateField(array $metaFields, \App\Model\Entity\MetaTemplate $savedMetaTemplate): bool
{ {
$metaField->set('meta_template_id', $savedMetaTemplateField->meta_template_id); $savedMetaTemplateFieldByName = Hash::combine($savedMetaTemplate['meta_template_fields'], '{n}.field', '{n}');
$metaField->set('meta_template_field_id', $savedMetaTemplateField->id); foreach ($metaFields as $i => $metaField) {
$metaField = $this->MetaTemplateFields->MetaFields->save($metaField); $savedMetaTemplateField = $savedMetaTemplateFieldByName[$metaField->field];
return !empty($metaField); $metaField->set('meta_template_id', $savedMetaTemplateField->meta_template_id);
$metaField->set('meta_template_field_id', $savedMetaTemplateField->id);
}
$entities = $this->MetaTemplateFields->MetaFields->saveMany($metaFields);
return !empty($entities);
} }
/** /**
@ -977,13 +1083,14 @@ class MetaTemplatesTable extends AppTable
$metaTemplateFieldByName[$metaTemplateField['field']] = $this->MetaTemplateFields->newEntity($metaTemplateField); $metaTemplateFieldByName[$metaTemplateField['field']] = $this->MetaTemplateFields->newEntity($metaTemplateField);
} }
foreach ($metaFields as $metaField) { foreach ($metaFields as $metaField) {
if ($existingMetaTemplate && $metaField->meta_template_id != $template->id) { if (empty($metaTemplateFieldByName[$metaField->field])) { // Meta-field was removed from the template
continue; $isValid = false;
} else {
$isValid = $this->MetaTemplateFields->MetaFields->isValidMetaFieldForMetaTemplateField(
$metaField->value,
$metaTemplateFieldByName[$metaField->field]
);
} }
$isValid = $this->MetaTemplateFields->MetaFields->isValidMetaFieldForMetaTemplateField(
$metaField->value,
$metaTemplateFieldByName[$metaField->field]
);
if ($isValid !== true) { if ($isValid !== true) {
$conflicting[] = $metaField; $conflicting[] = $metaField;
} }
@ -1027,27 +1134,12 @@ class MetaTemplatesTable extends AppTable
$result['conflictingEntities'] = Hash::extract($conflictingStatus, '{n}.parent_id'); $result['conflictingEntities'] = Hash::extract($conflictingStatus, '{n}.parent_id');
} }
} }
if (!empty($templateField['regex']) && $templateField['regex'] != $metaTemplateField->regex) {
$entitiesWithMetaFieldQuery = $this->MetaTemplateFields->MetaFields->find(); if (
$entitiesWithMetaFieldQuery (!empty($templateField['regex']) && $templateField['regex'] != $metaTemplateField->regex) ||
->enableHydration(false) !empty($templateField['values_list'])
->select([ ) {
'parent_id', $entities = $this->getEntitiesForMetaTemplateField($scope, $metaTemplateField->id, true);
])
->where([
'meta_template_field_id' => $metaTemplateField->id,
]);
$entitiesTable = $this->getTableForMetaTemplateScope($scope);
$entities = $entitiesTable->find()
->where(['id IN' => $entitiesWithMetaFieldQuery])
->contain([
'MetaFields' => [
'conditions' => [
'MetaFields.meta_template_field_id' => $metaTemplateField->id
]
]
])
->all()->toList();
$conflictingEntities = []; $conflictingEntities = [];
foreach ($entities as $entity) { foreach ($entities as $entity) {
foreach ($entity['meta_fields'] as $metaField) { foreach ($entity['meta_fields'] as $metaField) {
@ -1056,7 +1148,10 @@ class MetaTemplatesTable extends AppTable
$templateField $templateField
); );
if ($isValid !== true) { if ($isValid !== true) {
$conflictingEntities[] = $entity->id; $conflictingEntities[] = [
'parent_id' => $entity->id,
'meta_template_field_id' => $metaTemplateField->id,
];
break; break;
} }
} }
@ -1064,13 +1159,50 @@ class MetaTemplatesTable extends AppTable
if (!empty($conflictingEntities)) { if (!empty($conflictingEntities)) {
$result['automatically-updateable'] = $result['automatically-updateable'] && false; $result['automatically-updateable'] = $result['automatically-updateable'] && false;
$result['conflicts'][] = __('This field is instantiated with values not passing the validation anymore'); $result['conflicts'][] = __('This field is instantiated with values not passing the validation anymore.');
$result['conflictingEntities'] = array_merge($result['conflictingEntities'], $conflictingEntities); $result['conflictingEntities'] = array_merge($result['conflictingEntities'], $conflictingEntities);
} }
} }
return $result; return $result;
} }
/**
* Return all entities having the meta-fields using the provided meta-template-field.
*
* @param string $scope
* @param integer $metaTemplateFieldID The ID of the matching meta-template-field
* @param boolean $includeMatchingMetafields Should the entities also include the matching meta-fields
* @return array
*/
private function getEntitiesForMetaTemplateField(string $scope, int $metaTemplateFieldID, bool $includeMatchingMetafields=true): array
{
$entitiesTable = $this->getTableForMetaTemplateScope($scope);
$entitiesWithMetaFieldQuery = $this->MetaTemplateFields->MetaFields->find();
$entitiesWithMetaFieldQuery
->enableHydration(false)
->select([
'parent_id',
])
->where([
'meta_template_field_id' => $metaTemplateFieldID,
]);
$entitiesQuery = $entitiesTable->find()
->where(['id IN' => $entitiesWithMetaFieldQuery]);
if ($includeMatchingMetafields) {
$entitiesQuery->contain([
'MetaFields' => [
'conditions' => [
'MetaFields.meta_template_field_id' => $metaTemplateFieldID,
]
]
]);
}
return $entitiesQuery->all()->toList();
}
/** /**
* Check the conflict that would be introduced if the metaTemplate would be updated to the provided template * Check the conflict that would be introduced if the metaTemplate would be updated to the provided template
* *
@ -1087,7 +1219,7 @@ class MetaTemplatesTable extends AppTable
$templateMetaFields = $template['metaFields']; $templateMetaFields = $template['metaFields'];
} }
$conflicts = []; $conflicts = [];
$existingMetaTemplateFields = Hash::combine($metaTemplate->toArray(), 'meta_template_fields.{n}.field'); $existingMetaTemplateFields = Hash::combine($metaTemplate->toArray(), 'meta_template_fields.{n}.field', 'meta_template_fields.{n}');
foreach ($templateMetaFields as $newMetaField) { foreach ($templateMetaFields as $newMetaField) {
foreach ($metaTemplate->meta_template_fields as $metaField) { foreach ($metaTemplate->meta_template_fields as $metaField) {
if ($newMetaField['field'] == $metaField->field) { if ($newMetaField['field'] == $metaField->field) {
@ -1103,10 +1235,23 @@ class MetaTemplatesTable extends AppTable
} }
} }
if (!empty($existingMetaTemplateFields)) { if (!empty($existingMetaTemplateFields)) {
foreach ($existingMetaTemplateFields as $field => $tmp) { foreach ($existingMetaTemplateFields as $metaTemplateField) {
$conflicts[$field] = [ $query = $this->MetaTemplateFields->MetaFields->find();
$query
->enableHydration(false)
->select([
'parent_id',
'meta_template_field_id',
])
->where([
'meta_template_field_id' => $metaTemplateField['id'],
])
->group(['parent_id']);
$entityWithMetafieldToBeRemoved = $query->all()->toList();
$conflicts[$metaTemplateField['field']] = [
'automatically-updateable' => false, 'automatically-updateable' => false,
'conflicts' => [__('This field is intended to be removed')], 'conflicts' => [__('This field is intended to be removed')],
'conflictingEntities' => $entityWithMetafieldToBeRemoved,
]; ];
} }
} }
@ -1159,9 +1304,9 @@ class MetaTemplatesTable extends AppTable
$updateStatus['current_version'] = $metaTemplate->version; $updateStatus['current_version'] = $metaTemplate->version;
$updateStatus['next_version'] = $template['version']; $updateStatus['next_version'] = $template['version'];
$updateStatus['new'] = false; $updateStatus['new'] = false;
$updateStatus['automatically-updateable'] = false;
if ($metaTemplate->version >= $template['version']) { if ($metaTemplate->version >= $template['version']) {
$updateStatus['up-to-date'] = true; $updateStatus['up-to-date'] = true;
$updateStatus['automatically-updateable'] = false;
$updateStatus['conflicts'][] = __('Could not update the template. Local version is equal or newer.'); $updateStatus['conflicts'][] = __('Could not update the template. Local version is equal or newer.');
return $updateStatus; return $updateStatus;
} }
@ -1169,6 +1314,17 @@ class MetaTemplatesTable extends AppTable
$conflicts = $this->getMetaTemplateConflictsForMetaTemplate($metaTemplate, $template); $conflicts = $this->getMetaTemplateConflictsForMetaTemplate($metaTemplate, $template);
if (!empty($conflicts)) { if (!empty($conflicts)) {
$updateStatus['conflicts'] = $conflicts; $updateStatus['conflicts'] = $conflicts;
$updateStatus['automatically-updateable'] = false;
$emptySum = 0;
foreach ($conflicts as $fieldname => $fieldStatus) {
if (!empty($fieldStatus['conflictingEntities'])) {
break;
}
$emptySum += 1;
}
if ($emptySum == count($conflicts)) {
$updateStatus['automatically-updateable'] = true;
}
} else { } else {
$updateStatus['automatically-updateable'] = true; $updateStatus['automatically-updateable'] = true;
} }

View File

@ -198,10 +198,22 @@ echo $this->element('genericElements/IndexTable/index_table', [
} }
] ]
], ],
[
'open_modal' => '/metaTemplates/migrateMetafieldsToNewestTemplate/[onclick_params_data_path]',
'modal_params_data_path' => 'id',
'title' => __('Update meta-fields to the newest version of this meta-template'),
'icon' => 'arrow-circle-up',
'variant' => 'success',
'complex_requirement' => [
'function' => function ($row, $options) {
return !empty($row['updateStatus']['to-existing']) && empty($row['updateStatus']['can-be-removed']);
}
]
],
[ [
'open_modal' => '/metaTemplates/delete/[onclick_params_data_path]', 'open_modal' => '/metaTemplates/delete/[onclick_params_data_path]',
'modal_params_data_path' => 'id', 'modal_params_data_path' => 'id',
'title' => __('Get meta-fields that should be moved to the newest version of this meta-template'), 'title' => __('This meta-template doesn\'t have any meta-fields and can be safely removed.'),
'icon' => 'trash', 'icon' => 'trash',
'variant' => 'success', 'variant' => 'success',
'complex_requirement' => [ 'complex_requirement' => [

View File

@ -0,0 +1,102 @@
<?php
use Cake\Utility\Inflector;
use Cake\Routing\Router;
$urlNewestMetaTemplate = Router::url([
'controller' => 'metaTemplates',
'action' => 'view',
$newestMetaTemplate->id
]);
$bodyHtml = '';
$bodyHtml .= sprintf('<div><span>%s: </span><span class="font-monospace">%s</span></div>', __('Current version'), h($oldMetaTemplate->version));
$bodyHtml .= sprintf('<div><span>%s: </span><a href="%s" target="_blank" class="font-monospac">%s</a></div>', __('Newest version'), $urlNewestMetaTemplate, h($newestMetaTemplate->version));
$bodyHtml .= sprintf('<h4 class="my-2">%s</h4>', __('{0} Entities with meta-fields for the meta-template version <span class="font-monospace">{1}</span>', h($entityCount), h($oldMetaTemplate->version)));
// debug($conflictingEntities);
if (empty($conflictingEntities)) {
$bodyHtml .= $this->Bootstrap->alert([
'text' => __('All entities can updated automatically', count($conflictingEntities)),
'variant' => 'success',
'dismissible' => false,
]);
} else {
$bodyHtml .= $this->Bootstrap->alert([
'html' => sprintf(
'<ul>%s%s</ul>',
$this->Bootstrap->node('li', [], __('{0} entities can be updated automatically', $entityCount - count($conflictingEntities))),
$this->Bootstrap->node('li', [], __('{0} entities cannot be updated automatically and require manual migration', count($conflictingEntities)))
),
'variant' => 'warning',
'dismissible' => false,
]);
$bodyHtml .= '<ul>';
foreach ($conflictingEntities as $entity) {
$url = Router::url([
'controller' => 'metaTemplates',
'action' => 'migrateOldMetaTemplateToNewestVersionForEntity',
$oldMetaTemplate->id,
$entity->id,
]);
$bodyHtml .= sprintf(
'<li><a href="%s" target="_blank">%s</a> <span class="fw-light">%s<span></li>',
$url,
__('{0}::{1}', h(Inflector::humanize($oldMetaTemplate->scope)), $entity->id),
__('has {0} meta-fields to update', count($entity->meta_fields))
);
}
if (count($conflictingEntities) > 10) {
$bodyHtml .= sprintf('<li class="list-inline-item fw-light fs-7">%s</li>', __('{0} more entities', h(10 - count($conflictingEntities))));
}
$bodyHtml .= '</ul>';
}
$form = sprintf(
'<div class="d-none hidden-form-container">%s%s</div>',
$this->Form->create(null, [
'url' => [
'controller' => 'MetaTemplates',
'action' => 'migrateMetafieldsToNewestTemplate',
$oldMetaTemplate->id,
]
]),
$this->Form->end()
);
$bodyHtml .= $form;
$title = __('{0} has a new meta-template and meta-fields to be updated', sprintf('<i class="me-1">%s</i>', h($oldMetaTemplate->name)));
if (!empty($ajax)) {
echo $this->Bootstrap->modal([
'titleHtml' => $title,
'bodyHtml' => $bodyHtml,
'size' => 'lg',
'type' => 'confirm',
'confirmButton' => [
'text' => __('Migrate meta-fields'),
'variant' => 'success',
],
'confirmFunction' => 'migrateMetafieldsToNewestTemplate',
]);
} else {
echo $this->Bootstrap->node('h1', [], $title);
echo $bodyHtml;
echo $this->Bootstrap->button([
'text' => __('Migrate meta-fields'),
'variant' => 'success',
'onclick' => '$(".hidden-form-container form").submit()',
]);
}
?>
<script>
function migrateMetafieldsToNewestTemplate(modalObject, tmpApi) {
const $form = modalObject.$modal.find('form')
return tmpApi.postForm($form[0]).catch((errors) => {
const formHelper = new FormValidationHelper($form[0])
const errorHTMLNode = formHelper.buildValidationMessageNode(errors, true)
modalObject.$modal.find('div.form-error-container').append(errorHTMLNode)
return errors
})
}
</script>

View File

@ -18,6 +18,11 @@ if ($updateStatus['up-to-date']) {
'html' => __('This meta-template can be updated to version {0} (current: {1}).', sprintf('<strong>%s</strong>', h($templateOnDisk['version'])), h($metaTemplate->version)), 'html' => __('This meta-template can be updated to version {0} (current: {1}).', sprintf('<strong>%s</strong>', h($templateOnDisk['version'])), h($metaTemplate->version)),
'dismissible' => false, 'dismissible' => false,
]); ]);
$bodyHtml .= $this->Bootstrap->alert([
'variant' => 'success',
'text' => __('All meta-fields will be migrated to the newest version.'),
'dismissible' => false,
]);
$form = $this->element('genericElements/Form/genericForm', [ $form = $this->element('genericElements/Form/genericForm', [
'entity' => null, 'entity' => null,
'ajax' => false, 'ajax' => false,
@ -28,9 +33,9 @@ if ($updateStatus['up-to-date']) {
[ [
'field' => 'update_strategy', 'field' => 'update_strategy',
'type' => 'checkbox', 'type' => 'checkbox',
'value' => MetaTemplatesTable::UPDATE_STRATEGY_CREATE_NEW, 'value' => MetaTemplatesTable::UPDATE_STRATEGY_UPDATE_EXISTING,
'checked' => true, 'checked' => true,
] ],
], ],
'submit' => [ 'submit' => [
'action' => $this->request->getParam('action') 'action' => $this->request->getParam('action')
@ -42,7 +47,7 @@ if ($updateStatus['up-to-date']) {
$modalSize = 'xl'; $modalSize = 'xl';
$bodyHtml .= $this->Bootstrap->alert([ $bodyHtml .= $this->Bootstrap->alert([
'variant' => 'warning', 'variant' => 'warning',
'text' => __('Updating to version {0} cannot be done automatically as it introduces some conflicts.', h($templateOnDisk['version'])), 'html' => __('Updating to version {0} cannot be done automatically as it introduces some conflicts.', sprintf('<strong>%s</strong>', h($templateOnDisk['version']))),
'dismissible' => false, 'dismissible' => false,
]); ]);
$conflictTable = $this->element('MetaTemplates/conflictTable', [ $conflictTable = $this->element('MetaTemplates/conflictTable', [
@ -50,8 +55,11 @@ if ($updateStatus['up-to-date']) {
'metaTemplate' => $metaTemplate, 'metaTemplate' => $metaTemplate,
'templateOnDisk' => $templateOnDisk, 'templateOnDisk' => $templateOnDisk,
]); ]);
$conflictCount = array_reduce($templateStatus['conflicts'], function ($carry, $conflict) {
return $carry + count($conflict['conflictingEntities']);
}, 0);
$bodyHtml .= $this->Bootstrap->collapse([ $bodyHtml .= $this->Bootstrap->collapse([
'text' => __('View conflicts'), 'text' => __('View conflicts ({0})', $conflictCount),
'open' => false 'open' => false
], $conflictTable); ], $conflictTable);
$bodyHtml .= $this->element('MetaTemplates/conflictResolution', [ $bodyHtml .= $this->element('MetaTemplates/conflictResolution', [

View File

@ -40,12 +40,19 @@ foreach ($templatesUpdateStatus as $uuid => $status) {
if (!empty($status['new'])) { if (!empty($status['new'])) {
$tableHtml .= sprintf('<td>%s</td>', __('N/A')); $tableHtml .= sprintf('<td>%s</td>', __('N/A'));
} else { } else {
$tableHtml .= sprintf( if ($status['current_version'] == $status['next_version']) {
'<td>%s %s %s</td>', $tableHtml .= sprintf(
h($status['current_version']), '<td>%s</td>',
$this->Bootstrap->icon('arrow-right', ['class' => 'fs-8']), h($status['current_version'])
h($status['next_version']) );
); } else {
$tableHtml .= sprintf(
'<td>%s %s %s</td>',
h($status['current_version']),
$this->Bootstrap->icon('arrow-right', ['class' => 'fs-8']),
h($status['next_version'])
);
}
} }
if (!empty($status['new'])) { if (!empty($status['new'])) {
$numberOfUpdates += 1; $numberOfUpdates += 1;
@ -60,6 +67,8 @@ foreach ($templatesUpdateStatus as $uuid => $status) {
} }
if (!empty($status['new'])) { if (!empty($status['new'])) {
$tableHtml .= sprintf('<td>%s</td>', __('N/A')); $tableHtml .= sprintf('<td>%s</td>', __('N/A'));
} elseif (!empty($status['up-to-date'])) {
$tableHtml .= sprintf('<td>%s</td>', __('N/A'));
} else { } else {
$tableHtml .= sprintf('<td>%s</td>', !empty($status['conflicts']) ? $this->Bootstrap->icon('check') : $this->Bootstrap->icon('times')); $tableHtml .= sprintf('<td>%s</td>', !empty($status['conflicts']) ? $this->Bootstrap->icon('check') : $this->Bootstrap->icon('times'));
} }

View File

@ -1,9 +1,10 @@
<?php <?php
$create_new_allowed = true; $create_new_allowed = true;
$keep_all_allowed = false; $update_allowed = true;
$delete_all_allowed = false; $delete_all_allowed = false;
$totalAllowed = $create_new_allowed + $keep_all_allowed + $delete_all_allowed; $totalAllowed = $create_new_allowed + $update_allowed + $delete_all_allowed;
$maxWidth = 99 - ($create_new_allowed ? 33 : 0) - ($keep_all_allowed ? 33 : 0) - ($delete_all_allowed ? 33 : 0); $maxWidth = 99 - ($create_new_allowed ? 33 : 0) - ($update_allowed ? 33 : 0) - ($delete_all_allowed ? 33 : 0);
$defaultStrategy = 'update_existing';
$form = $this->element('genericElements/Form/genericForm', [ $form = $this->element('genericElements/Form/genericForm', [
'entity' => null, 'entity' => null,
@ -17,7 +18,7 @@ $form = $this->element('genericElements/Form/genericForm', [
'type' => 'radio', 'type' => 'radio',
'options' => [ 'options' => [
['value' => 'create_new', 'text' => 'create_new', 'id' => 'radio_create_new'], ['value' => 'create_new', 'text' => 'create_new', 'id' => 'radio_create_new'],
['value' => 'keep_both', 'text' => 'keep_both', 'id' => 'radio_keep_both'], ['value' => 'update_existing', 'text' => 'update', 'id' => 'radio_update'],
['value' => 'delete', 'text' => 'delete', 'id' => 'radio_delete'], ['value' => 'delete', 'text' => 'delete', 'id' => 'radio_delete'],
], ],
] ]
@ -33,10 +34,13 @@ $form = $this->element('genericElements/Form/genericForm', [
<div class="mt-3 d-flex justify-content-center"> <div class="mt-3 d-flex justify-content-center">
<div class="btn-group justify-content-center" role="group" aria-label="Basic radio toggle button group"> <div class="btn-group justify-content-center" role="group" aria-label="Basic radio toggle button group">
<?php if ($create_new_allowed) : ?> <?php if ($create_new_allowed) : ?>
<input type="radio" class="btn-check" name="btnradio" id="btnradio1" autocomplete="off" value="create_new" checked> <input type="radio" class="btn-check" name="btnradio" id="btnradio1" autocomplete="off" value="create_new" <?= $defaultStrategy == 'create_new' ? 'checked' : '' ?>>
<label class="btn btn-outline-primary mw-<?= $maxWidth ?>" for="btnradio1"> <label class="btn btn-outline-warning mw-<?= $maxWidth ?>" for="btnradio1">
<div> <div>
<h5 class="mb-3"><?= __('Create new template') ?></h5> <h5 class="mb-3">
<?= $defaultStrategy == 'create_new' ? $this->Bootstrap->badge(['text' => 'recommended', 'variant' => 'success', 'class' => ['mb-3', 'fs-8']]) : '' ?>
<?= __('Create new template') ?>
</h5>
<ul class="text-start fs-7"> <ul class="text-start fs-7">
<li><?= __('A new meta-template will be created and made default.') ?></li> <li><?= __('A new meta-template will be created and made default.') ?></li>
<li><?= __('The old meta-template will remain untouched.') ?></li> <li><?= __('The old meta-template will remain untouched.') ?></li>
@ -46,14 +50,17 @@ $form = $this->element('genericElements/Form/genericForm', [
</label> </label>
<?php endif; ?> <?php endif; ?>
<?php if ($keep_all_allowed) : ?> <?php if ($update_allowed) : ?>
<input type="radio" class="btn-check" name="btnradio" id="btnradio2" autocomplete="off" value="keep_both"> <input type="radio" class="btn-check" name="btnradio" id="btnradio2" autocomplete="off" value="update_existing" <?= $defaultStrategy == 'update_existing' ? 'checked' : '' ?>>
<label class="btn btn-outline-warning mw-<?= $maxWidth ?>" for="btnradio2"> <label class="btn btn-outline-primary mw-<?= $maxWidth ?>" for="btnradio2">
<div> <div>
<h5 class="mb-3"><?= __('Update non-conflicting') ?></h5> <div><?= $defaultStrategy == 'update_existing' ? $this->Bootstrap->badge(['text' => 'recommended', 'variant' => 'success', 'class' => ['mb-3']]) : '' ?></div>
<h5 class="mb-3">
<?= __('Update non-conflicting') ?>
</h5>
<ul class="text-start fs-7"> <ul class="text-start fs-7">
<li><?= __('Meta-fields not having conflicts will be migrated to the new meta-template.') ?></li> <li><?= __('Entities not having conflicts will have their meta-fields migrated to the new meta-template.') ?></li>
<li><?= __('Meta-fields having a conflicts will stay on their current meta-template.') ?></li> <li><?= __('Entities having a conflicts will stay on their current meta-template.') ?></li>
<li><?= __('Conflicts can be taken care of manually via the UI.') ?></li> <li><?= __('Conflicts can be taken care of manually via the UI.') ?></li>
</ul> </ul>
</div> </div>
@ -61,10 +68,13 @@ $form = $this->element('genericElements/Form/genericForm', [
<?php endif; ?> <?php endif; ?>
<?php if ($delete_all_allowed) : ?> <?php if ($delete_all_allowed) : ?>
<input type="radio" class="btn-check" name="btnradio" id="btnradio3" autocomplete="off" value="delete"> <input type="radio" class="btn-check" name="btnradio" id="btnradio3" autocomplete="off" value="delete" <?= $defaultStrategy == 'delete' ? 'checked' : '' ?>>
<label class="btn btn-outline-danger mw-<?= $maxWidth ?>" for="btnradio3"> <label class="btn btn-outline-danger mw-<?= $maxWidth ?>" for="btnradio3">
<div> <div>
<h5 class="mb-3"><?= __('Delete conflicting fields') ?></h5> <h5 class="mb-3">
<?= $defaultStrategy == 'delete' ? $this->Bootstrap->badge(['text' => 'recommended', 'variant' => 'success', 'class' => ['mb-3', 'fs-8']]) : '' ?>
<?= __('Delete conflicting fields') ?>
</h5>
<ul class="text-start fs-7"> <ul class="text-start fs-7">
<li><?= __('Meta-fields not satisfying the new meta-template definition will be deleted.') ?></li> <li><?= __('Meta-fields not satisfying the new meta-template definition will be deleted.') ?></li>
<li><?= __('All other meta-fields will be upgraded to the new meta-template.') ?></li> <li><?= __('All other meta-fields will be upgraded to the new meta-template.') ?></li>
@ -84,18 +94,18 @@ $form = $this->element('genericElements/Form/genericForm', [
(function() { (function() {
const $form = $('.conflict-resolution-form-container form') const $form = $('.conflict-resolution-form-container form')
const $create = $form.find('input#radio_create_new') const $create = $form.find('input#radio_create_new')
const $keep = $form.find('input#radio_keep_both') const $keep = $form.find('input#radio_update')
const $delete = $form.find('input#radio_delete') const $delete = $form.find('input#radio_delete')
$(document).ready(function() { $(document).ready(function() {
$('.conflict-resolution-picker').find('input[type="radio"]').change(function() { $('.conflict-resolution-picker').find('input[type="radio"]').change(function() {
updateSelected($(this).val()) updateSelected($(this).val())
}) })
updateSelected('create_new') updateSelected('<?= $defaultStrategy ?>')
}) })
function updateSelected(choice) { function updateSelected(choice) {
if (choice == 'keep_both') { if (choice == 'update_existing') {
$keep.prop('checked', true) $keep.prop('checked', true)
} else if (choice == 'delete') { } else if (choice == 'delete') {
$delete.prop('checked', true) $delete.prop('checked', true)

View File

@ -21,7 +21,7 @@ use Cake\Routing\Router;
</td> </td>
<td> <td>
<?php <?php
foreach ($fieldConflict['conflictingEntities'] as $i => $id) { foreach ($fieldConflict['conflictingEntities'] as $i => $metaEntity) {
if ($i > 0) { if ($i > 0) {
echo ', '; echo ', ';
} }
@ -32,9 +32,9 @@ use Cake\Routing\Router;
$url = Router::url([ $url = Router::url([
'controller' => Inflector::pluralize($templateStatus['existing_template']->scope), 'controller' => Inflector::pluralize($templateStatus['existing_template']->scope),
'action' => 'view', 'action' => 'view',
$id $metaEntity['parent_id']
]); ]);
echo sprintf('<a href="%s" target="_blank">%s</a>', $url, __('{0} #{1}', h(Inflector::humanize($templateStatus['existing_template']->scope)), h($id))); echo sprintf('<a href="%s" target="_blank">%s</a>', $url, __('{0} #{1}', h(Inflector::humanize($templateStatus['existing_template']->scope)), h($metaEntity['parent_id'])));
} }
?> ?>
</td> </td>