diff --git a/Module.php b/Module.php index f6b0c49f..02fbcd98 100644 --- a/Module.php +++ b/Module.php @@ -21,26 +21,34 @@ public function install(ServiceLocatorInterface $serviceLocator) { $connection = $serviceLocator->get('Omeka\Connection'); $sql = <<<'SQL' +CREATE TABLE csvimport_mapping_model ( + id INT AUTO_INCREMENT NOT NULL, + name VARCHAR(255) NOT NULL, + created DATETIME NOT NULL, + mapping LONGTEXT NOT NULL COMMENT '(DC2Type:json)', + PRIMARY KEY(id), + UNIQUE INDEX UNIQ_B0D508235E237E06 (name)) +DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB; CREATE TABLE csvimport_import ( - id INT AUTO_INCREMENT NOT NULL, - job_id INT NOT NULL, - undo_job_id INT DEFAULT NULL, - comment VARCHAR(255) DEFAULT NULL, - resource_type VARCHAR(255) NOT NULL, - has_err TINYINT(1) NOT NULL, - stats LONGTEXT NOT NULL COMMENT '(DC2Type:json_array)', - UNIQUE INDEX UNIQ_17B50881BE04EA9 (job_id), - UNIQUE INDEX UNIQ_17B508814C276F75 (undo_job_id), - PRIMARY KEY(id) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB; + id INT AUTO_INCREMENT NOT NULL, + job_id INT NOT NULL, + undo_job_id INT DEFAULT NULL, + comment VARCHAR(255) DEFAULT NULL, + resource_type VARCHAR(255) NOT NULL, + has_err TINYINT(1) NOT NULL, + stats LONGTEXT NOT NULL COMMENT '(DC2Type:json_array)', + UNIQUE INDEX UNIQ_17B50881BE04EA9 (job_id), + UNIQUE INDEX UNIQ_17B508814C276F75 (undo_job_id), + PRIMARY KEY(id)) +DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB; CREATE TABLE csvimport_entity ( - id INT AUTO_INCREMENT NOT NULL, - job_id INT NOT NULL, - entity_id INT NOT NULL, - resource_type VARCHAR(255) NOT NULL, - INDEX IDX_84D382F4BE04EA9 (job_id), - PRIMARY KEY(id) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB; + id INT AUTO_INCREMENT NOT NULL, + job_id INT NOT NULL, + entity_id INT NOT NULL, + resource_type VARCHAR(255) NOT NULL, + INDEX IDX_84D382F4BE04EA9 (job_id), + PRIMARY KEY(id)) +DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB; ALTER TABLE csvimport_import ADD CONSTRAINT FK_17B50881BE04EA9 FOREIGN KEY (job_id) REFERENCES job (id); ALTER TABLE csvimport_import ADD CONSTRAINT FK_17B508814C276F75 FOREIGN KEY (undo_job_id) REFERENCES job (id); ALTER TABLE csvimport_entity ADD CONSTRAINT FK_84D382F4BE04EA9 FOREIGN KEY (job_id) REFERENCES job (id); @@ -60,6 +68,7 @@ public function uninstall(ServiceLocatorInterface $serviceLocator) ALTER TABLE csvimport_import DROP FOREIGN KEY FK_17B50881BE04EA9; DROP TABLE IF EXISTS csvimport_entity; DROP TABLE IF EXISTS csvimport_import; +DROP TABLE IF EXISTS csvimport_mapping_model; SQL; $sqls = array_filter(array_map('trim', explode(';', $sql))); foreach ($sqls as $sql) { @@ -76,6 +85,24 @@ public function upgrade($oldVersion, $newVersion, ServiceLocatorInterface $servi ALTER TABLE csvimport_import ADD stats LONGTEXT NOT NULL COMMENT '(DC2Type:json_array)'; UPDATE csvimport_import SET stats = CONCAT('{"processed":{"', resource_type, '":', added_count, '}}'); ALTER TABLE csvimport_import DROP added_count; +SQL; + $sqls = array_filter(array_map('trim', explode(';', $sql))); + foreach ($sqls as $sql) { + $connection->exec($sql); + } + } + + if (version_compare($oldVersion, '2.7.0', '<')) { + $connection = $serviceLocator->get('Omeka\Connection'); + $sql = <<<'SQL' +CREATE TABLE csvimport_mapping_model ( + id INT AUTO_INCREMENT NOT NULL, + name VARCHAR(255) NOT NULL, + created DATETIME NOT NULL, + mapping LONGTEXT NOT NULL COMMENT '(DC2Type:json)', + PRIMARY KEY(id), + UNIQUE INDEX UNIQ_B0D508235E237E06 (name)) +DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB; SQL; $sqls = array_filter(array_map('trim', explode(';', $sql))); foreach ($sqls as $sql) { diff --git a/asset/js/csvimport.js b/asset/js/csvimport.js index 86f0ba62..74bb8119 100644 --- a/asset/js/csvimport.js +++ b/asset/js/csvimport.js @@ -2,7 +2,6 @@ * Initially based on Omeka S omeka2importer.js and resource-core.js. */ (function ($) { - $(document).ready(function() { /* * Init. @@ -14,12 +13,85 @@ var defaultSidebarHtml = null; var actionsHtml = ''; var batchEditCheckboxes = $('.column-select, .select-all'); var batchEditButton = $('#batch-edit-options'); + $(document).on('mapping.updated', newTable); + + newTable(); + + function newTable() { + resetActiveColumns(); + + /* + * Batch edit options. + */ + + $('.batch-edit input[type="checkbox"], .batch-edit .select-all').change(function() { + if ($('.column-select:checked').length > 0) { + batchEditButton.removeClass('inactive').addClass('active sidebar-content'); + } else { + batchEditButton.addClass('inactive').removeClass('active sidebar-content'); + } + }); + + /* + * Sidebar chooser (buttons on each mappable element). + */ + + $('.column-header + .actions a').on('click', function(e) { + e.preventDefault(); + if (activeElement !== null) { + activeElement.removeClass('active'); + } + if ($('.column-select:checked').length > 0) { + resetActiveColumns(); + } + activeElement = $(e.target).closest('tr.mappable'); + activeElement.addClass('active'); + + var actionElement = $(this); + $('.sidebar-chooser li').removeClass('active'); + actionElement.parent().addClass('active'); + var target = actionElement.data('sidebar-selector'); + + var sidebar = $(target); + if (!sidebar.hasClass('active') ) { + defaultSidebarHtml = sidebar.html(); + } + var columnName = activeElement.data('column'); + if (sidebar.find('.column-name').length > 0) { + $('.column-name').text(columnName); + } else { + sidebar.find('h3').append(' ' + columnName + ''); + } + + var currentSidebar = $('.sidebar.active'); + if (currentSidebar.attr('id') != target) { + currentSidebar.removeClass('active'); + sidebar.html(defaultSidebarHtml); + rebindInputs(sidebar); + } + + Omeka.openSidebar(sidebar); + populateSidebar(); + }); + + /* + * Actions on mapped columns. + */ + + // Remove mapping. + $('.section').on('click', 'a.remove-mapping', function(e) { + e.preventDefault(); + e.stopPropagation(); + $(this).parents('li.mapping').remove(); + }); + }; + /* * Rebinding chosen selects and property selector after sidebar hydration. */ @@ -60,47 +132,6 @@ }); } - /* - * Sidebar chooser (buttons on each mappable element). - */ - - $('.column-header + .actions a').on('click', function(e) { - e.preventDefault(); - if (activeElement !== null) { - activeElement.removeClass('active'); - } - if ($('.column-select:checked').length > 0) { - resetActiveColumns(); - } - activeElement = $(e.target).closest('tr.mappable'); - activeElement.addClass('active'); - - var actionElement = $(this); - $('.sidebar-chooser li').removeClass('active'); - actionElement.parent().addClass('active'); - var target = actionElement.data('sidebar-selector'); - - var sidebar = $(target); - if (!sidebar.hasClass('active') ) { - defaultSidebarHtml = sidebar.html(); - } - var columnName = activeElement.data('column'); - if (sidebar.find('.column-name').length > 0) { - $('.column-name').text(columnName); - } else { - sidebar.find('h3').append(' ' + columnName + ''); - } - - var currentSidebar = $('.sidebar.active'); - if (currentSidebar.attr('id') != target) { - currentSidebar.removeClass('active'); - sidebar.html(defaultSidebarHtml); - rebindInputs(sidebar); - } - - Omeka.openSidebar(sidebar); - populateSidebar(); - }); function populateSidebar() { $('.active.element .options :input:not(:disabled)').each(function() { @@ -119,17 +150,6 @@ }); } - /* - * Batch edit options. - */ - - $('.batch-edit input[type="checkbox"], .batch-edit .select-all').change(function() { - if ($('.column-select:checked').length > 0) { - batchEditButton.removeClass('inactive').addClass('active sidebar-content'); - } else { - batchEditButton.addClass('inactive').removeClass('active sidebar-content'); - } - }); $(document).on('click', '#batch-edit-options.active', function() { defaultSidebarHtml = $('#column-options').html(); @@ -341,17 +361,6 @@ batchEditButton.removeClass('active sidebar-content').addClass('inactive'); } - /* - * Actions on mapped columns. - */ - - // Remove mapping. - $('.section').on('click', 'a.remove-mapping', function(e) { - e.preventDefault(); - e.stopPropagation(); - $(this).parents('li.mapping').remove(); - }); - function applyMappings(flagName, flagValue, flagLiClass, flagLabel) { var hasFlag = activeElement.find('ul.mappings li.' + flagLiClass); if (flagValue == 'default') { diff --git a/asset/js/mappingselect.js b/asset/js/mappingselect.js new file mode 100644 index 00000000..916a6ffa --- /dev/null +++ b/asset/js/mappingselect.js @@ -0,0 +1,33 @@ +(function ($) { +$(document).ready(function() { + $(document).on('click', '#mapping-select-button', function(e) { + e.preventDefault(); // Stop normal form submission + + var form = $(this).closest('form'); + var formData = form.serialize(); // Collect all form inputs + + var sidebar = $(this).parents('.sidebar'); + Omeka.closeSidebar(sidebar); + + $.ajax({ + url: form.attr('action'), + method: 'POST', + data: formData, + success: function(response, status, xhr) { + if (xhr.status === 200 && response.trim().length > 0) { + var newTable = $('
').html(response).find('table'); // parse HTML + var currentTable = $('table'); + + // Replace only the inside of the table + currentTable.replaceWith(newTable); + + $(document).trigger("enhance.tablesaw"); + $(document).trigger('mapping.updated'); + } else { + console.error('Received an empty or invalid response from the server.'); + } + } + }); +}); +}); +})(jQuery) diff --git a/config/module.config.php b/config/module.config.php index e67a0e7b..9c85cf83 100644 --- a/config/module.config.php +++ b/config/module.config.php @@ -24,20 +24,29 @@ ], ], 'form_elements' => [ + 'invokables' => [ + 'CSVImport\Form\MappingModelEditForm' => Form\MappingModelEditForm::class, + 'CSVImport\Form\MappingModelSelectForm' => Form\MappingModelSelectForm::class, + ], 'factories' => [ 'CSVImport\Form\ImportForm' => Service\Form\ImportFormFactory::class, - 'CSVImport\Form\MappingForm' => Service\Form\MappingFormFactory::class, + 'CSVImport\Form\MappingModelForm' => Service\Form\MappingModelFormFactory::class, + 'CSVImport\Form\Element\MappingModelSelect' => Service\Form\Element\MappingModelSelectFactory::class, + 'CSVImport\Form\MappingModelSaveForm' => Service\Form\MappingModelSaveFormFactory::class, ], ], 'controllers' => [ 'factories' => [ 'CSVImport\Controller\Index' => Service\Controller\IndexControllerFactory::class, + 'CSVImport\Controller\Admin\MappingModel' => Service\Controller\Admin\MappingModelControllerFactory::class, ], ], 'controller_plugins' => [ 'factories' => [ 'automapHeadersToMetadata' => Service\ControllerPlugin\AutomapHeadersToMetadataFactory::class, 'findResourcesFromIdentifiers' => Service\ControllerPlugin\FindResourcesFromIdentifiersFactory::class, + 'loadMappingModel' => Service\ControllerPlugin\LoadMappingModelFactory::class, + 'saveMappingModel' => Service\ControllerPlugin\SaveMappingModelFactory::class, ], 'aliases' => [ 'findResourceFromIdentifier' => 'findResourcesFromIdentifiers', @@ -47,6 +56,7 @@ 'invokables' => [ 'csvimport_entities' => Api\Adapter\EntityAdapter::class, 'csvimport_imports' => Api\Adapter\ImportAdapter::class, + 'csvimport_mapping_models' => Api\Adapter\MappingModelAdapter::class, ], ], 'service_manager' => [ @@ -92,6 +102,35 @@ ], ], ], + 'mapping-model' => [ + 'type' => 'Segment', + 'options' => [ + 'route' => '/mapping-model[/:action]', + 'constraints' => [ + 'action' => '[a-zA-Z][a-zA-Z0-9_-]*', + ], + 'defaults' => [ + '__NAMESPACE__' => 'CSVImport\Controller\Admin', + 'controller' => 'MappingModel', + 'action' => 'browse', + ], + ], + ], + 'mapping-model-id' => [ + 'type' => 'Segment', + 'options' => [ + 'route' => '/mapping-model/:id[/:action]', + 'constraints' => [ + 'action' => '[a-zA-Z][a-zA-Z0-9_-]*', + 'id' => '\d+', + ], + 'defaults' => [ + '__NAMESPACE__' => 'CSVImport\Controller\Admin', + 'controller' => 'MappingModel', + 'action' => 'show', + ], + ], + ], ], ], ], @@ -123,6 +162,10 @@ 'action' => 'past-imports', 'resource' => 'CSVImport\Controller\Index', ], + [ + 'label' => 'Mapping Models', // @translate + 'route' => 'admin/csvimport/mapping-model', + ], ], ], ], @@ -138,7 +181,7 @@ ], ], 'js_translate_strings' => [ - 'Remove mapping', // @translate + 'Remove mapping model', // @translate ], 'csv_import' => [ 'sources' => [ diff --git a/config/module.ini b/config/module.ini index 4a035b55..29922adc 100644 --- a/config/module.ini +++ b/config/module.ini @@ -8,5 +8,5 @@ author_link = "https://omeka.org/" module_link = "https://omeka.org/s/docs/user-manual/modules/csvimport/" support_link = "https://forum.omeka.org/c/omeka-s/modules" configurable = false -version = "2.6.2" +version = "3.0.0" omeka_version_constraint = "^4.0.0" diff --git a/data/doctrine-proxies/__CG__CSVImportEntityCSVImportEntity.php b/data/doctrine-proxies/__CG__CSVImportEntityCSVImportEntity.php index 7cbf4b84..85e4b184 100644 --- a/data/doctrine-proxies/__CG__CSVImportEntityCSVImportEntity.php +++ b/data/doctrine-proxies/__CG__CSVImportEntityCSVImportEntity.php @@ -2,6 +2,7 @@ namespace DoctrineProxies\__CG__\CSVImport\Entity; + /** * DO NOT EDIT THIS FILE - IT WAS CREATED BY DOCTRINE'S PROXY GENERATOR */ @@ -12,39 +13,41 @@ class CSVImportEntity extends \CSVImport\Entity\CSVImportEntity implements \Doct * three parameters, being respectively the proxy object to be initialized, the method that triggered the * initialization process and an array of ordered parameters that were passed to that method. * - * @see \Doctrine\Common\Persistence\Proxy::__setInitializer + * @see \Doctrine\Common\Proxy\Proxy::__setInitializer */ public $__initializer__; /** * @var \Closure the callback responsible of loading properties that need to be copied in the cloned object * - * @see \Doctrine\Common\Persistence\Proxy::__setCloner + * @see \Doctrine\Common\Proxy\Proxy::__setCloner */ public $__cloner__; /** * @var boolean flag indicating if this object was already initialized * - * @see \Doctrine\Common\Persistence\Proxy::__isInitialized + * @see \Doctrine\Persistence\Proxy::__isInitialized */ public $__isInitialized__ = false; /** - * @var array properties to be lazy loaded, with keys being the property - * names and values being their default values + * @var array properties to be lazy loaded, indexed by property name + */ + public static $lazyPropertiesNames = array ( +); + + /** + * @var array default values of properties to be lazy loaded, with keys being the property names * - * @see \Doctrine\Common\Persistence\Proxy::__getLazyProperties + * @see \Doctrine\Common\Proxy\Proxy::__getLazyProperties */ - public static $lazyPropertiesDefaults = []; + public static $lazyPropertiesDefaults = array ( +); - /** - * @param \Closure $initializer - * @param \Closure $cloner - */ - public function __construct($initializer = null, $cloner = null) + public function __construct(?\Closure $initializer = null, ?\Closure $cloner = null) { $this->__initializer__ = $initializer; @@ -82,7 +85,7 @@ public function __wakeup() $existingProperties = get_object_vars($proxy); - foreach ($proxy->__getLazyProperties() as $property => $defaultValue) { + foreach ($proxy::$lazyPropertiesDefaults as $property => $defaultValue) { if ( ! array_key_exists($property, $existingProperties)) { $proxy->$property = $defaultValue; } @@ -103,7 +106,7 @@ public function __clone() /** * Forces initialization of the proxy */ - public function __load() + public function __load(): void { $this->__initializer__ && $this->__initializer__->__invoke($this, '__load', []); } @@ -112,7 +115,7 @@ public function __load() * {@inheritDoc} * @internal generated method: use only when explicitly handling proxy specific loading logic */ - public function __isInitialized() + public function __isInitialized(): bool { return $this->__isInitialized__; } @@ -121,7 +124,7 @@ public function __isInitialized() * {@inheritDoc} * @internal generated method: use only when explicitly handling proxy specific loading logic */ - public function __setInitialized($initialized) + public function __setInitialized($initialized): void { $this->__isInitialized__ = $initialized; } @@ -130,7 +133,7 @@ public function __setInitialized($initialized) * {@inheritDoc} * @internal generated method: use only when explicitly handling proxy specific loading logic */ - public function __setInitializer(\Closure $initializer = null) + public function __setInitializer(\Closure $initializer = null): void { $this->__initializer__ = $initializer; } @@ -139,7 +142,7 @@ public function __setInitializer(\Closure $initializer = null) * {@inheritDoc} * @internal generated method: use only when explicitly handling proxy specific loading logic */ - public function __getInitializer() + public function __getInitializer(): ?\Closure { return $this->__initializer__; } @@ -148,7 +151,7 @@ public function __getInitializer() * {@inheritDoc} * @internal generated method: use only when explicitly handling proxy specific loading logic */ - public function __setCloner(\Closure $cloner = null) + public function __setCloner(\Closure $cloner = null): void { $this->__cloner__ = $cloner; } @@ -157,7 +160,7 @@ public function __setCloner(\Closure $cloner = null) * {@inheritDoc} * @internal generated method: use only when explicitly handling proxy specific cloning logic */ - public function __getCloner() + public function __getCloner(): ?\Closure { return $this->__cloner__; } @@ -165,9 +168,10 @@ public function __getCloner() /** * {@inheritDoc} * @internal generated method: use only when explicitly handling proxy specific loading logic + * @deprecated no longer in use - generated code now relies on internal components rather than generated public API * @static */ - public function __getLazyProperties() + public function __getLazyProperties(): array { return self::$lazyPropertiesDefaults; } diff --git a/data/doctrine-proxies/__CG__CSVImportEntityCSVImportImport.php b/data/doctrine-proxies/__CG__CSVImportEntityCSVImportImport.php index f985216e..31736f44 100644 --- a/data/doctrine-proxies/__CG__CSVImportEntityCSVImportImport.php +++ b/data/doctrine-proxies/__CG__CSVImportEntityCSVImportImport.php @@ -2,6 +2,7 @@ namespace DoctrineProxies\__CG__\CSVImport\Entity; + /** * DO NOT EDIT THIS FILE - IT WAS CREATED BY DOCTRINE'S PROXY GENERATOR */ @@ -12,39 +13,41 @@ class CSVImportImport extends \CSVImport\Entity\CSVImportImport implements \Doct * three parameters, being respectively the proxy object to be initialized, the method that triggered the * initialization process and an array of ordered parameters that were passed to that method. * - * @see \Doctrine\Common\Persistence\Proxy::__setInitializer + * @see \Doctrine\Common\Proxy\Proxy::__setInitializer */ public $__initializer__; /** * @var \Closure the callback responsible of loading properties that need to be copied in the cloned object * - * @see \Doctrine\Common\Persistence\Proxy::__setCloner + * @see \Doctrine\Common\Proxy\Proxy::__setCloner */ public $__cloner__; /** * @var boolean flag indicating if this object was already initialized * - * @see \Doctrine\Common\Persistence\Proxy::__isInitialized + * @see \Doctrine\Persistence\Proxy::__isInitialized */ public $__isInitialized__ = false; /** - * @var array properties to be lazy loaded, with keys being the property - * names and values being their default values + * @var array properties to be lazy loaded, indexed by property name + */ + public static $lazyPropertiesNames = array ( +); + + /** + * @var array default values of properties to be lazy loaded, with keys being the property names * - * @see \Doctrine\Common\Persistence\Proxy::__getLazyProperties + * @see \Doctrine\Common\Proxy\Proxy::__getLazyProperties */ - public static $lazyPropertiesDefaults = []; + public static $lazyPropertiesDefaults = array ( +); - /** - * @param \Closure $initializer - * @param \Closure $cloner - */ - public function __construct($initializer = null, $cloner = null) + public function __construct(?\Closure $initializer = null, ?\Closure $cloner = null) { $this->__initializer__ = $initializer; @@ -82,7 +85,7 @@ public function __wakeup() $existingProperties = get_object_vars($proxy); - foreach ($proxy->__getLazyProperties() as $property => $defaultValue) { + foreach ($proxy::$lazyPropertiesDefaults as $property => $defaultValue) { if ( ! array_key_exists($property, $existingProperties)) { $proxy->$property = $defaultValue; } @@ -103,7 +106,7 @@ public function __clone() /** * Forces initialization of the proxy */ - public function __load() + public function __load(): void { $this->__initializer__ && $this->__initializer__->__invoke($this, '__load', []); } @@ -112,7 +115,7 @@ public function __load() * {@inheritDoc} * @internal generated method: use only when explicitly handling proxy specific loading logic */ - public function __isInitialized() + public function __isInitialized(): bool { return $this->__isInitialized__; } @@ -121,7 +124,7 @@ public function __isInitialized() * {@inheritDoc} * @internal generated method: use only when explicitly handling proxy specific loading logic */ - public function __setInitialized($initialized) + public function __setInitialized($initialized): void { $this->__isInitialized__ = $initialized; } @@ -130,7 +133,7 @@ public function __setInitialized($initialized) * {@inheritDoc} * @internal generated method: use only when explicitly handling proxy specific loading logic */ - public function __setInitializer(\Closure $initializer = null) + public function __setInitializer(\Closure $initializer = null): void { $this->__initializer__ = $initializer; } @@ -139,7 +142,7 @@ public function __setInitializer(\Closure $initializer = null) * {@inheritDoc} * @internal generated method: use only when explicitly handling proxy specific loading logic */ - public function __getInitializer() + public function __getInitializer(): ?\Closure { return $this->__initializer__; } @@ -148,7 +151,7 @@ public function __getInitializer() * {@inheritDoc} * @internal generated method: use only when explicitly handling proxy specific loading logic */ - public function __setCloner(\Closure $cloner = null) + public function __setCloner(\Closure $cloner = null): void { $this->__cloner__ = $cloner; } @@ -157,7 +160,7 @@ public function __setCloner(\Closure $cloner = null) * {@inheritDoc} * @internal generated method: use only when explicitly handling proxy specific cloning logic */ - public function __getCloner() + public function __getCloner(): ?\Closure { return $this->__cloner__; } @@ -165,9 +168,10 @@ public function __getCloner() /** * {@inheritDoc} * @internal generated method: use only when explicitly handling proxy specific loading logic + * @deprecated no longer in use - generated code now relies on internal components rather than generated public API * @static */ - public function __getLazyProperties() + public function __getLazyProperties(): array { return self::$lazyPropertiesDefaults; } diff --git a/data/doctrine-proxies/__CG__CSVImportEntityCSVImportMapping.php b/data/doctrine-proxies/__CG__CSVImportEntityCSVImportMapping.php new file mode 100644 index 00000000..11ca22aa --- /dev/null +++ b/data/doctrine-proxies/__CG__CSVImportEntityCSVImportMapping.php @@ -0,0 +1,272 @@ + properties to be lazy loaded, indexed by property name + */ + public static $lazyPropertiesNames = array ( +); + + /** + * @var array default values of properties to be lazy loaded, with keys being the property names + * + * @see \Doctrine\Common\Proxy\Proxy::__getLazyProperties + */ + public static $lazyPropertiesDefaults = array ( +); + + + + public function __construct(?\Closure $initializer = null, ?\Closure $cloner = null) + { + + $this->__initializer__ = $initializer; + $this->__cloner__ = $cloner; + } + + + + + + + + /** + * + * @return array + */ + public function __sleep() + { + if ($this->__isInitialized__) { + return ['__isInitialized__', 'id', 'name', 'created', 'mapping']; + } + + return ['__isInitialized__', 'id', 'name', 'created', 'mapping']; + } + + /** + * + */ + public function __wakeup() + { + if ( ! $this->__isInitialized__) { + $this->__initializer__ = function (CSVImportMappingModel $proxy) { + $proxy->__setInitializer(null); + $proxy->__setCloner(null); + + $existingProperties = get_object_vars($proxy); + + foreach ($proxy::$lazyPropertiesDefaults as $property => $defaultValue) { + if ( ! array_key_exists($property, $existingProperties)) { + $proxy->$property = $defaultValue; + } + } + }; + + } + } + + /** + * + */ + public function __clone() + { + $this->__cloner__ && $this->__cloner__->__invoke($this, '__clone', []); + } + + /** + * Forces initialization of the proxy + */ + public function __load(): void + { + $this->__initializer__ && $this->__initializer__->__invoke($this, '__load', []); + } + + /** + * {@inheritDoc} + * @internal generated method: use only when explicitly handling proxy specific loading logic + */ + public function __isInitialized(): bool + { + return $this->__isInitialized__; + } + + /** + * {@inheritDoc} + * @internal generated method: use only when explicitly handling proxy specific loading logic + */ + public function __setInitialized($initialized): void + { + $this->__isInitialized__ = $initialized; + } + + /** + * {@inheritDoc} + * @internal generated method: use only when explicitly handling proxy specific loading logic + */ + public function __setInitializer(\Closure $initializer = null): void + { + $this->__initializer__ = $initializer; + } + + /** + * {@inheritDoc} + * @internal generated method: use only when explicitly handling proxy specific loading logic + */ + public function __getInitializer(): ?\Closure + { + return $this->__initializer__; + } + + /** + * {@inheritDoc} + * @internal generated method: use only when explicitly handling proxy specific loading logic + */ + public function __setCloner(\Closure $cloner = null): void + { + $this->__cloner__ = $cloner; + } + + /** + * {@inheritDoc} + * @internal generated method: use only when explicitly handling proxy specific cloning logic + */ + public function __getCloner(): ?\Closure + { + return $this->__cloner__; + } + + /** + * {@inheritDoc} + * @internal generated method: use only when explicitly handling proxy specific loading logic + * @deprecated no longer in use - generated code now relies on internal components rather than generated public API + * @static + */ + public function __getLazyProperties(): array + { + return self::$lazyPropertiesDefaults; + } + + + /** + * {@inheritDoc} + */ + public function getId() + { + if ($this->__isInitialized__ === false) { + return (int) parent::getId(); + } + + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'getId', []); + + return parent::getId(); + } + + /** + * {@inheritDoc} + */ + public function getName() + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'getName', []); + + return parent::getName(); + } + + /** + * {@inheritDoc} + */ + public function setName($name) + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'setName', [$name]); + + return parent::setName($name); + } + + /** + * {@inheritDoc} + */ + public function getMapping() + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'getMapping', []); + + return parent::getMapping(); + } + + /** + * {@inheritDoc} + */ + public function setMapping($mapping) + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'setMapping', [$mapping]); + + return parent::setMapping($mapping); + } + + /** + * {@inheritDoc} + */ + public function getCreated() + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'getCreated', []); + + return parent::getCreated(); + } + + /** + * {@inheritDoc} + */ + public function prePersist(\Doctrine\ORM\Event\LifecycleEventArgs $eventArgs) + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'prePersist', [$eventArgs]); + + return parent::prePersist($eventArgs); + } + + /** + * {@inheritDoc} + */ + public function getResourceId() + { + + $this->__initializer__ && $this->__initializer__->__invoke($this, 'getResourceId', []); + + return parent::getResourceId(); + } + +} diff --git a/src/Api/Adapter/MappingModelAdapter.php b/src/Api/Adapter/MappingModelAdapter.php new file mode 100644 index 00000000..22f10dc3 --- /dev/null +++ b/src/Api/Adapter/MappingModelAdapter.php @@ -0,0 +1,62 @@ +getContent(); + + if (isset($data['name'])) { + $entity->setName($data['name']); + } + + if (isset($data['mapping'])) { + $entity->setMapping($data['mapping']); + } + } + + public function validateEntity(EntityInterface $entity, ErrorStore $errorStore) + { + if (!$entity->getName()) { + $errorStore->addError('o-module-csvimport-mappingmodel:name', 'A model must have a name to save it.'); // @translate + } + + if (!$entity->getMapping()) { + $errorStore->addError('o-module-csvimport-mappingmodel:mapping', 'Mapping model must exists.'); // @translate + } + } + + public function buildQuery(QueryBuilder $qb, array $query) + { + if (isset($query['name'])) { + $qb->andWhere($qb->expr()->eq( + 'omeka_root.name', + $this->createNamedParameter($qb, $query['name'])) + ); + } + } +} diff --git a/src/Api/Representation/MappingModelRepresentation.php b/src/Api/Representation/MappingModelRepresentation.php new file mode 100644 index 00000000..748ab3af --- /dev/null +++ b/src/Api/Representation/MappingModelRepresentation.php @@ -0,0 +1,55 @@ + $this->name(), + 'mapping' => $this->mapping(), + 'created' => $this->created(), + ]; + } + + public function getJsonLdType() + { + return 'o:CSVimportMappingModel'; + } + + public function name() + { + return $this->resource->getName(); + } + + public function mapping() + { + return $this->resource->getMapping(); + } + + public function created() + { + return $this->resource->getCreated(); + } + + public function adminUrl($action = null, $canonical = false) + { + $url = $this->getViewHelper('Url'); + return $url( + 'admin/csvimport/mapping-model-id', + [ + 'controller' => $this->getControllerName(), + 'action' => $action, + 'id' => $this->id(), + ], + ['force_canonical' => $canonical] + ); + } +} diff --git a/src/Controller/Admin/MappingModelController.php b/src/Controller/Admin/MappingModelController.php new file mode 100644 index 00000000..e9671029 --- /dev/null +++ b/src/Controller/Admin/MappingModelController.php @@ -0,0 +1,243 @@ +setBrowseDefaults('created'); + $response = $this->api()->search('csvimport_mapping_models'); + + $this->paginator($response->getTotalResults()); + + $formDeleteSelected = $this->getForm(ConfirmForm::class); + $formDeleteSelected->setAttribute('action', $this->url()->fromRoute(null, ['action' => 'batch-delete'], true)); + $formDeleteSelected->setButtonLabel('Confirm Delete'); // @translate + $formDeleteSelected->setAttribute('id', 'confirm-delete-selected'); + + $view = new ViewModel; + $mappingModels = $response->getContent(); + $view->setVariable('mappingModels', $mappingModels); + $view->setVariable('formDeleteSelected', $formDeleteSelected); + return $view; + } + + public function showAction() + { + $propertiesMap = []; + $properties = $this->api()->search('properties')->getContent(); + foreach ($properties as $property) { + $propertiesMap[$property->id()] = $property->term(); + } + + $mappingModel = $this->loadMappingModel($this->params('id'), []); + + $view = new ViewModel; + $view->setVariable('propertiesMap', $propertiesMap); + $view->setVariable('columns', $mappingModel['columns']); + unset($mappingModel['columns']); + $this->logger()->debug(json_encode($mappingModel)); + $view->setVariable('automaps', $mappingModel); + return $view; + } + + public function saveAction() + { + $response = $this->api()->search('csvimport_mapping_models'); + $mappings = $response->getContent(); + $mappingNames = []; + foreach ($mappings as $mapping) { + $mappingNames[] = $mapping->name(); + } + + $query = $this->params()->fromQuery(); + + $jobId = null; + if ($this->getRequest()->isPost()) { + if (!empty($this->params()->fromPost()['job_id'])) + $jobId = $this->params()->fromPost()['job_id']; + else { + $this->messenger()->addError('Job id not provided.'); // @translate; + return $this->redirect()->toRoute('admin/csvimport/past-imports', ['action' => 'browse'], true); + } + } + else { + if (!empty($query['job_id'])) { + $jobId = $query['job_id']; + } + else { + return $this->getResponse()->setStatusCode(404)->setContent('Job id not provided.'); // @translate + } + } + + $view = new ViewModel; + $form = $this->getForm(MappingModelSaveForm::class, ['job_id' => $jobId]); + $view->setVariable('form', $form); + $view->setTerminal(true); + $view->setTemplate('csv-import/admin/mapping-model/save-mapping-model'); + $view->setVariable('mappings', $mappingNames); + + if ($this->getRequest()->isPost()) { + $data = $this->params()->fromPost(); + $form->setData($data); + + if ($form->isValid()) { + + $job = null; + $job = $this->api()->read('jobs', $jobId)->getContent(); + if (empty($job)) { + $this->messenger()->addError('Could not find job with id %s.', $jobId); // @translate; + return $this->redirect()->toRoute('admin/csvimport/past-imports', ['action' => 'browse'], true); + } + + $args = $job->args(); + $args['override_mapping'] = $data['override_mapping'] ?? null; + $args['mapping_name'] = $data['mapping_name']; + if (!$this->saveMappingModel($args)) { + // TODO Keep user variables when the form is invalid. + $this->messenger()->addError('A mapping model with that name already exists.'); // @translate + return $this->redirect()->toRoute('admin/csvimport/past-imports', ['action' => 'browse'], true); + } + else { + $this->messenger()->addSuccess(sprintf('Mapping successfully saved as %s.', // @translate + $data['mapping_name'])); + return $this->redirect()->toRoute('admin/csvimport/past-imports', ['action' => 'browse'], true); + } + } + $this->messenger()->addFormErrors($form); + return $this->redirect()->toRoute('admin/csvimport/past-imports', ['action' => 'browse'], true); + } + + return $view; + } + + /* + * meant for JS + */ + public function selectMappingModelAction() + { + $response = $this->api()->search('csvimport_mapping_models'); + $mappings = $response->getContent(); + + $view = new ViewModel; + $form = $this->getForm(MappingModelSelectForm::class); + $view->setVariable('form', $form); + $view->setTerminal(true); + $view->setTemplate('csv-import/admin/mapping-model/select-mapping-model'); + + if ($this->getRequest()->isPost()) { + $data = $this->params()->fromPost(); + $form->setData($data); + if ($form->isValid()) { + $mappingId = $form->get('mapping_id')->getValue(); + + $session = new \Laminas\Session\Container('CsvImport'); + + $columns = $session->columns; + + $response = $this->api()->read('csvimport_mapping_models', $mappingId); + if ($response) { + $view = new ViewModel([ + 'automaps' => $this->loadMappingModel($mappingId, $columns), + 'columns' => $columns, + 'resourceType' => $session->resourceType, + ]); + $view->setTemplate('common/mapping-table'); + $view->setTerminal(true); // no layout + return $view; + } else { + return $this->getResponse()->setStatusCode(404)->setContent('Mapping model not found.'); // @translate + } + } + return $this->getResponse()->setStatusCode(400)->setContent('Mapping model selection form not valid.'); + } + + return $view; + } + + public function editAction() + { + $response = $this->api()->read('csvimport_mapping_models', $this->params('id')); + $mapping = $response->getContent(); + + $view = new ViewModel; + $form = $this->getForm(MappingModelEditForm::class); + $form->setAttribute('action', $mapping->url('edit')); + $form->setData([ + 'model_name' => $mapping->name(), + ]); + + $view->setVariable('form', $form); + $view->setTerminal(true); + $view->setTemplate('csv-import/admin/mapping-model/edit'); + $view->setVariable('mapping', $mapping); + + if ($this->getRequest()->isPost()) { + $data = $this->params()->fromPost(); + $form->setData($data); + if ($form->isValid()) { + $mappingName = $form->get('model_name')->getValue(); + + $response = $this->api($form)->update('csvimport_mapping_models', $this->params('id'), ['name' => $mappingName], [], ['isPartial' => true]); + if ($response) { + $this->messenger()->addSuccess('Mapping model successfully updated'); // @translate + return $this->redirect()->toRoute( + 'admin/csvimport/mapping-model', + ['action' => 'browse'], + true + ); + } + } else { + $this->messenger()->addFormErrors($form); + return $this->redirect()->toRoute( + 'admin/csvimport/mapping-model', + ['action' => 'browse'], + true + ); + } + } + return $view; + } + + public function deleteConfirmAction() + { + $response = $this->api()->read('csvimport_mapping_models', $this->params('id')); + $mappingModel = $response->getContent(); + + $view = new ViewModel; + $view->setTerminal(true); + $view->setTemplate('common/delete-confirm-details'); + $view->setVariable('resource', $mappingModel); + $view->setVariable('resourceLabel', 'Mapping'); // @translate + return $view; + } + + public function deleteAction() + { + if ($this->getRequest()->isPost()) { + $form = $this->getForm(ConfirmForm::class); + $form->setData($this->getRequest()->getPost()); + if ($form->isValid()) { + $response = $this->api($form)->delete('csvimport_mapping_models', $this->params('id')); + if ($response) { + $this->messenger()->addSuccess('Mapping model successfully deleted'); // @translate + } + } else { + $this->messenger()->addFormErrors($form); + } + } + return $this->redirect()->toRoute( + 'admin/csvimport/mapping-model', + ['action' => 'browse'], + true + ); + } +} diff --git a/src/Controller/IndexController.php b/src/Controller/IndexController.php index 3e3073b2..11608b97 100644 --- a/src/Controller/IndexController.php +++ b/src/Controller/IndexController.php @@ -2,7 +2,7 @@ namespace CSVImport\Controller; use CSVImport\Form\ImportForm; -use CSVImport\Form\MappingForm; +use CSVImport\Form\MappingModelForm; use CSVImport\Source\SourceInterface; use CSVImport\Job\Import; use finfo; @@ -123,8 +123,13 @@ public function mapAction() return $this->redirect()->toRoute('admin/csvimport'); } + $session = new \Laminas\Session\Container('CsvImport'); + + $session->columns = $columns; + $session->resourceType = $resourceType; + $mappingOptions['columns'] = $columns; - $form = $this->getForm(MappingForm::class, $mappingOptions); + $form = $this->getForm(MappingModelForm::class, $mappingOptions); $automapOptions = []; $automapOptions['check_names_alone'] = $args['automap_check_names_alone']; @@ -143,9 +148,10 @@ public function mapAction() $view->setVariable('mappings', $this->getMappingsForResource($resourceType)); $view->setVariable('mediaForms', $this->getMediaForms()); $view->setVariable('dataTypes', $this->getDataTypes()); + return $view; } else { - $form = $this->getForm(MappingForm::class, $mappingOptions); + $form = $this->getForm(MappingModelForm::class, $mappingOptions); $form->setData($post); if ($form->isValid()) { if (isset($post['basic-settings']) || isset($post['advanced-settings'])) { @@ -155,7 +161,10 @@ public function mapAction() } $args = $this->cleanArgs($post); + $session = new \Laminas\Session\Container('CsvImport'); + $args['columns'] = $session->columns; $this->saveUserSettings($args); + $dispatcher = $this->jobDispatcher(); $job = $dispatcher->dispatch('CSVImport\Job\Import', $args); // The CsvImport record is created in the job, so it doesn't diff --git a/src/Entity/CSVImportMappingModel.php b/src/Entity/CSVImportMappingModel.php new file mode 100644 index 00000000..c247d567 --- /dev/null +++ b/src/Entity/CSVImportMappingModel.php @@ -0,0 +1,87 @@ +id; + } + + public function getName() + { + return $this->name; + } + + public function setName($name) + { + $this->name = $name; + + return $this; + } + + public function getMapping() + { + return $this->mapping; + } + + public function setMapping($mapping) + { + $this->mapping = $mapping; + + return $this; + } + + public function getCreated() + { + return $this->created; + } + + /** + * @PrePersist + */ + public function prePersist(LifecycleEventArgs $eventArgs) + { + $this->created = new DateTime('now'); + } +} diff --git a/src/Form/Element/MappingModelSelect.php b/src/Form/Element/MappingModelSelect.php new file mode 100644 index 00000000..f929f395 --- /dev/null +++ b/src/Form/Element/MappingModelSelect.php @@ -0,0 +1,38 @@ +apiManager = $apiManager; + } + + /** + * @return ApiManager + */ + public function getApiManager() + { + return $this->apiManager; + } + + public function getValueOptions(): array + { + $valueOptions = []; + $mappings = $this->apiManager->search("csvimport_mapping_models", [])->getContent(); + foreach ($mappings as $mapping) { + $valueOptions[$mapping->id()] = $mapping->name(); + } + return $valueOptions; + } +} diff --git a/src/Form/MappingModelEditForm.php b/src/Form/MappingModelEditForm.php new file mode 100644 index 00000000..4dc5788b --- /dev/null +++ b/src/Form/MappingModelEditForm.php @@ -0,0 +1,22 @@ +add([ + 'name' => 'model_name', + 'type' => 'text', + 'options' => [ + 'label' => 'Mapping model name', //@translate + ], + 'attributes' => [ + 'required' => true, + ], + ]); + } +} diff --git a/src/Form/MappingForm.php b/src/Form/MappingModelForm.php similarity index 97% rename from src/Form/MappingForm.php rename to src/Form/MappingModelForm.php index 2db56d78..a5e4e4ce 100644 --- a/src/Form/MappingForm.php +++ b/src/Form/MappingModelForm.php @@ -9,7 +9,7 @@ use Omeka\Form\Element\SiteSelect; use Laminas\Form\Form; -class MappingForm extends Form +class MappingModelForm extends Form { protected $serviceLocator; @@ -84,7 +84,7 @@ public function init() 'type' => ResourceSelect::class, 'options' => [ 'label' => 'Resource template', // @translate - 'info' => 'Assign a resource template to all imported resources. Specific mappings can override this setting.', // @translate + 'info' => 'Assign a resource template to all imported resources. Specific mapping models can override this setting.', // @translate 'empty_option' => 'Select a template', // @translate 'resource_value_options' => [ 'resource' => 'resource_templates', @@ -107,7 +107,7 @@ public function init() 'type' => ResourceClassSelect::class, 'options' => [ 'label' => 'Class', // @translate - 'info' => 'Assign a resource class to all imported resources. Specific mappings can override this setting.', // @translate + 'info' => 'Assign a resource class to all imported resources. Specific mapping models can override this setting.', // @translate 'empty_option' => 'Select a class', // @translate ], 'attributes' => [ @@ -153,7 +153,7 @@ public function init() 'type' => 'radio', 'options' => [ 'label' => 'Visibility', // @translate - 'info' => 'Set visibility for all imported resources. Specific mappings can override this setting.', // @translate + 'info' => 'Set visibility for all imported resources. Specific mapping models can override this setting.', // @translate 'value_options' => [ '1' => 'Public', // @translate '0' => 'Private', // @translate @@ -171,7 +171,7 @@ public function init() 'type' => 'radio', 'options' => [ 'label' => 'Open/closed to additions', // @translate - 'info' => 'Set whether imported item sets are open to additions. Specific mappings can override this setting.', // @translate + 'info' => 'Set whether imported item sets are open to additions. Specific mapping models can override this setting.', // @translate 'value_options' => [ '1' => 'Open', // @translate '0' => 'Closed', // @translate @@ -221,7 +221,7 @@ public function init() 'type' => 'radio', 'options' => [ 'label' => 'Item sets open/closed to additions', // @translate - 'info' => 'Set whether imported item sets are open to additions. Specific mappings can override this setting.', // @translate + 'info' => 'Set whether imported item sets are open to additions. Specific mapping models can override this setting.', // @translate 'value_options' => [ '1' => 'Open', // @translate '0' => 'Closed', // @translate @@ -284,7 +284,7 @@ public function init() 'type' => 'text', 'options' => [ 'label' => 'Language', // @translate - 'info' => 'Language setting to apply to all imported literal data. Individual property mappings can override the setting here.', // @translate + 'info' => 'Language setting to apply to all imported literal data. Individual property mapping models can override the setting here.', // @translate ], 'attributes' => [ 'id' => 'global_language', diff --git a/src/Form/MappingModelSaveForm.php b/src/Form/MappingModelSaveForm.php new file mode 100644 index 00000000..361e83e4 --- /dev/null +++ b/src/Form/MappingModelSaveForm.php @@ -0,0 +1,41 @@ +setAttribute('action', 'mapping-model/save'); + + $this->add([ + 'name'=> 'job_id', + 'type'=> 'hidden', + 'attributes' => [ + 'value' => $this->getOption('job_id'), + ] + ]); + + $this->add([ + 'name' => 'mapping_name', + 'type' => 'text', + 'options' => [ + 'label' => 'Mapping model name', //@translate + ], + 'attributes' => [ + 'required' => true, + ], + ]); + $this->add([ + 'name' => 'override_mapping', + 'type' => Checkbox::class, + 'options' => [ + 'label' => 'Override mapping model if it already exists?', //@translate + ], + ]); + } +} diff --git a/src/Form/MappingModelSelectForm.php b/src/Form/MappingModelSelectForm.php new file mode 100644 index 00000000..467f49b9 --- /dev/null +++ b/src/Form/MappingModelSelectForm.php @@ -0,0 +1,31 @@ +setAttribute('action', '/admin/csvimport/mapping-model/selectMappingModel'); + + $this->add([ + 'name' => 'mapping_id', + 'type' => MappingModelSelect::class, + 'attributes' => [ + 'id' => 'mapping-id', + 'class' => 'chosen-select', + 'multiple' => false, + 'data-placeholder' => '', + ], + 'options' => [ + 'label' => 'Select mapping model', // @translate + 'resource_value_options' => [ + 'resource' => 'csvimport_mapping_models', + 'query' => [], + ], + ], + ]); + } +} diff --git a/src/Job/Import.php b/src/Job/Import.php index 614b13ae..a8d68e33 100644 --- a/src/Job/Import.php +++ b/src/Job/Import.php @@ -504,7 +504,7 @@ protected function checkMedias(array $data) $this->hasErr = true; $this->logger->err(new Message('A media to create is not attached to an item (%s).', // @translate empty($entityJson['o:source']) - ? (isset($entityJson['o:ingester']) ? $entityJson['o:ingester'] : 'unknown ingester') // @translate + ? ($entityJson['o:ingester'] ?? 'unknown ingester') // @translate : $entityJson['o:ingester'] . ': ' . $entityJson['o:source'])); } } diff --git a/src/Mapping/AbstractResourceMapping.php b/src/Mapping/AbstractResourceMapping.php index b86bc92a..45d98cd3 100644 --- a/src/Mapping/AbstractResourceMapping.php +++ b/src/Mapping/AbstractResourceMapping.php @@ -49,7 +49,7 @@ public function processRow(array $row) // First, pull in the global settings. $this->processGlobalArgs(); - $multivalueMap = isset($this->args['column-multivalue']) ? $this->args['column-multivalue'] : []; + $multivalueMap = $this->args['column-multivalue'] ?? []; $multivalueSeparator = $this->args['multivalue_separator']; foreach ($row as $index => $values) { if (empty($multivalueMap[$index])) { diff --git a/src/Mapping/MediaSourceMapping.php b/src/Mapping/MediaSourceMapping.php index 5a43e9fa..475d0d49 100644 --- a/src/Mapping/MediaSourceMapping.php +++ b/src/Mapping/MediaSourceMapping.php @@ -51,7 +51,7 @@ public function processRow(array $row) $mediaAdapters = $config['media_ingester_adapter']; $action = $this->args['action']; - $multivalueMap = isset($this->args['column-multivalue']) ? $this->args['column-multivalue'] : []; + $multivalueMap = $this->args['column-multivalue'] ?? []; $multivalueSeparator = $this->args['multivalue_separator']; foreach ($row as $index => $values) { if (isset($mediaMap[$index])) { diff --git a/src/Mapping/PropertyMapping.php b/src/Mapping/PropertyMapping.php index cf866be5..e1a6725b 100644 --- a/src/Mapping/PropertyMapping.php +++ b/src/Mapping/PropertyMapping.php @@ -58,12 +58,12 @@ public function processRow(array $row) $dataTypeAdapters = $this->getDataTypeAdapters(); // Get default option values. - $globalLanguage = isset($this->args['global_language']) ? $this->args['global_language'] : ''; + $globalLanguage = $this->args['global_language'] ?? ''; - $multivalueMap = isset($this->args['column-multivalue']) ? $this->args['column-multivalue'] : []; + $multivalueMap = $this->args['column-multivalue'] ?? []; $multivalueSeparator = $this->args['multivalue_separator']; - $resourceIdentifierPropertyMap = isset($this->args['column-resource-identifier-property']) ? $this->args['column-resource-identifier-property'] : []; + $resourceIdentifierPropertyMap = $this->args['column-resource-identifier-property'] ?? []; $findResourceFromIdentifier = $this->findResourceFromIdentifier; foreach ($row as $index => $values) { @@ -106,7 +106,7 @@ public function processRow(array $row) // Check if a label is provided after the url. // Note: A url has no space, but a uri may have. if (strpos($value, ' ')) { - list($valueId, $valueLabel) = explode(' ', $value, 2); + [$valueId, $valueLabel] = explode(' ', $value, 2); $valueLabel = trim($valueLabel); } else { $valueId = $value; diff --git a/src/Mvc/Controller/Plugin/LoadMappingModel.php b/src/Mvc/Controller/Plugin/LoadMappingModel.php new file mode 100644 index 00000000..ad498c18 --- /dev/null +++ b/src/Mvc/Controller/Plugin/LoadMappingModel.php @@ -0,0 +1,174 @@ +connection = $connection; + $this->api = $apiManager; + $this->logger = $logger; + } + + /** + * + */ + public function __invoke(int $id, array $columns) + { + // Fetch the mapping + $mapping = $this->api->read('csvimport_mapping_models', $id)->getContent(); + + if (empty($mapping)) { + // $this->logger()->debug(sprintf('No mapping with id %s found.', $id)); + return []; + } + + $mappingValue = json_decode($mapping->mapping(), true); + + $originalColumns = $mappingValue['columns']; + unset($mappingValue['columns']); // so we don't iterate over it after like it's an actual mapping + + // $this->logger->debug(sprintf('Old columns : ' . PHP_EOL . '%s' . PHP_EOL . 'New columns:' . PHP_EOL . '%s' . PHP_EOL, + // json_encode($originalColumns), json_encode($columns))); + + // Find the mapping between old column indexes and current columns, according to name matching + $oldToNewColumn = []; + if (!empty($columns)) { + foreach ($columns as $index => $column) { + $columnMapping = array_search($column, $originalColumns); + + if ($columnMapping !== false) { + $oldToNewColumn[strval($columnMapping)] = $index; + } + } + } else { + foreach ($originalColumns as $index => $column) { + $oldToNewColumn[strval($index)] = $index; + } + } + + // $this->logger->debug(sprintf('Column mapping: ' . PHP_EOL . '%s' . PHP_EOL, json_encode($oldToNewColumn))); + // $this->logger->debug(sprintf('Mapping in : ' . PHP_EOL . '%s' . PHP_EOL, json_encode($mappingValue))); + + $automap = []; + + // if no columns passed as argument it means there are no new columns, so we remind of the original ones + // used in MappingController + if (empty($columns)) { + $automap['columns'] = $originalColumns; + } + + /* + * Reading each entry that was sent by the user in the form + * The structure is complicated and undocumented. + */ + foreach ($mappingValue as $mappingValueColumnName => $mappingValueColumn) { + $name = ""; + if (str_contains($mappingValueColumnName, 'column-')) { + $name = explode('column-', $mappingValueColumnName)[1]; + + // properties of type property + if ($name == 'property') { + foreach ($mappingValueColumn as $index => $property) { + if (array_key_exists($index, $oldToNewColumn)) { + foreach ($property as $subindex => $subproperty) { + $element = []; + $element["name"] = $name; + $element["class"] = $name; + $element["multiple"] = true; + $element["special"] = " data-property-id=\"" . $subproperty . "\""; + $element["label"] = explode(':', $subindex)[1] ?? $subindex; + $element["value"] = $subproperty; + $automap[$oldToNewColumn[$index]][] = $element; + } + } + } + } + + // these are column options. Only one per column, for data simplicity we put it in $automap[ColumnIndex][0] + elseif ($name == "data-type" + || $name == "multivalue" + || $name == "language" + || $name == "private-values" + || $name == "resource-identifier-property") { + foreach ($mappingValueColumn as $index => $property) { + if (array_key_exists($index, $oldToNewColumn)) { + $automap[$oldToNewColumn[$index]][0][$name] = $property; + } + } + } + + // these are mappings that are other than properties + else { + foreach ($mappingValueColumn as $index => $property) { + if (array_key_exists($index, $oldToNewColumn)) { + $element = []; + $element["name"] = $name; + $element["class"] = $name; + $element["multiple"] = false; + $element["special"] = ""; + $element["value"] = $property; + $element["label"] = "label"; // @tdodo + $automap[$oldToNewColumn[$index]][] = $element; + } + } + } + } + } + + return $automap; + } +} diff --git a/src/Mvc/Controller/Plugin/SaveMappingModel.php b/src/Mvc/Controller/Plugin/SaveMappingModel.php new file mode 100644 index 00000000..a767372d --- /dev/null +++ b/src/Mvc/Controller/Plugin/SaveMappingModel.php @@ -0,0 +1,146 @@ +connection = $connection; + $this->api = $apiManager; + $this->logger = $logger; + } + + /** + * Save mapping. + */ + public function __invoke(array $args): bool + { + $shouldOverrideMapping = false; + $mappingName = $args['mapping_name']; + + // $this->logger->debug('[CSVImport] Mapping model name.'); + // $this->logger->debug(json_encode($mappingName)); + + $alreadyExistsContent = $this->api->search('csvimport_mapping_models', ['name' => $mappingName])->getContent(); + if (count($alreadyExistsContent) > 0) { + if (!empty($args['override_mapping'])) + { + $shouldOverrideMapping = true; + } + else + { + // $this->logger->debug('[CSVImport] Already existing mapping model.'); + // $this->logger->debug(json_encode($alreadyExistsContent)); + return false; + } + } + + if (empty($args['columns'])) { + // We first need to read the file to get the column names + // Because we need to remember the column names + $filePath = $args['filepath']; + $fileName = $args['filename']; + + // Check if file exists and is readable + if (!file_exists($filePath) || !is_readable($filePath)) { + // $this->logger->err(sprintf("[CSV Import]: File '%s' not found when saving mapping model.", $filePath)); // @translate + } + + // Open the file for reading + if (($handle = fopen($filePath, 'r')) !== false) { + // Read the first line as CSV (header row) + $args['columns'] = fgetcsv($handle); + + // Close file + fclose($handle); + + // Output the column names + if (!$args['columns']) { + // $this->logger->err(sprintf("[CSV Import]: Unable to read columns when saving mapping model.")); // @translate + } + } else { + // $this->logger->err(sprintf("[CSV Import]: File '%s' could not be opened when saving mapping model.", $filePath)); // @translate + } + + if (empty($args['columns'])) { + // $this->logger->err(sprintf("[CSV Import]: Unable to get columns from file '%s'.", $filePath)); // @translate + } + } + + // $this->logger->debug(sprintf("[CSV Import] Column names: " . PHP_EOL . "%s" . PHP_EOL, json_encode($args["columns"]))); + + // don't save irrelevant data + unset($args['filename']); + unset($args['filesize']); + unset($args['filepath']); + unset($args['media_type']); + unset($args['resource_type']); + unset($args['automap_check_names_alone']); + unset($args['mapping_name']); + unset($args['override_mapping']); + + // $this->logger->debug(sprintf('[CSV Import: Args to be saved my mapping model]' . PHP_EOL . '%s' . PHP_EOL, json_encode($args))); + + if ($shouldOverrideMapping) { + $this->api->update('csvimport_mapping_models', ['name' => $mappingName], ['mapping' => json_encode($args)], [], ['isPartial' => true]); + } + else { + $this->api->create('csvimport_mapping_models', ['mapping' => json_encode($args), 'name' => $mappingName]); + } + + return true; + } +} diff --git a/src/Service/Controller/Admin/MappingModelControllerFactory.php b/src/Service/Controller/Admin/MappingModelControllerFactory.php new file mode 100644 index 00000000..07b98201 --- /dev/null +++ b/src/Service/Controller/Admin/MappingModelControllerFactory.php @@ -0,0 +1,17 @@ +get('Omeka\Connection'), + $services->get('Omeka\ApiManager'), + $services->get('Omeka\Logger') + ); + } +} diff --git a/src/Service/ControllerPlugin/SaveMappingModelFactory.php b/src/Service/ControllerPlugin/SaveMappingModelFactory.php new file mode 100644 index 00000000..5040ec98 --- /dev/null +++ b/src/Service/ControllerPlugin/SaveMappingModelFactory.php @@ -0,0 +1,18 @@ +get('Omeka\Connection'), + $services->get('Omeka\ApiManager'), + $services->get('Omeka\Logger') + ); + } +} diff --git a/src/Service/Form/Element/MappingModelSelectFactory.php b/src/Service/Form/Element/MappingModelSelectFactory.php new file mode 100644 index 00000000..3471fc5e --- /dev/null +++ b/src/Service/Form/Element/MappingModelSelectFactory.php @@ -0,0 +1,16 @@ +setApiManager($services->get('Omeka\ApiManager')); + return $element; + } +} diff --git a/src/Service/Form/MappingFormFactory.php b/src/Service/Form/MappingModelFormFactory.php similarity index 67% rename from src/Service/Form/MappingFormFactory.php rename to src/Service/Form/MappingModelFormFactory.php index 38c3639d..766f994e 100644 --- a/src/Service/Form/MappingFormFactory.php +++ b/src/Service/Form/MappingModelFormFactory.php @@ -1,15 +1,15 @@ setServiceLocator($services); return $form; } diff --git a/src/Service/Form/MappingModelSaveFormFactory.php b/src/Service/Form/MappingModelSaveFormFactory.php new file mode 100644 index 00000000..8cb5892a --- /dev/null +++ b/src/Service/Form/MappingModelSaveFormFactory.php @@ -0,0 +1,21 @@ +setOption("job_id", $options["job_id"]); + return $form; + } +} diff --git a/view/common/mapping-table.phtml b/view/common/mapping-table.phtml new file mode 100644 index 00000000..2e94adb7 --- /dev/null +++ b/view/common/mapping-table.phtml @@ -0,0 +1,87 @@ +plugin('escapeHtml'); ?> + + + + + + + + + + $column): ?> + + + + + + + +
+ + + + translate("Column"); ?> + translate("Mappings"); ?>translate("Options"); ?>
+ + + + +
    +
  • + +
  • + +
+
+
    + + + +
  • > + translate($automaps[$index]['label']); ?> +
      +
    • +
    + +
  • + + +
  • > + translate($automap['label']); ?> +
      +
    • +
    + +
  • + + + +
+
+
    +
  • + translate('Data type:'); ?> + + > +
  • +
  • + translate("Multivalue"); ?> + " > +
  • +
  • + translate("Language:"); ?> + + > +
  • +
  • + translate("Private values"); ?> + " > +
  • +
  • + translate("Resource identifier property:"); ?> + + > +
  • +
+
\ No newline at end of file diff --git a/view/csv-import/admin/mapping-model/browse.phtml b/view/csv-import/admin/mapping-model/browse.phtml new file mode 100644 index 00000000..c1f2670d --- /dev/null +++ b/view/csv-import/admin/mapping-model/browse.phtml @@ -0,0 +1,117 @@ +plugin('translate'); +$escape = $this->plugin('escapeHtml'); +$this->htmlElement('body')->appendAttribute('class', 'mappings browse'); +?> + +pageTitle($translate('Mapping models'), 1, $this->translate('CSV Import')); ?> + +trigger('view.browse.before'); ?> + +
+ + + + + + + + + + + + + + + + +
+ +
+ name(); ?> +
    +
  • + hyperlink( + '', + $this->url('admin/csvimport/mapping-model-id', + [ + 'controller' => 'MappingModel', + 'action' => 'show', + 'id' => $model->id() + ] + ), + [ + 'class' => 'o-icon-view', + 'title' => $translate('View'), + ]) + ?> +
  • +
  • + hyperlink( + '', + '#', + [ + 'data-sidebar-content-url' => $this->url('admin/csvimport/mapping-model-id', + [ + 'controller' => 'MappingModel', + 'action' => 'edit', + 'id' => $model->id() + ] + ), + 'class' => 'o-icon-edit sidebar-content', + 'title' => $translate('Edit'), + 'data-sidebar-selector' => '#sidebar', + ] + ); ?> +
  • +
  • + hyperlink( + '', + '#', + [ + 'data-sidebar-content-url' => $this->url('admin/csvimport/mapping-model-id', + [ + 'controller' => 'MappingModel', + 'action' => 'deleteConfirm', + 'id' => $model->id() + ] + ), + 'class' => 'o-icon-delete sidebar-content', + 'title' => $translate('Delete'), + 'data-sidebar-selector' => '#sidebar', + ] + ); ?> +
  • +
+
i18n()->dateFormat($model->created())); ?>
+ +
+ +trigger('view.browse.after'); ?> + + + + + +
+

+
+ + diff --git a/view/csv-import/admin/mapping-model/edit.phtml b/view/csv-import/admin/mapping-model/edit.phtml new file mode 100644 index 00000000..2b0a6331 --- /dev/null +++ b/view/csv-import/admin/mapping-model/edit.phtml @@ -0,0 +1,14 @@ +prepare(); ?> + +pageTitle($this->translate('Edit mapping model'), 3); ?> + +form()->openTag($form); ?> + +formCollection($form); ?> + +
+ +
+ +form()->closeTag($form); ?> + diff --git a/view/csv-import/admin/mapping-model/save-mapping-model.phtml b/view/csv-import/admin/mapping-model/save-mapping-model.phtml new file mode 100644 index 00000000..88d7acd8 --- /dev/null +++ b/view/csv-import/admin/mapping-model/save-mapping-model.phtml @@ -0,0 +1,43 @@ +prepare(); ?> + +pageTitle($this->translate('Save mapping model'), 3); ?> + +form()->openTag($form); ?> + +formCollection($form); ?> + +
+ +
+ +form()->closeTag($form); ?> + + \ No newline at end of file diff --git a/view/csv-import/admin/mapping-model/select-mapping-model.phtml b/view/csv-import/admin/mapping-model/select-mapping-model.phtml new file mode 100644 index 00000000..f513d01d --- /dev/null +++ b/view/csv-import/admin/mapping-model/select-mapping-model.phtml @@ -0,0 +1,16 @@ +prepare(); ?> + +pageTitle($this->translate('Select mapping model'), 3); ?> + +form()->openTag($form); ?> + +formCollection($form); ?> + +
+ +
+ +form()->closeTag($form); ?> + diff --git a/view/csv-import/admin/mapping-model/show.phtml b/view/csv-import/admin/mapping-model/show.phtml new file mode 100644 index 00000000..d7948293 --- /dev/null +++ b/view/csv-import/admin/mapping-model/show.phtml @@ -0,0 +1,65 @@ +plugin('escapeHtml'); +$this->htmlElement('body')->appendAttribute('class', 'no-section-hashes'); +$this->headLink()->appendStylesheet($this->assetUrl('css/csvimport.css', 'CSVImport')); +?> + + + + + + + + + + + $column): ?> + + + + + + + +
translate("Column"); ?> translate("Mapping"); ?>translate("Options"); ?>
+ + +
    + + + +
  • > + translate($automaps[$index]['label']); ?> +
  • + + +
  • > + translate($automap['label']); ?> +
  • + + + +
