From f35695268f98087e37aca78abb6103e303ac6909 Mon Sep 17 00:00:00 2001 From: Ferdinand Kuhl Date: Thu, 9 Feb 2023 13:02:22 +0100 Subject: [PATCH] First working version --- .woodpecker/code-style.yml | 8 + .woodpecker/functional-tests.yml | 32 +++ Classes/Aspects/AssetExtensionsAspect.php | 22 ++ Classes/AssetAttributeTrait.php | 29 +++ Classes/Domain/Model/AssetAttribute.php | 100 +++++++++ .../CustomAttributesViewHelper.php | 21 ++ Configuration/Settings.yaml | 13 ++ Configuration/Views.yaml | 22 ++ Migrations/Mysql/Version20221018191523.php | 37 ++++ Migrations/Mysql/Version20221211170135.php | 25 +++ README.md | 40 ++++ Resources/Private/Templates/Asset/Edit.html | 206 ++++++++++++++++++ Tests/Functional/AssetAttributeTest.php | 50 +++++ composer.json | 38 ++++ 14 files changed, 643 insertions(+) create mode 100644 .woodpecker/code-style.yml create mode 100644 .woodpecker/functional-tests.yml create mode 100644 Classes/Aspects/AssetExtensionsAspect.php create mode 100644 Classes/AssetAttributeTrait.php create mode 100644 Classes/Domain/Model/AssetAttribute.php create mode 100644 Classes/ViewHelpers/CustomAttributesViewHelper.php create mode 100644 Configuration/Settings.yaml create mode 100644 Configuration/Views.yaml create mode 100644 Migrations/Mysql/Version20221018191523.php create mode 100644 Migrations/Mysql/Version20221211170135.php create mode 100644 README.md create mode 100644 Resources/Private/Templates/Asset/Edit.html create mode 100644 Tests/Functional/AssetAttributeTest.php create mode 100644 composer.json diff --git a/.woodpecker/code-style.yml b/.woodpecker/code-style.yml new file mode 100644 index 0000000..d689496 --- /dev/null +++ b/.woodpecker/code-style.yml @@ -0,0 +1,8 @@ +pipeline: + code-style: + image: composer + commands: + - composer global config repositories.repo-name vcs https://git.digital-competence.de/Packages/php-codesniffer + - composer global config --no-plugins allow-plugins.dealerdirect/phpcodesniffer-composer-installer true + - composer global require digicomp/php-codesniffer:@dev + - composer global exec -- phpcs --runtime-set ignore_warnings_on_exit 1 --standard=DigiComp Classes/ Tests/ Migrations diff --git a/.woodpecker/functional-tests.yml b/.woodpecker/functional-tests.yml new file mode 100644 index 0000000..0375187 --- /dev/null +++ b/.woodpecker/functional-tests.yml @@ -0,0 +1,32 @@ +workspace: + base: /woodpecker + path: package + +matrix: + PHP_VERSION: + include: + - FLOW_VERSION: 7.3 + PHP_VERSION: 7.4 + - FLOW_VERSION: 7.3 + PHP_VERSION: 8.1 + - FLOW_VERSION: 8.2 + PHP_VERSION: 8.2 + + +pipeline: + functional-tests: + image: "thecodingmachine/php:${PHP_VERSION}-v4-cli" + environment: + # Enable the PDO_SQLITE extension + - "PHP_EXTENSION_PDO_SQLITE=1" + - "FLOW_VERSION=${FLOW_VERSION}" + - "NEOS_BUILD_DIR=/woodpecker/Build-${FLOW_VERSION}" + commands: + - "sudo mkdir $NEOS_BUILD_DIR" + - "sudo chown -R docker:docker $NEOS_BUILD_DIR" + - "cd $NEOS_BUILD_DIR" + - "composer create-project --no-install neos/flow-base-distribution:^$FLOW_VERSION ." + - "composer config repositories.repo-name path /woodpecker/package" + - "composer config --no-plugins allow-plugins.neos/composer-plugin true" + - "composer require digicomp/asset-attributes:@dev" + - "bin/phpunit --configuration Build/BuildEssentials/PhpUnit/FunctionalTests.xml Packages/Application/DigiComp.AssetAttributes/Tests/Functional" diff --git a/Classes/Aspects/AssetExtensionsAspect.php b/Classes/Aspects/AssetExtensionsAspect.php new file mode 100644 index 0000000..8bd5de7 --- /dev/null +++ b/Classes/Aspects/AssetExtensionsAspect.php @@ -0,0 +1,22 @@ + + */ + protected $attributes; +} diff --git a/Classes/AssetAttributeTrait.php b/Classes/AssetAttributeTrait.php new file mode 100644 index 0000000..6f8f168 --- /dev/null +++ b/Classes/AssetAttributeTrait.php @@ -0,0 +1,29 @@ + + */ + public function getAttributes(): Collection + { + if ($this->attributes === null) { + $this->attributes = new ArrayCollection(); + } + return $this->attributes; + } + + /** + * @param Collection $attributes + */ + public function setAttributes(Collection $attributes): void + { + $this->attributes = $attributes; + } +} diff --git a/Classes/Domain/Model/AssetAttribute.php b/Classes/Domain/Model/AssetAttribute.php new file mode 100644 index 0000000..1eda844 --- /dev/null +++ b/Classes/Domain/Model/AssetAttribute.php @@ -0,0 +1,100 @@ +name = $name; + $this->value = $value; + if (!$urlValue) { + $urlValue = $value; + } + $this->urlValue = $urlValue; + } + + public function initializeObject(): void + { + if ($this->urlValue === $this->value) { + $this->urlValue = \str_replace( + \array_column($this->replacementMap, 'key'), + \array_column($this->replacementMap, 'value'), + $this->urlValue + ); + $this->urlValue = \urlencode(\strtolower($this->urlValue)); + } + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return string + */ + public function getValue(): string + { + return $this->value; + } + + /** + * @return string + */ + public function getUrlValue(): string + { + return $this->urlValue; + } + + /** + * @return string + */ + public function __toString(): string + { + return $this->getName() . ': ' . $this->getValue(); + } +} diff --git a/Classes/ViewHelpers/CustomAttributesViewHelper.php b/Classes/ViewHelpers/CustomAttributesViewHelper.php new file mode 100644 index 0000000..17715c7 --- /dev/null +++ b/Classes/ViewHelpers/CustomAttributesViewHelper.php @@ -0,0 +1,21 @@ +customAssetProperties))->toArray(); + } +} diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml new file mode 100644 index 0000000..f67309e --- /dev/null +++ b/Configuration/Settings.yaml @@ -0,0 +1,13 @@ +DigiComp: + AssetAttributes: + urlReplacements: + - + key: "," + value: "-" + - + key: "/" + value: "-" + - + key: "+" + value: "-" + customAssetProperties: [] diff --git a/Configuration/Views.yaml b/Configuration/Views.yaml new file mode 100644 index 0000000..6f2720d --- /dev/null +++ b/Configuration/Views.yaml @@ -0,0 +1,22 @@ +- + requestFilter: "isFormat('html') && isPackage('Neos.Media.Browser')" + options: + templatePathAndFilenamePattern: "@templateRoot/@subpackage/Asset/@action.@format" + templateRootPaths: + "DigiComp.AssetAttributes": "resource://DigiComp.AssetAttributes/Private/Templates" + "Neos.Media.Browser": "resource://Neos.Media.Browser/Private/Templates" + partialRootPaths: + "Neos.Neos": "resource://Neos.Neos/Private/Partials" + "Neos.Media.Browser": "resource://Neos.Media.Browser/Private/Partials" + +- + requestFilter: "parentRequest.isPackage('Neos.Neos') && isFormat('html') && isPackage('Neos.Media.Browser')" + options: + templateRootPaths: + "DigiComp.AssetAttributes": "resource://DigiComp.AssetAttributes/Private/Templates" + "Neos.Media.Browser": "resource://Neos.Media.Browser/Private/Templates" + layoutRootPaths: + "Neos.Media.Browser": "resource://Neos.Media.Browser/Private/Layouts/Module" + partialRootPaths: + "Neos.Neos": "resource://Neos.Neos/Private/Partials" + "Neos.Media.Browser": "resource://Neos.Media.Browser/Private/Partials" diff --git a/Migrations/Mysql/Version20221018191523.php b/Migrations/Mysql/Version20221018191523.php new file mode 100644 index 0000000..db8c158 --- /dev/null +++ b/Migrations/Mysql/Version20221018191523.php @@ -0,0 +1,37 @@ +abortIf( + !$this->connection->getDatabasePlatform() instanceof MySqlPlatform, + "Migration can only be executed safely on '\Doctrine\DBAL\Platforms\MySqlPlatform'." + ); + + $this->addSql('CREATE TABLE digicomp_assetattributes_domain_model_assetattribute (persistence_object_identifier VARCHAR(40) NOT NULL, name VARCHAR(255) NOT NULL, value VARCHAR(255) NOT NULL, urlvalue VARCHAR(255) NOT NULL, UNIQUE INDEX UNIQ_2EFCAB7C5E237E061D775834 (name, value), PRIMARY KEY(persistence_object_identifier))'); + $this->addSql('CREATE TABLE neos_media_domain_model_asset_attributes_join (media_asset VARCHAR(40) NOT NULL, assetattributes_assetattribute VARCHAR(40) NOT NULL, INDEX IDX_68EDD3AD1DB69EED (media_asset), INDEX IDX_68EDD3ADE9AACB21 (assetattributes_assetattribute), PRIMARY KEY(media_asset, assetattributes_assetattribute))'); + $this->addSql('ALTER TABLE neos_media_domain_model_asset_attributes_join ADD CONSTRAINT FK_68EDD3AD1DB69EED FOREIGN KEY (media_asset) REFERENCES neos_media_domain_model_asset (persistence_object_identifier)'); + $this->addSql('ALTER TABLE neos_media_domain_model_asset_attributes_join ADD CONSTRAINT FK_68EDD3ADE9AACB21 FOREIGN KEY (assetattributes_assetattribute) REFERENCES digicomp_assetattributes_domain_model_assetattribute (persistence_object_identifier)'); + } + + public function down(Schema $schema): void + { + $this->abortIf( + !$this->connection->getDatabasePlatform() instanceof MySqlPlatform, + "Migration can only be executed safely on '\Doctrine\DBAL\Platforms\MySqlPlatform'." + ); + + $this->addSql('ALTER TABLE neos_media_domain_model_asset_attributes_join DROP FOREIGN KEY FK_68EDD3ADE9AACB21'); + $this->addSql('DROP TABLE digicomp_assetattributes_domain_model_assetattribute'); + $this->addSql('DROP TABLE neos_media_domain_model_asset_attributes_join'); + } +} diff --git a/Migrations/Mysql/Version20221211170135.php b/Migrations/Mysql/Version20221211170135.php new file mode 100644 index 0000000..7d2e432 --- /dev/null +++ b/Migrations/Mysql/Version20221211170135.php @@ -0,0 +1,25 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql('ALTER TABLE digicomp_assetattributes_domain_model_assetattribute DROP INDEX UNIQ_2EFCAB7C5E237E061D775834, ADD INDEX IDX_2EFCAB7C5E237E061D775834 (name, value)'); + } + + public function down(Schema $schema): void + { + $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql('ALTER TABLE digicomp_assetattributes_domain_model_assetattribute DROP INDEX IDX_2EFCAB7C5E237E061D775834, ADD UNIQUE INDEX UNIQ_2EFCAB7C5E237E061D775834 (name, value)'); + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..4226d0f --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +DigiComp.AssetAttributes +------------------------ + +![Build status](https://ci.digital-competence.de/api/badges/Packages/DigiComp.AssetAttributes/status.svg) + +This package allows you to extend Neos Media assets with custom attributes. + +This extension overwrites the original edit template of `neos/media-browser` - that way you get all of your custom properties and matching form fields in the classic asset editor. + +You can add new attributes, by adding them to your `Settings.yaml`: + + +```yaml +DigiComp: + AssetAttributes: + customAssetProperties: + author: + type: 'textarea' # or empty for textfields + position: 'end' +``` + +Each asset instance will get an "attributes" property introduced, you can work with in PHP or DQL. + +Examples: + +- Working with Asset instances: + ```php + $assetObject->getAttributes()->set($key, new AssetAttribute($key, $value)); + ``` + +- querying with DQL: + ```dql + SELECT att FROM Neos\Media\Domain\Model\Asset a JOIN a.attributes att + ``` + +- working with query objects: + ```php + $query = new \Neos\Flow\Persistence\Doctrine\Query(\Neos\Media\Domain\Model\Asset::class); + $query->setOrderings(['attributes.value' => \Neos\Flow\Persistence\QueryInterface::ORDER_ASCENDING]); + ``` diff --git a/Resources/Private/Templates/Asset/Edit.html b/Resources/Private/Templates/Asset/Edit.html new file mode 100644 index 0000000..523d7d3 --- /dev/null +++ b/Resources/Private/Templates/Asset/Edit.html @@ -0,0 +1,206 @@ +{namespace m=Neos\Media\ViewHelpers} +{namespace neos=Neos\Neos\ViewHelpers} +{namespace assetAttributes=DigiComp\AssetAttributes\ViewHelpers} + + +Edit view + + + + +

