diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index ffc9d42c..8012d757 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -11,7 +11,7 @@ jobs: matrix: include: - php-version: 8.3 - docker-image: anzusystems/php:3.0.0-php83-cli-vipsffmpeg + docker-image: anzusystems/php:3.1.0-php83-cli-vipsffmpeg services: elasticsearch: diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fcecbf9..8facd855 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [1.8.0](https://github.com/anzusystems/core-dam-bundle/compare/1.7.0...1.8.0) (2024-02-27) +### Changes +* New elasticsearch index `distribution` +* Allow search asset by multiple licences +* Support for `LicenceGroups` + * updated voters + ## [1.7.0](https://github.com/anzusystems/core-dam-bundle/compare/1.6.1...1.7.0) (2024-01-18) ### Changes * `FileFactory` clears png metadata when file is created from storage diff --git a/Dockerfile b/Dockerfile index 2e975ab5..f40db7a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM anzusystems/php:3.0.0-php83-cli-vipsffmpeg +FROM anzusystems/php:3.1.0-php83-cli-vipsffmpeg # ### Basic arguments and variables ARG DOCKER_USER_ID diff --git a/composer.json b/composer.json index 2e8ce648..68b5c5c5 100644 --- a/composer.json +++ b/composer.json @@ -8,14 +8,15 @@ "require": { "php": ">=8.3", "ext-json": "*", + "ext-pcntl": "*", "ext-mongodb": "*", "ext-redis": "*", "ext-simplexml": "*", "ext-vips": "*", "ext-xmlreader": "*", "ext-zend-opcache": "*", - "anzusystems/common-bundle": "^4.0", - "doctrine/orm": ">=2.10", + "anzusystems/common-bundle": "^5.0", + "doctrine/orm": "^2.10", "elasticsearch/elasticsearch": "^8.6", "fakerphp/faker": "^1.20", "google/apiclient": "^2.12", diff --git a/psalm.xml b/psalm.xml index 4a80151f..c7db75af 100644 --- a/psalm.xml +++ b/psalm.xml @@ -20,6 +20,7 @@ + diff --git a/src/Controller/Api/Adm/V1/AssetController.php b/src/Controller/Api/Adm/V1/AssetController.php index 095a2a01..fa538033 100644 --- a/src/Controller/Api/Adm/V1/AssetController.php +++ b/src/Controller/Api/Adm/V1/AssetController.php @@ -17,6 +17,7 @@ use AnzuSystems\CoreDamBundle\Domain\AssetMetadata\AssetMetadataBulkFacade; use AnzuSystems\CoreDamBundle\Elasticsearch\Decorator\AssetAdmElasticsearchDecorator; use AnzuSystems\CoreDamBundle\Elasticsearch\SearchDto\AssetAdmSearchDto; +use AnzuSystems\CoreDamBundle\Elasticsearch\SearchDto\AssetAdmSearchLicenceCollectionDto; use AnzuSystems\CoreDamBundle\Entity\Asset; use AnzuSystems\CoreDamBundle\Entity\AssetFile; use AnzuSystems\CoreDamBundle\Entity\AssetLicence; @@ -32,8 +33,10 @@ use AnzuSystems\CoreDamBundle\Security\Permission\DamPermissions; use AnzuSystems\SerializerBundle\Attributes\SerializeParam; use AnzuSystems\SerializerBundle\Exception\SerializerException; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\NonUniqueResultException; +use Elastic\Elasticsearch\Exception\ElasticsearchException; use OpenApi\Attributes as OA; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -77,23 +80,43 @@ public function create(#[SerializeParam] AssetAdmCreateDto $assetDto, AssetLicen * @throws SerializerException * @throws ValidationException * @throws AppReadOnlyModeException + * @throws ElasticsearchException */ #[Route('/licence/{assetLicence}/search', name: 'search_by_licence', methods: [Request::METHOD_GET])] #[OAParameterPath('search', description: 'Searched asset.'), OAResponse([AssetAdmListDto::class])] - public function searchByLicence(AssetLicence $assetLicence, #[SerializeParam] AssetAdmSearchDto $searchDto): JsonResponse + public function searchByLicence(AssetLicence $assetLicence, #[SerializeParam] AssetAdmSearchLicenceCollectionDto $searchDto): JsonResponse { App::throwOnReadOnlyMode(); $this->denyAccessUnlessGranted(DamPermissions::DAM_ASSET_VIEW, $assetLicence); - $searchDto->setLicences([$assetLicence]); + $searchDto->setLicences(new ArrayCollection([$assetLicence])); return $this->okResponse( - $this->elasticSearch->searchInfiniteList($searchDto, $assetLicence->getExtSystem()) + $this->elasticSearch->searchInfiniteList($searchDto) ); } /** * @throws SerializerException * @throws ValidationException + * @throws AppReadOnlyModeException + * @throws ElasticsearchException + */ + #[Route('/licence/search', name: 'search', methods: [Request::METHOD_GET])] + #[OAParameterPath('search', description: 'Searched asset.'), OAResponse([AssetAdmListDto::class])] + public function search(#[SerializeParam] AssetAdmSearchLicenceCollectionDto $searchDto): JsonResponse + { + App::throwOnReadOnlyMode(); + $this->denyAccessUnlessGranted(DamPermissions::DAM_ASSET_VIEW, $searchDto); + + return $this->okResponse( + $this->elasticSearch->searchInfiniteList($searchDto) + ); + } + + /** + * @throws SerializerException + * @throws ValidationException + * @throws ElasticsearchException */ #[Route('/ext-system/{extSystem}/search', name: 'search_by_ext_system', methods: [Request::METHOD_GET])] #[OAParameterPath('search', description: 'Searched asset.'), OAResponse([AssetAdmListDto::class])] @@ -102,7 +125,7 @@ public function searchByExtSystem(ExtSystem $extSystem, #[SerializeParam] AssetA $this->denyAccessUnlessGranted(DamPermissions::DAM_ASSET_VIEW, $extSystem); return $this->okResponse( - $this->elasticSearch->searchInfiniteList($searchDto, $extSystem) + $this->elasticSearch->searchInfiniteListByExtSystem($searchDto, $extSystem) ); } diff --git a/src/Controller/Api/Adm/V1/AssetLicenceGroupController.php b/src/Controller/Api/Adm/V1/AssetLicenceGroupController.php new file mode 100644 index 00000000..806b22b7 --- /dev/null +++ b/src/Controller/Api/Adm/V1/AssetLicenceGroupController.php @@ -0,0 +1,102 @@ +denyAccessUnlessGranted(DamPermissions::DAM_ASSET_LICENCE_GROUP_VIEW, $assetLicenceGroup); + + return $this->okResponse($assetLicenceGroup); + } + + /** + * Get list of items. + * + * @throws ORMException + */ + #[Route('', name: 'get_list', methods: [Request::METHOD_GET])] + #[OAResponseInfiniteList(AssetLicenceGroup::class)] + public function getList(ApiParams $apiParams): JsonResponse + { + $this->denyAccessUnlessGranted(DamPermissions::DAM_ASSET_LICENCE_GROUP_LIST); + + return $this->okResponse( + $this->repository->findByApiParamsWithInfiniteListing($apiParams), + ); + } + + /** + * Create item. + * + * @throws AppReadOnlyModeException + * @throws ValidationException + */ + #[Route('', name: 'create', methods: [Request::METHOD_POST])] + #[OARequest(AssetLicenceGroup::class), OAResponseCreated(AssetLicenceGroup::class), OAResponseValidation] + public function create(#[SerializeParam] AssetLicenceGroup $licenceGroup): JsonResponse + { + App::throwOnReadOnlyMode(); + $this->denyAccessUnlessGranted(DamPermissions::DAM_ASSET_LICENCE_GROUP_CREATE); + + return $this->createdResponse( + $this->facade->create($licenceGroup) + ); + } + + /** + * Update item. + * + * @throws AppReadOnlyModeException + * @throws ValidationException + */ + #[Route('/{assetLicenceGroup}', name: 'update', methods: [Request::METHOD_PUT])] + #[OAParameterPath('assetLicence'), OARequest(AssetLicenceGroup::class), OAResponse(AssetLicenceGroup::class), OAResponseValidation] + public function update(AssetLicenceGroup $assetLicenceGroup, #[SerializeParam] AssetLicenceGroup $newAssetLicenceGroup): JsonResponse + { + App::throwOnReadOnlyMode(); + $this->denyAccessUnlessGranted(DamPermissions::DAM_ASSET_LICENCE_GROUP_UPDATE, $assetLicenceGroup); + + return $this->okResponse( + $this->facade->update($assetLicenceGroup, $newAssetLicenceGroup) + ); + } +} diff --git a/src/Controller/Api/Adm/V1/DistributionController.php b/src/Controller/Api/Adm/V1/DistributionController.php index d87e0a37..f2d134cf 100644 --- a/src/Controller/Api/Adm/V1/DistributionController.php +++ b/src/Controller/Api/Adm/V1/DistributionController.php @@ -5,16 +5,21 @@ namespace AnzuSystems\CoreDamBundle\Controller\Api\Adm\V1; use AnzuSystems\CommonBundle\ApiFilter\ApiParams; +use AnzuSystems\CommonBundle\Exception\ValidationException; use AnzuSystems\CommonBundle\Model\OpenApi\Parameter\OAParameterPath; use AnzuSystems\CommonBundle\Model\OpenApi\Response\OAResponse; use AnzuSystems\CoreDamBundle\Controller\Api\AbstractApiController; use AnzuSystems\CoreDamBundle\Domain\Distribution\DistributionPermissionFacade; +use AnzuSystems\CoreDamBundle\Elasticsearch\Decorator\DistributionAdmElasticsearchDecorator; +use AnzuSystems\CoreDamBundle\Elasticsearch\SearchDto\DistributionAdmSearchDto; use AnzuSystems\CoreDamBundle\Entity\Asset; use AnzuSystems\CoreDamBundle\Entity\AssetFile; use AnzuSystems\CoreDamBundle\Entity\Distribution; use AnzuSystems\CoreDamBundle\Model\Decorator\DistributionServiceAuthorization; use AnzuSystems\CoreDamBundle\Repository\Decorator\DistributionRepositoryDecorator; use AnzuSystems\CoreDamBundle\Security\Permission\DamPermissions; +use AnzuSystems\SerializerBundle\Attributes\SerializeParam; +use AnzuSystems\SerializerBundle\Exception\SerializerException; use Doctrine\ORM\Exception\ORMException; use OpenApi\Attributes as OA; use Symfony\Component\HttpFoundation\JsonResponse; @@ -28,9 +33,23 @@ final class DistributionController extends AbstractApiController public function __construct( private readonly DistributionRepositoryDecorator $distributionRepository, private readonly DistributionPermissionFacade $distributionPermissionFacade, + private readonly DistributionAdmElasticsearchDecorator $elasticSearch, ) { } + /** + * @throws SerializerException + * @throws ValidationException + */ + #[Route('/search', name: 'search', methods: [Request::METHOD_GET])] + #[OAParameterPath('search', description: 'Searched.'), OAResponse([Distribution::class])] + public function search(#[SerializeParam] DistributionAdmSearchDto $searchDto): JsonResponse + { + $this->denyAccessUnlessGranted(DamPermissions::DAM_DISTRIBUTION_VIEW, $searchDto); + + return $this->okResponse($this->elasticSearch->searchInfiniteList($searchDto)); + } + /** * Get one item. */ diff --git a/src/Controller/Api/Adm/V1/JwDistributionController.php b/src/Controller/Api/Adm/V1/JwDistributionController.php index 20e48e94..50e81744 100644 --- a/src/Controller/Api/Adm/V1/JwDistributionController.php +++ b/src/Controller/Api/Adm/V1/JwDistributionController.php @@ -11,7 +11,7 @@ use AnzuSystems\Contracts\Exception\AppReadOnlyModeException; use AnzuSystems\CoreDamBundle\App; use AnzuSystems\CoreDamBundle\Controller\Api\AbstractApiController; -use AnzuSystems\CoreDamBundle\Domain\JwDistribution\JwAbstractDistributionFacade; +use AnzuSystems\CoreDamBundle\Domain\JwDistribution\JwDistributionFacade; use AnzuSystems\CoreDamBundle\Entity\AssetFile; use AnzuSystems\CoreDamBundle\Entity\JwDistribution; use AnzuSystems\CoreDamBundle\Entity\YoutubeDistribution; @@ -30,7 +30,7 @@ final class JwDistributionController extends AbstractApiController { public function __construct( - private readonly JwAbstractDistributionFacade $jwDistributionFacade, + private readonly JwDistributionFacade $jwDistributionFacade, private readonly AssetRepository $assetRepository, ) { } diff --git a/src/Controller/Api/Adm/V1/YoutubeDistributionController.php b/src/Controller/Api/Adm/V1/YoutubeDistributionController.php index 034a76f0..551b9b49 100644 --- a/src/Controller/Api/Adm/V1/YoutubeDistributionController.php +++ b/src/Controller/Api/Adm/V1/YoutubeDistributionController.php @@ -12,7 +12,7 @@ use AnzuSystems\CoreDamBundle\App; use AnzuSystems\CoreDamBundle\Controller\Api\AbstractApiController; use AnzuSystems\CoreDamBundle\Distribution\Modules\Youtube\YoutubeAuthenticator; -use AnzuSystems\CoreDamBundle\Domain\YoutubeDistribution\YoutubeAbstractDistributionFacade; +use AnzuSystems\CoreDamBundle\Domain\YoutubeDistribution\YoutubeDistributionFacade; use AnzuSystems\CoreDamBundle\Entity\AssetFile; use AnzuSystems\CoreDamBundle\Entity\YoutubeDistribution; use AnzuSystems\CoreDamBundle\Exception\DomainException; @@ -38,7 +38,7 @@ final class YoutubeDistributionController extends AbstractApiController { public function __construct( - private readonly YoutubeAbstractDistributionFacade $youtubeDistributionFacade, + private readonly YoutubeDistributionFacade $youtubeDistributionFacade, private readonly YoutubeAuthenticator $youtubeAuthenticator, private readonly AssetRepository $assetRepository, ) { diff --git a/src/Distribution/DistributionBroker.php b/src/Distribution/DistributionBroker.php index 615713fb..81c5c151 100644 --- a/src/Distribution/DistributionBroker.php +++ b/src/Distribution/DistributionBroker.php @@ -6,6 +6,7 @@ use AnzuSystems\CommonBundle\Util\ResourceLocker; use AnzuSystems\CoreDamBundle\Domain\Asset\AssetPropertiesRefresher; +use AnzuSystems\CoreDamBundle\Domain\Distribution\DistributionStatusFacade; use AnzuSystems\CoreDamBundle\Domain\Distribution\DistributionStatusManager; use AnzuSystems\CoreDamBundle\Entity\Distribution; use AnzuSystems\CoreDamBundle\Exception\DistributionFailedException; @@ -27,12 +28,12 @@ final class DistributionBroker { use MessageBusAwareTrait; - private const LOCK_PREFIX_NAME = 'distribution_'; + private const string LOCK_PREFIX_NAME = 'distribution_'; public function __construct( private readonly ResourceLocker $resourceLocker, private readonly DistributionRepository $repository, - private readonly DistributionStatusManager $distributionStatusManager, + private readonly DistributionStatusFacade $distributionStatusFacade, private readonly ModuleProvider $moduleProvider, private readonly DamLogger $damLogger, private readonly AssetPropertiesRefresher $propertiesRefresher, @@ -59,7 +60,7 @@ public function startDistribution(Distribution $distribution): void public function redistribute(Distribution $distribution): void { $this->resourceLocker->lock($this->getLockName($distribution)); - $this->distributionStatusManager->toDistributing($distribution); + $this->distributionStatusFacade->toDistributing($distribution); if ($this->repository->isNotBlockByNotFinished($distribution)) { $this->messageBus->dispatch(new DistributeMessage($distribution)); @@ -73,14 +74,14 @@ public function redistribute(Distribution $distribution): void */ public function toDistributing(Distribution $distribution): void { - $this->distributionStatusManager->toDistributing($distribution); + $this->distributionStatusFacade->toDistributing($distribution); $module = $this->moduleProvider->provideModule($distribution->getDistributionService(), true); try { $module->distribute($distribution); } catch (DistributionFailedException $exception) { $distribution->setFailReason($exception->getFailReason()); - $this->distributionStatusManager->toFailed($distribution); + $this->distributionStatusFacade->toFailed($distribution); return; } catch (Throwable $e) { @@ -93,11 +94,11 @@ public function toDistributing(Distribution $distribution): void ); $distribution->setFailReason(DistributionFailReason::Unknown); - $this->distributionStatusManager->toFailed($distribution); + $this->distributionStatusFacade->toFailed($distribution); } if ($module instanceof RemoteProcessingDistributionModuleInterface) { - $this->distributionStatusManager->toRemoteProcessing($distribution); + $this->distributionStatusFacade->toRemoteProcessing($distribution); $this->messageBus->dispatch(new DistributionRemoteProcessingCheckMessage($distribution)); return; @@ -126,7 +127,7 @@ public function checkRemoteProcessing(Distribution $distribution): void $exception->getMessage(), )); $distribution->setFailReason(DistributionFailReason::RemoteProcessFailed); - $this->distributionStatusManager->toFailed($distribution); + $this->distributionStatusFacade->toFailed($distribution); } } } @@ -136,7 +137,7 @@ public function checkRemoteProcessing(Distribution $distribution): void */ public function remoteProcessed(Distribution $distribution): void { - $this->distributionStatusManager->toDistributed($distribution); + $this->distributionStatusFacade->toDistributed($distribution); $this->messageBus->dispatch(new AssetRefreshPropertiesMessage($distribution->getAssetId())); foreach ($distribution->getBlocks() as $blockedDistribution) { diff --git a/src/Domain/AssetLicenceGroup/AssetLicenceGroupFacade.php b/src/Domain/AssetLicenceGroup/AssetLicenceGroupFacade.php new file mode 100644 index 00000000..0afc789b --- /dev/null +++ b/src/Domain/AssetLicenceGroup/AssetLicenceGroupFacade.php @@ -0,0 +1,39 @@ +validator->validate($assetLicenceGroup); + + return $this->assetLicenceGroupManager->create($assetLicenceGroup); + } + + /** + * @throws ValidationException + */ + public function update(AssetLicenceGroup $assetLicenceGroup, AssetLicenceGroup $newAssetLicenceGroup): AssetLicenceGroup + { + $this->validator->validate($newAssetLicenceGroup, $assetLicenceGroup); + + return $this->assetLicenceGroupManager->update($assetLicenceGroup, $newAssetLicenceGroup); + } +} diff --git a/src/Domain/AssetLicenceGroup/AssetLicenceGroupManager.php b/src/Domain/AssetLicenceGroup/AssetLicenceGroupManager.php new file mode 100644 index 00000000..a5c7595b --- /dev/null +++ b/src/Domain/AssetLicenceGroup/AssetLicenceGroupManager.php @@ -0,0 +1,41 @@ +trackCreation($assetLicenceGroup); + $this->entityManager->persist($assetLicenceGroup); + $this->flush($flush); + + return $assetLicenceGroup; + } + + public function update(AssetLicenceGroup $assetLicenceGroup, AssetLicenceGroup $newAssetLicenceGroup, bool $flush = true): AssetLicenceGroup + { + $this->trackModification($assetLicenceGroup); + $assetLicenceGroup + ->setName($newAssetLicenceGroup->getName()) + ; + $this->colUpdate( + oldCollection: $assetLicenceGroup->getLicences(), + newCollection: $newAssetLicenceGroup->getLicences() + ); + $this->flush($flush); + + return $assetLicenceGroup; + } + + public function delete(AssetLicenceGroup $assetLicenceGroup, bool $flush = true): void + { + $this->entityManager->remove($assetLicenceGroup); + $this->flush($flush); + } +} diff --git a/src/Domain/CustomDistribution/CustomDistributionFacade.php b/src/Domain/CustomDistribution/CustomDistributionFacade.php index b2112663..7eee6151 100644 --- a/src/Domain/CustomDistribution/CustomDistributionFacade.php +++ b/src/Domain/CustomDistribution/CustomDistributionFacade.php @@ -48,7 +48,7 @@ public function preparePayload(AssetFile $assetFile, string $distributionService } $distribution = $adapter->preparePayload($assetFile, $distributionService); - $this->distributionBodyBuilder->setBaseFields($distributionService, $distribution); + $this->distributionBodyBuilder->setBaseFields($assetFile, $distributionService, $distribution); $this->distributionBodyBuilder->setWriterProperties( $distributionService, $assetFile->getAsset(), diff --git a/src/Domain/Distribution/AbstractDistributionFacade.php b/src/Domain/Distribution/AbstractDistributionFacade.php index ac0cb744..74c1c673 100644 --- a/src/Domain/Distribution/AbstractDistributionFacade.php +++ b/src/Domain/Distribution/AbstractDistributionFacade.php @@ -48,6 +48,9 @@ public function distribute(AssetFile $assetFile, Distribution $distribution): Di { $distribution->setAssetId((string) $assetFile->getAsset()->getId()); $distribution->setAssetFileId((string) $assetFile->getId()); + $distribution->setAsset($assetFile->getAsset()); + $distribution->setAssetFile($assetFile); + $this->validator->validate($distribution); $this->distributionManagerProvider->get($distribution::class)->create($distribution); diff --git a/src/Domain/Distribution/DistributionBodyBuilder.php b/src/Domain/Distribution/DistributionBodyBuilder.php index 627c8c9f..b552d32a 100644 --- a/src/Domain/Distribution/DistributionBodyBuilder.php +++ b/src/Domain/Distribution/DistributionBodyBuilder.php @@ -21,12 +21,20 @@ public function __construct( ) { } - public function setBaseFields(string $distributionService, Distribution $targetDistribution): void - { + public function setBaseFields( + AssetFile $assetFile, + string $distributionService, + Distribution $targetDistribution + ): void { $targetDistribution->setId(''); $targetDistribution->setCreatedBy($this->userProvider->getCurrentUser()); $targetDistribution->setModifiedBy($this->userProvider->getCurrentUser()); $targetDistribution->setDistributionService($distributionService); + $targetDistribution->setAssetFile($assetFile); + $targetDistribution->setAsset($assetFile->getAsset()); + + $targetDistribution->setAssetFileId((string) $assetFile->getId()); + $targetDistribution->setAssetId((string) $assetFile->getAsset()->getId()); } public function getKeywords(AssetFile $assetFile): array diff --git a/src/Domain/Distribution/DistributionStatusFacade.php b/src/Domain/Distribution/DistributionStatusFacade.php new file mode 100644 index 00000000..7e79ccec --- /dev/null +++ b/src/Domain/Distribution/DistributionStatusFacade.php @@ -0,0 +1,63 @@ +setStatus($distribution, DistributionProcessStatus::Distributing); + } + + public function toRemoteProcessing(Distribution $distribution): Distribution + { + return $this->setStatus($distribution, DistributionProcessStatus::RemoteProcessing); + } + + public function toDistributed(Distribution $distribution): Distribution + { + return $this->setStatus($distribution, DistributionProcessStatus::Distributed); + } + + public function toFailed(Distribution $distribution): Distribution + { + return $this->setStatus($distribution, DistributionProcessStatus::Failed); + } + + private function setStatus(Distribution $distribution, DistributionProcessStatus $status): Distribution + { + try { + $this->distributionStatusManager->beginTransaction(); + + $this->distributionStatusManager->setStatus($distribution, $status); + $this->indexManager->index($distribution); + $this->distributionStatusManager->commit(); + } catch (Throwable $exception) { + $this->distributionStatusManager->rollback(); + + throw new RuntimeException('distribution_status_change_failed', 0, $exception); + } + + $this->dispatcher->dispatch(new DistributionStatusEvent($distribution, $status)); + + return $distribution; + } +} diff --git a/src/Domain/Distribution/DistributionStatusManager.php b/src/Domain/Distribution/DistributionStatusManager.php index be968b60..24680ccd 100644 --- a/src/Domain/Distribution/DistributionStatusManager.php +++ b/src/Domain/Distribution/DistributionStatusManager.php @@ -4,50 +4,27 @@ namespace AnzuSystems\CoreDamBundle\Domain\Distribution; +use AnzuSystems\CoreDamBundle\Domain\AbstractManager; use AnzuSystems\CoreDamBundle\Entity\Distribution; -use AnzuSystems\CoreDamBundle\Event\DistributionStatusEvent; use AnzuSystems\CoreDamBundle\Model\Enum\DistributionProcessStatus; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -final class DistributionStatusManager +final class DistributionStatusManager extends AbstractManager { public function __construct( - private readonly EventDispatcherInterface $dispatcher, private readonly DistributionManagerProvider $managerProvider, ) { } - public function toDistributing(Distribution $distribution): Distribution - { - return $this->setStatus($distribution, DistributionProcessStatus::Distributing); - } - - public function toRemoteProcessing(Distribution $distribution): Distribution - { - return $this->setStatus($distribution, DistributionProcessStatus::RemoteProcessing); - } - - public function toDistributed(Distribution $distribution): Distribution - { - return $this->setStatus($distribution, DistributionProcessStatus::Distributed); - } - - public function toFailed(Distribution $distribution): Distribution - { - return $this->setStatus($distribution, DistributionProcessStatus::Failed); - } - - private function setStatus(Distribution $distribution, DistributionProcessStatus $status): Distribution - { + public function setStatus( + Distribution $distribution, + DistributionProcessStatus $status, + bool $flush = true + ): Distribution { $distribution->setStatus($status); - $this->managerProvider->get($distribution::class)->updateExisting($distribution); - - $this->dispatcher->dispatch( - new DistributionStatusEvent( - $distribution, - $status - ) - ); + $this->managerProvider + ->get($distribution::class) + ->updateExisting($distribution, $flush) + ; return $distribution; } diff --git a/src/Domain/JwDistribution/JwAbstractDistributionFacade.php b/src/Domain/JwDistribution/JwDistributionFacade.php similarity index 84% rename from src/Domain/JwDistribution/JwAbstractDistributionFacade.php rename to src/Domain/JwDistribution/JwDistributionFacade.php index 6c29cd7f..f6c055c1 100644 --- a/src/Domain/JwDistribution/JwAbstractDistributionFacade.php +++ b/src/Domain/JwDistribution/JwDistributionFacade.php @@ -10,7 +10,7 @@ use AnzuSystems\CoreDamBundle\Entity\JwDistribution; use Doctrine\ORM\NonUniqueResultException; -class JwAbstractDistributionFacade extends AbstractDistributionFacade +class JwDistributionFacade extends AbstractDistributionFacade { public function __construct( private readonly DistributionBodyBuilder $distributionBodyBuilder, @@ -23,7 +23,7 @@ public function __construct( public function preparePayload(AssetFile $assetFile, string $distributionService): JwDistribution { $distribution = new JwDistribution(); - $this->distributionBodyBuilder->setBaseFields($distributionService, $distribution); + $this->distributionBodyBuilder->setBaseFields($assetFile, $distributionService, $distribution); $this->distributionBodyBuilder->setWriterProperties( $distributionService, diff --git a/src/Domain/User/UserManager.php b/src/Domain/User/UserManager.php index 29feceb3..0c5e0902 100644 --- a/src/Domain/User/UserManager.php +++ b/src/Domain/User/UserManager.php @@ -27,6 +27,7 @@ public function deletePersonalData(DamUser $user, bool $flush = true): DamUser $user ->setAssetLicences(new ArrayCollection()) ->setUserToExtSystems(new ArrayCollection()) + ->setLicenceGroups(new ArrayCollection()) ->setPermissions([]) ->setPermissionGroups(new ArrayCollection()) ->setAllowedAssetExternalProviders([]) diff --git a/src/Domain/YoutubeDistribution/YoutubeAbstractDistributionFacade.php b/src/Domain/YoutubeDistribution/YoutubeDistributionFacade.php similarity index 95% rename from src/Domain/YoutubeDistribution/YoutubeAbstractDistributionFacade.php rename to src/Domain/YoutubeDistribution/YoutubeDistributionFacade.php index b799b92e..dace1536 100644 --- a/src/Domain/YoutubeDistribution/YoutubeAbstractDistributionFacade.php +++ b/src/Domain/YoutubeDistribution/YoutubeDistributionFacade.php @@ -19,7 +19,7 @@ use Psr\Cache\InvalidArgumentException; use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException; -class YoutubeAbstractDistributionFacade extends AbstractDistributionFacade +class YoutubeDistributionFacade extends AbstractDistributionFacade { public function __construct( private readonly YoutubeDataStorage $youtubeDataStorage, @@ -42,7 +42,7 @@ public function preparePayload(AssetFile $assetFile, string $distributionService } $distribution = new YoutubeDistribution(); - $this->distributionBodyBuilder->setBaseFields($distributionService, $distribution); + $this->distributionBodyBuilder->setBaseFields($assetFile, $distributionService, $distribution); $this->distributionBodyBuilder->setWriterProperties( $distributionService, $assetFile->getAsset(), diff --git a/src/Elasticsearch/Decorator/AssetAdmElasticsearchDecorator.php b/src/Elasticsearch/Decorator/AssetAdmElasticsearchDecorator.php index 8fde8c77..11c9e540 100644 --- a/src/Elasticsearch/Decorator/AssetAdmElasticsearchDecorator.php +++ b/src/Elasticsearch/Decorator/AssetAdmElasticsearchDecorator.php @@ -9,10 +9,14 @@ use AnzuSystems\CommonBundle\Traits\ValidatorAwareTrait; use AnzuSystems\CoreDamBundle\Elasticsearch\ElasticSearch; use AnzuSystems\CoreDamBundle\Elasticsearch\SearchDto\AssetAdmSearchDto; +use AnzuSystems\CoreDamBundle\Elasticsearch\SearchDto\AssetAdmSearchLicenceCollectionDto; use AnzuSystems\CoreDamBundle\Entity\Asset; +use AnzuSystems\CoreDamBundle\Entity\AssetLicence; use AnzuSystems\CoreDamBundle\Entity\ExtSystem; +use AnzuSystems\CoreDamBundle\Exception\ForbiddenOperationException; use AnzuSystems\CoreDamBundle\Model\Dto\Asset\AssetAdmListDto; use AnzuSystems\SerializerBundle\Exception\SerializerException; +use Elastic\Elasticsearch\Exception\ElasticsearchException; final class AssetAdmElasticsearchDecorator { @@ -26,18 +30,43 @@ public function __construct( /** * @throws SerializerException * @throws ValidationException + * @throws ElasticsearchException */ - public function searchInfiniteList(AssetAdmSearchDto $searchDto, ExtSystem $extSystem): ApiInfiniteResponseList + public function searchInfiniteList(AssetAdmSearchLicenceCollectionDto $searchDto): ApiInfiniteResponseList { $this->validator->validate($searchDto); - $list = $this->elasticSearch->searchInfiniteList($searchDto, $extSystem); - - return $list - ->setData( - array_map( - fn (Asset $asset): AssetAdmListDto => AssetAdmListDto::getInstance($asset), - $list->getData() - ) - ); + + $licence = $searchDto->getLicences()->first(); + if (false === ($licence instanceof AssetLicence)) { + throw new ForbiddenOperationException(ForbiddenOperationException::ERROR_MESSAGE); + } + + return $this->decorate( + $this->elasticSearch->searchInfiniteList($searchDto, $licence->getExtSystem()) + ); + } + + /** + * @throws SerializerException + * @throws ElasticsearchException + * @throws ValidationException + */ + public function searchInfiniteListByExtSystem(AssetAdmSearchDto $searchDto, ExtSystem $extSystem): ApiInfiniteResponseList + { + $this->validator->validate($searchDto); + + return $this->decorate( + $this->elasticSearch->searchInfiniteList($searchDto, $extSystem) + ); + } + + private function decorate(ApiInfiniteResponseList $list): ApiInfiniteResponseList + { + return $list->setData( + array_map( + fn (Asset $asset): AssetAdmListDto => AssetAdmListDto::getInstance($asset), + $list->getData() + ) + ); } } diff --git a/src/Elasticsearch/Decorator/DistributionAdmElasticsearchDecorator.php b/src/Elasticsearch/Decorator/DistributionAdmElasticsearchDecorator.php new file mode 100644 index 00000000..bf1d3ee8 --- /dev/null +++ b/src/Elasticsearch/Decorator/DistributionAdmElasticsearchDecorator.php @@ -0,0 +1,46 @@ +validator->validate($searchDto); + + $licence = $searchDto->getLicences()->first(); + if (false === ($licence instanceof AssetLicence)) { + throw new ForbiddenOperationException(ForbiddenOperationException::ERROR_MESSAGE); + } + + return $this->elasticSearch->searchInfiniteList($searchDto, $licence->getExtSystem()); + } +} diff --git a/src/Elasticsearch/IndexBuilder.php b/src/Elasticsearch/IndexBuilder.php index 3add84c2..995c28c3 100644 --- a/src/Elasticsearch/IndexBuilder.php +++ b/src/Elasticsearch/IndexBuilder.php @@ -5,6 +5,7 @@ namespace AnzuSystems\CoreDamBundle\Elasticsearch; use AnzuSystems\Contracts\Entity\Interfaces\BaseIdentifiableInterface; +use AnzuSystems\Contracts\Entity\Interfaces\IndexableInterface; use AnzuSystems\CoreDamBundle\Command\Traits\OutputUtilTrait; use AnzuSystems\CoreDamBundle\Domain\Configuration\ExtSystemConfigurationProvider; use AnzuSystems\CoreDamBundle\Elasticsearch\IndexDefinition\IndexDefinitionFactory; @@ -92,15 +93,14 @@ private function buildIndex(RebuildIndexConfig $config): void { $this->writeln(sprintf('Indexing %s...', $config->getIndexName())); - /** @var AbstractAnzuRepository $repo */ - $repo = $this->entityManager->getRepository($this->indexSettings->getEntityClassName($config->getIndexName())); - - $count = $repo->getAllCountForIndexRebuild($config); + /** @var AbstractAnzuRepository $repository */ + $repository = $this->entityManager->getRepository($this->indexSettings->getEntityClassName($config->getIndexName())); + $count = $repository->getAllCountForIndexRebuild($config); $progressBar = $this->outputUtil->createProgressBar($count); $this->configureProgressBar($progressBar); if ($config->hasNotIdUntil()) { - $maxId = $repo->getMaxIdForIndexRebuild($config); + $maxId = $repository->getMaxIdForIndexRebuild($config); if (empty($maxId)) { $this->writeln(sprintf('Skipping %s, nothing to index...', $config->getIndexName())); @@ -111,7 +111,7 @@ private function buildIndex(RebuildIndexConfig $config): void do { $payload = ['body' => []]; /** @var ExtSystemIndexableInterface $item */ - foreach ($repo->getAllForIndexRebuild($config) as $item) { + foreach ($repository->getAllForIndexRebuild($config) as $item) { $payload['body'][] = [ 'index' => [ '_index' => $this->indexSettings->getFullIndexNameByEntity($item), diff --git a/src/Elasticsearch/IndexDefinition/CustomDataIndexDefinitionFactory.php b/src/Elasticsearch/IndexDefinition/CustomDataIndexDefinitionFactory.php index 50863abf..57bed1d0 100644 --- a/src/Elasticsearch/IndexDefinition/CustomDataIndexDefinitionFactory.php +++ b/src/Elasticsearch/IndexDefinition/CustomDataIndexDefinitionFactory.php @@ -12,7 +12,7 @@ final class CustomDataIndexDefinitionFactory { - private const METADATA_PREFIX = 'custom_data_'; + private const string METADATA_PREFIX = 'custom_data_'; private readonly iterable $indexBuilders; diff --git a/src/Elasticsearch/IndexFactory/DistributionIndexFactory.php b/src/Elasticsearch/IndexFactory/DistributionIndexFactory.php new file mode 100644 index 00000000..d3b36f35 --- /dev/null +++ b/src/Elasticsearch/IndexFactory/DistributionIndexFactory.php @@ -0,0 +1,34 @@ + $entity->getId(), + 'extId' => $entity->getExtId(), + 'service' => $entity->getDiscriminator(), + 'serviceSlug' => $entity->getDistributionService(), + 'status' => $entity->getStatus()->toString(), + 'assetId' => $entity->getAsset()?->getId(), + 'assetFileId' => $entity->getAssetFile()?->getId(), + 'licenceId' => $entity->getAssetFile()?->getLicence()?->getId(), + 'createdAt' => $entity->getCreatedAt()->getTimestamp(), + ]; + } +} diff --git a/src/Elasticsearch/IndexSettings.php b/src/Elasticsearch/IndexSettings.php index 7f9b92b4..b71e0fad 100644 --- a/src/Elasticsearch/IndexSettings.php +++ b/src/Elasticsearch/IndexSettings.php @@ -4,15 +4,16 @@ namespace AnzuSystems\CoreDamBundle\Elasticsearch; +use AnzuSystems\Contracts\Entity\Interfaces\IndexableInterface; use AnzuSystems\CoreDamBundle\App; use AnzuSystems\CoreDamBundle\Domain\Configuration\ConfigurationProvider; use AnzuSystems\CoreDamBundle\Entity\Interfaces\ExtSystemIndexableInterface; use AnzuSystems\CoreDamBundle\Model\Enum\Language; -final class IndexSettings +final readonly class IndexSettings { public function __construct( - private readonly ConfigurationProvider $configurationProvider, + private ConfigurationProvider $configurationProvider, ) { } @@ -39,7 +40,7 @@ public function getFullIndexNameBySlug(string $indexName, string $slug): string public function getFullIndexNameByEntity(ExtSystemIndexableInterface $entity): string { - return $this->getFullIndexNameBySlug($entity::getResourceName(), $entity->getExtSystem()->getSlug()); + return $this->getFullIndexNameBySlug($entity::getIndexName(), $entity->getExtSystem()->getSlug()); } public function hasElasticLanguageDictionary(Language $language): bool diff --git a/src/Elasticsearch/QueryFactory/AssetQueryFactory.php b/src/Elasticsearch/QueryFactory/AssetQueryFactory.php index e1a82811..5b067cf2 100644 --- a/src/Elasticsearch/QueryFactory/AssetQueryFactory.php +++ b/src/Elasticsearch/QueryFactory/AssetQueryFactory.php @@ -7,6 +7,7 @@ use AnzuSystems\CoreDamBundle\Domain\CustomForm\CustomFormProvider; use AnzuSystems\CoreDamBundle\Elasticsearch\IndexDefinition\CustomDataIndexDefinitionFactory; use AnzuSystems\CoreDamBundle\Elasticsearch\SearchDto\AssetAdmSearchDto; +use AnzuSystems\CoreDamBundle\Elasticsearch\SearchDto\AssetAdmSearchLicenceCollectionDto; use AnzuSystems\CoreDamBundle\Elasticsearch\SearchDto\SearchDtoInterface; use AnzuSystems\CoreDamBundle\Entity\AssetLicence; use AnzuSystems\CoreDamBundle\Entity\CustomFormElement; @@ -23,6 +24,7 @@ public function getSupportedSearchDtoClasses(): array { return [ AssetAdmSearchDto::class, + AssetAdmSearchLicenceCollectionDto::class, ]; } @@ -38,8 +40,6 @@ protected function getMust(SearchDtoInterface $searchDto, ExtSystem $extSystem): $customDataFields = array_unique($customDataFields); $customDataFields = array_merge($customDataFields, ['title']); - // $searchDto->getText() - if ($searchDto->getText()) { return [ 'multi_match' => [ @@ -122,16 +122,41 @@ protected function getFilter(SearchDtoInterface $searchDto): array $this->applyRangeFilter($filter, 'slotsCount', $searchDto->getSlotsCountFrom(), $searchDto->getSlotsCountUntil()); $this->applyRangeFilter($filter, 'createdAt', $searchDto->getCreatedAtFrom()?->getTimestamp(), $searchDto->getCreatedAtUntil()?->getTimestamp()); - if (false === empty($searchDto->getLicences())) { - $filter[] = ['terms' => ['licence' => array_map( - fn (AssetLicence $assetLicence): int => (int) $assetLicence->getId(), - $searchDto->getLicences() - )]]; + if ($searchDto instanceof AssetAdmSearchLicenceCollectionDto) { + $this->applyLicenceCollectionFilter($filter, $searchDto); } return $filter; } + private function applyLicenceCollectionFilter(array &$filter, AssetAdmSearchLicenceCollectionDto $dto): void + { + if ($dto->getLicences()->isEmpty()) { + return; + } + + if (1 === $dto->getLicences()->count()) { + $licence = $dto->getLicences()->first(); + if (false === $licence instanceof AssetLicence) { + return; + } + + $filter[] = ['terms' => ['licence' => [(int) $licence->getId()]]]; + + return; + } + + $terms = []; + foreach ($dto->getLicences() as $licenceId) { + $terms[] = ['term' => ['licence' => $licenceId->getId()]]; + } + $filter[] = [ + 'bool' => [ + 'should' => $terms, + ], + ]; + } + private function applyRangeFilter(array &$filter, string $key, ?int $from, ?int $until): void { $range = []; diff --git a/src/Elasticsearch/QueryFactory/DistributionQueryFactory.php b/src/Elasticsearch/QueryFactory/DistributionQueryFactory.php new file mode 100644 index 00000000..d806c427 --- /dev/null +++ b/src/Elasticsearch/QueryFactory/DistributionQueryFactory.php @@ -0,0 +1,56 @@ +getService())) { + $filter[] = ['terms' => ['service' => [$searchDto->getService()]]]; + } + if (false === (null === $searchDto->getServiceSlug())) { + $filter[] = ['terms' => ['serviceSlug' => [$searchDto->getServiceSlug()]]]; + } + if (false === (null === $searchDto->getStatus())) { + $filter[] = ['terms' => ['status' => [$searchDto->getStatus()->toString()]]]; + } + if (false === (null === $searchDto->getExtId())) { + $filter[] = ['terms' => ['extId' => [$searchDto->getExtId()]]]; + } + + if (false === $searchDto->getLicences()->isEmpty()) { + $terms = []; + foreach ($searchDto->getLicences() as $licenceId) { + $terms[] = ['term' => ['licenceId' => $licenceId->getId()]]; + } + + $filter[] = [ + 'bool' => [ + 'should' => $terms, + ], + ]; + } + + return $filter; + } +} diff --git a/src/Elasticsearch/SearchDto/AssetAdmSearchDto.php b/src/Elasticsearch/SearchDto/AssetAdmSearchDto.php index bd5307e5..4143907f 100644 --- a/src/Elasticsearch/SearchDto/AssetAdmSearchDto.php +++ b/src/Elasticsearch/SearchDto/AssetAdmSearchDto.php @@ -10,13 +10,16 @@ use AnzuSystems\CoreDamBundle\Model\Enum\AssetStatus; use AnzuSystems\CoreDamBundle\Model\Enum\AssetType; use AnzuSystems\CoreDamBundle\Model\Enum\ImageOrientation; +use AnzuSystems\CoreDamBundle\Serializer\Handler\Handlers\LicenceCollectionHandler; use AnzuSystems\CoreDamBundle\Validator\Constraints as AppAssert; use AnzuSystems\SerializerBundle\Attributes\Serialize; use AnzuSystems\SerializerBundle\Handler\Handlers\ArrayStringHandler; use DateTimeImmutable; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Symfony\Component\Validator\Constraints as Assert; -final class AssetAdmSearchDto extends AbstractSearchDto +class AssetAdmSearchDto extends AbstractSearchDto { #[Serialize] protected string $text = ''; @@ -85,10 +88,14 @@ final class AssetAdmSearchDto extends AbstractSearchDto #[Assert\Count(max: 20, maxMessage: ValidationException::ERROR_FIELD_LENGTH_MAX)] protected array $distributedInServices = []; - /** - * @var array - */ - protected array $licences = []; + #[Serialize(handler: LicenceCollectionHandler::class, type: AssetLicence::class)] + #[Assert\Count( + min: 1, + max: 20, + minMessage: ValidationException::ERROR_FIELD_RANGE_MIN, + maxMessage: ValidationException::ERROR_FIELD_RANGE_MAX + )] + protected Collection $licences; #[Serialize] private ?int $shortestDimensionFrom = null; @@ -307,18 +314,6 @@ public function setInPodcast(?bool $inPodcast): self return $this; } - public function getLicences(): array - { - return $this->licences; - } - - public function setLicences(array $licences): self - { - $this->licences = $licences; - - return $this; - } - public function getPixelSizeFrom(): ?int { return $this->pixelSizeFrom; diff --git a/src/Elasticsearch/SearchDto/AssetAdmSearchLicenceCollectionDto.php b/src/Elasticsearch/SearchDto/AssetAdmSearchLicenceCollectionDto.php new file mode 100644 index 00000000..a1cd971e --- /dev/null +++ b/src/Elasticsearch/SearchDto/AssetAdmSearchLicenceCollectionDto.php @@ -0,0 +1,56 @@ +setLicences(new ArrayCollection()); + } + + /** + * @return Collection + */ + public function getLicences(): Collection + { + return $this->licences; + } + + /** + * @param Collection $licences + */ + public function setLicences(Collection $licences): self + { + $this->licences = $licences; + + return $this; + } +} diff --git a/src/Elasticsearch/SearchDto/DistributionAdmSearchDto.php b/src/Elasticsearch/SearchDto/DistributionAdmSearchDto.php new file mode 100644 index 00000000..88c1bdbb --- /dev/null +++ b/src/Elasticsearch/SearchDto/DistributionAdmSearchDto.php @@ -0,0 +1,115 @@ +setLicences(new ArrayCollection()); + } + + public function getIndexName(): string + { + return Distribution::getResourceName(); + } + + public function getService(): ?string + { + return $this->service; + } + + public function setService(?string $service): void + { + $this->service = $service; + } + + public function getServiceSlug(): ?string + { + return $this->serviceSlug; + } + + public function setServiceSlug(?string $serviceSlug): void + { + $this->serviceSlug = $serviceSlug; + } + + public function getStatus(): ?DistributionProcessStatus + { + return $this->status; + } + + public function setStatus(?DistributionProcessStatus $status): void + { + $this->status = $status; + } + + public function getExtId(): ?string + { + return $this->extId; + } + + public function setExtId(?string $extId): void + { + $this->extId = $extId; + } + + /** + * @return Collection + */ + public function getLicences(): Collection + { + return $this->licences; + } + + /** + * @param Collection $licences + */ + public function setLicences(Collection $licences): self + { + $this->licences = $licences; + + return $this; + } +} diff --git a/src/Elasticsearch/SearchDto/LicenceCollectionInterface.php b/src/Elasticsearch/SearchDto/LicenceCollectionInterface.php new file mode 100644 index 00000000..802ef3ab --- /dev/null +++ b/src/Elasticsearch/SearchDto/LicenceCollectionInterface.php @@ -0,0 +1,16 @@ + + */ + public function getLicences(): Collection; +} diff --git a/src/Entity/Asset.php b/src/Entity/Asset.php index cf3faabb..6ca29392 100644 --- a/src/Entity/Asset.php +++ b/src/Entity/Asset.php @@ -50,7 +50,6 @@ class Asset implements use NotifyToTrait; #[ORM\ManyToMany(targetEntity: Author::class, fetch: App::DOCTRINE_EXTRA_LAZY, indexBy: 'id')] - #[ORM\JoinTable] private Collection $authors; #[ORM\OneToOne(targetEntity: AssetFile::class)] @@ -59,7 +58,6 @@ class Asset implements private ?AssetFile $mainFile; #[ORM\ManyToMany(targetEntity: Keyword::class, fetch: App::DOCTRINE_EXTRA_LAZY, indexBy: 'id')] - #[ORM\JoinTable] private Collection $keywords; #[ORM\OneToMany(mappedBy: 'asset', targetEntity: PodcastEpisode::class, fetch: App::DOCTRINE_EXTRA_LAZY)] @@ -380,4 +378,9 @@ public function setAssetFileProperties(AssetFileProperties $assetFileProperties) return $this; } + + public static function getIndexName(): string + { + return self::getResourceName(); + } } diff --git a/src/Entity/AssetLicence.php b/src/Entity/AssetLicence.php index c79f9af6..ba435617 100644 --- a/src/Entity/AssetLicence.php +++ b/src/Entity/AssetLicence.php @@ -54,6 +54,14 @@ class AssetLicence implements IdentifiableInterface, UserTrackingInterface, Time #[ORM\Cache(usage: App::CACHE_STRATEGY)] private ExtSystem $extSystem; + /** + * Asset belongs to external system (e.g. Blogs, CMS, ...) + */ + #[ORM\ManyToMany(targetEntity: AssetLicenceGroup::class, inversedBy: 'licences')] + #[ORM\JoinTable('asset_licence_in_group')] + #[Serialize(handler: EntityIdHandler::class, type: AssetLicenceGroup::class)] + private Collection $groups; + /** * External system licence ID (e.g. BlogId) */ @@ -79,6 +87,7 @@ public function __construct() $this->setExtId(null); $this->setUsers(new ArrayCollection()); $this->setLimitedFiles(false); + $this->setGroups(new ArrayCollection()); } public function getName(): string @@ -158,4 +167,21 @@ public function getLicence(): self { return $this; } + + /** + * @return Collection + */ + public function getGroups(): Collection + { + return $this->groups; + } + + /** + * @param Collection $groups + */ + public function setGroups(Collection $groups): self + { + $this->groups = $groups; + return $this; + } } diff --git a/src/Entity/AssetLicenceGroup.php b/src/Entity/AssetLicenceGroup.php new file mode 100644 index 00000000..eda0c9b7 --- /dev/null +++ b/src/Entity/AssetLicenceGroup.php @@ -0,0 +1,129 @@ +setName(''); + $this->setLicences(new ArrayCollection()); + $this->setExtSystem(new ExtSystem()); + $this->setUsers(new ArrayCollection()); + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + return $this; + } + + public function getExtSystem(): ExtSystem + { + return $this->extSystem; + } + + public function setExtSystem(ExtSystem $extSystem): self + { + $this->extSystem = $extSystem; + return $this; + } + + /** + * @return Collection + */ + public function getLicences(): Collection + { + return $this->licences; + } + + /** + * @param Collection $licences + */ + public function setLicences(Collection $licences): self + { + $this->licences = $licences; + return $this; + } + + public function getUsers(): Collection + { + return $this->users; + } + + public function setUsers(Collection $users): self + { + $this->users = $users; + return $this; + } +} diff --git a/src/Entity/Author.php b/src/Entity/Author.php index 1871a148..7d7058fa 100644 --- a/src/Entity/Author.php +++ b/src/Entity/Author.php @@ -126,4 +126,9 @@ public function setType(AuthorType $type): self return $this; } + + public static function getIndexName(): string + { + return self::getResourceName(); + } } diff --git a/src/Entity/DamUser.php b/src/Entity/DamUser.php index 44381994..8e603c6a 100644 --- a/src/Entity/DamUser.php +++ b/src/Entity/DamUser.php @@ -24,7 +24,6 @@ abstract class DamUser extends AnzuUser protected array $allowedDistributionServices = []; #[ORM\ManyToMany(targetEntity: AssetLicence::class, inversedBy: 'users', fetch: App::DOCTRINE_EXTRA_LAZY, indexBy: 'id')] - #[ORM\JoinTable] #[Serialize(handler: EntityIdHandler::class, type: AssetLicence::class)] protected Collection $assetLicences; @@ -33,6 +32,12 @@ abstract class DamUser extends AnzuUser #[Serialize(handler: EntityIdHandler::class, type: ExtSystem::class)] protected Collection $adminToExtSystems; + #[ORM\ManyToMany(targetEntity: AssetLicenceGroup::class, inversedBy: 'users', fetch: App::DOCTRINE_EXTRA_LAZY, indexBy: 'id')] + #[ORM\JoinTable(name: 'user_in_licence_groups')] + #[Serialize(handler: EntityIdHandler::class, type: ExtSystem::class)] + #[ORM\Cache(usage: App::CACHE_STRATEGY)] + protected Collection $licenceGroups; + #[ORM\ManyToMany(targetEntity: ExtSystem::class, fetch: App::DOCTRINE_EXTRA_LAZY, indexBy: 'id')] #[ORM\JoinTable(name: 'users_to_ext_systems')] #[Serialize(handler: EntityIdHandler::class, type: ExtSystem::class)] @@ -136,10 +141,29 @@ public function getUserToExtSystems(): Collection * * @param Collection $userToExtSystems */ - public function setUserToExtSystems(Collection $userToExtSystems): self + public function setUserToExtSystems(Collection $userToExtSystems): static { $this->userToExtSystems = $userToExtSystems; return $this; } + + /** + * @return Collection + */ + public function getLicenceGroups(): Collection + { + return $this->licenceGroups; + } + + /** + * @template TKey of array-key + * + * @param Collection $licenceGroups + */ + public function setLicenceGroups(Collection $licenceGroups): static + { + $this->licenceGroups = $licenceGroups; + return $this; + } } diff --git a/src/Entity/Distribution.php b/src/Entity/Distribution.php index c891e7c9..0f271ec4 100644 --- a/src/Entity/Distribution.php +++ b/src/Entity/Distribution.php @@ -10,11 +10,13 @@ use AnzuSystems\Contracts\Entity\Interfaces\UuidIdentifiableInterface; use AnzuSystems\Contracts\Entity\Traits\TimeTrackingTrait; use AnzuSystems\Contracts\Entity\Traits\UserTrackingTrait; +use AnzuSystems\CoreDamBundle\Entity\Interfaces\ExtSystemIndexableInterface; use AnzuSystems\CoreDamBundle\Entity\Interfaces\NotifiableInterface; use AnzuSystems\CoreDamBundle\Entity\Traits\NotifyToTrait; use AnzuSystems\CoreDamBundle\Entity\Traits\UuidIdentityTrait; use AnzuSystems\CoreDamBundle\Model\Enum\DistributionFailReason; use AnzuSystems\CoreDamBundle\Model\Enum\DistributionProcessStatus; +use AnzuSystems\CoreDamBundle\Repository\DistributionRepository; use AnzuSystems\CoreDamBundle\Validator\Constraints as AppAssert; use AnzuSystems\SerializerBundle\Attributes\Serialize; use AnzuSystems\SerializerBundle\Handler\Handlers\EntityIdHandler; @@ -25,23 +27,25 @@ use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; -#[ORM\Entity] +#[ORM\Entity(repositoryClass: DistributionRepository::class)] #[AppAssert\Distribution] #[ORM\InheritanceType('SINGLE_TABLE')] #[ORM\Index(fields: ['assetFileId'], name: 'IDX_asset_file_id')] #[ORM\Index(fields: ['assetId'], name: 'IDX_asset_id')] #[ORM\Index(fields: ['assetFileId', 'distributionService'], name: 'IDX_asset_file_id_distribution_service')] #[ORM\Index(fields: ['status'], name: 'IDX_status')] -class Distribution implements +abstract class Distribution implements UuidIdentifiableInterface, UserTrackingInterface, TimeTrackingInterface, - NotifiableInterface + NotifiableInterface, + ExtSystemIndexableInterface { use UuidIdentityTrait; use UserTrackingTrait; use TimeTrackingTrait; use NotifyToTrait; + public const string INDEX_NAME = 'distribution'; #[ORM\Column(type: Types::STRING, length: 36)] protected string $assetFileId; @@ -57,8 +61,15 @@ class Distribution implements #[Assert\NotBlank(message: ValidationException::ERROR_FIELD_EMPTY)] protected string $distributionService; + #[ORM\ManyToOne(targetEntity: Asset::class)] + #[ORM\JoinColumn(name: 'distribution_asset_id', nullable: true, onDelete: 'CASCADE')] + protected ?Asset $asset; + + #[ORM\ManyToOne(targetEntity: AssetFile::class)] + #[ORM\JoinColumn(name: 'distribution_asset_file_id', nullable: true, onDelete: 'CASCADE')] + protected ?AssetFile $assetFile; + #[ORM\ManyToMany(targetEntity: self::class, mappedBy: 'blockedBy')] - #[ORM\JoinTable] protected Collection $blocks; #[ORM\ManyToMany(targetEntity: self::class, inversedBy: 'blocks')] @@ -93,6 +104,16 @@ public function __construct() $this->setDistributionData([]); $this->setFailReason(DistributionFailReason::None); $this->setPublishAt(null); + $this->setAsset(null); + $this->setAssetFile(null); + } + + abstract public function getDiscriminator(): string; + + + public static function getIndexName(): string + { + return self::INDEX_NAME; } public function getPublishAt(): ?DateTimeImmutable @@ -231,4 +252,30 @@ public function setFailReason(DistributionFailReason $failReason): static return $this; } + + public function getAsset(): ?Asset + { + return $this->asset; + } + + public function setAsset(?Asset $asset): void + { + $this->asset = $asset; + } + + public function getAssetFile(): ?AssetFile + { + return $this->assetFile; + } + + public function setAssetFile(?AssetFile $assetFile): void + { + $this->assetFile = $assetFile; + } + + public function getExtSystem(): ExtSystem + { + // todo after release, make assetFile non nullable + return $this->assetFile?->getExtSystem() ?? new ExtSystem(); + } } diff --git a/src/Entity/Interfaces/ExtSystemIndexableInterface.php b/src/Entity/Interfaces/ExtSystemIndexableInterface.php index 3ea680ff..fe2ec66b 100644 --- a/src/Entity/Interfaces/ExtSystemIndexableInterface.php +++ b/src/Entity/Interfaces/ExtSystemIndexableInterface.php @@ -8,4 +8,5 @@ interface ExtSystemIndexableInterface extends ExtSystemInterface, IndexableInterface { + public static function getIndexName(): string; } diff --git a/src/Entity/JwDistribution.php b/src/Entity/JwDistribution.php index 14bffa9e..58d4b7cd 100644 --- a/src/Entity/JwDistribution.php +++ b/src/Entity/JwDistribution.php @@ -13,6 +13,8 @@ #[ORM\Entity(repositoryClass: JwDistributionRepository::class)] class JwDistribution extends Distribution { + public const string DISCRIMINATOR = 'jwDistribution'; + #[ORM\Embedded(JwTexts::class)] #[Assert\Valid] #[Serialize] @@ -35,4 +37,9 @@ public function setTexts(JwTexts $texts): self return $this; } + + public function getDiscriminator(): string + { + return self::DISCRIMINATOR; + } } diff --git a/src/Entity/Keyword.php b/src/Entity/Keyword.php index d772eec0..9f965769 100644 --- a/src/Entity/Keyword.php +++ b/src/Entity/Keyword.php @@ -91,4 +91,9 @@ public function setFlags(KeywordFlags $flags): self return $this; } + + public static function getIndexName(): string + { + return self::getResourceName(); + } } diff --git a/src/Entity/Traits/NotifyToTrait.php b/src/Entity/Traits/NotifyToTrait.php index cb98aa21..3190da2f 100644 --- a/src/Entity/Traits/NotifyToTrait.php +++ b/src/Entity/Traits/NotifyToTrait.php @@ -10,7 +10,6 @@ trait NotifyToTrait { #[ORM\ManyToOne(targetEntity: DamUser::class)] - #[ORM\JoinColumn] protected ?DamUser $notifyTo = null; public function getNotifyTo(): ?DamUser diff --git a/src/Entity/Traits/UserTrackingTrait.php b/src/Entity/Traits/UserTrackingTrait.php index 0b949325..394aed96 100644 --- a/src/Entity/Traits/UserTrackingTrait.php +++ b/src/Entity/Traits/UserTrackingTrait.php @@ -14,12 +14,10 @@ trait UserTrackingTrait { #[ORM\ManyToOne(targetEntity: DamUser::class)] - #[ORM\JoinColumn] #[Serialize(handler: EntityIdHandler::class, type: new ContainerParam(DamUser::class))] protected DamUser $createdBy; #[ORM\ManyToOne(targetEntity: DamUser::class)] - #[ORM\JoinColumn] #[Serialize(handler: EntityIdHandler::class, type: new ContainerParam(DamUser::class))] protected DamUser $modifiedBy; diff --git a/src/Entity/YoutubeDistribution.php b/src/Entity/YoutubeDistribution.php index 5fdba3ef..f4cab626 100644 --- a/src/Entity/YoutubeDistribution.php +++ b/src/Entity/YoutubeDistribution.php @@ -18,6 +18,8 @@ #[AppAssert\Youtube] class YoutubeDistribution extends Distribution { + public const string DISCRIMINATOR = 'youtubeDistribution'; + #[ORM\Embedded(YoutubeTexts::class)] #[Assert\Valid] #[Serialize] @@ -126,4 +128,9 @@ public function setLanguage(string $language): self return $this; } + + public function getDiscriminator(): string + { + return self::DISCRIMINATOR; + } } diff --git a/src/Repository/AssetLicenceGroupRepository.php b/src/Repository/AssetLicenceGroupRepository.php new file mode 100644 index 00000000..04b07607 --- /dev/null +++ b/src/Repository/AssetLicenceGroupRepository.php @@ -0,0 +1,21 @@ + + * + * @method AssetLicenceGroup|null find($id, $lockMode = null, $lockVersion = null) + * @method AssetLicenceGroup|null findOneBy($id, $lockMode = null, $lockVersion = null) + */ +final class AssetLicenceGroupRepository extends AbstractAnzuRepository +{ + protected function getEntityClass(): string + { + return AssetLicenceGroup::class; + } +} diff --git a/src/Repository/AssetLicenceRepository.php b/src/Repository/AssetLicenceRepository.php index 2803ba45..61a0432b 100644 --- a/src/Repository/AssetLicenceRepository.php +++ b/src/Repository/AssetLicenceRepository.php @@ -6,6 +6,8 @@ use AnzuSystems\CoreDamBundle\Entity\AssetLicence; use AnzuSystems\CoreDamBundle\Entity\ExtSystem; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; /** * @extends AbstractAnzuRepository @@ -25,6 +27,15 @@ public function findOneByExtSystemAndExtId(ExtSystem $extSystem, string $extId): ]); } + public function findByIds(array $ids): Collection + { + return new ArrayCollection( + $this->findBy([ + 'id' => $ids, + ]) + ); + } + protected function getEntityClass(): string { return AssetLicence::class; diff --git a/src/Resources/config/elasticsearch/distribution.php b/src/Resources/config/elasticsearch/distribution.php new file mode 100644 index 00000000..b9db1cd3 --- /dev/null +++ b/src/Resources/config/elasticsearch/distribution.php @@ -0,0 +1,42 @@ +parameters() + ->set( + 'anzu_systems.dam_bundle.index_distribution', + [ + 'id' => [ + 'type' => 'keyword', + ], + 'extId' => [ + 'type' => 'keyword', + ], + 'service' => [ + 'type' => 'keyword', + ], + 'serviceSlug' => [ + 'type' => 'keyword', + ], + 'status' => [ + 'type' => 'keyword', + ], + 'assetId' => [ + 'type' => 'keyword', + ], + 'assetFileId' => [ + 'type' => 'keyword', + ], + 'licenceId' => [ + 'type' => 'keyword', + ], + 'createdAt' => [ + 'type' => 'date', + ], + ] + ); +}; diff --git a/src/Resources/config/services.php b/src/Resources/config/services.php index 68770770..f4e78aae 100644 --- a/src/Resources/config/services.php +++ b/src/Resources/config/services.php @@ -5,6 +5,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; use AnzuSystems\CoreDamBundle\Elasticsearch\IndexBuilder; +use AnzuSystems\CoreDamBundle\Entity\Distribution; use AnzuSystems\CoreDamBundle\FileSystem\NameGenerator\DirectoryNameGenerator; use AnzuSystems\CoreDamBundle\FileSystem\NameGenerator\DirectoryNamGeneratorInterface; use AnzuSystems\CoreDamBundle\FileSystem\NameGenerator\FileNameGenerator; @@ -35,6 +36,7 @@ 'asset' => param('anzu_systems.dam_bundle.index_asset'), 'keyword' => param('anzu_systems.dam_bundle.index_keyword'), 'author' => param('anzu_systems.dam_bundle.index_author'), + Distribution::INDEX_NAME => param('anzu_systems.dam_bundle.index_distribution'), ]) ; diff --git a/src/Security/Permission/DamPermissions.php b/src/Security/Permission/DamPermissions.php index 4d25703d..87e3437b 100644 --- a/src/Security/Permission/DamPermissions.php +++ b/src/Security/Permission/DamPermissions.php @@ -65,6 +65,13 @@ class DamPermissions public const DAM_ASSET_LICENCE_LIST = 'dam_assetLicence_list'; public const DAM_ASSET_LICENCE_UI = 'dam_assetLicence_ui'; + // AssetLicenceGroup + public const DAM_ASSET_LICENCE_GROUP_CREATE = 'dam_assetLicenceGroup_create'; + public const DAM_ASSET_LICENCE_GROUP_UPDATE = 'dam_assetLicenceGroup_update'; + public const DAM_ASSET_LICENCE_GROUP_VIEW = 'dam_assetLicenceGroup_view'; + public const DAM_ASSET_LICENCE_GROUP_LIST = 'dam_assetLicenceGroup_list'; + public const DAM_ASSET_LICENCE_GROUP_UI = 'dam_assetLicenceGroup_ui'; + // Author public const DAM_AUTHOR_CREATE = 'dam_author_create'; public const DAM_AUTHOR_UPDATE = 'dam_author_update'; @@ -107,6 +114,7 @@ class DamPermissions // Distribution public const DAM_DISTRIBUTION_ACCESS = 'dam_distribution_access'; + public const DAM_DISTRIBUTION_VIEW = 'dam_distribution_view'; // Asset External Provider public const DAM_ASSET_EXTERNAL_PROVIDER_ACCESS = 'dam_assetExternalProvider_access'; @@ -229,6 +237,13 @@ class DamPermissions self::DAM_JOB_CREATE, self::DAM_JOB_DELETE, self::DAM_JOB_UI, + self::DAM_DISTRIBUTION_VIEW, + self::DAM_DISTRIBUTION_ACCESS, + self::DAM_ASSET_LICENCE_GROUP_VIEW, + self::DAM_ASSET_LICENCE_GROUP_UPDATE, + self::DAM_ASSET_LICENCE_GROUP_LIST, + self::DAM_ASSET_LICENCE_GROUP_CREATE, + self::DAM_ASSET_LICENCE_GROUP_UI, ]; public static function default(int $defaultGrant = Grant::DENY): array diff --git a/src/Security/Voter/AssetLicenceAwareVoter.php b/src/Security/Voter/AssetLicenceAwareVoter.php index 4ca1c936..5b88d04f 100644 --- a/src/Security/Voter/AssetLicenceAwareVoter.php +++ b/src/Security/Voter/AssetLicenceAwareVoter.php @@ -17,6 +17,7 @@ */ final class AssetLicenceAwareVoter extends AbstractVoter { + use LicenceVoterTrait; /** * @param AssetLicenceInterface $subject * @param DamUser $user @@ -33,11 +34,7 @@ protected function permissionVote(string $attribute, mixed $subject, AnzuUser $u $assetLicence = $subject->getLicence(); - if ($user->getAssetLicences()->containsKey((int) $assetLicence->getId())) { - return true; - } - - return $user->getAdminToExtSystems()->containsKey((int) $assetLicence->getExtSystem()->getId()); + return $this->licencePermissionGranted($assetLicence, $user); } protected function getSupportedPermissions(): array diff --git a/src/Security/Voter/BaseVoter.php b/src/Security/Voter/BaseVoter.php index edf042ce..689aac7a 100644 --- a/src/Security/Voter/BaseVoter.php +++ b/src/Security/Voter/BaseVoter.php @@ -26,6 +26,10 @@ protected function getSupportedPermissions(): array DamPermissions::DAM_JOB_VIEW, DamPermissions::DAM_JOB_CREATE, DamPermissions::DAM_JOB_DELETE, + DamPermissions::DAM_ASSET_LICENCE_GROUP_CREATE, + DamPermissions::DAM_ASSET_LICENCE_GROUP_LIST, + DamPermissions::DAM_ASSET_LICENCE_GROUP_UPDATE, + DamPermissions::DAM_ASSET_LICENCE_GROUP_VIEW, ]; } } diff --git a/src/Security/Voter/CollectionListAwareVoter.php b/src/Security/Voter/CollectionListAwareVoter.php new file mode 100644 index 00000000..c79d3096 --- /dev/null +++ b/src/Security/Voter/CollectionListAwareVoter.php @@ -0,0 +1,55 @@ + + */ +final class CollectionListAwareVoter extends AbstractVoter +{ + use LicenceVoterTrait; + + /** + * @param ExtSystemInterface $subject + * @param DamUser $user + */ + protected function permissionVote(string $attribute, mixed $subject, AnzuUser $user): bool + { + if (false === parent::permissionVote($attribute, $subject, $user)) { + return false; + } + if (false === ($subject instanceof LicenceCollectionInterface)) { + return false; + } + if ($subject->getLicences()->isEmpty()) { + return false; + } + + foreach ($subject->getLicences() as $licence) { + if (false === $this->licencePermissionGranted($licence, $user)) { + return false; + } + } + + return true; + } + + protected function getSupportedPermissions(): array + { + return [ + DamPermissions::DAM_ASSET_VIEW, + DamPermissions::DAM_DISTRIBUTION_VIEW, + ]; + } +} diff --git a/src/Security/Voter/LicenceVoterTrait.php b/src/Security/Voter/LicenceVoterTrait.php new file mode 100644 index 00000000..9ce9555f --- /dev/null +++ b/src/Security/Voter/LicenceVoterTrait.php @@ -0,0 +1,29 @@ +getAdminToExtSystems()->containsKey((int) $licence->getExtSystem()->getId()) + || $user->getUserToExtSystems()->containsKey((int) $licence->getExtSystem()->getId()) + || $user->getAssetLicences()->containsKey((int) $licence->getId()) + || $this->licenceGroupGrants($licence, $user) + ; + } + + private function licenceGroupGrants(AssetLicence $licence, DamUser $user): bool + { + foreach ($user->getLicenceGroups() as $licenceGroup) { + if ($licenceGroup->getLicences()->containsKey((int) $licence->getId())) { + return true; + } + } + + return false; + } +} diff --git a/src/Serializer/Handler/Handlers/LicenceCollectionHandler.php b/src/Serializer/Handler/Handlers/LicenceCollectionHandler.php new file mode 100644 index 00000000..fd9f18ce --- /dev/null +++ b/src/Serializer/Handler/Handlers/LicenceCollectionHandler.php @@ -0,0 +1,68 @@ + (int) $item, + explode(',', $value) + ); + + if (count($ids) > self::MAX_IDS) { + throw new SerializerException('Licence collection size '); + } + + return $this->entityIdHandler->deserialize($ids, $metadata); + } + + throw new SerializerException('Unsupported value for ' . self::class . '::' . __FUNCTION__); + } +} diff --git a/src/Validator/Constraints/AssetLicenceGroup.php b/src/Validator/Constraints/AssetLicenceGroup.php new file mode 100644 index 00000000..abbe1576 --- /dev/null +++ b/src/Validator/Constraints/AssetLicenceGroup.php @@ -0,0 +1,18 @@ +getLicences() as $licence) { + if ($licence->getExtSystem()->getId() !== $value->getExtSystem()->getId()) { + $this->context->buildViolation(ValidationException::ERROR_FIELD_INVALID) + ->atPath('licences') + ->addViolation(); + + break; + } + } + } +} diff --git a/src/Validator/Constraints/DistributionValidator.php b/src/Validator/Constraints/DistributionValidator.php index 8fcd2e76..2edb249b 100644 --- a/src/Validator/Constraints/DistributionValidator.php +++ b/src/Validator/Constraints/DistributionValidator.php @@ -24,9 +24,9 @@ final class DistributionValidator extends ConstraintValidator { - private const BLOCKED_BY_NOT_SUPPORTED = 'blocked_by_not_supported'; - private const BLOCKED_BY_INVALID_ASSET_FILE = 'blocked_by_invalid_asset_file'; - private const BLOCKED_BY_STRATEGY = 'blocked_by_strategy'; + private const string BLOCKED_BY_NOT_SUPPORTED = 'blocked_by_not_supported'; + private const string BLOCKED_BY_INVALID_ASSET_FILE = 'blocked_by_invalid_asset_file'; + private const string BLOCKED_BY_STRATEGY = 'blocked_by_strategy'; public function __construct( private readonly ExtSystemConfigurationProvider $extSystemConfigurationProvider, diff --git a/src/Validator/Constraints/YoutubeValidator.php b/src/Validator/Constraints/YoutubeValidator.php index c35077b1..2c2b780c 100644 --- a/src/Validator/Constraints/YoutubeValidator.php +++ b/src/Validator/Constraints/YoutubeValidator.php @@ -6,7 +6,7 @@ use AnzuSystems\CommonBundle\Exception\ValidationException; use AnzuSystems\CoreDamBundle\Distribution\Modules\Youtube\YoutubeAuthenticator; -use AnzuSystems\CoreDamBundle\Domain\YoutubeDistribution\YoutubeAbstractDistributionFacade; +use AnzuSystems\CoreDamBundle\Domain\YoutubeDistribution\YoutubeDistributionFacade; use AnzuSystems\CoreDamBundle\Entity\YoutubeDistribution; use AnzuSystems\CoreDamBundle\Model\Dto\Youtube\PlaylistDto; use AnzuSystems\CoreDamBundle\Model\Dto\Youtube\YoutubeLanguageDto; @@ -20,7 +20,7 @@ final class YoutubeValidator extends ConstraintValidator { public function __construct( - private readonly YoutubeAbstractDistributionFacade $playlistFacade, + private readonly YoutubeDistributionFacade $playlistFacade, private readonly YoutubeAuthenticator $youtubeAuthenticator, ) { } diff --git a/tests/Controller/Api/Adm/V1/AssetLicenceGroupControllerTest.php b/tests/Controller/Api/Adm/V1/AssetLicenceGroupControllerTest.php new file mode 100644 index 00000000..affe41c0 --- /dev/null +++ b/tests/Controller/Api/Adm/V1/AssetLicenceGroupControllerTest.php @@ -0,0 +1,209 @@ +getApiClient(User::ID_ADMIN); + + $response = $client->get(AssetLicenceGroupUrl::getOne(AssetLicenceGroupFixtures::LICENCE_GROUP_ID)); + $this->assertSame(Response::HTTP_OK, $response->getStatusCode()); + + $assetLicence = $this->serializer->deserialize( + $response->getContent(), + AssetLicenceGroup::class + ); + + $fromDb = self::getContainer() + ->get(AssetLicenceGroupRepository::class) + ->find(AssetLicenceGroupFixtures::LICENCE_GROUP_ID); + + $this->assertSame($fromDb->getId(), $assetLicence->getId()); + $this->assertSame($fromDb->getName(), $assetLicence->getName()); + $this->assertSame($fromDb->getExtSystem()->getId(), $assetLicence->getExtSystem()->getId()); + } + + /** + * @throws SerializerException + */ + public function testGetListSuccess(): void + { + $client = $this->getApiClient(User::ID_ADMIN); + + $response = $client->get(AssetLicenceGroupUrl::getList()); + $this->assertSame(Response::HTTP_OK, $response->getStatusCode()); + + $assetLicence = $this->serializer->deserialize( + $response->getContent(), + ApiInfiniteResponseList::class + ); + + $this->assertGreaterThan(0, count($assetLicence->getData())); + } + + + /** + * @dataProvider createSuccessDataProvider + * + * @param array{name: string, extSystem: int, licences: int[]} $requestJson + * + * @throws SerializerException + */ + public function testCreateSuccess(array $requestJson, int $expectedResponseStatusCode): void + { + $client = $this->getApiClient(User::ID_ADMIN); + + $response = $client->post(AssetLicenceGroupUrl::createPath(), $requestJson); + $this->assertSame($expectedResponseStatusCode, $response->getStatusCode()); + + $assetLicenceGroup = $this->serializer->deserialize( + $response->getContent(), + AssetLicenceGroup::class + ); + + $this->assertSame($requestJson['name'], $assetLicenceGroup->getName()); + $this->assertSame($requestJson['extSystem'], $assetLicenceGroup->getExtSystem()->getId()); + $this->assertSame($requestJson['licences'], $assetLicenceGroup->getLicences()->map( + fn (AssetLicence $licence): int => (int) $licence->getId() + )->toArray()); + } + + /** + * @return list + */ + public function createSuccessDataProvider(): array + { + return [ + [ + 'requestJson' => [ + 'name' => 'test', + 'extSystem' => ExtSystemFixtures::ID_BLOG, + 'licences' => [TestAssetLicenceFixtures::LICENCE_ID], + ], + 'expectedResponseStatusCode' => Response::HTTP_CREATED, + ], + ]; + } + + /** + * @dataProvider createFailureDataProvider + */ + public function testCreateFailure(array $requestJson, array $validationErrors): void + { + $client = $this->getApiClient(User::ID_ADMIN); + + $response = $client->post(AssetLicenceGroupUrl::createPath(), $requestJson); + $this->assertSame(Response::HTTP_UNPROCESSABLE_ENTITY, $response->getStatusCode()); + + $content = json_decode($response->getContent(), true); + $this->assertValidationErrors($content, $validationErrors); + } + + public function createFailureDataProvider(): array + { + return [ + [ + 'requestJson' => [ + 'name' => 'a', + 'extSystem' => 0, + ], + 'validationErrors' => [ + 'name' => [ + ValidationException::ERROR_FIELD_LENGTH_MIN, + ], + 'extSystem' => [ + ValidationException::ERROR_FIELD_EMPTY, + ], + ], + ], + [ + 'requestJson' => [ + 'name' => 'Group 100', + 'extSystem' => 4, + ], + 'validationErrors' => [ + 'name' => [ + ValidationException::ERROR_FIELD_UNIQUE, + ] + ], + ], + [ + 'requestJson' => [ + 'name' => 'Group', + 'extSystem' => 4, + 'licences' => [AssetLicenceFixtures::DEFAULT_LICENCE_ID] + ], + 'validationErrors' => [ + 'licences' => [ + ValidationException::ERROR_FIELD_INVALID, + ] + ], + ], + ]; + } + + /** + * @dataProvider updateSuccessDataProvider + * + * @throws SerializerException + */ + public function testUpdateSuccess(array $requestJson, int $expectedResponseStatusCode): void + { + $client = $this->getApiClient(User::ID_ADMIN); + + $id = $requestJson['id']; + $response = $client->put(AssetLicenceGroupUrl::update($id), $requestJson); + $this->assertSame($expectedResponseStatusCode, $response->getStatusCode()); + + $assetLicenceGroup = $this->serializer->deserialize( + $response->getContent(), + AssetLicenceGroup::class + ); + + $this->assertSame($requestJson['name'], $assetLicenceGroup->getName()); + $this->assertSame($requestJson['extSystem'], $assetLicenceGroup->getExtSystem()->getId()); + $this->assertSame($requestJson['licences'], $assetLicenceGroup->getLicences()->map( + fn (AssetLicence $licence): int => (int) $licence->getId() + )->toArray()); + } + + public function updateSuccessDataProvider(): array + { + return [ + [ + 'requestJson' => [ + 'id' => AssetLicenceGroupFixtures::LICENCE_GROUP_ID, + 'name' => 'test (updated)', + 'extSystem' => ExtSystemFixtures::ID_BLOG, + 'licences' => [TestAssetLicenceFixtures::LICENCE_ID, TestAssetLicenceFixtures::LICENCE_2_ID], + ], + 'expectedResponseStatusCode' => Response::HTTP_OK, + ], + ]; + } +} diff --git a/tests/Domain/Job/JobPodcastSynchronizerProcessorTest.php b/tests/Domain/Job/JobPodcastSynchronizerProcessorTest.php index b5182f8c..320baf08 100644 --- a/tests/Domain/Job/JobPodcastSynchronizerProcessorTest.php +++ b/tests/Domain/Job/JobPodcastSynchronizerProcessorTest.php @@ -39,7 +39,7 @@ protected function setUp(): void public function testFullSyncProcess(): void { - $job = $this->entityManager->find(Job::class, JobFixtures::ID_FULL_PODCAST_SYNCHRONYZER_JOB); + $job = $this->entityManager->getRepository(JobPodcastSynchronizer::class)->findBy(['fullSync' => true])[0]; $this->assertInstanceOf(JobPodcastSynchronizer::class, $job); $this->synchronizerProcessor->setBulkSize(2); @@ -80,7 +80,7 @@ public function testFullSyncProcess(): void public function testSpecificPodcastSyncProcess(): void { - $job = $this->entityManager->find(Job::class, JobFixtures::ID_SINGLE_PODCAST_SYNCHRONYZER_JOB); + $job = $this->entityManager->getRepository(JobPodcastSynchronizer::class)->findBy(['fullSync' => false])[0]; $this->assertInstanceOf(JobPodcastSynchronizer::class, $job); $this->synchronizerProcessor->setBulkSize(2); @@ -99,7 +99,7 @@ public function testSpecificPodcastSyncProcess(): void public function testSpecificPodcastSyncProcessAndImportFrom(): void { - $job = $this->entityManager->find(Job::class, JobFixtures::ID_SINGLE_PODCAST_SYNCHRONYZER_JOB); + $job = $this->entityManager->getRepository(JobPodcastSynchronizer::class)->findBy(['fullSync' => false])[0]; $this->assertInstanceOf(JobPodcastSynchronizer::class, $job); $this->synchronizerProcessor->setBulkSize(2); diff --git a/tests/Domain/Job/JobUserDataDeleteProcessorTest.php b/tests/Domain/Job/JobUserDataDeleteProcessorTest.php index e539b000..bf06ee37 100644 --- a/tests/Domain/Job/JobUserDataDeleteProcessorTest.php +++ b/tests/Domain/Job/JobUserDataDeleteProcessorTest.php @@ -5,6 +5,7 @@ namespace AnzuSystems\CoreDamBundle\Tests\Domain\Job; use AnzuSystems\CommonBundle\Domain\Job\JobProcessor; +use AnzuSystems\CommonBundle\Domain\Job\JobRunner; use AnzuSystems\CommonBundle\Entity\Job; use AnzuSystems\CommonBundle\Entity\JobUserDataDelete; use AnzuSystems\CommonBundle\Model\Enum\JobStatus; @@ -41,23 +42,21 @@ public function testProcess(): void $this->jobUserDataDeleteProcessor->setBulkSize(1); // 1. Process the first bulk - $this->jobProcessor->process(); - $job = $this->entityManager->find(Job::class, JobFixtures::ID_DELETE_BLOG_USER_JOB); + $job = $this->entityManager->getRepository(JobUserDataDelete::class)->findAll()[0]; + $this->jobProcessor->process($job); $this->assertInstanceOf(JobUserDataDelete::class, $job); $this->assertSame(JobStatus::AwaitingBatchProcess, $job->getStatus()); $this->assertSame(1, $job->getBatchProcessedIterationCount()); // 2. Process the second bulk - $this->jobProcessor->process(); - $job = $this->entityManager->find(Job::class, JobFixtures::ID_DELETE_BLOG_USER_JOB); + $this->jobProcessor->process($job); $this->assertInstanceOf(JobUserDataDelete::class, $job); $this->assertSame(JobStatus::AwaitingBatchProcess, $job->getStatus()); $this->assertSame(2, $job->getBatchProcessedIterationCount()); // 3. Process the third bulk - $this->jobProcessor->process(); - $job = $this->entityManager->find(Job::class, JobFixtures::ID_DELETE_BLOG_USER_JOB); + $this->jobProcessor->process($job); $this->assertInstanceOf(JobUserDataDelete::class, $job); $this->assertSame(2, $job->getBatchProcessedIterationCount()); $this->assertSame(JobStatus::Done, $job->getStatus()); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index e7f569c3..93dd489b 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -65,7 +65,7 @@ # Database fixtures $input = new ArrayInput([ - 'command' => 'anzusystems:fixtures:generate', + 'command' => 'anzusystems:fixtures:generate' ]); $input->setInteractive(false); $app->run($input, $output); diff --git a/tests/config/services/services.php b/tests/config/services/services.php index d077e06b..4ea9212c 100644 --- a/tests/config/services/services.php +++ b/tests/config/services/services.php @@ -7,25 +7,22 @@ use AnzuSystems\CommonBundle\AnzuSystemsCommonBundle; use AnzuSystems\CommonBundle\Domain\Job\JobManager; use AnzuSystems\CommonBundle\Exception\Handler\ValidationExceptionHandler; -use AnzuSystems\CoreDamBundle\Cache\ImageRouteGenerator; use AnzuSystems\CoreDamBundle\DataFixtures\AssetLicenceFixtures as BaseAssetLicenceFixtures; use AnzuSystems\CoreDamBundle\Domain\AssetFile\AssetFileStatusFacadeProvider; use AnzuSystems\CoreDamBundle\Domain\AssetLicence\AssetLicenceManager; +use AnzuSystems\CoreDamBundle\Domain\AssetLicenceGroup\AssetLicenceGroupManager; use AnzuSystems\CoreDamBundle\Domain\AssetSlot\AssetSlotFactory; -use AnzuSystems\CoreDamBundle\Domain\Configuration\AllowListConfiguration; -use AnzuSystems\CoreDamBundle\Domain\Configuration\ConfigurationProvider; -use AnzuSystems\CoreDamBundle\Domain\Configuration\ExtSystemConfigurationProvider; use AnzuSystems\CoreDamBundle\Domain\CustomForm\CustomFormFactory; use AnzuSystems\CoreDamBundle\Domain\CustomForm\CustomFormManager; use AnzuSystems\CoreDamBundle\Domain\DistributionCategory\DistributionCategoryManager; use AnzuSystems\CoreDamBundle\Domain\ExtSystem\ExtSystemManager; use AnzuSystems\CoreDamBundle\Domain\Image\ImageFactory; use AnzuSystems\CoreDamBundle\Domain\Image\ImageManager; -use AnzuSystems\CoreDamBundle\Domain\Image\ImageUrlFactory; use AnzuSystems\CoreDamBundle\Domain\User\UserManager; use AnzuSystems\CoreDamBundle\FileSystem\FileSystemProvider; use AnzuSystems\CoreDamBundle\Repository\AssetLicenceRepository; use AnzuSystems\CoreDamBundle\Tests\Data\Fixtures\AssetLicenceFixtures; +use AnzuSystems\CoreDamBundle\Tests\Data\Fixtures\AssetLicenceGroupFixtures; use AnzuSystems\CoreDamBundle\Tests\Data\Fixtures\DistributionCategoryFixtures; use AnzuSystems\CoreDamBundle\Tests\Data\Fixtures\UserFixtures; use AnzuSystems\CoreDamBundle\Tests\Data\Fixtures\CustomFormElementFixtures; @@ -34,7 +31,6 @@ use AnzuSystems\CoreDamBundle\Tests\Data\Fixtures\JobFixtures; use AnzuSystems\CoreDamBundle\Tests\Data\Fixtures\SystemUserFixtures; use AnzuSystems\CoreDamBundle\Tests\HttpClient\BaseClient; -use AnzuSystems\CoreDamBundle\Tests\HttpClient\DownloadFileClient; use AnzuSystems\CoreDamBundle\Tests\HttpClient\JwClientMock; use AnzuSystems\CoreDamBundle\Tests\HttpClient\RssPodcastMock; use Doctrine\ORM\EntityManagerInterface; @@ -64,6 +60,11 @@ ->call('setEntityManager', [service(EntityManagerInterface::class)]) ->tag(AnzuSystemsCommonBundle::TAG_DATA_FIXTURE); + $services->set(AssetLicenceGroupFixtures::class) + ->arg('$assetLicenceGroupManager', service(AssetLicenceGroupManager::class)) + ->call('setEntityManager', [service(EntityManagerInterface::class)]) + ->tag(AnzuSystemsCommonBundle::TAG_DATA_FIXTURE); + $services->set(DistributionCategoryFixtures::class) ->arg('$distributionCategoryManager', service(DistributionCategoryManager::class)) ->call('setEntityManager', [service(EntityManagerInterface::class)]) diff --git a/tests/data/Fixtures/AssetLicenceFixtures.php b/tests/data/Fixtures/AssetLicenceFixtures.php index ba3ccdae..51d4a056 100644 --- a/tests/data/Fixtures/AssetLicenceFixtures.php +++ b/tests/data/Fixtures/AssetLicenceFixtures.php @@ -17,8 +17,8 @@ */ final class AssetLicenceFixtures extends AbstractFixtures { - public const LICENCE_ID = BaseAssetLicenceFixtures::DEFAULT_LICENCE_ID + 1; - public const LICENCE_2_ID = BaseAssetLicenceFixtures::DEFAULT_LICENCE_ID + 2; + public const int LICENCE_ID = BaseAssetLicenceFixtures::DEFAULT_LICENCE_ID + 1; + public const int LICENCE_2_ID = BaseAssetLicenceFixtures::DEFAULT_LICENCE_ID + 2; public function __construct( private readonly AssetLicenceManager $assetLicenceManager, diff --git a/tests/data/Fixtures/AssetLicenceGroupFixtures.php b/tests/data/Fixtures/AssetLicenceGroupFixtures.php new file mode 100644 index 00000000..413042c9 --- /dev/null +++ b/tests/data/Fixtures/AssetLicenceGroupFixtures.php @@ -0,0 +1,73 @@ + + */ +final class AssetLicenceGroupFixtures extends AbstractFixtures +{ + public const int LICENCE_GROUP_ID = 100; + + public function __construct( + private readonly AssetLicenceGroupManager $assetLicenceGroupManager, + ) { + } + + public static function getIndexKey(): string + { + return AssetLicenceGroup::class; + } + + public static function getDependencies(): array + { + return [AssetLicenceFixtures::class]; + } + + public function useCustomId(): bool + { + return true; + } + + public function load(ProgressBar $progressBar): void + { + /** @var AssetLicenceGroup $assetLicenceGroup */ + foreach ($progressBar->iterate($this->getData()) as $assetLicenceGroup) { + $assetLicenceGroup = $this->assetLicenceGroupManager->create($assetLicenceGroup); + $this->addToRegistry($assetLicenceGroup, (int) $assetLicenceGroup->getId()); + } + } + + private function getData(): Generator + { + /** @var AssetLicence $licence */ + $licence = $this->entityManager->find( + AssetLicence::class, + AssetLicenceFixtures::LICENCE_ID + ); + + /** @var ExtSystem $blogExtSystem */ + $blogExtSystem = $this->entityManager->find( + ExtSystem::class, + 4 + ); + + yield (new AssetLicenceGroup()) + ->setId(self::LICENCE_GROUP_ID) + ->setName('Group 100') + ->setExtSystem($blogExtSystem) + ->setLicences(new ArrayCollection([$licence])) + ; + } +} diff --git a/tests/data/Fixtures/JobFixtures.php b/tests/data/Fixtures/JobFixtures.php index 28b1275b..6f0c1afa 100644 --- a/tests/data/Fixtures/JobFixtures.php +++ b/tests/data/Fixtures/JobFixtures.php @@ -20,10 +20,6 @@ */ final class JobFixtures extends AbstractFixtures { - public const ID_DELETE_BLOG_USER_JOB = 1; - public const ID_FULL_PODCAST_SYNCHRONYZER_JOB = 2; - public const ID_SINGLE_PODCAST_SYNCHRONYZER_JOB = 3; - public function __construct( private readonly JobManager $jobManager, ) { @@ -31,7 +27,7 @@ public function __construct( public static function getIndexKey(): string { - return Job::class; + return JobPodcastSynchronizer::class; } public static function getDependencies(): array @@ -39,11 +35,6 @@ public static function getDependencies(): array return [UserFixtures::class]; } - public function useCustomId(): bool - { - return true; - } - public function load(ProgressBar $progressBar): void { /** @var Job $job */ @@ -55,18 +46,17 @@ public function load(ProgressBar $progressBar): void private function getData(): Generator { yield (new JobUserDataDelete()) - ->setId(self::ID_DELETE_BLOG_USER_JOB) ->setTargetUserId(User::ID_BLOG_USER) ->setAnonymizeUser(true) ; yield (new JobPodcastSynchronizer()) ->setFullSync(true) - ->setId(self::ID_FULL_PODCAST_SYNCHRONYZER_JOB); + ; yield (new JobPodcastSynchronizer()) ->setFullSync(false) ->setPodcastId(PodcastFixtures::PODCAST_1) - ->setId(self::ID_FULL_PODCAST_SYNCHRONYZER_JOB); + ; } } diff --git a/tests/data/Model/AssetLicenceGroupUrl.php b/tests/data/Model/AssetLicenceGroupUrl.php new file mode 100644 index 00000000..7658db8e --- /dev/null +++ b/tests/data/Model/AssetLicenceGroupUrl.php @@ -0,0 +1,30 @@ +