+
+
    +
  • + translate('Data type:'); ?> + +
  • +
  • + translate("Multivalue"); ?> +
  • +
  • + translate("Language:"); ?> + +
  • +
  • + translate("Private values"); ?> +
  • +
  • + translate("Resource identifier property:"); ?> + +
  • +
+
diff --git a/view/csv-import/index/map.phtml b/view/csv-import/index/map.phtml index f310f84d..f9837759 100644 --- a/view/csv-import/index/map.phtml +++ b/view/csv-import/index/map.phtml @@ -4,6 +4,7 @@ $escapeHtml = $this->plugin('escapeHtml'); $this->htmlElement('body')->appendAttribute('class', 'no-section-hashes'); $this->headLink()->appendStylesheet($this->assetUrl('css/csvimport.css', 'CSVImport')); $this->headScript()->appendFile($this->assetUrl('js/csvimport.js', 'CSVImport')); +$this->headScript()->appendFile($this->assetUrl('js/mappingselect.js', 'CSVImport')); $resourceTypeLabels = [ 'items' => $this->translate('items'), @@ -56,85 +57,46 @@ $pageTitle = isset($resourceTypeLabels[$resourceType])
- translate('Batch edit options'); ?> + +
+
    +
  • hyperlink( + '', + '#', + [ + 'data-sidebar-content-url' => $this->url('admin/csvimport/mapping-model', + [ + 'controller' => 'MappingModel', + 'action' => 'selectMappingModel', + ] + ), + 'class' => 'o-icon-menu sidebar-content', + 'title' => $this->translate('Select mapping model'), + 'data-sidebar-selector' => '#sidebar', + ] + ); ?> + translate('Load a saved mapping model'); ?> +
  • +
+
- - - - - - - - - - $column): ?> - - - - - - - -
- - - - translate("Column"); ?> - translate("Mappings"); ?>translate("Options"); ?>
- - - - -
    -
  • - -
  • - -