{neos:backend.translate(id: 'connectionError', package: 'Neos.Media.Browser')}

+

{connectionError.message} ({connectionError.code})

+
+ + + + + + +
+
+ +
+
+
+ + + {neos:backend.translate(id: 'basics', package: 'Neos.Media.Browser')} + + + + + + + + + {neos:backend.translate(id: 'basics', package: 'Neos.Media.Browser')} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ {neos:backend.translate(id: 'metadata', package: 'Neos.Media.Browser')} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{neos:backend.translate(id: 'mediaSource', package: 'Neos.Media.Browser')}{assetProxy.assetSource.label}
{neos:backend.translate(id: 'metadata.filename', package: 'Neos.Media.Browser')}{assetProxy.filename}
{neos:backend.translate(id: 'metadata.lastModified', package: 'Neos.Media.Browser')}{assetProxy.lastModified -> m:format.relativeDate()}
{neos:backend.translate(id: 'metadata.fileSize', package: 'Neos.Media.Browser')}{assetProxy.fileSize -> f:format.bytes()}
{neos:backend.translate(id: 'metadata.iptcProperties.CopyrightNotice', package: 'Neos.Media.Browser')}{assetProxy.iptcProperties.CopyrightNotice}
{neos:backend.translate(id: 'metadata.dimensions', package: 'Neos.Media.Browser')}{assetProxy.widthInPixels} x {assetProxy.heightInPixels}
{neos:backend.translate(id: 'metadata.type', package: 'Neos.Media.Browser')}{assetProxy.mediaType}
{neos:backend.translate(id: 'metadata.identifier', package: 'Neos.Media.Browser')}{assetProxy.localAssetIdentifier}
+ + + {neos:backend.translate(id: 'relatedNodes', quantity: '{assetProxy.asset.usageCount}', arguments: {0: assetProxy.asset.usageCount}, package: 'Neos.Media.Browser')} + + +
+
+
+ +
+
+ +
+
+
+
+ +
+ {neos:backend.translate(id: 'message.reallyDeleteAsset', arguments: {0: assetProxy.label}, package: 'Neos.Media.Browser')} +
+
+
+

+ {neos:backend.translate(id: 'message.willBeDeleted', package: 'Neos.Media.Browser')}
+ {neos:backend.translate(id: 'message.operationCannotBeUndone', package: 'Neos.Media.Browser')} +

+
+
+
+ +
+
+
+
+ +
+ + + +
+
+ +
+
+
+ + + +
+ + {assetProxy.label} + +
+
+ + + + diff --git a/Tests/Functional/AssetAttributeTest.php b/Tests/Functional/AssetAttributeTest.php new file mode 100644 index 0000000..9682bf6 --- /dev/null +++ b/Tests/Functional/AssetAttributeTest.php @@ -0,0 +1,50 @@ +persistenceManager instanceof PersistenceManager) { + $this->markTestSkipped('Doctrine persistence is not enabled'); + } + $this->resourceManager = $this->objectManager->get(ResourceManager::class); + } + + /** + * @test + */ + public function itFindsAssetsHavingAnOwnAttribute(): void + { + $resource = $this->resourceManager->importResourceFromContent('hello world', 'hello-world.txt'); + $asset = new Asset($resource); + $asset->getAttributes()->set('author', new AssetAttribute('author', 'Joe')); + $this->persistenceManager->add($asset); + + $resource2 = $this->resourceManager->importResourceFromContent('hello universe', 'hello-universe.txt'); + $asset2 = new Asset($resource2); + $this->persistenceManager->add($asset2); + + $this->persistenceManager->persistAll(); + + $query = new Query(Asset::class); + $result = $query->matching($query->logicalAnd([ + $query->equals('attributes.name', 'author'), + $query->equals('attributes.value', 'Joe'), + ]))->execute(); + static::assertCount(1, $result); + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..d72b014 --- /dev/null +++ b/composer.json @@ -0,0 +1,38 @@ +{ + "description": "Attributes for media assets", + "type": "neos-package", + "name": "digicomp/asset-attributes", + "require": { + "neos/media": "^7.3.5 | ^8.0", + "php": "^7.4 | ^8.0" + }, + "suggest": { + "neos/media-browser": "^7.3.10 | ^8.0" + }, + "autoload": { + "psr-4": { + "DigiComp\\AssetAttributes\\": "Classes/" + } + }, + "extra": { + "neos": { + "package-key": "DigiComp.AssetAttributes" + } + }, + "authors": [ + { + "name": "Ferdinand Kuhl", + "email": "f.kuhl@digital-competence.de", + "homepage": "https://www.digital-competence.de", + "role": "Developer" + } + ], + "license": "MIT", + "homepage": "https://git.digital-competence.de/Packages/DigiComp.AssetAttributes", + "keywords": [ + "Neos", + "Flow", + "media", + "asset" + ] +}