-
-
    - - -
  • > - translate($automaps[$index]['label']); ?> -
      -
    • -
    - -
  • - -
-
-
    -
  • - translate('Data type:'); ?> - - -
  • -
  • - translate("Multivalue"); ?> - -
  • -
  • - translate("Language:"); ?> - - -
  • -
  • - translate("Private values"); ?> - -
  • -
  • - translate("Resource identifier property:"); ?> - - -
  • -
-
+ partial('common/mapping-table', ['resourceType' => $resourceType, 'columns' => $columns, 'automaps' => $automaps]); ?> partial('common/mapping-sidebar'); ?> partial('common/options-sidebar', ['dataTypes' => $dataTypes]); ?> form()->closeTag($form); ?> + + + diff --git a/view/csv-import/index/past-imports.phtml b/view/csv-import/index/past-imports.phtml index 8b7a80fe..9600a0e9 100644 --- a/view/csv-import/index/past-imports.phtml +++ b/view/csv-import/index/past-imports.phtml @@ -26,6 +26,7 @@ $this->headLink()->appendStylesheet($this->assetUrl('css/csvimport.css', 'CSVImp translate('Result'));?> translate('Status'));?> translate('Owner'));?> + translate('Save'));?> @@ -103,9 +104,43 @@ $this->headLink()->appendStylesheet($this->assetUrl('css/csvimport.css', 'CSVImp echo $this->hyperlink($owner->name(), $this->url('admin/id', array('controller' => 'user', 'action' => 'show', 'id' => $owner->id()))); endif; ?> + + hyperlink( + '', + '#', + [ + 'data-sidebar-content-url' => $this->url( + 'admin/csvimport/mapping-model', + [ + 'controller' => 'MappingModel', + 'action' => 'save', + ], + [ + 'query' => [ + 'job_id' => $job->id(), + ], + ] + ), + 'class' => 'fa fal fa-save sidebar-content', + 'title' => $escape($this->translate('Save')), + 'data-sidebar-selector' => '#sidebar', + ], + ); + } ?> + + +