diff --git a/.changes/unreleased/Feature-20250722-155039.yaml b/.changes/unreleased/Feature-20250722-155039.yaml new file mode 100644 index 000000000..28f9031b9 --- /dev/null +++ b/.changes/unreleased/Feature-20250722-155039.yaml @@ -0,0 +1,6 @@ +kind: Feature +body: Show filters on list pages unfolded by default +time: 2025-07-22T15:50:39.338057044+02:00 +custom: + Issue: "399" + SchemaChange: No schema change diff --git a/.changes/unreleased/Fixed-20250611-164623.yaml b/.changes/unreleased/Fixed-20250611-164623.yaml deleted file mode 100644 index 8bb956c34..000000000 --- a/.changes/unreleased/Fixed-20250611-164623.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: Fixed -body: Fix admin entity edit actions for event admin entities and activity reason (category) entities -time: 2025-06-11T16:46:23.113506434+02:00 -custom: - Issue: "" - SchemaChange: No schema change diff --git a/.changes/unreleased/UX-20250722-132637.yaml b/.changes/unreleased/UX-20250722-132637.yaml new file mode 100644 index 000000000..9f0766794 --- /dev/null +++ b/.changes/unreleased/UX-20250722-132637.yaml @@ -0,0 +1,6 @@ +kind: UX +body: Limit display of participations in event list +time: 2025-07-22T13:26:37.500656935+02:00 +custom: + Issue: "" + SchemaChange: No schema change diff --git a/.changes/v4.0.0.md b/.changes/v4.0.0.md new file mode 100644 index 000000000..6063736a8 --- /dev/null +++ b/.changes/v4.0.0.md @@ -0,0 +1,74 @@ +## v4.0.0 - 2025-07-08 +### Feature +* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works +### Fixed +* ([#390](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/390)) Display the list of participant in the results, even if there is only one participant and that the search result display the requestor +* Fix admin entity edit actions for event admin entities and activity reason (category) entities +* Fix translations for social action fields in admin form: results, goals, evaluations +### DX +* Rewrite exports to run them asynchronously + + **Schema Change**: Add columns or tables +* Allow TranslatableMessage in flash messages +### UX +* Improve labeling of fields in person resource creation form + + +**Release notes** + +- Add new methods to serialize data using the rector rule +- Remove all references to the Request in filters, aggregators, filters. Actually, the most frequent occurence is `$security->getUser()`. +- Refactor manually the initializeQuery method +- Remove the injection of ExportManager into the constructor of each export element: + + ```diff + + - class MyFormatter implements FormatterInterface + + class MyFormatter implements FormatterInterface, \Chill\MainBundle\Export\ExportManagerAwareInterface + { + + use \Chill\MainBundle\Export\Helper\ExportManagerAwareTrait; + + - public function __construct(private ExportManager $exportmanager) {} + + public function MyMethod(): void + { + - $this->exportManager->getFilter('alias'); + + $this->getExportManager()->getFilter('alias'); + } + } + ``` +- configure messenger to handle export in a queue: + +```diff +# config/packages/messenger.yaml +framework: + messenger: + routing: ++ 'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority +``` + +- add missing methods to exports, aggregators, filters, formatter: + + ```php + public function normalizeFormData(array $formData): array; + + public function denormalizeFormData(array $formData, int $fromVersion): array; + ``` + + There are rector rules to generate those methods: + + - `Chill\Utils\Rector\Rector\ChillBundleAddNormalizationMethodsOnExportRector` + + See: + + ```php + // upgrade chill exports + $rectorConfig->rules([\Chill\Utils\Rector\Rector\ChillBundleAddNormalizationMethodsOnExportRector::class]); + ``` + + This rule will create most of the work necessary, but some manuals changes are still necessary: + + - we must set manually the correct repository for method `denormalizeDoctrineEntity`; + - when the form data contains some entities, and the form type is not one of EntityType::class, PickUserDynamicType::class, PickUserLocationType::class, PickThirdpartyDynamicType::class, Select2CountryType::class, then we must handle the normalization manually (using the `\Chill\MainBundle\Export\ExportDataNormalizerTrait`) + + diff --git a/.changes/v4.0.1.md b/.changes/v4.0.1.md new file mode 100644 index 000000000..882b78709 --- /dev/null +++ b/.changes/v4.0.1.md @@ -0,0 +1,4 @@ +## v4.0.1 - 2025-07-08 +### Fixed +* Fix package.json for compilation + diff --git a/.changes/v4.0.2.md b/.changes/v4.0.2.md new file mode 100644 index 000000000..8761b48f2 --- /dev/null +++ b/.changes/v4.0.2.md @@ -0,0 +1,4 @@ +## v4.0.2 - 2025-07-09 +### Fixed +* Fix add missing translation +* Fix the transfer of evaluations and documents during of accompanyingperiodwork diff --git a/.junie/guidelines.md b/.junie/guidelines.md index eace2f4fa..97a2be27d 100644 --- a/.junie/guidelines.md +++ b/.junie/guidelines.md @@ -185,14 +185,57 @@ When we need to use a DateTime or DateTimeImmutable that need to express "now", `Symfony\Component\Clock\ClockInterface`, where possible. This is usually not possible in doctrine entities, where injection does not work when restoring an entity from database, but usually possible in services. +In test, we use `\Symfony\Component\Clock\MockClock` which is an implementation of `Symfony\Component\Clock\ClockInterface` +where we have full and easy control of the date. + ### Testing Information The project uses PHPUnit for testing. Each bundle has its own test suite, and there's also a global test suite at the root level. +#### Use of mock in tests + +##### General mocking + For creating mock, we prefer using prophecy (library phpspec/prophecy). +##### Useful helpers and tips that avoid create a mock + +Some notable implementations that are tests helper, and avoid to create a mock: + +- `\Psr\Log\NullLogger`, an implementation of `\Psr\Log\LoggerInterface`; +- `\Symfony\Component\Clock\MockClock`, an implementation of `Symfony\Component\Clock\ClockInterface` (already mentioned above); +- `\Symfony\Component\HttpClient\MockHttpClient`, an implementation of `\Symfony\Contracts\HttpClient\HttpClientInterface`; +- When using `\Symfony\Component\Mailer\MailerInterface`, we can create the mock with "InMemoryTransport": + + ```php + use Symfony\Component\Mailer\Transport\InMemoryTransport; + use \Symfony\Component\Mailer\Mailer; + + $transport = new InMemoryTransport(); + $mailer = new Mailer($transport); + + // After sending: + $messages = $transport->getSent(); // array of SentMessage + ``` +- When using `\Symfony\Contracts\EventDispatcher\EventDispatcherInterface`, we can use directly an instance of `\Symfony\Component\EventDispatcher\EventDispatcher`; + +##### When we prefer not creating a mock + +- When we use Doctrine Entities related to the project, we prefer not to use a mock: we instantiate them directly (unless it requires too much code to write); + +##### Mocking final and readonly classes + +Classes marked as final can't be mocked. To avoid that, either: + +- we remove the `final` keyword from the class; +- we extract an interface from the final class. + +This must be a decision made by a human, not by an AI. Every AI task must abort with an explicit message in that case. + #### Running Tests +The tests are run from the project's root (not from the bundle's root). + ```bash # Run all tests vendor/bin/phpunit diff --git a/CHANGELOG.md b/CHANGELOG.md index a00af6c44..332db3e10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,91 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). +## v4.0.2 - 2025-07-09 +### Fixed +* Fix add missing translation +* Fix the transfer of evaluations and documents during of accompanyingperiodwork + +## v4.0.1 - 2025-07-08 +### Fixed +* Fix package.json for compilation + + +## v4.0.0 - 2025-07-08 +### Feature +* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works +### Fixed +* ([#390](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/390)) Display the list of participant in the results, even if there is only one participant and that the search result display the requestor +* Fix admin entity edit actions for event admin entities and activity reason (category) entities +* Fix translations for social action fields in admin form: results, goals, evaluations +### DX +* Rewrite exports to run them asynchronously + + **Schema Change**: Add columns or tables +* Allow TranslatableMessage in flash messages +### UX +* Improve labeling of fields in person resource creation form + + +**Release notes** + +- Add new methods to serialize data using the rector rule +- Remove all references to the Request in filters, aggregators, filters. Actually, the most frequent occurence is `$security->getUser()`. +- Refactor manually the initializeQuery method +- Remove the injection of ExportManager into the constructor of each export element: + + ```diff + + - class MyFormatter implements FormatterInterface + + class MyFormatter implements FormatterInterface, \Chill\MainBundle\Export\ExportManagerAwareInterface + { + + use \Chill\MainBundle\Export\Helper\ExportManagerAwareTrait; + + - public function __construct(private ExportManager $exportmanager) {} + + public function MyMethod(): void + { + - $this->exportManager->getFilter('alias'); + + $this->getExportManager()->getFilter('alias'); + } + } + ``` +- configure messenger to handle export in a queue: + +```diff +# config/packages/messenger.yaml +framework: + messenger: + routing: ++ 'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority +``` + +- add missing methods to exports, aggregators, filters, formatter: + + ```php + public function normalizeFormData(array $formData): array; + + public function denormalizeFormData(array $formData, int $fromVersion): array; + ``` + + There are rector rules to generate those methods: + + - `Chill\Utils\Rector\Rector\ChillBundleAddNormalizationMethodsOnExportRector` + + See: + + ```php + // upgrade chill exports + $rectorConfig->rules([\Chill\Utils\Rector\Rector\ChillBundleAddNormalizationMethodsOnExportRector::class]); + ``` + + This rule will create most of the work necessary, but some manuals changes are still necessary: + + - we must set manually the correct repository for method `denormalizeDoctrineEntity`; + - when the form data contains some entities, and the form type is not one of EntityType::class, PickUserDynamicType::class, PickUserLocationType::class, PickThirdpartyDynamicType::class, Select2CountryType::class, then we must handle the normalization manually (using the `\Chill\MainBundle\Export\ExportDataNormalizerTrait`) + + + ## v3.12.1 - 2025-06-30 ### Fixed * Fix loading of the list of documents diff --git a/config/packages/chill.yaml b/config/packages/chill.yaml index 76578a2c7..6ed6b6984 100644 --- a/config/packages/chill.yaml +++ b/config/packages/chill.yaml @@ -17,6 +17,7 @@ chill_main: acl: form_show_scopes: true form_show_centers: true + filter_stats_by_center: true access_global_history: false access_user_change_password: true access_permissions_group_list: true diff --git a/config/packages/messenger.yaml b/config/packages/messenger.yaml index 72808c9a4..7ca02530f 100644 --- a/config/packages/messenger.yaml +++ b/config/packages/messenger.yaml @@ -3,7 +3,6 @@ framework: # Uncomment this (and the failed transport below) to send failed messages to this transport for later handling. failure_transport: failed - transports: # those transports are added by chill-bundles recipes sync: sync:// @@ -17,7 +16,9 @@ framework: async: ~ auto_setup: true - priority: '%env(MESSENGER_TRANSPORT_DSN)%/priority' + priority: + dsn: '%env(MESSENGER_TRANSPORT_DSN)%/priority' + # end of transports added by chill-bundles recipes # https://symfony.com/doc/current/messenger.html#transport-configuration failed: 'doctrine://default?queue_name=failed' @@ -59,6 +60,10 @@ framework: 'Chill\MainBundle\Workflow\Messenger\PostSignatureStateChangeMessage': priority 'Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage': async 'Chill\MainBundle\Service\Workflow\CancelStaleWorkflowMessage': async + 'Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage': async + 'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority + 'Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage': async + 'Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage': async # end of routes added by chill-bundles recipes # Route your messages to the transports # 'App\Message\YourMessage': async diff --git a/docs/source/_static/code/exports/BirthdateFilter.php b/docs/source/_static/code/exports/BirthdateFilter.php index d32b5676f..8501c53ca 100644 --- a/docs/source/_static/code/exports/BirthdateFilter.php +++ b/docs/source/_static/code/exports/BirthdateFilter.php @@ -12,14 +12,16 @@ declare(strict_types=1); namespace Chill\PersonBundle\Export\Filter; use Chill\MainBundle\Export\ExportElementValidatedInterface; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; -use DateTime; use Doctrine\ORM\Query\Expr; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Validator\Context\ExecutionContextInterface; class BirthdateFilter implements ExportElementValidatedInterface, FilterInterface { + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; + // add specific role for this filter public function addRole(): ?string { @@ -28,7 +30,7 @@ class BirthdateFilter implements ExportElementValidatedInterface, FilterInterfac } // here, we alter the query created by Export - public function alterQuery(\Doctrine\ORM\QueryBuilder $qb, $data): void + public function alterQuery(\Doctrine\ORM\QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $where = $qb->getDQLPart('where'); // we create the clause here @@ -52,7 +54,7 @@ class BirthdateFilter implements ExportElementValidatedInterface, FilterInterfac } // we give information on which type of export this filter applies - public function applyOn() + public function applyOn(): string { return 'person'; } @@ -74,23 +76,39 @@ class BirthdateFilter implements ExportElementValidatedInterface, FilterInterfac 'format' => 'dd-MM-yyyy', ]); } + + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['date_from' => $this->normalizeDate($formData['date_from']), 'date_to' => $this->normalizeDate($formData['date_to'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['date_from' => $this->denormalizeDate($formData['date_from']), 'date_to' => $this->denormalizeDate($formData['date_to'])]; + } + public function getFormDefaultData(): array { - return ['date_from' => new DateTime(), 'date_to' => new DateTime()]; + return ['date_from' => new \DateTime(), 'date_to' => new \DateTime()]; } // here, we create a simple string which will describe the action of // the filter in the Response - public function describeAction($data, $format = 'string') + public function describeAction($data, ExportGenerationContext $context): string|\Symfony\Contracts\Translation\TranslatableInterface|array { return ['Filtered by person\'s birtdate: ' - . 'between %date_from% and %date_to%', [ + .'between %date_from% and %date_to%', [ '%date_from%' => $data['date_from']->format('d-m-Y'), '%date_to%' => $data['date_to']->format('d-m-Y'), ], ]; } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'Filter by person\'s birthdate'; } @@ -106,13 +124,13 @@ class BirthdateFilter implements ExportElementValidatedInterface, FilterInterfac if (null === $date_from) { $context->buildViolation('The "date from" should not be empty') - //->atPath('date_from') + // ->atPath('date_from') ->addViolation(); } if (null === $date_to) { $context->buildViolation('The "date to" should not be empty') - //->atPath('date_to') + // ->atPath('date_to') ->addViolation(); } @@ -121,7 +139,7 @@ class BirthdateFilter implements ExportElementValidatedInterface, FilterInterfac && $date_from >= $date_to ) { $context->buildViolation('The date "date to" should be after the ' - . 'date given in "date from" field') + .'date given in "date from" field') ->addViolation(); } } diff --git a/docs/source/_static/code/exports/CountPerson.php b/docs/source/_static/code/exports/CountPerson.php index b864ef61c..cd4132f38 100644 --- a/docs/source/_static/code/exports/CountPerson.php +++ b/docs/source/_static/code/exports/CountPerson.php @@ -36,6 +36,18 @@ class CountPerson implements ExportInterface { // this export does not add any form } + public function getNormalizationVersion(): int + { + return 1; + } + public function normalizeFormData(array $formData): array + { + return []; + } + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } public function getFormDefaultData(): array { return []; @@ -60,29 +72,29 @@ class CountPerson implements ExportInterface }; } - public function getQueryKeys($data) + public function getQueryKeys($data): array { // this array match the result keys in the query. We have only // one column. return ['export_result']; } - public function getResult($query, $data) + public function getResult($query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array { return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'Count peoples'; } - public function getType() + public function getType(): string { return Declarations::PERSON_TYPE; } - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): \Doctrine\ORM\QueryBuilder { // we gather all center the user choose. $centers = array_map(static fn ($el) => $el['center'], $acl); diff --git a/docs/source/development/export-sequence.puml b/docs/source/development/export-sequence.puml new file mode 100644 index 000000000..0d0c77c14 --- /dev/null +++ b/docs/source/development/export-sequence.puml @@ -0,0 +1,84 @@ +@startuml +'https://plantuml.com/sequence-diagram + +autonumber + +User -> ExportController: configure export using form +activate ExportController +ExportController -> ExportForm: build form +activate ExportForm + +loop for every ExportElement (Filter, Aggregator) + ExportForm -> ExportElement: `buildForm` + activate ExportElement + ExportElement -> ExportForm: add form to builders + deactivate ExportElement +end + +ExportForm -> ExportController +deactivate ExportForm + +ExportController -> User: show form +deactivate ExportController + +note left of User: Configure the export:\ncheck filters, aggregators, … + +User -> ExportController: post configuration of the export +activate ExportController + +ExportController -> ExportForm: `getData` +activate ExportForm +ExportForm -> ExportController: return data: list of entities, etc. +deactivate ExportForm + +loop for every ExportElement (Filter, Aggregator) + ExportController -> ExportElement: serializeData (data) + activate ExportElement + ExportElement -> ExportController: return serializedData (simple array with string, int, …) + deactivate ExportElement +end + +ExportController -> Database: `INSERT INTO RequestGeneration_table` (insert new entity) +ExportController -> MessageQueue: warn about a new request +activate MessageQueue +ExportController -> User: "ok, generation is in process" +deactivate ExportController + +note left of User: The user see a waiting screen + +MessageQueue -> MessengerConsumer: forward the message to the MessengerConsumer +deactivate MessageQueue +activate MessengerConsumer +MessengerConsumer -> Database: `SELECT * FROM RequestGeneration_table WHERE id = %s` +activate Database +Database -> MessengerConsumer: return RequestGeneration with serializedData +deactivate Database + +loop for every ExportElement (Filter, Aggregator) + MessengerConsumer -> ExportElement: deserializeData + activate ExportElement + ExportElement -> MessengerConsumer: return data (list of entities, etc.) from the serialized array + deactivate ExportElement + MessengerConsumer -> ExportElement: alter the sql query (`ExportElement::alterQuery`) + activate ExportElement + ExportElement -> MessengerConsumer: return the query with WHERE and GROUP BY clauses + deactivate ExportElement +end + +MessengerConsumer -> MessengerConsumer: prepare the export +MessengerConsumer -> MessengerConsumer: save the export as a stored object +MessengerConsumer -> Database: `UPDATE RequestGeneration_table SET ready = true` +deactivate MessengerConsumer + +User -> ExportController: pull every 5s to know if the export is generated +activate ExportController +ExportController -> User: warn the export is generated +deactivate ExportController + +User -> ExportController: download the export from object storage + + + + + +@enduml diff --git a/docs/source/development/messages-to-users.rst b/docs/source/development/messages-to-users.rst index a935ee855..244638d0b 100644 --- a/docs/source/development/messages-to-users.rst +++ b/docs/source/development/messages-to-users.rst @@ -15,24 +15,31 @@ Messages to users, flashbags and buttons Flashbags ========== -The four following levels are defined : +The four following levels are defined : +-----------+----------------------------------------------------------------------------------------------+ |Key |Intent | +===========+==============================================================================================+ -|alert |A message not linked with the user action, but which should require an action or a | -| |correction. | -+-----------+----------------------------------------------------------------------------------------------+ |success |The user action succeeds. | +-----------+----------------------------------------------------------------------------------------------+ |notice |A simple message to give information to the user. The message may be linked or not linked with| | |the user action. | +-----------+----------------------------------------------------------------------------------------------+ -|warning |A message linked with an action, the user should correct. | -+-----------+----------------------------------------------------------------------------------------------+ |error |The user's action failed: he must correct something to process the action. | +-----------+----------------------------------------------------------------------------------------------+ +We can use :code:`TranslatableMessage` (and other :code:`TranslatableMessageInterface` instances) into the controller: + +.. code-block:: php + + // in a controller action: + if (($session = $request->getSession()) instanceof Session) { + $session->getFlashBag()->add( + 'success', + new TranslatableMessage('saved_export.Saved export is saved!') + ); + } + .. seealso:: `Flash Messages on Symfony documentation `_ @@ -66,7 +73,7 @@ To add the action on button, use them as class along with ``sc-button`` : | | | - Submitting this form will remove the entity | +-----------+----------------+------------------------------------------------------------------------------+ | Edit | ``bt-edit`` or | Link to a form to edit an entity | -| | ``bt-update`` | | +| | ``bt-update`` | | +-----------+----------------+------------------------------------------------------------------------------+ | Save | ``bt-save`` | Submitting this form will save change on the entity | +-----------+----------------+------------------------------------------------------------------------------+ diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 7c969f941..1a66e5be2 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2154,11 +2154,6 @@ parameters: count: 1 path: src/Bundle/ChillMainBundle/Export/Formatter/SpreadSheetFormatter.php - - - message: "#^Instanceof between string and DateTimeInterface will always evaluate to false\\.$#" - count: 1 - path: src/Bundle/ChillMainBundle/Export/Formatter/SpreadsheetListFormatter.php - - message: "#^PHPDoc tag @var for property Chill\\\\MainBundle\\\\Export\\\\Helper\\\\ExportAddressHelper\\:\\:\\$unitNamesKeysCache contains unresolvable type\\.$#" count: 1 diff --git a/phpstan.dist.neon b/phpstan.dist.neon index feb453c81..60c36771e 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -3,7 +3,7 @@ parameters: paths: - src/ - utils/ - tmpDir: .cache/ + tmpDir: var/cache/phpstan reportUnmatchedIgnoredErrors: false excludePaths: - .php_cs* diff --git a/phpunit.rector.xml b/phpunit.rector.xml index 25e0d25aa..73fbee8e5 100644 --- a/phpunit.rector.xml +++ b/phpunit.rector.xml @@ -1,13 +1,24 @@ - - - - utils/rector/tests - - - - - utils/rector/src - - + + + + utils/rector/tests + + + + + utils/rector/src + + diff --git a/rector.php b/rector.php index 8dcb48f28..6933c0e6d 100644 --- a/rector.php +++ b/rector.php @@ -17,16 +17,17 @@ use Rector\Symfony\Set\SymfonySetList; return static function (RectorConfig $rectorConfig): void { $rectorConfig->paths([ - __DIR__.'/docs', - __DIR__.'/src', + __DIR__ . '/docs', + __DIR__ . '/src', + __DIR__ . '/rector.php', ]); $rectorConfig->skip([ Rector\Php55\Rector\String_\StringClassNameToClassConstantRector::class => __DIR__.'src/Bundle/ChillMainBundle/Service/Notifier/LegacyOvhCloudFactory.php', ]); -// $rectorConfig->symfonyContainerXml(__DIR__.'/var/cache/dev/test/App_KernelTestDebugContainer.xml '); - $rectorConfig->symfonyContainerPhp(__DIR__.'/tests/symfony-container.php'); + //$rectorConfig->symfonyContainerXml(__DIR__ . '/var/cache/dev/test/App_KernelTestDebugContainer.xml '); + //$rectorConfig->symfonyContainerPhp(__DIR__ . '/tests/symfony-container.php'); // $rectorConfig->cacheClass(\Rector\Caching\ValueObject\Storage\FileCacheStorage::class); // $rectorConfig->cacheDirectory(__DIR__ . '/.cache/rector'); @@ -44,9 +45,9 @@ return static function (RectorConfig $rectorConfig): void { $rectorConfig->rule(Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromStrictTypedPropertyRector::class); // part of the symfony 54 rules - $rectorConfig->rule(Rector\Symfony\Symfony53\Rector\StaticPropertyFetch\KernelTestCaseContainerPropertyDeprecationRector::class); - $rectorConfig->rule(Rector\Symfony\Symfony60\Rector\MethodCall\GetHelperControllerToServiceRector::class); - $rectorConfig->disableParallel(); + $rectorConfig->rule(\Rector\Symfony\Symfony53\Rector\StaticPropertyFetch\KernelTestCaseContainerPropertyDeprecationRector::class); + $rectorConfig->rule(\Rector\Symfony\Symfony60\Rector\MethodCall\GetHelperControllerToServiceRector::class); + //$rectorConfig->disableParallel(); // define sets of rules $rectorConfig->sets([ diff --git a/src/Bundle/ChillActivityBundle/Controller/ActivityReasonController.php b/src/Bundle/ChillActivityBundle/Controller/ActivityReasonController.php index 344baa7cf..0c65505ed 100644 --- a/src/Bundle/ChillActivityBundle/Controller/ActivityReasonController.php +++ b/src/Bundle/ChillActivityBundle/Controller/ActivityReasonController.php @@ -17,7 +17,6 @@ use Chill\ActivityBundle\Repository\ActivityReasonRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * ActivityReason controller. diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByActivityNumberAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByActivityNumberAggregator.php index e9e8fb474..e304a9d7f 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByActivityNumberAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByActivityNumberAggregator.php @@ -24,7 +24,7 @@ class ByActivityNumberAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { $qb ->addSelect('(SELECT COUNT(activity.id) FROM '.Activity::class.' activity WHERE activity.accompanyingPeriod = acp) AS activity_by_number_aggregator') @@ -41,12 +41,27 @@ class ByActivityNumberAggregator implements AggregatorInterface // No form needed } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data) + public function getLabels($key, array $values, $data): callable { return static function ($value) { if ('_header' === $value) { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByActivityTypeAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByActivityTypeAggregator.php index ba3f87f6b..79af0d8fe 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByActivityTypeAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByActivityTypeAggregator.php @@ -46,6 +46,21 @@ final readonly class ByActivityTypeAggregator implements AggregatorInterface ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['after_date' => $formData['after_date']->normalize(), 'before_date' => $formData['before_date']->normalize()]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['after_date' => \Chill\MainBundle\Service\RollingDate\RollingDate::fromNormalized($formData['after_date']), 'before_date' => \Chill\MainBundle\Service\RollingDate\RollingDate::fromNormalized($formData['before_date'])]; + } + public function getFormDefaultData(): array { return [ @@ -54,7 +69,7 @@ final readonly class ByActivityTypeAggregator implements AggregatorInterface ]; } - public function getLabels($key, array $values, mixed $data) + public function getLabels($key, array $values, mixed $data): callable { return function (int|string|null $value): string { if ('_header' === $value) { @@ -69,12 +84,12 @@ final readonly class ByActivityTypeAggregator implements AggregatorInterface }; } - public function getQueryKeys($data) + public function getQueryKeys($data): array { return [self::PREFIX.'_actype_id']; } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'export.aggregator.acp.by_activity_type.title'; } @@ -84,7 +99,7 @@ final readonly class ByActivityTypeAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { $p = self::PREFIX; @@ -115,7 +130,7 @@ final readonly class ByActivityTypeAggregator implements AggregatorInterface ->addGroupBy("{$p}_actype_id"); } - public function applyOn() + public function applyOn(): string { return Declarations::ACP_TYPE; } diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/BySocialActionAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/BySocialActionAggregator.php index 78b68be27..8585f79e5 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/BySocialActionAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/BySocialActionAggregator.php @@ -27,7 +27,7 @@ class BySocialActionAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { if (!\in_array('actsocialaction', $qb->getAllAliases(), true)) { $qb->leftJoin('activity.socialActions', 'actsocialaction'); @@ -47,12 +47,27 @@ class BySocialActionAggregator implements AggregatorInterface // no form } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data) + public function getLabels($key, array $values, $data): callable { return function ($value) { if ('_header' === $value) { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/BySocialIssueAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/BySocialIssueAggregator.php index b7826013f..8932abaf0 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/BySocialIssueAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/BySocialIssueAggregator.php @@ -27,7 +27,7 @@ class BySocialIssueAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { if (!\in_array('actsocialissue', $qb->getAllAliases(), true)) { $qb->leftJoin('activity.socialIssues', 'actsocialissue'); @@ -47,12 +47,27 @@ class BySocialIssueAggregator implements AggregatorInterface // no form } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data) + public function getLabels($key, array $values, $data): callable { return function ($value): string { if ('_header' === $value) { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityLocationAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityLocationAggregator.php index c55b05d99..007b42fdb 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityLocationAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityLocationAggregator.php @@ -25,7 +25,7 @@ final readonly class ActivityLocationAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { if (!\in_array('actloc', $qb->getAllAliases(), true)) { $qb->leftJoin('activity.location', 'actloc'); @@ -44,12 +44,27 @@ final readonly class ActivityLocationAggregator implements AggregatorInterface // no form required for this aggregator } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data): \Closure + public function getLabels($key, array $values, $data): callable { return function ($value): string { if ('_header' === $value) { @@ -69,7 +84,7 @@ final readonly class ActivityLocationAggregator implements AggregatorInterface return [self::KEY]; } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'export.aggregator.activity.by_location.Title'; } diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityPresenceAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityPresenceAggregator.php index b2abda6c5..1a161840a 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityPresenceAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityPresenceAggregator.php @@ -24,12 +24,27 @@ final readonly class ActivityPresenceAggregator implements AggregatorInterface public function buildForm(FormBuilderInterface $builder): void {} + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, mixed $data) + public function getLabels($key, array $values, mixed $data): callable { return function (int|string|null $value): string { if ('_header' === $value) { @@ -44,7 +59,7 @@ final readonly class ActivityPresenceAggregator implements AggregatorInterface }; } - public function getQueryKeys($data) + public function getQueryKeys($data): array { return ['activity_presence_aggregator_attendee']; } @@ -59,13 +74,13 @@ final readonly class ActivityPresenceAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { $qb->addSelect('IDENTITY(activity.attendee) AS activity_presence_aggregator_attendee'); $qb->addGroupBy('activity_presence_aggregator_attendee'); } - public function applyOn() + public function applyOn(): string { return Declarations::ACTIVITY; } diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityReasonAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityReasonAggregator.php index 967fa214d..58b8788cc 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityReasonAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityReasonAggregator.php @@ -36,7 +36,7 @@ class ActivityReasonAggregator implements AggregatorInterface, ExportElementVali return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { // add select element if ('reasons' === $data['level']) { @@ -89,6 +89,21 @@ class ActivityReasonAggregator implements AggregatorInterface, ExportElementVali ); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['level' => $formData['level']]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['level' => $formData['level']]; + } + public function getFormDefaultData(): array { return [ @@ -96,7 +111,7 @@ class ActivityReasonAggregator implements AggregatorInterface, ExportElementVali ]; } - public function getLabels($key, array $values, $data) + public function getLabels($key, array $values, $data): callable { return function ($value) use ($data) { if ('_header' === $value) { @@ -125,7 +140,7 @@ class ActivityReasonAggregator implements AggregatorInterface, ExportElementVali }; } - public function getQueryKeys($data) + public function getQueryKeys($data): array { // add select element if ('reasons' === $data['level']) { @@ -139,7 +154,7 @@ class ActivityReasonAggregator implements AggregatorInterface, ExportElementVali throw new \RuntimeException('The data provided are not recognised.'); } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'Aggregate by activity reason'; } diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityTypeAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityTypeAggregator.php index 4e51f867f..b7cb73021 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityTypeAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityTypeAggregator.php @@ -29,7 +29,7 @@ class ActivityTypeAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { if (!\in_array('acttype', $qb->getAllAliases(), true)) { $qb->leftJoin('activity.activityType', 'acttype'); @@ -49,12 +49,27 @@ class ActivityTypeAggregator implements AggregatorInterface // no form required for this aggregator } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data): \Closure + public function getLabels($key, array $values, $data): callable { return function (int|string|null $value): string { if ('_header' === $value) { @@ -74,7 +89,7 @@ class ActivityTypeAggregator implements AggregatorInterface return [self::KEY]; } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'Aggregate by activity type'; } diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUserAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUserAggregator.php index b9956c92e..6a8f1875b 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUserAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUserAggregator.php @@ -29,7 +29,7 @@ class ActivityUserAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { // add select element $qb->addSelect(sprintf('IDENTITY(activity.user) AS %s', self::KEY)); @@ -48,12 +48,27 @@ class ActivityUserAggregator implements AggregatorInterface // nothing to add } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function getLabels($key, $values, $data): \Closure + public function getLabels($key, $values, $data): callable { return function ($value) { if ('_header' === $value) { @@ -70,7 +85,7 @@ class ActivityUserAggregator implements AggregatorInterface }; } - public function getQueryKeys($data) + public function getQueryKeys($data): array { return [self::KEY]; } diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersAggregator.php index 19174be06..62594b0c0 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersAggregator.php @@ -27,7 +27,7 @@ class ActivityUsersAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { if (!\in_array('actusers', $qb->getAllAliases(), true)) { $qb->leftJoin('activity.users', 'actusers'); @@ -48,12 +48,27 @@ class ActivityUsersAggregator implements AggregatorInterface // nothing to add on the form } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data) + public function getLabels($key, array $values, $data): callable { return function ($value) { if ('_header' === $value) { @@ -70,12 +85,12 @@ class ActivityUsersAggregator implements AggregatorInterface }; } - public function getQueryKeys($data) + public function getQueryKeys($data): array { return ['activity_users_aggregator']; } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'Aggregate by activity users'; } diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersJobAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersJobAggregator.php index 9b37bda2a..14ffff521 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersJobAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersJobAggregator.php @@ -34,7 +34,7 @@ class ActivityUsersJobAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { $p = self::PREFIX; @@ -65,14 +65,29 @@ class ActivityUsersJobAggregator implements AggregatorInterface return Declarations::ACTIVITY; } - public function buildForm(FormBuilderInterface $builder) {} + public function buildForm(FormBuilderInterface $builder): void {} + + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data) + public function getLabels($key, array $values, $data): callable { return function ($value): string { if ('_header' === $value) { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersScopeAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersScopeAggregator.php index fb2743ed2..260f4cd10 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersScopeAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersScopeAggregator.php @@ -34,7 +34,7 @@ class ActivityUsersScopeAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { $p = self::PREFIX; @@ -65,14 +65,29 @@ class ActivityUsersScopeAggregator implements AggregatorInterface return Declarations::ACTIVITY; } - public function buildForm(FormBuilderInterface $builder) {} + public function buildForm(FormBuilderInterface $builder): void {} + + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data) + public function getLabels($key, array $values, $data): callable { return function ($value): string { if ('_header' === $value) { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ByCreatorAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ByCreatorAggregator.php index a8250c3dc..8da8019e9 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ByCreatorAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ByCreatorAggregator.php @@ -27,7 +27,7 @@ class ByCreatorAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { $qb->addSelect('IDENTITY(activity.createdBy) AS creator_aggregator'); $qb->addGroupBy('creator_aggregator'); @@ -43,12 +43,27 @@ class ByCreatorAggregator implements AggregatorInterface // no form } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data) + public function getLabels($key, array $values, $data): callable { return function ($value): string { if ('_header' === $value) { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ByThirdpartyAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ByThirdpartyAggregator.php index a7dc60cf6..ed4cc699e 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ByThirdpartyAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ByThirdpartyAggregator.php @@ -27,7 +27,7 @@ class ByThirdpartyAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { if (!\in_array('acttparty', $qb->getAllAliases(), true)) { $qb->leftJoin('activity.thirdParties', 'acttparty'); @@ -47,12 +47,27 @@ class ByThirdpartyAggregator implements AggregatorInterface // no form } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data) + public function getLabels($key, array $values, $data): callable { return function ($value): string { if ('_header' === $value) { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/CreatorJobAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/CreatorJobAggregator.php index 62c417e4b..d42fd48b4 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/CreatorJobAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/CreatorJobAggregator.php @@ -34,7 +34,7 @@ class CreatorJobAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { $p = self::PREFIX; @@ -65,14 +65,29 @@ class CreatorJobAggregator implements AggregatorInterface return Declarations::ACTIVITY; } - public function buildForm(FormBuilderInterface $builder) {} + public function buildForm(FormBuilderInterface $builder): void {} + + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data) + public function getLabels($key, array $values, $data): callable { return function ($value): string { if ('_header' === $value) { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/CreatorScopeAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/CreatorScopeAggregator.php index b844b2fd5..b50457a02 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/CreatorScopeAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/CreatorScopeAggregator.php @@ -34,7 +34,7 @@ class CreatorScopeAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { $p = self::PREFIX; @@ -65,14 +65,29 @@ class CreatorScopeAggregator implements AggregatorInterface return Declarations::ACTIVITY; } - public function buildForm(FormBuilderInterface $builder) {} + public function buildForm(FormBuilderInterface $builder): void {} + + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data) + public function getLabels($key, array $values, $data): callable { return function ($value): string { if ('_header' === $value) { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/DateAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/DateAggregator.php index 72fd01049..58321383e 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/DateAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/DateAggregator.php @@ -32,7 +32,7 @@ class DateAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { $order = null; @@ -76,12 +76,27 @@ class DateAggregator implements AggregatorInterface ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['frequency' => $formData['frequency']]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['frequency' => $formData['frequency']]; + } + public function getFormDefaultData(): array { return ['frequency' => self::DEFAULT_CHOICE]; } - public function getLabels($key, array $values, $data) + public function getLabels($key, array $values, $data): callable { return static function ($value) use ($data): string { if ('_header' === $value) { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/LocationTypeAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/LocationTypeAggregator.php index 372ec8c2c..3497313df 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/LocationTypeAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/LocationTypeAggregator.php @@ -27,7 +27,7 @@ class LocationTypeAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { if (!\in_array('actloc', $qb->getAllAliases(), true)) { $qb->leftJoin('activity.location', 'actloc'); @@ -47,12 +47,27 @@ class LocationTypeAggregator implements AggregatorInterface // no form } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data) + public function getLabels($key, array $values, $data): callable { return function ($value): string { if ('_header' === $value) { diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/PersonAggregators/HouseholdAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/PersonAggregators/HouseholdAggregator.php index 19bb2793b..8eb3054c7 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/PersonAggregators/HouseholdAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/PersonAggregators/HouseholdAggregator.php @@ -29,12 +29,27 @@ final readonly class HouseholdAggregator implements AggregatorInterface // nothing to add here } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, mixed $data) + public function getLabels($key, array $values, mixed $data): callable { return function (int|string|null $value): string|int { if ('_header' === $value) { @@ -49,12 +64,12 @@ final readonly class HouseholdAggregator implements AggregatorInterface }; } - public function getQueryKeys($data) + public function getQueryKeys($data): array { return ['activity_household_agg']; } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'export.aggregator.person.by_household.title'; } @@ -64,7 +79,7 @@ final readonly class HouseholdAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { $qb->join( HouseholdMember::class, @@ -92,7 +107,7 @@ final readonly class HouseholdAggregator implements AggregatorInterface ->addGroupBy('activity_household_agg'); } - public function applyOn() + public function applyOn(): string { return Declarations::ACTIVITY_PERSON; } diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/PersonAggregators/PersonAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/PersonAggregators/PersonAggregator.php index 2879a678d..cff7e26d2 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/PersonAggregators/PersonAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/PersonAggregators/PersonAggregator.php @@ -26,6 +26,21 @@ final readonly class PersonAggregator implements AggregatorInterface // nothing to add here } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; @@ -36,12 +51,12 @@ final readonly class PersonAggregator implements AggregatorInterface return $this->labelPersonHelper->getLabel($key, $values, 'export.aggregator.person.by_person.person'); } - public function getQueryKeys($data) + public function getQueryKeys($data): array { return ['activity_by_person_agg']; } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'export.aggregator.person.by_person.title'; } @@ -51,14 +66,14 @@ final readonly class PersonAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { $qb ->addSelect('IDENTITY(activity.person) AS activity_by_person_agg') ->addGroupBy('activity_by_person_agg'); } - public function applyOn() + public function applyOn(): string { return Declarations::ACTIVITY_PERSON; } diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/PersonsAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/PersonsAggregator.php index 4e996d523..b9189001c 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/PersonsAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/PersonsAggregator.php @@ -32,6 +32,21 @@ final readonly class PersonsAggregator implements AggregatorInterface // nothing to add here } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; @@ -46,12 +61,12 @@ final readonly class PersonsAggregator implements AggregatorInterface return $this->labelPersonHelper->getLabel($key, $values, 'export.aggregator.activity.by_persons.Persons'); } - public function getQueryKeys($data) + public function getQueryKeys($data): array { return [self::PREFIX.'_pid']; } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'export.aggregator.activity.by_persons.Group activity by persons'; } @@ -61,7 +76,7 @@ final readonly class PersonsAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { $p = self::PREFIX; @@ -71,7 +86,7 @@ final readonly class PersonsAggregator implements AggregatorInterface ->addGroupBy("{$p}_pid"); } - public function applyOn() + public function applyOn(): string { return Declarations::ACTIVITY; } diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/SentReceivedAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/SentReceivedAggregator.php index 774968544..5b17e52ee 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/SentReceivedAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/SentReceivedAggregator.php @@ -26,7 +26,7 @@ class SentReceivedAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { $qb->addSelect('activity.sentReceived AS activity_sentreceived_aggregator') ->addGroupBy('activity_sentreceived_aggregator'); @@ -42,6 +42,21 @@ class SentReceivedAggregator implements AggregatorInterface // No form needed } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; diff --git a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/AvgActivityDuration.php b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/AvgActivityDuration.php index d8b204e8c..756f81d57 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/AvgActivityDuration.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/AvgActivityDuration.php @@ -15,6 +15,7 @@ use Chill\ActivityBundle\Export\Declarations; use Chill\ActivityBundle\Repository\ActivityRepository; use Chill\ActivityBundle\Security\Authorization\ActivityStatsVoter; use Chill\MainBundle\Export\AccompanyingCourseExportHelper; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\ExportInterface; use Chill\MainBundle\Export\FormatterInterface; use Chill\MainBundle\Export\GroupedExportInterface; @@ -38,6 +39,21 @@ class AvgActivityDuration implements ExportInterface, GroupedExportInterface public function buildForm(FormBuilderInterface $builder) {} + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; @@ -72,7 +88,7 @@ class AvgActivityDuration implements ExportInterface, GroupedExportInterface return ['export_avg_activity_duration']; } - public function getResult($query, $data) + public function getResult($query, $data, ExportGenerationContext $context): array { return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); } @@ -87,7 +103,7 @@ class AvgActivityDuration implements ExportInterface, GroupedExportInterface return Declarations::ACTIVITY; } - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + public function initiateQuery(array $requiredModifiers, array $acl, array $data, ExportGenerationContext $context): \Doctrine\ORM\QueryBuilder { $centers = array_map(static fn ($el) => $el['center'], $acl); diff --git a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/AvgActivityVisitDuration.php b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/AvgActivityVisitDuration.php index 734ecca85..49ea42483 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/AvgActivityVisitDuration.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/AvgActivityVisitDuration.php @@ -15,6 +15,7 @@ use Chill\ActivityBundle\Entity\Activity; use Chill\ActivityBundle\Export\Declarations; use Chill\ActivityBundle\Security\Authorization\ActivityStatsVoter; use Chill\MainBundle\Export\AccompanyingCourseExportHelper; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\ExportInterface; use Chill\MainBundle\Export\FormatterInterface; use Chill\MainBundle\Export\GroupedExportInterface; @@ -46,6 +47,21 @@ class AvgActivityVisitDuration implements ExportInterface, GroupedExportInterfac // TODO: Implement buildForm() method. } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; @@ -80,7 +96,7 @@ class AvgActivityVisitDuration implements ExportInterface, GroupedExportInterfac return ['export_avg_activity_visit_duration']; } - public function getResult($query, $data) + public function getResult($query, $data, ExportGenerationContext $context): array { return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); } @@ -95,7 +111,7 @@ class AvgActivityVisitDuration implements ExportInterface, GroupedExportInterfac return Declarations::ACTIVITY; } - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + public function initiateQuery(array $requiredModifiers, array $acl, array $data, ExportGenerationContext $context): \Doctrine\ORM\QueryBuilder { $centers = array_map(static fn ($el) => $el['center'], $acl); diff --git a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/CountActivity.php b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/CountActivity.php index dfd5d966f..a1f0a30d8 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/CountActivity.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/CountActivity.php @@ -43,6 +43,21 @@ class CountActivity implements ExportInterface, GroupedExportInterface public function buildForm(FormBuilderInterface $builder) {} + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; @@ -77,7 +92,7 @@ class CountActivity implements ExportInterface, GroupedExportInterface return ['export_count_activity']; } - public function getResult($query, $data) + public function getResult($query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array { return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); } @@ -92,7 +107,7 @@ class CountActivity implements ExportInterface, GroupedExportInterface return Declarations::ACTIVITY; } - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): \Doctrine\ORM\QueryBuilder { $centers = array_map(static fn ($el) => $el['center'], $acl); diff --git a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/CountHouseholdOnActivity.php b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/CountHouseholdOnActivity.php index f9fbc8643..61b8f7b2e 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/CountHouseholdOnActivity.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/CountHouseholdOnActivity.php @@ -44,6 +44,21 @@ final readonly class CountHouseholdOnActivity implements ExportInterface, Groupe public function buildForm(FormBuilderInterface $builder): void {} + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; @@ -78,7 +93,7 @@ final readonly class CountHouseholdOnActivity implements ExportInterface, Groupe return ['export_count_activity']; } - public function getResult($query, $data) + public function getResult($query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array { return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); } @@ -93,7 +108,7 @@ final readonly class CountHouseholdOnActivity implements ExportInterface, Groupe return Declarations::ACTIVITY; } - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): \Doctrine\ORM\QueryBuilder { $centers = array_map(static fn ($el) => $el['center'], $acl); diff --git a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/CountPersonsOnActivity.php b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/CountPersonsOnActivity.php index c49b9d4ad..0ce5f722e 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/CountPersonsOnActivity.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/CountPersonsOnActivity.php @@ -43,6 +43,21 @@ class CountPersonsOnActivity implements ExportInterface, GroupedExportInterface public function buildForm(FormBuilderInterface $builder) {} + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; @@ -77,7 +92,7 @@ class CountPersonsOnActivity implements ExportInterface, GroupedExportInterface return ['export_count_activity']; } - public function getResult($query, $data) + public function getResult($query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array { return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); } @@ -92,7 +107,7 @@ class CountPersonsOnActivity implements ExportInterface, GroupedExportInterface return Declarations::ACTIVITY; } - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): \Doctrine\ORM\QueryBuilder { $centers = array_map(static fn ($el) => $el['center'], $acl); diff --git a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/ListActivity.php b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/ListActivity.php index eb210682d..ac877d9dd 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/ListActivity.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/ListActivity.php @@ -17,6 +17,7 @@ use Chill\ActivityBundle\Export\Export\ListActivityHelper; use Chill\ActivityBundle\Security\Authorization\ActivityStatsVoter; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Export\AccompanyingCourseExportHelper; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\GroupedExportInterface; use Chill\MainBundle\Export\Helper\TranslatableStringExportLabelHelper; use Chill\MainBundle\Export\ListInterface; @@ -38,6 +39,21 @@ final readonly class ListActivity implements ListInterface, GroupedExportInterfa $this->helper->buildForm($builder); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; @@ -85,12 +101,12 @@ final readonly class ListActivity implements ListInterface, GroupedExportInterfa ); } - public function getResult($query, $data) + public function getResult($query, $data, ExportGenerationContext $context): array { return $this->helper->getResult($query, $data); } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return ListActivityHelper::MSG_KEY.'List activity linked to a course'; } @@ -100,10 +116,8 @@ final readonly class ListActivity implements ListInterface, GroupedExportInterfa return $this->helper->getType(); } - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + public function initiateQuery(array $requiredModifiers, array $acl, array $data, ExportGenerationContext $context): \Doctrine\ORM\QueryBuilder { - $centers = array_map(static fn ($el) => $el['center'], $acl); - $qb = $this->entityManager->createQueryBuilder(); $qb @@ -114,7 +128,7 @@ final readonly class ListActivity implements ListInterface, GroupedExportInterfa ->leftJoin('acppart.person', 'person') ->andWhere('acppart.startDate != acppart.endDate OR acppart.endDate IS NULL'); - $this->filterListAccompanyingPeriodHelper->addFilterAccompanyingPeriods($qb, $requiredModifiers, $acl, $data); + $this->filterListAccompanyingPeriodHelper->addFilterAccompanyingPeriods($qb, $requiredModifiers, $acl, $context->byUser, $data); $qb // some grouping are necessary diff --git a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/SumActivityDuration.php b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/SumActivityDuration.php index d9e2b9ce8..151018907 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/SumActivityDuration.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/SumActivityDuration.php @@ -40,9 +40,21 @@ class SumActivityDuration implements ExportInterface, GroupedExportInterface $this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center']; } - public function buildForm(FormBuilderInterface $builder): void + public function buildForm(FormBuilderInterface $builder) {} + + public function getNormalizationVersion(): int { - // TODO: Implement buildForm() method. + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; } public function getFormDefaultData(): array @@ -79,7 +91,7 @@ class SumActivityDuration implements ExportInterface, GroupedExportInterface return ['export_sum_activity_duration']; } - public function getResult($query, $data) + public function getResult($query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array { return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); } @@ -94,7 +106,7 @@ class SumActivityDuration implements ExportInterface, GroupedExportInterface return Declarations::ACTIVITY; } - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): \Doctrine\ORM\QueryBuilder { $centers = array_map(static fn ($el) => $el['center'], $acl); diff --git a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/SumActivityVisitDuration.php b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/SumActivityVisitDuration.php index 152223bd1..735d55182 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/SumActivityVisitDuration.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/SumActivityVisitDuration.php @@ -40,9 +40,21 @@ class SumActivityVisitDuration implements ExportInterface, GroupedExportInterfac $this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center']; } - public function buildForm(FormBuilderInterface $builder): void + public function buildForm(FormBuilderInterface $builder) {} + + public function getNormalizationVersion(): int { - // TODO: Implement buildForm() method. + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; } public function getFormDefaultData(): array @@ -79,7 +91,7 @@ class SumActivityVisitDuration implements ExportInterface, GroupedExportInterfac return ['export_sum_activity_visit_duration']; } - public function getResult($query, $data) + public function getResult($query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array { return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); } @@ -94,7 +106,7 @@ class SumActivityVisitDuration implements ExportInterface, GroupedExportInterfac return Declarations::ACTIVITY; } - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): \Doctrine\ORM\QueryBuilder { $centers = array_map(static fn ($el) => $el['center'], $acl); diff --git a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/CountActivity.php b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/CountActivity.php index e0a95b1f5..0239aa481 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/CountActivity.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/CountActivity.php @@ -35,6 +35,21 @@ class CountActivity implements ExportInterface, GroupedExportInterface public function buildForm(FormBuilderInterface $builder) {} + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; @@ -64,17 +79,17 @@ class CountActivity implements ExportInterface, GroupedExportInterface return static fn ($value) => '_header' === $value ? 'Number of activities linked to a person' : $value; } - public function getQueryKeys($data) + public function getQueryKeys($data): array { return ['export_count_activity']; } - public function getResult($query, $data) + public function getResult($query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array { return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'Count activities linked to a person'; } @@ -84,7 +99,7 @@ class CountActivity implements ExportInterface, GroupedExportInterface return Declarations::ACTIVITY; } - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): \Doctrine\ORM\QueryBuilder { $centers = array_map(static fn ($el) => $el['center'], $acl); diff --git a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/CountHouseholdOnActivity.php b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/CountHouseholdOnActivity.php index 08f063df9..07afdcf54 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/CountHouseholdOnActivity.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/CountHouseholdOnActivity.php @@ -36,6 +36,21 @@ final readonly class CountHouseholdOnActivity implements ExportInterface, Groupe public function buildForm(FormBuilderInterface $builder): void {} + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; @@ -65,17 +80,17 @@ final readonly class CountHouseholdOnActivity implements ExportInterface, Groupe return static fn ($value) => '_header' === $value ? 'export.export.count_household_on_activity_person.header' : $value; } - public function getQueryKeys($data) + public function getQueryKeys($data): array { return ['export_count_activity']; } - public function getResult($query, $data) + public function getResult($query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array { return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'export.export.count_household_on_activity_person.title'; } @@ -85,7 +100,7 @@ final readonly class CountHouseholdOnActivity implements ExportInterface, Groupe return Declarations::ACTIVITY; } - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): \Doctrine\ORM\QueryBuilder { $centers = array_map(static fn ($el) => $el['center'], $acl); diff --git a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/ListActivity.php b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/ListActivity.php index 7225607bc..e6c8e7c6a 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/ListActivity.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/ListActivity.php @@ -78,6 +78,21 @@ class ListActivity implements ListInterface, GroupedExportInterface ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['fields' => $formData['fields']]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['fields' => $formData['fields']]; + } + public function getFormDefaultData(): array { return []; @@ -167,17 +182,17 @@ class ListActivity implements ListInterface, GroupedExportInterface } } - public function getQueryKeys($data) + public function getQueryKeys($data): array { return $data['fields']; } - public function getResult($query, $data) + public function getResult($query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array { return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'List activity linked to a person'; } @@ -187,7 +202,7 @@ class ListActivity implements ListInterface, GroupedExportInterface return Declarations::ACTIVITY; } - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): \Doctrine\ORM\QueryBuilder { $centers = array_map(static fn ($el) => $el['center'], $acl); diff --git a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/StatActivityDuration.php b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/StatActivityDuration.php index 547dbf6bc..e971fe129 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/StatActivityDuration.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/LinkedToPerson/StatActivityDuration.php @@ -49,6 +49,21 @@ class StatActivityDuration implements ExportInterface, GroupedExportInterface public function buildForm(FormBuilderInterface $builder) {} + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; @@ -84,17 +99,17 @@ class StatActivityDuration implements ExportInterface, GroupedExportInterface return static fn (string $value) => '_header' === $value ? $header : $value; } - public function getQueryKeys($data) + public function getQueryKeys($data): array { return ['export_stat_activity']; } - public function getResult($query, $data) + public function getResult($query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array { return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { if (self::SUM === $this->action) { return 'Sum activity linked to a person duration'; @@ -108,7 +123,7 @@ class StatActivityDuration implements ExportInterface, GroupedExportInterface return Declarations::ACTIVITY; } - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): \Doctrine\ORM\QueryBuilder { $centers = array_map( static fn (array $el): Center => $el['center'], diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/ActivityTypeFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/ActivityTypeFilter.php index 79cc1fb2e..5953f95d8 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/ActivityTypeFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/ActivityTypeFilter.php @@ -14,6 +14,7 @@ namespace Chill\ActivityBundle\Export\Filter\ACPFilters; use Chill\ActivityBundle\Entity\Activity; use Chill\ActivityBundle\Entity\ActivityType; use Chill\ActivityBundle\Repository\ActivityTypeRepositoryInterface; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Form\Type\PickRollingDateType; use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface; @@ -25,6 +26,7 @@ use Symfony\Component\Form\FormBuilderInterface; final readonly class ActivityTypeFilter implements FilterInterface { + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; private const BASE_EXISTS = 'SELECT 1 FROM '.Activity::class.' act_type_filter_activity WHERE act_type_filter_activity.accompanyingPeriod = acp'; public function __construct( @@ -38,7 +40,7 @@ final readonly class ActivityTypeFilter implements FilterInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $exists = self::BASE_EXISTS; @@ -62,7 +64,7 @@ final readonly class ActivityTypeFilter implements FilterInterface } } - public function applyOn() + public function applyOn(): string { return Declarations::ACP_TYPE; } @@ -92,6 +94,21 @@ final readonly class ActivityTypeFilter implements FilterInterface ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['accepted_activitytypes' => $this->normalizeDoctrineEntity($formData['accepted_activitytypes']), 'date_after' => $formData['date_after']?->normalize(), 'date_before' => $formData['date_before']?->normalize()]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['accepted_activitytypes' => $this->denormalizeDoctrineEntity($formData['accepted_activitytypes'], $this->activityTypeRepository), 'date_after' => \Chill\MainBundle\Service\RollingDate\RollingDate::fromNormalized($formData['date_after']), 'date_before' => \Chill\MainBundle\Service\RollingDate\RollingDate::fromNormalized($formData['date_before'])]; + } + public function getFormDefaultData(): array { return [ @@ -101,7 +118,7 @@ final readonly class ActivityTypeFilter implements FilterInterface ]; } - public function describeAction($data, $format = 'string'): array + public function describeAction($data, ExportGenerationContext $context): array { $types = []; diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialActionFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialActionFilter.php index 8007b87e3..37011fb25 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialActionFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialActionFilter.php @@ -12,23 +12,28 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Export\Filter\ACPFilters; use Chill\ActivityBundle\Export\Declarations; +use Chill\MainBundle\Export\ExportDataNormalizerTrait; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\PersonBundle\Entity\SocialWork\SocialAction; use Chill\PersonBundle\Form\Type\PickSocialActionType; +use Chill\PersonBundle\Repository\SocialWork\SocialActionRepository; use Chill\PersonBundle\Templating\Entity\SocialActionRender; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; class BySocialActionFilter implements FilterInterface { - public function __construct(private readonly SocialActionRender $actionRender) {} + use ExportDataNormalizerTrait; + + public function __construct(private readonly SocialActionRender $actionRender, private readonly SocialActionRepository $socialActionRepository) {} public function addRole(): ?string { return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { if (!\in_array('actsocialaction', $qb->getAllAliases(), true)) { $qb->join('activity.socialActions', 'actsocialaction'); @@ -55,12 +60,29 @@ class BySocialActionFilter implements FilterInterface ]); } - public function getFormDefaultData(): array + public function getNormalizationVersion(): int { - return []; + return 1; } - public function describeAction($data, $format = 'string'): array + public function normalizeFormData(array $formData): array + { + return ['accepted_socialactions' => $this->normalizeDoctrineEntity($formData['accepted_socialactions'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['accepted_socialactions' => $this->denormalizeDoctrineEntity($formData['accepted_socialactions'], $this->socialActionRepository)]; + } + + public function getFormDefaultData(): array + { + return [ + 'accepted_socialactions' => [], + ]; + } + + public function describeAction($data, ExportGenerationContext $context): array { $actions = []; diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialIssueFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialIssueFilter.php index 86788a2b8..15a3e7e2e 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialIssueFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialIssueFilter.php @@ -12,23 +12,28 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Export\Filter\ACPFilters; use Chill\ActivityBundle\Export\Declarations; +use Chill\MainBundle\Export\ExportDataNormalizerTrait; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\PersonBundle\Entity\SocialWork\SocialIssue; use Chill\PersonBundle\Form\Type\PickSocialIssueType; +use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository; use Chill\PersonBundle\Templating\Entity\SocialIssueRender; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; class BySocialIssueFilter implements FilterInterface { - public function __construct(private readonly SocialIssueRender $issueRender) {} + use ExportDataNormalizerTrait; + + public function __construct(private readonly SocialIssueRender $issueRender, private readonly SocialIssueRepository $issueRepository) {} public function addRole(): ?string { return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { if (!\in_array('actsocialissue', $qb->getAllAliases(), true)) { $qb->join('activity.socialIssues', 'actsocialissue'); @@ -55,12 +60,27 @@ class BySocialIssueFilter implements FilterInterface ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['accepted_socialissues' => $this->normalizeDoctrineEntity($formData['accepted_socialissues'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['accepted_socialissues' => $this->denormalizeDoctrineEntity($formData['accepted_socialissues'], $this->issueRepository)]; + } + public function getFormDefaultData(): array { return []; } - public function describeAction($data, $format = 'string'): array + public function describeAction($data, ExportGenerationContext $context): array { $issues = []; diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/HasNoActivityFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/HasNoActivityFilter.php index 5fd46e6e5..15c517e51 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/HasNoActivityFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/HasNoActivityFilter.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Export\Filter\ACPFilters; use Chill\ActivityBundle\Entity\Activity; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\PersonBundle\Export\Declarations; use Doctrine\ORM\QueryBuilder; @@ -27,7 +28,7 @@ class HasNoActivityFilter implements FilterInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $qb ->andWhere(' @@ -48,12 +49,27 @@ class HasNoActivityFilter implements FilterInterface // no form needed } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function describeAction($data, $format = 'string'): array + public function describeAction($data, ExportGenerationContext $context): array { return ['Filtered acp which has no activities', []]; } diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/PeriodHavingActivityBetweenDatesFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/PeriodHavingActivityBetweenDatesFilter.php index e01ba5282..da2dfb3c3 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/PeriodHavingActivityBetweenDatesFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/PeriodHavingActivityBetweenDatesFilter.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Export\Filter\ACPFilters; use Chill\ActivityBundle\Entity\Activity; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Form\Type\PickRollingDateType; use Chill\MainBundle\Service\RollingDate\RollingDate; @@ -25,7 +26,7 @@ final readonly class PeriodHavingActivityBetweenDatesFilter implements FilterInt private RollingDateConverterInterface $rollingDateConverter, ) {} - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'export.filter.activity.course_having_activity_between_date.Title'; } @@ -41,6 +42,21 @@ final readonly class PeriodHavingActivityBetweenDatesFilter implements FilterInt ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['start_date' => $formData['start_date']->normalize(), 'end_date' => $formData['end_date']->normalize()]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['start_date' => RollingDate::fromNormalized($formData['start_date']), 'end_date' => RollingDate::fromNormalized($formData['end_date'])]; + } + public function getFormDefaultData(): array { return [ @@ -49,7 +65,7 @@ final readonly class PeriodHavingActivityBetweenDatesFilter implements FilterInt ]; } - public function describeAction($data, $format = 'string') + public function describeAction($data, ExportGenerationContext $context): string|\Symfony\Contracts\Translation\TranslatableInterface|array { return [ 'export.filter.activity.course_having_activity_between_date.Only course having an activity between from and to', @@ -65,7 +81,7 @@ final readonly class PeriodHavingActivityBetweenDatesFilter implements FilterInt return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $alias = 'act_period_having_act_betw_date_alias'; $from = 'act_period_having_act_betw_date_start'; @@ -82,7 +98,7 @@ final readonly class PeriodHavingActivityBetweenDatesFilter implements FilterInt ->setParameter($to, $this->rollingDateConverter->convert($data['end_date'])); } - public function applyOn() + public function applyOn(): string { return \Chill\PersonBundle\Export\Declarations::ACP_TYPE; } diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ActivityDateFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ActivityDateFilter.php index 203536c8c..bc254abae 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ActivityDateFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ActivityDateFilter.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Export\Filter; use Chill\ActivityBundle\Export\Declarations; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Form\Type\PickRollingDateType; use Chill\MainBundle\Service\RollingDate\RollingDate; @@ -30,7 +31,7 @@ class ActivityDateFilter implements FilterInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $where = $qb->getDQLPart('where'); $clause = $qb->expr()->between( @@ -72,12 +73,27 @@ class ActivityDateFilter implements FilterInterface ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['date_from' => $formData['date_from']->normalize(), 'date_to' => $formData['date_to']->normalize()]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['date_from' => RollingDate::fromNormalized($formData['date_from']), 'date_to' => RollingDate::fromNormalized($formData['date_to'])]; + } + public function getFormDefaultData(): array { return ['date_from' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), 'date_to' => new RollingDate(RollingDate::T_TODAY)]; } - public function describeAction($data, $format = 'string') + public function describeAction($data, ExportGenerationContext $context): string|\Symfony\Contracts\Translation\TranslatableInterface|array { return [ 'Filtered by date of activity: only between %date_from% and %date_to%', @@ -88,7 +104,7 @@ class ActivityDateFilter implements FilterInterface ]; } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'Filtered by date activity'; } diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ActivityPresenceFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ActivityPresenceFilter.php index 130dcde92..d431fa260 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ActivityPresenceFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ActivityPresenceFilter.php @@ -13,6 +13,8 @@ namespace Chill\ActivityBundle\Export\Filter; use Chill\ActivityBundle\Entity\ActivityPresence; use Chill\ActivityBundle\Export\Declarations; +use Chill\ActivityBundle\Repository\ActivityPresenceRepositoryInterface; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Doctrine\Common\Collections\Collection; @@ -23,12 +25,15 @@ use Symfony\Contracts\Translation\TranslatorInterface; final readonly class ActivityPresenceFilter implements FilterInterface { + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; + public function __construct( private TranslatableStringHelperInterface $translatableStringHelper, private TranslatorInterface $translator, + private ActivityPresenceRepositoryInterface $activityPresenceRepository, ) {} - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'export.filter.activity.by_presence.Filter activity by activity presence'; } @@ -45,12 +50,27 @@ final readonly class ActivityPresenceFilter implements FilterInterface ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['presences' => $this->normalizeDoctrineEntity($formData['presences'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['presences' => $this->denormalizeDoctrineEntity($formData['presences'], $this->activityPresenceRepository)]; + } + public function getFormDefaultData(): array { return []; } - public function describeAction($data, $format = 'string') + public function describeAction($data, ExportGenerationContext $context): string|\Symfony\Contracts\Translation\TranslatableInterface|array { $presences = array_map( fn (ActivityPresence $presence) => $this->translatableStringHelper->localize($presence->getName()), @@ -68,14 +88,14 @@ final readonly class ActivityPresenceFilter implements FilterInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $qb ->andWhere('activity.attendee IN (:activity_presence_filter_presences)') ->setParameter('activity_presence_filter_presences', $data['presences']); } - public function applyOn() + public function applyOn(): string { return Declarations::ACTIVITY; } diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ActivityTypeFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ActivityTypeFilter.php index c263227c6..99d10cf4d 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ActivityTypeFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ActivityTypeFilter.php @@ -15,6 +15,7 @@ use Chill\ActivityBundle\Entity\ActivityType; use Chill\ActivityBundle\Export\Declarations; use Chill\ActivityBundle\Repository\ActivityTypeRepositoryInterface; use Chill\MainBundle\Export\ExportElementValidatedInterface; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Doctrine\ORM\QueryBuilder; @@ -24,6 +25,8 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; class ActivityTypeFilter implements ExportElementValidatedInterface, FilterInterface { + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; + public function __construct( protected TranslatableStringHelperInterface $translatableStringHelper, protected ActivityTypeRepositoryInterface $activityTypeRepository, @@ -34,7 +37,7 @@ class ActivityTypeFilter implements ExportElementValidatedInterface, FilterInter return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $clause = $qb->expr()->in('activity.activityType', ':selected_activity_types'); @@ -70,12 +73,27 @@ class ActivityTypeFilter implements ExportElementValidatedInterface, FilterInter ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['types' => $this->normalizeDoctrineEntity($formData['types'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['types' => $this->denormalizeDoctrineEntity($formData['types'], $this->activityTypeRepository)]; + } + public function getFormDefaultData(): array { return []; } - public function describeAction($data, $format = 'string') + public function describeAction($data, ExportGenerationContext $context): string|\Symfony\Contracts\Translation\TranslatableInterface|array { // collect all the reasons'name used in this filter in one array $reasonsNames = array_map( @@ -88,7 +106,7 @@ class ActivityTypeFilter implements ExportElementValidatedInterface, FilterInter ]]; } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'Filter by activity type'; } diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ActivityUsersFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ActivityUsersFilter.php index 8db45b862..a3c70f9fe 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ActivityUsersFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ActivityUsersFilter.php @@ -12,26 +12,30 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Export\Filter; use Chill\ActivityBundle\Export\Declarations; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; -use Chill\MainBundle\Form\Type\PickUserDynamicType; +use Chill\MainBundle\Form\Type\PickUserOrMeDynamicType; +use Chill\MainBundle\Repository\UserRepositoryInterface; use Chill\MainBundle\Templating\Entity\UserRender; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; -class ActivityUsersFilter implements FilterInterface +final readonly class ActivityUsersFilter implements FilterInterface { - public function __construct(private readonly UserRender $userRender) {} + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; + + public function __construct(private UserRender $userRender, private UserRepositoryInterface $userRepository) {} public function addRole(): ?string { return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $orX = $qb->expr()->orX(); - foreach ($data['accepted_users'] as $key => $user) { + foreach ($this->userOrMe($data['accepted_users'], $exportGenerationContext) as $key => $user) { $orX->add($qb->expr()->isMemberOf(':activity_users_filter_u'.$key, 'activity.users')); $qb->setParameter('activity_users_filter_u'.$key, $user); } @@ -39,29 +43,44 @@ class ActivityUsersFilter implements FilterInterface $qb->andWhere($orX); } - public function applyOn() + public function applyOn(): string { return Declarations::ACTIVITY; } public function buildForm(FormBuilderInterface $builder): void { - $builder->add('accepted_users', PickUserDynamicType::class, [ + $builder->add('accepted_users', PickUserOrMeDynamicType::class, [ 'multiple' => true, 'label' => 'Users', ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['accepted_users' => $this->normalizeUserOrMe($formData['accepted_users'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['accepted_users' => $this->denormalizeUserOrMe($formData['accepted_users'], $this->userRepository)]; + } + public function getFormDefaultData(): array { return []; } - public function describeAction($data, $format = 'string') + public function describeAction($data, ExportGenerationContext $context): string|\Symfony\Contracts\Translation\TranslatableInterface|array { $users = []; - foreach ($data['accepted_users'] as $u) { + foreach ($this->userOrMe($data['accepted_users'], $context) as $u) { $users[] = $this->userRender->renderString($u, []); } diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ByCreatorFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ByCreatorFilter.php index ccf07b098..bdad045c8 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ByCreatorFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ByCreatorFilter.php @@ -12,28 +12,32 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Export\Filter; use Chill\ActivityBundle\Export\Declarations; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; -use Chill\MainBundle\Form\Type\PickUserDynamicType; +use Chill\MainBundle\Form\Type\PickUserOrMeDynamicType; +use Chill\MainBundle\Repository\UserRepositoryInterface; use Chill\MainBundle\Templating\Entity\UserRender; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; class ByCreatorFilter implements FilterInterface { - public function __construct(private readonly UserRender $userRender) {} + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; + + public function __construct(private readonly UserRender $userRender, private readonly UserRepositoryInterface $userRepository) {} public function addRole(): ?string { return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $qb ->andWhere( $qb->expr()->in('activity.createdBy', ':users') ) - ->setParameter('users', $data['accepted_users']); + ->setParameter('users', $this->userOrMe($data['accepted_users'], $exportGenerationContext)); } public function applyOn(): string @@ -43,21 +47,36 @@ class ByCreatorFilter implements FilterInterface public function buildForm(FormBuilderInterface $builder): void { - $builder->add('accepted_users', PickUserDynamicType::class, [ + $builder->add('accepted_users', PickUserOrMeDynamicType::class, [ 'multiple' => true, ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['accepted_users' => $this->normalizeUserOrMe($formData['accepted_users'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['accepted_users' => $this->denormalizeUserOrMe($formData['accepted_users'], $this->userRepository)]; + } + public function getFormDefaultData(): array { return []; } - public function describeAction($data, $format = 'string'): array + public function describeAction($data, ExportGenerationContext $context): array { $users = []; - foreach ($data['accepted_users'] as $u) { + foreach ($this->userOrMe($data['accepted_users'], $context) as $u) { $users[] = $this->userRender->renderString($u, []); } diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/CreatorJobFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/CreatorJobFilter.php index 36994d9eb..dd87200c3 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/CreatorJobFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/CreatorJobFilter.php @@ -14,6 +14,7 @@ namespace Chill\ActivityBundle\Export\Filter; use Chill\ActivityBundle\Export\Declarations; use Chill\MainBundle\Entity\User\UserJobHistory; use Chill\MainBundle\Entity\UserJob; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Repository\UserJobRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelper; @@ -26,6 +27,7 @@ use Symfony\Contracts\Translation\TranslatorInterface; final readonly class CreatorJobFilter implements FilterInterface { + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; private const PREFIX = 'acp_act_filter_creator_job'; public function __construct( @@ -39,7 +41,7 @@ final readonly class CreatorJobFilter implements FilterInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $p = self::PREFIX; @@ -90,7 +92,22 @@ final readonly class CreatorJobFilter implements FilterInterface ]); } - public function describeAction($data, $format = 'string'): array + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['jobs' => $this->normalizeDoctrineEntity($formData['jobs'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['jobs' => $this->denormalizeDoctrineEntity($formData['jobs'], $this->userJobRepository)]; + } + + public function describeAction($data, ExportGenerationContext $context): array { $jobs = array_map( fn (UserJob $job) => $this->translatableStringHelper->localize($job->getLabel()), diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/CreatorScopeFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/CreatorScopeFilter.php index 1fee9635e..bb695d68c 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/CreatorScopeFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/CreatorScopeFilter.php @@ -14,6 +14,7 @@ namespace Chill\ActivityBundle\Export\Filter; use Chill\ActivityBundle\Export\Declarations; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User\UserScopeHistory; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Repository\ScopeRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelper; @@ -24,6 +25,7 @@ use Symfony\Component\Form\FormBuilderInterface; class CreatorScopeFilter implements FilterInterface { + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; private const PREFIX = 'acp_act_filter_creator_scope'; public function __construct( @@ -36,7 +38,7 @@ class CreatorScopeFilter implements FilterInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $p = self::PREFIX; @@ -86,7 +88,22 @@ class CreatorScopeFilter implements FilterInterface ]); } - public function describeAction($data, $format = 'string'): array + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['scopes' => $this->normalizeDoctrineEntity($formData['scopes'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['scopes' => $this->denormalizeDoctrineEntity($formData['scopes'], $this->scopeRepository)]; + } + + public function describeAction($data, ExportGenerationContext $context): array { $scopes = []; diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/EmergencyFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/EmergencyFilter.php index 5ccc6fc7a..60923ad41 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/EmergencyFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/EmergencyFilter.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Export\Filter; use Chill\ActivityBundle\Export\Declarations; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Doctrine\ORM\Query\Expr\Andx; use Doctrine\ORM\QueryBuilder; @@ -35,7 +36,7 @@ class EmergencyFilter implements FilterInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $where = $qb->getDQLPart('where'); @@ -66,12 +67,27 @@ class EmergencyFilter implements FilterInterface ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['accepted_emergency' => $formData['accepted_emergency']]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['accepted_emergency' => $formData['accepted_emergency']]; + } + public function getFormDefaultData(): array { return ['accepted_emergency' => self::DEFAULT_CHOICE]; } - public function describeAction($data, $format = 'string'): array + public function describeAction($data, ExportGenerationContext $context): array { return [ 'Filtered by emergency: only %emergency%', [ diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/LocationFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/LocationFilter.php index 7525381ed..cb1f18bf8 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/LocationFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/LocationFilter.php @@ -12,19 +12,27 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Export\Filter; use Chill\ActivityBundle\Export\Declarations; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Form\Type\PickUserLocationType; +use Chill\MainBundle\Repository\LocationRepository; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; -class LocationFilter implements FilterInterface +final readonly class LocationFilter implements FilterInterface { + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; + + public function __construct( + private LocationRepository $locationRepository, + ) {} + public function addRole(): ?string { return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $qb->andWhere( $qb->expr()->in('activity.location', ':location') @@ -46,12 +54,27 @@ class LocationFilter implements FilterInterface ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['accepted_location' => $this->normalizeDoctrineEntity($formData['accepted_location'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['accepted_location' => $this->denormalizeDoctrineEntity($formData['accepted_location'], $this->locationRepository)]; + } + public function getFormDefaultData(): array { return []; } - public function describeAction($data, $format = 'string'): array + public function describeAction($data, ExportGenerationContext $context): array { $locations = []; diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/LocationTypeFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/LocationTypeFilter.php index 6eaf409eb..3a9fff0bc 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/LocationTypeFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/LocationTypeFilter.php @@ -12,8 +12,11 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Export\Filter; use Chill\ActivityBundle\Export\Declarations; +use Chill\MainBundle\Export\ExportDataNormalizerTrait; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Form\Type\PickLocationTypeType; +use Chill\MainBundle\Repository\LocationTypeRepository; use Chill\MainBundle\Templating\TranslatableStringHelper; use Doctrine\ORM\Query\Expr\Andx; use Doctrine\ORM\QueryBuilder; @@ -21,14 +24,16 @@ use Symfony\Component\Form\FormBuilderInterface; class LocationTypeFilter implements FilterInterface { - public function __construct(private readonly TranslatableStringHelper $translatableStringHelper) {} + use ExportDataNormalizerTrait; + + public function __construct(private readonly TranslatableStringHelper $translatableStringHelper, private LocationTypeRepository $locationTypeRepository) {} public function addRole(): ?string { return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { if (!\in_array('actloc', $qb->getAllAliases(), true)) { $qb->join('activity.location', 'actloc'); @@ -60,12 +65,27 @@ class LocationTypeFilter implements FilterInterface ]); } - public function getFormDefaultData(): array + public function getNormalizationVersion(): int { - return []; + return 1; } - public function describeAction($data, $format = 'string'): array + public function normalizeFormData(array $formData): array + { + return ['accepted_locationtype' => $this->normalizeDoctrineEntity($formData['accepted_locationtype'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['accepted_locationtype' => $this->denormalizeDoctrineEntity($formData['accepted_locationtype'], $this->locationTypeRepository)]; + } + + public function getFormDefaultData(): array + { + return ['accepted_locationtype' => []]; + } + + public function describeAction($data, ExportGenerationContext $context): array { $types = []; diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/ActivityReasonFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/ActivityReasonFilter.php index 6ddc2c49e..4802fc7ac 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/ActivityReasonFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/ActivityReasonFilter.php @@ -15,6 +15,7 @@ use Chill\ActivityBundle\Entity\ActivityReason; use Chill\ActivityBundle\Export\Declarations; use Chill\ActivityBundle\Repository\ActivityReasonRepository; use Chill\MainBundle\Export\ExportElementValidatedInterface; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Templating\TranslatableStringHelper; use Doctrine\Common\Collections\Collection; @@ -26,6 +27,8 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; class ActivityReasonFilter implements ExportElementValidatedInterface, FilterInterface { + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; + public function __construct(protected TranslatableStringHelper $translatableStringHelper, protected ActivityReasonRepository $activityReasonRepository) {} public function addRole(): ?string @@ -33,7 +36,7 @@ class ActivityReasonFilter implements ExportElementValidatedInterface, FilterInt return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $where = $qb->getDQLPart('where'); $join = $qb->getDQLPart('join'); @@ -70,12 +73,27 @@ class ActivityReasonFilter implements ExportElementValidatedInterface, FilterInt ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['reasons' => $this->normalizeDoctrineEntity($formData['reasons'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['reasons' => $this->denormalizeDoctrineEntity($formData['reasons'], $this->activityReasonRepository)]; + } + public function getFormDefaultData(): array { return []; } - public function describeAction($data, $format = 'string') + public function describeAction($data, ExportGenerationContext $context): string|\Symfony\Contracts\Translation\TranslatableInterface|array { // collect all the reasons'name used in this filter in one array $reasonsNames = array_map( @@ -91,7 +109,7 @@ class ActivityReasonFilter implements ExportElementValidatedInterface, FilterInt ]; } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'Filter by reason'; } diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/PersonHavingActivityBetweenDateFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/PersonHavingActivityBetweenDateFilter.php index 65cb5dbb6..673aac25e 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/PersonHavingActivityBetweenDateFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/PersonHavingActivityBetweenDateFilter.php @@ -15,6 +15,7 @@ use Chill\ActivityBundle\Entity\Activity; use Chill\ActivityBundle\Entity\ActivityReason; use Chill\ActivityBundle\Repository\ActivityReasonRepository; use Chill\MainBundle\Export\ExportElementValidatedInterface; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Form\Type\PickRollingDateType; use Chill\MainBundle\Service\RollingDate\RollingDate; @@ -39,7 +40,7 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { // create a subquery for activity $sqb = $qb->getEntityManager()->createQueryBuilder(); @@ -116,6 +117,21 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem } } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['date_from_rolling' => $formData['date_from_rolling']->normalize(), 'date_to_rolling' => $formData['date_to_rolling']->normalize()]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['date_from_rolling' => RollingDate::fromNormalized($formData['date_from_rolling']), 'date_to_rolling' => RollingDate::fromNormalized($formData['date_to_rolling'])]; + } + public function getFormDefaultData(): array { return [ @@ -125,7 +141,7 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem ]; } - public function describeAction($data, $format = 'string'): array + public function describeAction($data, ExportGenerationContext $context): array { return [ [] === $data['reasons'] ? diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/PersonsFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/PersonsFilter.php index dd47df699..820830e23 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/PersonsFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/PersonsFilter.php @@ -13,6 +13,7 @@ namespace Chill\ActivityBundle\Export\Filter; use Chill\ActivityBundle\Export\Declarations; use Chill\ActivityBundle\Tests\Export\Filter\PersonsFilterTest; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\PersonBundle\Form\Type\PickPersonDynamicType; use Chill\PersonBundle\Templating\Entity\PersonRenderInterface; @@ -33,7 +34,7 @@ final readonly class PersonsFilter implements FilterInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $p = self::PREFIX; @@ -47,7 +48,7 @@ final readonly class PersonsFilter implements FilterInterface $qb->andWhere($orX); } - public function applyOn() + public function applyOn(): string { return Declarations::ACTIVITY; } @@ -60,6 +61,21 @@ final readonly class PersonsFilter implements FilterInterface ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['accepted_persons' => $formData['accepted_persons']]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['accepted_persons' => $formData['accepted_persons']]; + } + public function getFormDefaultData(): array { return [ @@ -67,7 +83,7 @@ final readonly class PersonsFilter implements FilterInterface ]; } - public function describeAction($data, $format = 'string') + public function describeAction($data, ExportGenerationContext $context): string|\Symfony\Contracts\Translation\TranslatableInterface|array { $users = []; diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/SentReceivedFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/SentReceivedFilter.php index e73f8e864..ba85ff731 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/SentReceivedFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/SentReceivedFilter.php @@ -13,6 +13,7 @@ namespace Chill\ActivityBundle\Export\Filter; use Chill\ActivityBundle\Entity\Activity; use Chill\ActivityBundle\Export\Declarations; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Doctrine\ORM\Query\Expr\Andx; use Doctrine\ORM\QueryBuilder; @@ -36,7 +37,7 @@ class SentReceivedFilter implements FilterInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $where = $qb->getDQLPart('where'); @@ -68,12 +69,27 @@ class SentReceivedFilter implements FilterInterface ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['accepted_sentreceived' => $formData['accepted_sentreceived']]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['accepted_sentreceived' => $formData['accepted_sentreceived']]; + } + public function getFormDefaultData(): array { return ['accepted_sentreceived' => self::DEFAULT_CHOICE]; } - public function describeAction($data, $format = 'string'): array + public function describeAction($data, ExportGenerationContext $context): array { $sentreceived = array_flip(self::CHOICES)[$data['accepted_sentreceived']]; diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/UserFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/UserFilter.php index 9bf98ce65..2a63be54a 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/UserFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/UserFilter.php @@ -12,23 +12,27 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Export\Filter; use Chill\ActivityBundle\Export\Declarations; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; -use Chill\MainBundle\Form\Type\PickUserDynamicType; +use Chill\MainBundle\Form\Type\PickUserOrMeDynamicType; +use Chill\MainBundle\Repository\UserRepositoryInterface; use Chill\MainBundle\Templating\Entity\UserRender; use Doctrine\ORM\Query\Expr\Andx; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; -class UserFilter implements FilterInterface +final readonly class UserFilter implements FilterInterface { - public function __construct(private readonly UserRender $userRender) {} + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; + + public function __construct(private UserRender $userRender, private UserRepositoryInterface $userRepository) {} public function addRole(): ?string { return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $where = $qb->getDQLPart('where'); @@ -41,7 +45,7 @@ class UserFilter implements FilterInterface } $qb->add('where', $where); - $qb->setParameter('users', $data['accepted_users']); + $qb->setParameter('users', $this->userOrMe($data['accepted_users'], $exportGenerationContext)); } public function applyOn(): string @@ -51,22 +55,37 @@ class UserFilter implements FilterInterface public function buildForm(FormBuilderInterface $builder): void { - $builder->add('accepted_users', PickUserDynamicType::class, [ + $builder->add('accepted_users', PickUserOrMeDynamicType::class, [ 'multiple' => true, 'label' => 'Creators', ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['accepted_users' => $this->normalizeUserOrMe($formData['accepted_users'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['accepted_users' => $this->denormalizeUserOrMe($formData['accepted_users'], $this->userRepository)]; + } + public function getFormDefaultData(): array { return []; } - public function describeAction($data, $format = 'string'): array + public function describeAction($data, ExportGenerationContext $context): array { $users = []; - foreach ($data['accepted_users'] as $u) { + foreach ($this->userOrMe($data['accepted_users'], $context) as $u) { $users[] = $this->userRender->renderString($u, []); } diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/UsersJobFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/UsersJobFilter.php index be7baa7d4..f5bf9f2ae 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/UsersJobFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/UsersJobFilter.php @@ -15,6 +15,7 @@ use Chill\ActivityBundle\Entity\Activity; use Chill\ActivityBundle\Export\Declarations; use Chill\MainBundle\Entity\User\UserJobHistory; use Chill\MainBundle\Entity\UserJob; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Repository\UserJobRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; @@ -25,6 +26,7 @@ use Symfony\Component\Form\FormBuilderInterface; class UsersJobFilter implements FilterInterface { + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; private const PREFIX = 'act_filter_user_job'; public function __construct( @@ -37,7 +39,7 @@ class UsersJobFilter implements FilterInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $p = self::PREFIX; @@ -60,7 +62,7 @@ class UsersJobFilter implements FilterInterface ); } - public function applyOn() + public function applyOn(): string { return Declarations::ACTIVITY; } @@ -77,7 +79,22 @@ class UsersJobFilter implements FilterInterface ]); } - public function describeAction($data, $format = 'string') + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['jobs' => $this->normalizeDoctrineEntity($formData['jobs'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['jobs' => $this->denormalizeDoctrineEntity($formData['jobs'], $this->userJobRepository)]; + } + + public function describeAction($data, ExportGenerationContext $context): string|\Symfony\Contracts\Translation\TranslatableInterface|array { return ['export.filter.activity.by_users_job.Filtered activity by users job: only %jobs%', [ '%jobs%' => implode( @@ -97,7 +114,7 @@ class UsersJobFilter implements FilterInterface ]; } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'export.filter.activity.by_users_job.Filter by users job'; } diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/UsersScopeFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/UsersScopeFilter.php index 40ed3dc89..83af6701c 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/UsersScopeFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/UsersScopeFilter.php @@ -15,6 +15,7 @@ use Chill\ActivityBundle\Entity\Activity; use Chill\ActivityBundle\Export\Declarations; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User\UserScopeHistory; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Repository\ScopeRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; @@ -25,6 +26,7 @@ use Symfony\Component\Form\FormBuilderInterface; class UsersScopeFilter implements FilterInterface { + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; private const PREFIX = 'act_filter_user_scope'; public function __construct( @@ -37,7 +39,7 @@ class UsersScopeFilter implements FilterInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $p = self::PREFIX; @@ -77,7 +79,22 @@ class UsersScopeFilter implements FilterInterface ]); } - public function describeAction($data, $format = 'string'): array + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['scopes' => $this->normalizeDoctrineEntity($formData['scopes'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['scopes' => $this->denormalizeDoctrineEntity($formData['scopes'], $this->scopeRepository)]; + } + + public function describeAction($data, ExportGenerationContext $context): array { return ['export.filter.activity.by_users_scope.Filtered activity by users scope: only %scopes%', [ '%scopes%' => implode( diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityPresenceRepositoryInterface.php b/src/Bundle/ChillActivityBundle/Repository/ActivityPresenceRepositoryInterface.php index 228d70856..0efc8d056 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityPresenceRepositoryInterface.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityPresenceRepositoryInterface.php @@ -12,8 +12,9 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Repository; use Chill\ActivityBundle\Entity\ActivityPresence; +use Doctrine\Persistence\ObjectRepository; -interface ActivityPresenceRepositoryInterface +interface ActivityPresenceRepositoryInterface extends ObjectRepository { public function find($id): ?ActivityPresence; diff --git a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/store.js b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/store.js index f29e2e6d4..acd616dc9 100644 --- a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/store.js +++ b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/store.js @@ -2,7 +2,7 @@ import "es6-promise/auto"; import { createStore } from "vuex"; import { postLocation } from "./api"; import prepareLocations from "./store.locations.js"; -import { makeFetch } from "ChillMainAssets/lib/api/apiMethods"; +import { fetchResults, makeFetch } from "ChillMainAssets/lib/api/apiMethods"; const debug = process.env.NODE_ENV !== "production"; //console.log('window.activity', window.activity); @@ -365,11 +365,11 @@ const store = createStore({ const accompanyingPeriodId = state.activity.accompanyingPeriod.id; const url = `/api/1.0/person/accompanying-course/${accompanyingPeriodId}/works.json`; try { - const works = await makeFetch("GET", url); - // console.log("works", works); + const works = await fetchResults(url); + // console.log('works', works); commit("setAccompanyingPeriodWorks", works); } catch (error) { - console.error("Failed to fetch accompanying period works:", error); + console.error("Failed to fetch works:", error); } }, getWhoAmI({ commit }) { diff --git a/src/Bundle/ChillActivityBundle/Tests/Export/Filter/ByCreatorFilterTest.php b/src/Bundle/ChillActivityBundle/Tests/Export/Filter/ByCreatorFilterTest.php index d0e6cd6b4..9fc76360b 100644 --- a/src/Bundle/ChillActivityBundle/Tests/Export/Filter/ByCreatorFilterTest.php +++ b/src/Bundle/ChillActivityBundle/Tests/Export/Filter/ByCreatorFilterTest.php @@ -45,14 +45,20 @@ final class ByCreatorFilterTest extends AbstractFilterTest ->from(User::class, 'u') ->select('u') ->getQuery() + ->setMaxResults(1) ->getResult(); $data = []; + foreach ($array as $a) { $data[] = [ 'accepted_users' => $a, ]; } + $data[] = [ + 'accepted_users' => 'me', + ]; + return $data; } diff --git a/src/Bundle/ChillActivityBundle/Tests/Export/Filter/UserFilterTest.php b/src/Bundle/ChillActivityBundle/Tests/Export/Filter/UserFilterTest.php index 6f4f5d9ef..268ac3dab 100644 --- a/src/Bundle/ChillActivityBundle/Tests/Export/Filter/UserFilterTest.php +++ b/src/Bundle/ChillActivityBundle/Tests/Export/Filter/UserFilterTest.php @@ -55,6 +55,10 @@ final class UserFilterTest extends AbstractFilterTest ]; } + $data[] = [ + 'accepted_users' => 'me', + ]; + return $data; } diff --git a/src/Bundle/ChillActivityBundle/Validator/Constraints/ActivityValidity.php b/src/Bundle/ChillActivityBundle/Validator/Constraints/ActivityValidity.php index 107816181..ca85f2687 100644 --- a/src/Bundle/ChillActivityBundle/Validator/Constraints/ActivityValidity.php +++ b/src/Bundle/ChillActivityBundle/Validator/Constraints/ActivityValidity.php @@ -30,7 +30,7 @@ class ActivityValidity extends Constraint public $socialIssuesMessage = 'For this type of activity, you must add at least one social issue'; - public function getTargets(): string|array + public function getTargets(): string { return self::CLASS_CONSTRAINT; } diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByActivityTypeAggregator.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByActivityTypeAggregator.php index 452f9b80c..fff522ef1 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByActivityTypeAggregator.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByActivityTypeAggregator.php @@ -30,7 +30,7 @@ class ByActivityTypeAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { $qb->addSelect('IDENTITY(aside.type) AS by_aside_activity_type_aggregator') ->addGroupBy('by_aside_activity_type_aggregator'); @@ -46,12 +46,27 @@ class ByActivityTypeAggregator implements AggregatorInterface // No form needed } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data) + public function getLabels($key, array $values, $data): callable { return function ($value): string { if ('_header' === $value) { diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByLocationAggregator.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByLocationAggregator.php index b5ca1022b..1c9ae467e 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByLocationAggregator.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByLocationAggregator.php @@ -26,12 +26,27 @@ class ByLocationAggregator implements AggregatorInterface // no form } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data) + public function getLabels($key, array $values, $data): callable { return function ($value): string { if ('_header' === $value) { @@ -60,7 +75,7 @@ class ByLocationAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { $qb->addSelect('IDENTITY(aside.location) AS by_aside_activity_location_aggregator') ->addGroupBy('by_aside_activity_location_aggregator'); diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByUserJobAggregator.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByUserJobAggregator.php index bbecab1e9..6a34acf81 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByUserJobAggregator.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByUserJobAggregator.php @@ -34,7 +34,7 @@ class ByUserJobAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { $p = self::PREFIX; @@ -65,14 +65,29 @@ class ByUserJobAggregator implements AggregatorInterface return Declarations::ASIDE_ACTIVITY_TYPE; } - public function buildForm(FormBuilderInterface $builder) {} + public function buildForm(FormBuilderInterface $builder): void {} + + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data) + public function getLabels($key, array $values, $data): callable { return function ($value): string { if ('_header' === $value) { diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByUserScopeAggregator.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByUserScopeAggregator.php index b7b8b6330..f5d74490a 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByUserScopeAggregator.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByUserScopeAggregator.php @@ -34,7 +34,7 @@ class ByUserScopeAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { $p = self::PREFIX; @@ -64,14 +64,29 @@ class ByUserScopeAggregator implements AggregatorInterface return Declarations::ASIDE_ACTIVITY_TYPE; } - public function buildForm(FormBuilderInterface $builder) {} + public function buildForm(FormBuilderInterface $builder): void {} + + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data) + public function getLabels($key, array $values, $data): callable { return function ($value): string { if ('_header' === $value) { diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Export/AvgAsideActivityDuration.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/AvgAsideActivityDuration.php index 70922b6ae..6cf542d2d 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Export/AvgAsideActivityDuration.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/AvgAsideActivityDuration.php @@ -26,6 +26,21 @@ class AvgAsideActivityDuration implements ExportInterface, GroupedExportInterfac public function buildForm(FormBuilderInterface $builder) {} + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; @@ -60,7 +75,7 @@ class AvgAsideActivityDuration implements ExportInterface, GroupedExportInterfac return ['export_avg_aside_activity_duration']; } - public function getResult($query, $data) + public function getResult($query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array { return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); } @@ -75,7 +90,7 @@ class AvgAsideActivityDuration implements ExportInterface, GroupedExportInterfac return Declarations::ASIDE_ACTIVITY_TYPE; } - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): \Doctrine\ORM\QueryBuilder { $qb = $this->repository->createQueryBuilder('aside'); diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Export/CountAsideActivity.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/CountAsideActivity.php index 6d1eed5fe..4bd7659ee 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Export/CountAsideActivity.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/CountAsideActivity.php @@ -26,6 +26,21 @@ class CountAsideActivity implements ExportInterface, GroupedExportInterface public function buildForm(FormBuilderInterface $builder) {} + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; @@ -63,7 +78,7 @@ class CountAsideActivity implements ExportInterface, GroupedExportInterface return ['export_result']; } - public function getResult($query, $data) + public function getResult($query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array { return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); } @@ -78,7 +93,7 @@ class CountAsideActivity implements ExportInterface, GroupedExportInterface return Declarations::ASIDE_ACTIVITY_TYPE; } - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): \Doctrine\ORM\QueryBuilder { $qb = $this->repository->createQueryBuilder('aside'); diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Export/ListAsideActivity.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/ListAsideActivity.php index 206aa5232..e6fcea5ee 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Export/ListAsideActivity.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/ListAsideActivity.php @@ -46,6 +46,21 @@ final readonly class ListAsideActivity implements ListInterface, GroupedExportIn public function buildForm(FormBuilderInterface $builder): void {} + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; @@ -142,7 +157,7 @@ final readonly class ListAsideActivity implements ListInterface, GroupedExportIn }; } - public function getQueryKeys($data) + public function getQueryKeys($data): array { return [ 'id', @@ -160,12 +175,12 @@ final readonly class ListAsideActivity implements ListInterface, GroupedExportIn ]; } - public function getResult($query, $data): array + public function getResult($query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array { return $query->getQuery()->getResult(AbstractQuery::HYDRATE_ARRAY); } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'export.aside_activity.List of aside activities'; } @@ -175,7 +190,7 @@ final readonly class ListAsideActivity implements ListInterface, GroupedExportIn return Declarations::ASIDE_ACTIVITY_TYPE; } - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): \Doctrine\ORM\QueryBuilder { $qb = $this->em->createQueryBuilder() ->from(AsideActivity::class, 'aside') diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Export/SumAsideActivityDuration.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/SumAsideActivityDuration.php index 0fd318902..18f78f3f8 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Export/SumAsideActivityDuration.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/SumAsideActivityDuration.php @@ -26,6 +26,21 @@ class SumAsideActivityDuration implements ExportInterface, GroupedExportInterfac public function buildForm(FormBuilderInterface $builder) {} + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; @@ -60,7 +75,7 @@ class SumAsideActivityDuration implements ExportInterface, GroupedExportInterfac return ['export_sum_aside_activity_duration']; } - public function getResult($query, $data) + public function getResult($query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array { return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); } @@ -75,7 +90,7 @@ class SumAsideActivityDuration implements ExportInterface, GroupedExportInterfac return Declarations::ASIDE_ACTIVITY_TYPE; } - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): \Doctrine\ORM\QueryBuilder { $qb = $this->repository ->createQueryBuilder('aside'); diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByActivityTypeFilter.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByActivityTypeFilter.php index 37f1e3c33..d96f74b7f 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByActivityTypeFilter.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByActivityTypeFilter.php @@ -15,6 +15,7 @@ use Chill\AsideActivityBundle\Entity\AsideActivityCategory; use Chill\AsideActivityBundle\Export\Declarations; use Chill\AsideActivityBundle\Repository\AsideActivityCategoryRepository; use Chill\AsideActivityBundle\Templating\Entity\CategoryRender; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Doctrine\Common\Collections\Collection; @@ -24,10 +25,13 @@ use Symfony\Component\Form\FormBuilderInterface; class ByActivityTypeFilter implements FilterInterface { + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; + public function __construct( private readonly CategoryRender $categoryRender, private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly AsideActivityCategoryRepository $asideActivityTypeRepository, + private readonly AsideActivityCategoryRepository $asideActivityCategoryRepository, ) {} public function addRole(): ?string @@ -35,7 +39,7 @@ class ByActivityTypeFilter implements FilterInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $clause = $qb->expr()->in('aside.type', ':types'); @@ -68,12 +72,27 @@ class ByActivityTypeFilter implements FilterInterface ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['types' => $this->normalizeDoctrineEntity($formData['types'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['types' => $this->denormalizeDoctrineEntity($formData['types'], $this->asideActivityCategoryRepository)]; + } + public function getFormDefaultData(): array { return []; } - public function describeAction($data, $format = 'string'): array + public function describeAction($data, ExportGenerationContext $context): array { $types = array_map( fn (AsideActivityCategory $t): string => $this->translatableStringHelper->localize($t->getTitle()), diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByDateFilter.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByDateFilter.php index c3a41a450..4521d7a94 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByDateFilter.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByDateFilter.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\AsideActivityBundle\Export\Filter; use Chill\AsideActivityBundle\Export\Declarations; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Form\Type\PickRollingDateType; use Chill\MainBundle\Service\RollingDate\RollingDate; @@ -29,7 +30,7 @@ class ByDateFilter implements FilterInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $clause = $qb->expr()->between( 'aside.date', @@ -64,6 +65,21 @@ class ByDateFilter implements FilterInterface ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['date_from' => $formData['date_from']->normalize(), 'date_to' => $formData['date_to']->normalize()]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['date_from' => RollingDate::fromNormalized($formData['date_from']), 'date_to' => RollingDate::fromNormalized($formData['date_to'])]; + } + public function getFormDefaultData(): array { return [ @@ -72,7 +88,7 @@ class ByDateFilter implements FilterInterface ]; } - public function describeAction($data, $format = 'string'): array + public function describeAction($data, ExportGenerationContext $context): array { return ['export.filter.Filtered by aside activities between %dateFrom% and %dateTo%', [ '%dateFrom%' => $this->rollingDateConverter->convert($data['date_from'])->format('d-m-Y'), diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByLocationFilter.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByLocationFilter.php index 70fbf8587..5bc966c10 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByLocationFilter.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByLocationFilter.php @@ -14,8 +14,10 @@ namespace Chill\AsideActivityBundle\Export\Filter; use Chill\AsideActivityBundle\Export\Declarations; use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Form\Type\PickUserLocationType; +use Chill\MainBundle\Repository\LocationRepository; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; @@ -23,8 +25,11 @@ use Symfony\Bundle\SecurityBundle\Security; final readonly class ByLocationFilter implements FilterInterface { + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; + public function __construct( private Security $security, + private LocationRepository $locationRepository, ) {} public function getTitle(): string @@ -38,6 +43,21 @@ final readonly class ByLocationFilter implements FilterInterface ->add('locations', PickUserLocationType::class); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['locations' => $this->normalizeDoctrineEntity($formData['locations'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['locations' => $this->denormalizeDoctrineEntity($formData['locations'], $this->locationRepository)]; + } + public function getFormDefaultData(): array { $user = $this->security->getUser(); @@ -53,7 +73,7 @@ final readonly class ByLocationFilter implements FilterInterface ]; } - public function describeAction($data, $format = 'string'): array + public function describeAction($data, ExportGenerationContext $context): array { $extractFunction = fn (Location $l): string => $l->getName(); if ($data['locations'] instanceof Collection) { @@ -72,7 +92,7 @@ final readonly class ByLocationFilter implements FilterInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $clause = $qb->expr()->in('aside.location', ':locations'); diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserFilter.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserFilter.php index d915f62dc..04e02d3ff 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserFilter.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserFilter.php @@ -12,28 +12,32 @@ declare(strict_types=1); namespace Chill\AsideActivityBundle\Export\Filter; use Chill\AsideActivityBundle\Export\Declarations; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; -use Chill\MainBundle\Form\Type\PickUserDynamicType; +use Chill\MainBundle\Form\Type\PickUserOrMeDynamicType; +use Chill\MainBundle\Repository\UserRepository; use Chill\MainBundle\Templating\Entity\UserRender; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; -class ByUserFilter implements FilterInterface +final readonly class ByUserFilter implements FilterInterface { - public function __construct(private readonly UserRender $userRender) {} + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; + + public function __construct(private UserRender $userRender, private UserRepository $userRepository) {} public function addRole(): ?string { return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $clause = $qb->expr()->in('aside.agent', ':users'); $qb ->andWhere($clause) - ->setParameter('users', $data['accepted_users']); + ->setParameter('users', $this->userOrMe($data['accepted_users'], $exportGenerationContext)); } public function applyOn(): string @@ -43,22 +47,37 @@ class ByUserFilter implements FilterInterface public function buildForm(FormBuilderInterface $builder): void { - $builder->add('accepted_users', PickUserDynamicType::class, [ + $builder->add('accepted_users', PickUserOrMeDynamicType::class, [ 'multiple' => true, 'label' => 'Creators', ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['accepted_users' => $this->normalizeUserOrMe($formData['accepted_users'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['accepted_users' => $this->denormalizeUserOrMe($formData['accepted_users'], $this->userRepository)]; + } + public function getFormDefaultData(): array { return []; } - public function describeAction($data, $format = 'string'): array + public function describeAction($data, ExportGenerationContext $context): array { $users = []; - foreach ($data['accepted_users'] as $u) { + foreach ($this->userOrMe($data['accepted_users'], $context) as $u) { $users[] = $this->userRender->renderString($u, []); } diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserJobFilter.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserJobFilter.php index c6bb3603f..3cf8d8322 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserJobFilter.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserJobFilter.php @@ -15,6 +15,7 @@ use Chill\AsideActivityBundle\Entity\AsideActivity; use Chill\AsideActivityBundle\Export\Declarations; use Chill\MainBundle\Entity\User\UserJobHistory; use Chill\MainBundle\Entity\UserJob; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Repository\UserJobRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; @@ -25,6 +26,7 @@ use Symfony\Component\Form\FormBuilderInterface; class ByUserJobFilter implements FilterInterface { + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; private const PREFIX = 'aside_act_filter_user_job'; public function __construct( @@ -37,7 +39,7 @@ class ByUserJobFilter implements FilterInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $p = self::PREFIX; @@ -77,7 +79,22 @@ class ByUserJobFilter implements FilterInterface ]); } - public function describeAction($data, $format = 'string'): array + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['jobs' => $this->normalizeDoctrineEntity($formData['jobs'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['jobs' => $this->denormalizeDoctrineEntity($formData['jobs'], $this->userJobRepository)]; + } + + public function describeAction($data, ExportGenerationContext $context): array { return ['export.filter.by_user_job.Filtered aside activities by user jobs: only %jobs%', [ '%jobs%' => implode( diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserScopeFilter.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserScopeFilter.php index 66ea4316d..387cc11da 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserScopeFilter.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserScopeFilter.php @@ -15,6 +15,7 @@ use Chill\AsideActivityBundle\Entity\AsideActivity; use Chill\AsideActivityBundle\Export\Declarations; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User\UserScopeHistory; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Repository\ScopeRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; @@ -25,6 +26,7 @@ use Symfony\Component\Form\FormBuilderInterface; class ByUserScopeFilter implements FilterInterface { + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; private const PREFIX = 'aside_act_filter_user_scope'; public function __construct( @@ -37,7 +39,7 @@ class ByUserScopeFilter implements FilterInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $p = self::PREFIX; @@ -77,7 +79,22 @@ class ByUserScopeFilter implements FilterInterface ]); } - public function describeAction($data, $format = 'string') + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['scopes' => $this->normalizeDoctrineEntity($formData['scopes'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['scopes' => $this->denormalizeDoctrineEntity($formData['scopes'], $this->scopeRepository)]; + } + + public function describeAction($data, ExportGenerationContext $context): string|\Symfony\Contracts\Translation\TranslatableInterface|array { return ['export.filter.by_user_scope.Filtered aside activities by user scope: only %scopes%', [ '%scopes%' => implode( diff --git a/src/Bundle/ChillAsideActivityBundle/src/Tests/Export/Export/ListAsideActivityTest.php b/src/Bundle/ChillAsideActivityBundle/src/Tests/Export/Export/ListAsideActivityTest.php index aae93c17b..25f93bc3e 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Tests/Export/Export/ListAsideActivityTest.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Tests/Export/Export/ListAsideActivityTest.php @@ -12,6 +12,8 @@ declare(strict_types=1); namespace Chill\AsideActivityBundle\Tests\Export\Export; use Chill\AsideActivityBundle\Export\Export\ListAsideActivity; +use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Export\ExportGenerationContext; use Doctrine\ORM\AbstractQuery; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; @@ -33,7 +35,7 @@ class ListAsideActivityTest extends KernelTestCase public function testExecuteQuery(): void { - $qb = $this->listAsideActivity->initiateQuery([], [], []) + $qb = $this->listAsideActivity->initiateQuery([], [], [], new ExportGenerationContext(new User())) ->setMaxResults(1); $results = $qb->getQuery()->getResult(AbstractQuery::HYDRATE_ARRAY); diff --git a/src/Bundle/ChillCalendarBundle/Export/Aggregator/AgentAggregator.php b/src/Bundle/ChillCalendarBundle/Export/Aggregator/AgentAggregator.php index 96cf3cee4..40be6542d 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Aggregator/AgentAggregator.php +++ b/src/Bundle/ChillCalendarBundle/Export/Aggregator/AgentAggregator.php @@ -27,7 +27,7 @@ final readonly class AgentAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { if (!\in_array('caluser', $qb->getAllAliases(), true)) { $qb->join('cal.mainUser', 'caluser'); @@ -47,12 +47,27 @@ final readonly class AgentAggregator implements AggregatorInterface // no form } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data): \Closure + public function getLabels($key, array $values, $data): callable { return function ($value): string { if ('_header' === $value) { diff --git a/src/Bundle/ChillCalendarBundle/Export/Aggregator/CancelReasonAggregator.php b/src/Bundle/ChillCalendarBundle/Export/Aggregator/CancelReasonAggregator.php index 8a2e66112..718fcd8bd 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Aggregator/CancelReasonAggregator.php +++ b/src/Bundle/ChillCalendarBundle/Export/Aggregator/CancelReasonAggregator.php @@ -27,7 +27,7 @@ class CancelReasonAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { // TODO: still needs to take into account calendars without a cancel reason somehow if (!\in_array('calcancel', $qb->getAllAliases(), true)) { @@ -48,12 +48,27 @@ class CancelReasonAggregator implements AggregatorInterface // no form } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data): \Closure + public function getLabels($key, array $values, $data): callable { return function ($value): string { if ('_header' === $value) { diff --git a/src/Bundle/ChillCalendarBundle/Export/Aggregator/JobAggregator.php b/src/Bundle/ChillCalendarBundle/Export/Aggregator/JobAggregator.php index d05fe736c..420ff8d2f 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Aggregator/JobAggregator.php +++ b/src/Bundle/ChillCalendarBundle/Export/Aggregator/JobAggregator.php @@ -34,7 +34,7 @@ final readonly class JobAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { $p = self::PREFIX; @@ -67,12 +67,27 @@ final readonly class JobAggregator implements AggregatorInterface public function buildForm(FormBuilderInterface $builder): void {} + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data): \Closure + public function getLabels($key, array $values, $data): callable { return function ($value): string { if ('_header' === $value) { diff --git a/src/Bundle/ChillCalendarBundle/Export/Aggregator/LocationAggregator.php b/src/Bundle/ChillCalendarBundle/Export/Aggregator/LocationAggregator.php index 53209f648..94bc0fdea 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Aggregator/LocationAggregator.php +++ b/src/Bundle/ChillCalendarBundle/Export/Aggregator/LocationAggregator.php @@ -26,7 +26,7 @@ final readonly class LocationAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { if (!\in_array('calloc', $qb->getAllAliases(), true)) { $qb->join('cal.location', 'calloc'); @@ -45,12 +45,27 @@ final readonly class LocationAggregator implements AggregatorInterface // no form } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data): \Closure + public function getLabels($key, array $values, $data): callable { return function ($value): string { if ('_header' === $value) { diff --git a/src/Bundle/ChillCalendarBundle/Export/Aggregator/LocationTypeAggregator.php b/src/Bundle/ChillCalendarBundle/Export/Aggregator/LocationTypeAggregator.php index c975c03d3..f84c6b3d7 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Aggregator/LocationTypeAggregator.php +++ b/src/Bundle/ChillCalendarBundle/Export/Aggregator/LocationTypeAggregator.php @@ -27,7 +27,7 @@ final readonly class LocationTypeAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { if (!\in_array('calloc', $qb->getAllAliases(), true)) { $qb->join('cal.location', 'calloc'); @@ -47,12 +47,27 @@ final readonly class LocationTypeAggregator implements AggregatorInterface // no form } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data): \Closure + public function getLabels($key, array $values, $data): callable { return function ($value): string { if ('_header' === $value) { diff --git a/src/Bundle/ChillCalendarBundle/Export/Aggregator/MonthYearAggregator.php b/src/Bundle/ChillCalendarBundle/Export/Aggregator/MonthYearAggregator.php index 8b4ab3753..ea899ef54 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Aggregator/MonthYearAggregator.php +++ b/src/Bundle/ChillCalendarBundle/Export/Aggregator/MonthYearAggregator.php @@ -23,7 +23,7 @@ class MonthYearAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { $qb->addSelect("to_char(cal.startDate, 'MM-YYYY') AS month_year_aggregator"); // $qb->addSelect("extract(month from age(cal.startDate, cal.endDate)) AS month_aggregator"); @@ -40,12 +40,27 @@ class MonthYearAggregator implements AggregatorInterface // No form needed } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data): \Closure + public function getLabels($key, array $values, $data): callable { return static function ($value): string { if ('_header' === $value) { diff --git a/src/Bundle/ChillCalendarBundle/Export/Aggregator/ScopeAggregator.php b/src/Bundle/ChillCalendarBundle/Export/Aggregator/ScopeAggregator.php index d20578370..85dac1f3e 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Aggregator/ScopeAggregator.php +++ b/src/Bundle/ChillCalendarBundle/Export/Aggregator/ScopeAggregator.php @@ -34,7 +34,7 @@ final readonly class ScopeAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { $p = self::PREFIX; @@ -67,12 +67,27 @@ final readonly class ScopeAggregator implements AggregatorInterface public function buildForm(FormBuilderInterface $builder): void {} + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data): \Closure + public function getLabels($key, array $values, $data): callable { return function ($value): string { if ('_header' === $value) { diff --git a/src/Bundle/ChillCalendarBundle/Export/Aggregator/UrgencyAggregator.php b/src/Bundle/ChillCalendarBundle/Export/Aggregator/UrgencyAggregator.php index cb48441fc..cc5114a4f 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Aggregator/UrgencyAggregator.php +++ b/src/Bundle/ChillCalendarBundle/Export/Aggregator/UrgencyAggregator.php @@ -33,7 +33,7 @@ class UrgencyAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { $qb->addSelect('cal.urgent AS urgency_aggregator'); $qb->addGroupBy('urgency_aggregator'); @@ -49,12 +49,27 @@ class UrgencyAggregator implements AggregatorInterface // no form } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data): \Closure + public function getLabels($key, array $values, $data): callable { return function ($value): string { if ('_header' === $value) { diff --git a/src/Bundle/ChillCalendarBundle/Export/Export/CountCalendars.php b/src/Bundle/ChillCalendarBundle/Export/Export/CountCalendars.php index 75afe8bbc..64b7599a8 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Export/CountCalendars.php +++ b/src/Bundle/ChillCalendarBundle/Export/Export/CountCalendars.php @@ -34,6 +34,21 @@ class CountCalendars implements ExportInterface, GroupedExportInterface // No form necessary } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; @@ -71,7 +86,7 @@ class CountCalendars implements ExportInterface, GroupedExportInterface return ['export_result']; } - public function getResult($query, $data) + public function getResult($query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array { return $query->getQuery()->getResult(AbstractQuery::HYDRATE_SCALAR); } @@ -89,7 +104,7 @@ class CountCalendars implements ExportInterface, GroupedExportInterface /** * Initiate the query. */ - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []): QueryBuilder + public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): QueryBuilder { $centers = array_map(static fn ($el) => $el['center'], $acl); diff --git a/src/Bundle/ChillCalendarBundle/Export/Export/StatCalendarAvgDuration.php b/src/Bundle/ChillCalendarBundle/Export/Export/StatCalendarAvgDuration.php index b69185a17..afda99ee9 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Export/StatCalendarAvgDuration.php +++ b/src/Bundle/ChillCalendarBundle/Export/Export/StatCalendarAvgDuration.php @@ -31,6 +31,21 @@ class StatCalendarAvgDuration implements ExportInterface, GroupedExportInterface // no form needed } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; @@ -68,7 +83,7 @@ class StatCalendarAvgDuration implements ExportInterface, GroupedExportInterface return ['export_result']; } - public function getResult($query, $data) + public function getResult($query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array { return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); } @@ -83,7 +98,7 @@ class StatCalendarAvgDuration implements ExportInterface, GroupedExportInterface return Declarations::CALENDAR_TYPE; } - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []): QueryBuilder + public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): QueryBuilder { $qb = $this->calendarRepository->createQueryBuilder('cal'); diff --git a/src/Bundle/ChillCalendarBundle/Export/Export/StatCalendarSumDuration.php b/src/Bundle/ChillCalendarBundle/Export/Export/StatCalendarSumDuration.php index 8ea23014c..80369e354 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Export/StatCalendarSumDuration.php +++ b/src/Bundle/ChillCalendarBundle/Export/Export/StatCalendarSumDuration.php @@ -31,6 +31,21 @@ class StatCalendarSumDuration implements ExportInterface, GroupedExportInterface // no form needed } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; @@ -68,7 +83,7 @@ class StatCalendarSumDuration implements ExportInterface, GroupedExportInterface return ['export_result']; } - public function getResult($query, $data) + public function getResult($query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array { return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); } @@ -83,7 +98,7 @@ class StatCalendarSumDuration implements ExportInterface, GroupedExportInterface return Declarations::CALENDAR_TYPE; } - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []): QueryBuilder + public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): QueryBuilder { $qb = $this->calendarRepository->createQueryBuilder('cal'); diff --git a/src/Bundle/ChillCalendarBundle/Export/Filter/AgentFilter.php b/src/Bundle/ChillCalendarBundle/Export/Filter/AgentFilter.php index 9e75dd59e..b19ce3596 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Filter/AgentFilter.php +++ b/src/Bundle/ChillCalendarBundle/Export/Filter/AgentFilter.php @@ -13,23 +13,27 @@ namespace Chill\CalendarBundle\Export\Filter; use Chill\CalendarBundle\Export\Declarations; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Repository\UserRepositoryInterface; use Chill\MainBundle\Templating\Entity\UserRender; use Doctrine\ORM\Query\Expr\Andx; use Doctrine\ORM\QueryBuilder; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\FormBuilderInterface; -class AgentFilter implements FilterInterface +final readonly class AgentFilter implements FilterInterface { - public function __construct(private readonly UserRender $userRender) {} + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; + + public function __construct(private UserRender $userRender, private UserRepositoryInterface $userRepository) {} public function addRole(): ?string { return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $where = $qb->getDQLPart('where'); $clause = $qb->expr()->in('cal.mainUser', ':agents'); @@ -59,12 +63,27 @@ class AgentFilter implements FilterInterface ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['accepted_agents' => $this->normalizeDoctrineEntity($formData['accepted_agents'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['accepted_agents' => $this->denormalizeDoctrineEntity($formData['accepted_agents'], $this->userRepository)]; + } + public function getFormDefaultData(): array { return []; } - public function describeAction($data, $format = 'string'): array + public function describeAction($data, ExportGenerationContext $context): array { $users = []; diff --git a/src/Bundle/ChillCalendarBundle/Export/Filter/BetweenDatesFilter.php b/src/Bundle/ChillCalendarBundle/Export/Filter/BetweenDatesFilter.php index 4326a7acb..dfb450ea9 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Filter/BetweenDatesFilter.php +++ b/src/Bundle/ChillCalendarBundle/Export/Filter/BetweenDatesFilter.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\CalendarBundle\Export\Filter; use Chill\CalendarBundle\Export\Declarations; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Form\Type\PickRollingDateType; use Chill\MainBundle\Service\RollingDate\RollingDate; @@ -28,7 +29,7 @@ class BetweenDatesFilter implements FilterInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $clause = $qb->expr()->andX( $qb->expr()->gte('cal.startDate', ':dateFrom'), @@ -59,12 +60,27 @@ class BetweenDatesFilter implements FilterInterface ->add('date_to', PickRollingDateType::class, []); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['date_from' => $formData['date_from']->normalize(), 'date_to' => $formData['date_to']->normalize()]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['date_from' => RollingDate::fromNormalized($formData['date_from']), 'date_to' => RollingDate::fromNormalized($formData['date_to'])]; + } + public function getFormDefaultData(): array { return ['date_from' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), 'date_to' => new RollingDate(RollingDate::T_TODAY)]; } - public function describeAction($data, $format = 'string'): array + public function describeAction($data, ExportGenerationContext $context): array { return ['Filtered by calendars between %dateFrom% and %dateTo%', [ '%dateFrom%' => $this->rollingDateConverter->convert($data['date_from'])->format('d-m-Y'), diff --git a/src/Bundle/ChillCalendarBundle/Export/Filter/CalendarRangeFilter.php b/src/Bundle/ChillCalendarBundle/Export/Filter/CalendarRangeFilter.php index 1cc86e2c1..dbdc297a8 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Filter/CalendarRangeFilter.php +++ b/src/Bundle/ChillCalendarBundle/Export/Filter/CalendarRangeFilter.php @@ -19,6 +19,7 @@ declare(strict_types=1); namespace Chill\CalendarBundle\Export\Filter; use Chill\CalendarBundle\Export\Declarations; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; @@ -41,7 +42,7 @@ class CalendarRangeFilter implements FilterInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { if (null !== $data['hasCalendarRange']) { $qb->andWhere($qb->expr()->isNotNull('cal.calendarRange')); @@ -66,12 +67,27 @@ class CalendarRangeFilter implements FilterInterface ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['hasCalendarRange' => $formData['hasCalendarRange']]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['hasCalendarRange' => $formData['hasCalendarRange']]; + } + public function getFormDefaultData(): array { return ['hasCalendarRange' => self::DEFAULT_CHOICE]; } - public function describeAction($data, $format = 'string'): array + public function describeAction($data, ExportGenerationContext $context): array { $choice = ''; diff --git a/src/Bundle/ChillCalendarBundle/Export/Filter/JobFilter.php b/src/Bundle/ChillCalendarBundle/Export/Filter/JobFilter.php index d6b3a9fea..1885d6dcb 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Filter/JobFilter.php +++ b/src/Bundle/ChillCalendarBundle/Export/Filter/JobFilter.php @@ -14,6 +14,7 @@ namespace Chill\CalendarBundle\Export\Filter; use Chill\CalendarBundle\Export\Declarations; use Chill\MainBundle\Entity\User\UserJobHistory; use Chill\MainBundle\Entity\UserJob; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Repository\UserJobRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelper; @@ -24,6 +25,7 @@ use Symfony\Component\Form\FormBuilderInterface; final readonly class JobFilter implements FilterInterface { + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; private const PREFIX = 'cal_filter_job'; public function __construct( @@ -36,7 +38,7 @@ final readonly class JobFilter implements FilterInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $p = self::PREFIX; @@ -84,7 +86,22 @@ final readonly class JobFilter implements FilterInterface ]); } - public function describeAction($data, $format = 'string'): array + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['job' => $this->normalizeDoctrineEntity($formData['job'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['job' => $this->denormalizeDoctrineEntity($formData['job'], $this->userJobRepository)]; + } + + public function describeAction($data, ExportGenerationContext $context): array { $userJobs = []; diff --git a/src/Bundle/ChillCalendarBundle/Export/Filter/ScopeFilter.php b/src/Bundle/ChillCalendarBundle/Export/Filter/ScopeFilter.php index 6d6268aa5..447d3750e 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Filter/ScopeFilter.php +++ b/src/Bundle/ChillCalendarBundle/Export/Filter/ScopeFilter.php @@ -14,6 +14,7 @@ namespace Chill\CalendarBundle\Export\Filter; use Chill\CalendarBundle\Export\Declarations; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User\UserScopeHistory; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Repository\ScopeRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelper; @@ -25,6 +26,7 @@ use Symfony\Contracts\Translation\TranslatorInterface; class ScopeFilter implements FilterInterface { + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; private const PREFIX = 'cal_filter_scope'; public function __construct( @@ -38,7 +40,7 @@ class ScopeFilter implements FilterInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $p = self::PREFIX; @@ -86,7 +88,22 @@ class ScopeFilter implements FilterInterface ]); } - public function describeAction($data, $format = 'string'): array + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['scope' => $this->normalizeDoctrineEntity($formData['scope'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['scope' => $this->denormalizeDoctrineEntity($formData['scope'], $this->scopeRepository)]; + } + + public function describeAction($data, ExportGenerationContext $context): array { $scopes = []; diff --git a/src/Bundle/ChillCalendarBundle/Service/ShortMessageNotification/CalendarForShortMessageProvider.php b/src/Bundle/ChillCalendarBundle/Service/ShortMessageNotification/CalendarForShortMessageProvider.php index 31b870ed4..1820aa3bf 100644 --- a/src/Bundle/ChillCalendarBundle/Service/ShortMessageNotification/CalendarForShortMessageProvider.php +++ b/src/Bundle/ChillCalendarBundle/Service/ShortMessageNotification/CalendarForShortMessageProvider.php @@ -24,7 +24,11 @@ use Doctrine\ORM\EntityManagerInterface; class CalendarForShortMessageProvider { - public function __construct(private readonly CalendarRepository $calendarRepository, private readonly EntityManagerInterface $em, private readonly RangeGeneratorInterface $rangeGenerator) {} + public function __construct( + private readonly CalendarRepository $calendarRepository, + private readonly EntityManagerInterface $em, + private readonly RangeGeneratorInterface $rangeGenerator, + ) {} /** * Generate calendars instance. diff --git a/src/Bundle/ChillCalendarBundle/Tests/Service/ShortMessageNotification/CalendarForShortMessageProviderTest.php b/src/Bundle/ChillCalendarBundle/Tests/Service/ShortMessageNotification/CalendarForShortMessageProviderTest.php index d9f410514..7b1f13266 100644 --- a/src/Bundle/ChillCalendarBundle/Tests/Service/ShortMessageNotification/CalendarForShortMessageProviderTest.php +++ b/src/Bundle/ChillCalendarBundle/Tests/Service/ShortMessageNotification/CalendarForShortMessageProviderTest.php @@ -21,7 +21,6 @@ namespace Chill\CalendarBundle\Tests\Service\ShortMessageNotification; use Chill\CalendarBundle\Entity\Calendar; use Chill\CalendarBundle\Repository\CalendarRepository; use Chill\CalendarBundle\Service\ShortMessageNotification\CalendarForShortMessageProvider; -use Chill\CalendarBundle\Service\ShortMessageNotification\DefaultRangeGenerator; use Chill\CalendarBundle\Service\ShortMessageNotification\RangeGeneratorInterface; use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\TestCase; @@ -82,10 +81,16 @@ final class CalendarForShortMessageProviderTest extends TestCase $em = $this->prophesize(EntityManagerInterface::class); $em->clear()->shouldBeCalled(); + $calendarRangeGenerator = $this->prophesize(RangeGeneratorInterface::class); + $calendarRangeGenerator->generateRange(Argument::any())->willReturn([ + 'startDate' => new \DateTimeImmutable('yesterday'), + 'endDate' => new \DateTimeImmutable('now'), + ]); + $provider = new CalendarForShortMessageProvider( $calendarRepository->reveal(), $em->reveal(), - new DefaultRangeGenerator() + $calendarRangeGenerator->reveal(), ); $calendars = iterator_to_array($provider->getCalendars(new \DateTimeImmutable('now'))); @@ -103,26 +108,32 @@ final class CalendarForShortMessageProviderTest extends TestCase Argument::type(\DateTimeImmutable::class), Argument::type('int'), Argument::exact(0) - )->will(static fn ($args) => array_fill(0, 1, new Calendar()))->shouldBeCalledTimes(1); + )->will(static fn ($args) => array_fill(0, 10, new Calendar()))->shouldBeCalledTimes(1); $calendarRepository->findByNotificationAvailable( Argument::type(\DateTimeImmutable::class), Argument::type(\DateTimeImmutable::class), Argument::type('int'), - Argument::not(0) + Argument::exact(10) )->will(static fn ($args) => [])->shouldBeCalledTimes(1); $em = $this->prophesize(EntityManagerInterface::class); $em->clear()->shouldBeCalled(); + $calendarRangeGenerator = $this->prophesize(RangeGeneratorInterface::class); + $calendarRangeGenerator->generateRange(Argument::any())->willReturn([ + 'startDate' => new \DateTimeImmutable('yesterday'), + 'endDate' => new \DateTimeImmutable('now'), + ]); + $provider = new CalendarForShortMessageProvider( $calendarRepository->reveal(), $em->reveal(), - new DefaultRangeGenerator() + $calendarRangeGenerator->reveal(), ); $calendars = iterator_to_array($provider->getCalendars(new \DateTimeImmutable('now'))); - $this->assertEquals(1, \count($calendars)); + $this->assertEquals(10, \count($calendars)); $this->assertContainsOnly(Calendar::class, $calendars); } } diff --git a/src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php b/src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php index 81d230e67..e90164a2a 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php @@ -13,7 +13,13 @@ namespace Chill\DocStoreBundle\Repository; use Chill\DocStoreBundle\Entity\StoredObject; +/** + * @template T of object + */ interface AssociatedEntityToStoredObjectInterface { + /** + * @return T|null + */ public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object; } diff --git a/src/Bundle/ChillEventBundle/Export/Aggregator/EventDateAggregator.php b/src/Bundle/ChillEventBundle/Export/Aggregator/EventDateAggregator.php index 859ddcb3c..bdb28f9c3 100644 --- a/src/Bundle/ChillEventBundle/Export/Aggregator/EventDateAggregator.php +++ b/src/Bundle/ChillEventBundle/Export/Aggregator/EventDateAggregator.php @@ -32,7 +32,7 @@ class EventDateAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { $order = null; @@ -76,12 +76,27 @@ class EventDateAggregator implements AggregatorInterface ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['frequency' => $formData['frequency']]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['frequency' => $formData['frequency']]; + } + public function getFormDefaultData(): array { return ['frequency' => self::DEFAULT_CHOICE]; } - public function getLabels($key, array $values, $data) + public function getLabels($key, array $values, $data): callable { return static function ($value) use ($data): string { if ('_header' === $value) { diff --git a/src/Bundle/ChillEventBundle/Export/Aggregator/EventTypeAggregator.php b/src/Bundle/ChillEventBundle/Export/Aggregator/EventTypeAggregator.php index e96fd4786..7aafb3791 100644 --- a/src/Bundle/ChillEventBundle/Export/Aggregator/EventTypeAggregator.php +++ b/src/Bundle/ChillEventBundle/Export/Aggregator/EventTypeAggregator.php @@ -29,7 +29,7 @@ class EventTypeAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { if (!\in_array('eventtype', $qb->getAllAliases(), true)) { $qb->leftJoin('event.type', 'eventtype'); @@ -49,12 +49,27 @@ class EventTypeAggregator implements AggregatorInterface // no form required for this aggregator } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data): \Closure + public function getLabels($key, array $values, $data): callable { return function (int|string|null $value): string { if ('_header' === $value) { @@ -74,7 +89,7 @@ class EventTypeAggregator implements AggregatorInterface return [self::KEY]; } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'Group by event type'; } diff --git a/src/Bundle/ChillEventBundle/Export/Aggregator/RoleAggregator.php b/src/Bundle/ChillEventBundle/Export/Aggregator/RoleAggregator.php index 8303b4429..20f663d4e 100644 --- a/src/Bundle/ChillEventBundle/Export/Aggregator/RoleAggregator.php +++ b/src/Bundle/ChillEventBundle/Export/Aggregator/RoleAggregator.php @@ -29,7 +29,7 @@ class RoleAggregator implements AggregatorInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void { if (!\in_array('event_part', $qb->getAllAliases(), true)) { $qb->leftJoin('event_part.role', 'role'); @@ -49,12 +49,27 @@ class RoleAggregator implements AggregatorInterface // no form required for this aggregator } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; } - public function getLabels($key, array $values, $data): \Closure + public function getLabels($key, array $values, $data): callable { return function (int|string|null $value): string { if ('_header' === $value) { @@ -74,7 +89,7 @@ class RoleAggregator implements AggregatorInterface return [self::KEY]; } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'Group by participant role'; } diff --git a/src/Bundle/ChillEventBundle/Export/Export/CountEventParticipations.php b/src/Bundle/ChillEventBundle/Export/Export/CountEventParticipations.php index 5c5d5bf05..d95fb4ecb 100644 --- a/src/Bundle/ChillEventBundle/Export/Export/CountEventParticipations.php +++ b/src/Bundle/ChillEventBundle/Export/Export/CountEventParticipations.php @@ -36,6 +36,21 @@ readonly class CountEventParticipations implements ExportInterface, GroupedExpor public function buildForm(FormBuilderInterface $builder) {} + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; @@ -65,17 +80,17 @@ readonly class CountEventParticipations implements ExportInterface, GroupedExpor return static fn ($value) => '_header' === $value ? 'Count event participants' : $value; } - public function getQueryKeys($data) + public function getQueryKeys($data): array { return ['export_count_event_participants']; } - public function getResult($query, $data) + public function getResult($query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array { return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'Count event participants'; } @@ -85,7 +100,7 @@ readonly class CountEventParticipations implements ExportInterface, GroupedExpor return Declarations::EVENT_PARTICIPANTS; } - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): \Doctrine\ORM\QueryBuilder { $centers = array_map(static fn ($el) => $el['center'], $acl); diff --git a/src/Bundle/ChillEventBundle/Export/Export/CountEvents.php b/src/Bundle/ChillEventBundle/Export/Export/CountEvents.php index 7cb98b5a6..fe51fdfd8 100644 --- a/src/Bundle/ChillEventBundle/Export/Export/CountEvents.php +++ b/src/Bundle/ChillEventBundle/Export/Export/CountEvents.php @@ -36,6 +36,21 @@ readonly class CountEvents implements ExportInterface, GroupedExportInterface public function buildForm(FormBuilderInterface $builder) {} + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return []; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return []; + } + public function getFormDefaultData(): array { return []; @@ -65,17 +80,17 @@ readonly class CountEvents implements ExportInterface, GroupedExportInterface return static fn ($value) => '_header' === $value ? 'Number of events' : $value; } - public function getQueryKeys($data) + public function getQueryKeys($data): array { return ['export_count_event']; } - public function getResult($query, $data) + public function getResult($query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array { return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'Count events'; } @@ -85,7 +100,7 @@ readonly class CountEvents implements ExportInterface, GroupedExportInterface return Declarations::EVENT; } - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): \Doctrine\ORM\QueryBuilder { $centers = array_map(static fn ($el) => $el['center'], $acl); diff --git a/src/Bundle/ChillEventBundle/Export/Filter/EventDateFilter.php b/src/Bundle/ChillEventBundle/Export/Filter/EventDateFilter.php index 2e3153ea1..e7b245fbc 100644 --- a/src/Bundle/ChillEventBundle/Export/Filter/EventDateFilter.php +++ b/src/Bundle/ChillEventBundle/Export/Filter/EventDateFilter.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\EventBundle\Export\Filter; use Chill\EventBundle\Export\Declarations; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Form\Type\PickRollingDateType; use Chill\MainBundle\Service\RollingDate\RollingDate; @@ -30,7 +31,7 @@ class EventDateFilter implements FilterInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $where = $qb->getDQLPart('where'); $clause = $qb->expr()->between( @@ -72,12 +73,27 @@ class EventDateFilter implements FilterInterface ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['date_from' => $formData['date_from']->normalize(), 'date_to' => $formData['date_to']->normalize()]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['date_from' => RollingDate::fromNormalized($formData['date_from']), 'date_to' => RollingDate::fromNormalized($formData['date_to'])]; + } + public function getFormDefaultData(): array { return ['date_from' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), 'date_to' => new RollingDate(RollingDate::T_TODAY)]; } - public function describeAction($data, $format = 'string') + public function describeAction($data, ExportGenerationContext $context): string|\Symfony\Contracts\Translation\TranslatableInterface|array { return [ 'Filtered by date of event: only between %date_from% and %date_to%', @@ -88,7 +104,7 @@ class EventDateFilter implements FilterInterface ]; } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'Filtered by event date'; } diff --git a/src/Bundle/ChillEventBundle/Export/Filter/EventTypeFilter.php b/src/Bundle/ChillEventBundle/Export/Filter/EventTypeFilter.php index b5087e901..5314a0bc6 100644 --- a/src/Bundle/ChillEventBundle/Export/Filter/EventTypeFilter.php +++ b/src/Bundle/ChillEventBundle/Export/Filter/EventTypeFilter.php @@ -15,6 +15,7 @@ use Chill\EventBundle\Entity\EventType; use Chill\EventBundle\Export\Declarations; use Chill\EventBundle\Repository\EventTypeRepository; use Chill\MainBundle\Export\ExportElementValidatedInterface; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Doctrine\ORM\QueryBuilder; @@ -24,6 +25,8 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; class EventTypeFilter implements ExportElementValidatedInterface, FilterInterface { + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; + public function __construct( protected TranslatableStringHelperInterface $translatableStringHelper, protected EventTypeRepository $eventTypeRepository, @@ -34,7 +37,7 @@ class EventTypeFilter implements ExportElementValidatedInterface, FilterInterfac return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $clause = $qb->expr()->in('event.type', ':selected_event_types'); @@ -61,12 +64,27 @@ class EventTypeFilter implements ExportElementValidatedInterface, FilterInterfac ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['types' => $this->normalizeDoctrineEntity($formData['types'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['types' => $this->denormalizeDoctrineEntity($formData['types'], $this->eventTypeRepository)]; + } + public function getFormDefaultData(): array { return []; } - public function describeAction($data, $format = 'string') + public function describeAction($data, ExportGenerationContext $context): string|\Symfony\Contracts\Translation\TranslatableInterface|array { $typeNames = array_map( fn (EventType $t): string => $this->translatableStringHelper->localize($t->getName()), @@ -78,7 +96,7 @@ class EventTypeFilter implements ExportElementValidatedInterface, FilterInterfac ]]; } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'Filtered by event type'; } diff --git a/src/Bundle/ChillEventBundle/Export/Filter/RoleFilter.php b/src/Bundle/ChillEventBundle/Export/Filter/RoleFilter.php index 0d0206a43..4ef2ec2aa 100644 --- a/src/Bundle/ChillEventBundle/Export/Filter/RoleFilter.php +++ b/src/Bundle/ChillEventBundle/Export/Filter/RoleFilter.php @@ -15,6 +15,7 @@ use Chill\EventBundle\Entity\Role; use Chill\EventBundle\Export\Declarations; use Chill\EventBundle\Repository\RoleRepository; use Chill\MainBundle\Export\ExportElementValidatedInterface; +use Chill\MainBundle\Export\ExportGenerationContext; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Doctrine\ORM\QueryBuilder; @@ -24,6 +25,8 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; class RoleFilter implements ExportElementValidatedInterface, FilterInterface { + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; + public function __construct( protected TranslatableStringHelperInterface $translatableStringHelper, protected RoleRepository $roleRepository, @@ -34,7 +37,7 @@ class RoleFilter implements ExportElementValidatedInterface, FilterInterface return null; } - public function alterQuery(QueryBuilder $qb, $data): void + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void { $clause = $qb->expr()->in('event_part.role', ':selected_part_roles'); @@ -61,12 +64,27 @@ class RoleFilter implements ExportElementValidatedInterface, FilterInterface ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['part_roles' => $this->normalizeDoctrineEntity($formData['part_roles'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['part_roles' => $this->denormalizeDoctrineEntity($formData['part_roles'], $this->roleRepository)]; + } + public function getFormDefaultData(): array { return []; } - public function describeAction($data, $format = 'string') + public function describeAction($data, ExportGenerationContext $context): string|\Symfony\Contracts\Translation\TranslatableInterface|array { $roleNames = array_map( fn (Role $r): string => $this->translatableStringHelper->localize($r->getName()), @@ -78,7 +96,7 @@ class RoleFilter implements ExportElementValidatedInterface, FilterInterface ]]; } - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'Filter by participant roles'; } diff --git a/src/Bundle/ChillEventBundle/Resources/views/Event/page_list.html.twig b/src/Bundle/ChillEventBundle/Resources/views/Event/page_list.html.twig index bb1ffa24e..4d887d3c5 100644 --- a/src/Bundle/ChillEventBundle/Resources/views/Event/page_list.html.twig +++ b/src/Bundle/ChillEventBundle/Resources/views/Event/page_list.html.twig @@ -54,14 +54,14 @@ block js %} {% if e.participations|length > 0 %}
{{ "Participations" | trans }} : - {% for part in e.participations|slice(0, 20) %} {% include + {% for part in e.participations|slice(0, 5) %} {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { targetEntity: { name: 'person', id: part.person.id }, action: 'show', displayBadge: true, buttonText: part.person|chill_entity_render_string, isDead: - part.person.deathdate is not null } %} {% endfor %} {% if - e.participations|length > 20 %} - {{ 'events.and_other_count_participants'|trans({'count': e.participations|length - 20}) }} + part.person.deathdate is not null } %} {% endfor %} + {% if e.participations|length > 5 %} + {{ 'events.and_other_count_participants'|trans({'count': e.participations|length - 5}) }} {% endif %}
{% endif %} diff --git a/src/Bundle/ChillJobBundle/src/Export/ListCV.php b/src/Bundle/ChillJobBundle/src/Export/ListCV.php index 757ef4c06..274f4128f 100644 --- a/src/Bundle/ChillJobBundle/src/Export/ListCV.php +++ b/src/Bundle/ChillJobBundle/src/Export/ListCV.php @@ -33,6 +33,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; */ class ListCV implements ListInterface, ExportElementValidatedInterface { + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; /** * @var array */ @@ -71,14 +72,14 @@ class ListCV implements ListInterface, ExportElementValidatedInterface * * @param mixed $data the data, as returned by the user */ - public function validateForm($data, ExecutionContextInterface $context) {} + public function validateForm($data, ExecutionContextInterface $context): void {} /** * get a title, which will be used in UI (and translated). * * @return string */ - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'Liste des CVs par personne'; } @@ -132,15 +133,28 @@ class ListCV implements ListInterface, ExportElementValidatedInterface ; } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['fields' => $formData['fields'], 'reportdate_min' => $this->normalizeDate($formData['reportdate_min']), 'reportdate_max' => $this->normalizeDate($formData['reportdate_max'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['fields' => $formData['fields'], 'reportdate_min' => $this->denormalizeDate($formData['reportdate_min']), 'reportdate_max' => $this->denormalizeDate($formData['reportdate_max'])]; + } + /** * Return the Export's type. This will inform _on what_ export will apply. * Most of the type, it will be a string which references an entity. * * Example of types : Chill\PersonBundle\Export\Declarations::PERSON_TYPE - * - * @return string */ - public function getType() + public function getType(): string { return Person::class; } @@ -156,22 +170,7 @@ class ListCV implements ListInterface, ExportElementValidatedInterface return 'Crée une liste des CVs en fonction de différents paramètres.'; } - /** - * The initial query, which will be modified by ModifiersInterface - * (i.e. AggregatorInterface, FilterInterface). - * - * This query should take into account the `$acl` and restrict result only to - * what the user is allowed to see. (Do not show personal data the user - * is not allowed to see). - * - * The returned object should be an instance of QueryBuilder or NativeQuery. - * - * @param array $acl an array where each row has a `center` key containing the Chill\MainBundle\Entity\Center, and `circles` keys containing the reachable circles. Example: `array( array('center' => $centerA, 'circles' => array($circleA, $circleB) ) )` - * @param array $data the data from the form, if any - * - * @return QueryBuilder|\Doctrine\ORM\NativeQuery the query to execute - */ - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): QueryBuilder { return $this->entityManager->createQueryBuilder() ->from('ChillPersonBundle:Person', 'person'); @@ -215,8 +214,6 @@ class ListCV implements ListInterface, ExportElementValidatedInterface * this function will return `array('count_id')`. * * @param mixed[] $data the data from the export's form (added by self::buildForm) - * - * @return array */ public function getQueryKeys($data): array { @@ -239,7 +236,7 @@ class ListCV implements ListInterface, ExportElementValidatedInterface * @param QueryBuilder|\Doctrine\ORM\NativeQuery $qb * @param mixed[] $data the data from the export's form (added by self::buildForm) */ - public function getResult($qb, $data) + public function getResult($qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array { $qb->select('person.id'); diff --git a/src/Bundle/ChillJobBundle/src/Export/ListFrein.php b/src/Bundle/ChillJobBundle/src/Export/ListFrein.php index b82c53a44..874bbd975 100644 --- a/src/Bundle/ChillJobBundle/src/Export/ListFrein.php +++ b/src/Bundle/ChillJobBundle/src/Export/ListFrein.php @@ -34,6 +34,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; */ class ListFrein implements ListInterface, ExportElementValidatedInterface { + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; /** * @var array */ @@ -84,14 +85,14 @@ class ListFrein implements ListInterface, ExportElementValidatedInterface * * @param mixed $data the data, as returned by the user */ - public function validateForm($data, ExecutionContextInterface $context) {} + public function validateForm($data, ExecutionContextInterface $context): void {} /** * get a title, which will be used in UI (and translated). * * @return string */ - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'Liste des freins identifiés par personne'; } @@ -145,15 +146,28 @@ class ListFrein implements ListInterface, ExportElementValidatedInterface ; } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['fields' => $formData['fields'], 'reportdate_min' => $this->normalizeDate($formData['reportdate_min']), 'reportdate_max' => $this->normalizeDate($formData['reportdate_max'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['fields' => $formData['fields'], 'reportdate_min' => $this->denormalizeDate($formData['reportdate_min']), 'reportdate_max' => $this->denormalizeDate($formData['reportdate_max'])]; + } + /** * Return the Export's type. This will inform _on what_ export will apply. * Most of the type, it will be a string which references an entity. * * Example of types : Chill\PersonBundle\Export\Declarations::PERSON_TYPE - * - * @return string */ - public function getType() + public function getType(): string { return Person::class; } @@ -169,22 +183,7 @@ class ListFrein implements ListInterface, ExportElementValidatedInterface return 'Crée une liste des personnes et de leurs freins identifiés en fonction de différents paramètres.'; } - /** - * The initial query, which will be modified by ModifiersInterface - * (i.e. AggregatorInterface, FilterInterface). - * - * This query should take into account the `$acl` and restrict result only to - * what the user is allowed to see. (Do not show personal data the user - * is not allowed to see). - * - * The returned object should be an instance of QueryBuilder or NativeQuery. - * - * @param array $acl an array where each row has a `center` key containing the Chill\MainBundle\Entity\Center, and `circles` keys containing the reachable circles. Example: `array( array('center' => $centerA, 'circles' => array($circleA, $circleB) ) )` - * @param array $data the data from the form, if any - * - * @return QueryBuilder|\Doctrine\ORM\NativeQuery the query to execute - */ - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): QueryBuilder { return $this->entityManager->createQueryBuilder() ->from(Person::class, 'person'); @@ -238,10 +237,8 @@ class ListFrein implements ListInterface, ExportElementValidatedInterface * this function will return `array('count_id')`. * * @param mixed[] $data the data from the export's form (added by self::buildForm) - * - * @return array */ - public function getQueryKeys($data) + public function getQueryKeys($data): array { $freins = self::FREINS; $fields = []; @@ -332,7 +329,7 @@ class ListFrein implements ListInterface, ExportElementValidatedInterface * @param QueryBuilder|\Doctrine\ORM\NativeQuery $qb * @param mixed[] $data the data from the export's form (added by self::buildForm) */ - public function getResult($qb, $data) + public function getResult($qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array { $qb->select('person.id'); diff --git a/src/Bundle/ChillJobBundle/src/Export/ListProjetProfessionnel.php b/src/Bundle/ChillJobBundle/src/Export/ListProjetProfessionnel.php index 34258096c..e2170c332 100644 --- a/src/Bundle/ChillJobBundle/src/Export/ListProjetProfessionnel.php +++ b/src/Bundle/ChillJobBundle/src/Export/ListProjetProfessionnel.php @@ -34,6 +34,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; */ class ListProjetProfessionnel implements ListInterface, ExportElementValidatedInterface { + use \Chill\MainBundle\Export\ExportDataNormalizerTrait; /** * @var array */ @@ -89,14 +90,14 @@ class ListProjetProfessionnel implements ListInterface, ExportElementValidatedIn * * @param mixed $data the data, as returned by the user */ - public function validateForm($data, ExecutionContextInterface $context) {} + public function validateForm($data, ExecutionContextInterface $context): void {} /** * get a title, which will be used in UI (and translated). * * @return string */ - public function getTitle() + public function getTitle(): string|\Symfony\Contracts\Translation\TranslatableInterface { return 'Liste des projets professionnels par personne'; } @@ -151,15 +152,28 @@ class ListProjetProfessionnel implements ListInterface, ExportElementValidatedIn ; } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['fields' => $formData['fields'], 'reportdate_min' => $this->normalizeDate($formData['reportdate_min']), 'reportdate_max' => $this->normalizeDate($formData['reportdate_max'])]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['fields' => $formData['fields'], 'reportdate_min' => $this->denormalizeDate($formData['reportdate_min']), 'reportdate_max' => $this->denormalizeDate($formData['reportdate_max'])]; + } + /** * Return the Export's type. This will inform _on what_ export will apply. * Most of the type, it will be a string which references an entity. * * Example of types : Chill\PersonBundle\Export\Declarations::PERSON_TYPE - * - * @return string */ - public function getType() + public function getType(): string { return Person::class; } @@ -175,22 +189,7 @@ class ListProjetProfessionnel implements ListInterface, ExportElementValidatedIn return 'Crée une liste des personnes et de leur projet professionnel en fonction de différents paramètres.'; } - /** - * The initial query, which will be modified by ModifiersInterface - * (i.e. AggregatorInterface, FilterInterface). - * - * This query should take into account the `$acl` and restrict result only to - * what the user is allowed to see. (Do not show personal data the user - * is not allowed to see). - * - * The returned object should be an instance of QueryBuilder or NativeQuery. - * - * @param array $acl an array where each row has a `center` key containing the Chill\MainBundle\Entity\Center, and `circles` keys containing the reachable circles. Example: `array( array('center' => $centerA, 'circles' => array($circleA, $circleB) ) )` - * @param array $data the data from the form, if any - * - * @return QueryBuilder|\Doctrine\ORM\NativeQuery the query to execute - */ - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): QueryBuilder { return $this->entityManager->createQueryBuilder() ->from('ChillPersonBundle:Person', 'person'); @@ -244,10 +243,8 @@ class ListProjetProfessionnel implements ListInterface, ExportElementValidatedIn * this function will return `array('count_id')`. * * @param mixed[] $data the data from the export's form (added by self::buildForm) - * - * @return array */ - public function getQueryKeys($data) + public function getQueryKeys($data): array { $projet_professionnel = self::PPROF; @@ -370,7 +367,7 @@ class ListProjetProfessionnel implements ListInterface, ExportElementValidatedIn * @param QueryBuilder|\Doctrine\ORM\NativeQuery $qb * @param mixed[] $data the data from the export's form (added by self::buildForm) */ - public function getResult($qb, $data) + public function getResult($qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array { $qb->select('person.id'); diff --git a/src/Bundle/ChillMainBundle/ChillMainBundle.php b/src/Bundle/ChillMainBundle/ChillMainBundle.php index b360cf239..16e41290b 100644 --- a/src/Bundle/ChillMainBundle/ChillMainBundle.php +++ b/src/Bundle/ChillMainBundle/ChillMainBundle.php @@ -14,13 +14,13 @@ namespace Chill\MainBundle; use Chill\MainBundle\Cron\CronJobInterface; use Chill\MainBundle\CRUD\CompilerPass\CRUDControllerCompilerPass; use Chill\MainBundle\DependencyInjection\CompilerPass\ACLFlagsCompilerPass; -use Chill\MainBundle\DependencyInjection\CompilerPass\ExportsCompilerPass; use Chill\MainBundle\DependencyInjection\CompilerPass\MenuCompilerPass; use Chill\MainBundle\DependencyInjection\CompilerPass\NotificationCounterCompilerPass; use Chill\MainBundle\DependencyInjection\CompilerPass\SearchableServicesCompilerPass; use Chill\MainBundle\DependencyInjection\CompilerPass\TimelineCompilerClass; use Chill\MainBundle\DependencyInjection\CompilerPass\WidgetsCompilerPass; use Chill\MainBundle\DependencyInjection\ConfigConsistencyCompilerPass; +use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterface; use Chill\MainBundle\Notification\NotificationHandlerInterface; use Chill\MainBundle\Routing\LocalMenuBuilderInterface; use Chill\MainBundle\Search\SearchApiInterface; @@ -62,11 +62,12 @@ class ChillMainBundle extends Bundle ->addTag('chill_main.entity_info_provider'); $container->registerForAutoconfiguration(ProvideRoleInterface::class) ->addTag('chill_main.provide_role'); + $container->registerForAutoconfiguration(NotificationFlagProviderInterface::class) + ->addTag('chill_main.notification_flag_provider'); $container->addCompilerPass(new SearchableServicesCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0); $container->addCompilerPass(new ConfigConsistencyCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0); $container->addCompilerPass(new TimelineCompilerClass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0); - $container->addCompilerPass(new ExportsCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0); $container->addCompilerPass(new WidgetsCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0); $container->addCompilerPass(new NotificationCounterCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0); $container->addCompilerPass(new MenuCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0); diff --git a/src/Bundle/ChillMainBundle/Controller/ExportController.php b/src/Bundle/ChillMainBundle/Controller/ExportController.php index e15100bab..693c0e55e 100644 --- a/src/Bundle/ChillMainBundle/Controller/ExportController.php +++ b/src/Bundle/ChillMainBundle/Controller/ExportController.php @@ -11,36 +11,39 @@ declare(strict_types=1); namespace Chill\MainBundle\Controller; +use Chill\MainBundle\Entity\ExportGeneration; use Chill\MainBundle\Entity\SavedExport; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Export\DirectExportInterface; +use Chill\MainBundle\Export\ExportConfigNormalizer; +use Chill\MainBundle\Export\ExportConfigProcessor; use Chill\MainBundle\Export\ExportFormHelper; use Chill\MainBundle\Export\ExportInterface; use Chill\MainBundle\Export\ExportManager; -use Chill\MainBundle\Form\SavedExportType; +use Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage; use Chill\MainBundle\Form\Type\Export\ExportType; use Chill\MainBundle\Form\Type\Export\FormatterType; use Chill\MainBundle\Form\Type\Export\PickCenterType; -use Chill\MainBundle\Redis\ChillRedis; -use Chill\MainBundle\Repository\SavedExportRepositoryInterface; +use Chill\MainBundle\Repository\SavedExportOrExportGenerationRepository; +use Chill\MainBundle\Security\Authorization\ChillExportVoter; use Chill\MainBundle\Security\Authorization\SavedExportVoter; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; -use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\Clock\ClockInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface; -use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Routing\Annotation\Route; -use Symfony\Bundle\SecurityBundle\Security; -use Symfony\Contracts\Translation\TranslatorInterface; +use Symfony\Component\Security\Core\Security; /** * Class ExportController @@ -51,117 +54,23 @@ class ExportController extends AbstractController private readonly bool $filterStatsByCenters; public function __construct( - private readonly ChillRedis $redis, private readonly ExportManager $exportManager, private readonly FormFactoryInterface $formFactory, private readonly LoggerInterface $logger, - private readonly RequestStack $requestStack, - private readonly TranslatorInterface $translator, + private readonly SessionInterface $session, private readonly EntityManagerInterface $entityManager, private readonly ExportFormHelper $exportFormHelper, - private readonly SavedExportRepositoryInterface $savedExportRepository, private readonly Security $security, ParameterBagInterface $parameterBag, + private readonly MessageBusInterface $messageBus, + private readonly ClockInterface $clock, + private readonly ExportConfigNormalizer $exportConfigNormalizer, + private readonly SavedExportOrExportGenerationRepository $savedExportOrExportGenerationRepository, + private readonly ExportConfigProcessor $exportConfigProcessor, ) { $this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center']; } - #[\Symfony\Component\Routing\Attribute\Route(path: '/{_locale}/exports/download/{alias}', name: 'chill_main_export_download', methods: ['GET'])] - public function downloadResultAction(Request $request, mixed $alias): Response - { - /** @var ExportManager $exportManager */ - $exportManager = $this->exportManager; - $export = $exportManager->getExport($alias); - $key = $request->query->get('key', null); - $savedExport = $this->getSavedExportFromRequest($request); - - [$dataCenters, $dataExport, $dataFormatter] = $this->rebuildData($key, $savedExport); - - $formatterAlias = $exportManager->getFormatterAlias($dataExport['export']); - - if (null !== $formatterAlias) { - $formater = $exportManager->getFormatter($formatterAlias); - } else { - $formater = null; - } - - $viewVariables = [ - 'alias' => $alias, - 'export' => $export, - 'export_group' => $this->getExportGroup($export), - 'saved_export' => $savedExport, - ]; - - if ($formater instanceof \Chill\MainBundle\Export\Formatter\CSVListFormatter) { - // due to a bug in php, we add the mime type in the download view - $viewVariables['mime_type'] = 'text/csv'; - } - - return $this->render('@ChillMain/Export/download.html.twig', $viewVariables); - } - - /** - * Generate a report. - * - * This action must work with GET queries. - * - * @param string $alias - */ - #[\Symfony\Component\Routing\Attribute\Route(path: '/{_locale}/exports/generate/{alias}', name: 'chill_main_export_generate', methods: ['GET'])] - public function generateAction(Request $request, $alias): Response - { - /** @var ExportManager $exportManager */ - $exportManager = $this->exportManager; - $key = $request->query->get('key', null); - $savedExport = $this->getSavedExportFromRequest($request); - - [$dataCenters, $dataExport, $dataFormatter] = $this->rebuildData($key, $savedExport); - - return $exportManager->generate( - $alias, - $dataCenters['centers'], - $dataExport['export'], - null !== $dataFormatter ? $dataFormatter['formatter'] : [] - ); - } - - /** - * @throws \RedisException - */ - #[\Symfony\Component\Routing\Attribute\Route(path: '/{_locale}/exports/generate-from-saved/{id}', name: 'chill_main_export_generate_from_saved')] - public function generateFromSavedExport(#[MapEntity(id: 'id')] SavedExport $savedExport): RedirectResponse - { - $this->denyAccessUnlessGranted(SavedExportVoter::GENERATE, $savedExport); - - $key = md5(uniqid((string) random_int(0, mt_getrandmax()), false)); - - $this->redis->setEx($key, 3600, \serialize($savedExport->getOptions())); - - return $this->redirectToRoute( - 'chill_main_export_download', - [ - 'alias' => $savedExport->getExportAlias(), - 'key' => $key, 'prevent_save' => true, - 'returnPath' => $this->generateUrl('chill_main_export_saved_list_my'), - ] - ); - } - - /** - * Render the list of available exports. - */ - #[\Symfony\Component\Routing\Attribute\Route(path: '/{_locale}/exports/', name: 'chill_main_export_index')] - public function indexAction(): Response - { - $exportManager = $this->exportManager; - - $exports = $exportManager->getExportsGrouped(true); - - return $this->render('@ChillMain/Export/layout.html.twig', [ - 'grouped_exports' => $exports, - ]); - } - /** * handle the step to build a query for an export. * @@ -174,7 +83,7 @@ class ExportController extends AbstractController * 3. 'generate': gather data from session from the previous steps, and * make a redirection to the "generate" action with data in query (HTTP GET) */ - #[\Symfony\Component\Routing\Attribute\Route(path: '/{_locale}/exports/new/{alias}', name: 'chill_main_export_new')] + #[Route(path: '/{_locale}/exports/new/{alias}', name: 'chill_main_export_new')] public function newAction(Request $request, string $alias): Response { // first check for ACL @@ -198,64 +107,6 @@ class ExportController extends AbstractController }; } - #[\Symfony\Component\Routing\Attribute\Route(path: '/{_locale}/export/saved/update-from-key/{id}/{key}', name: 'chill_main_export_saved_edit_options_from_key')] - public function editSavedExportOptionsFromKey(#[MapEntity(id: 'id')] SavedExport $savedExport, string $key): Response - { - $this->denyAccessUnlessGranted('ROLE_USER'); - $user = $this->getUser(); - - if (!$user instanceof User) { - throw new AccessDeniedHttpException(); - } - - $data = $this->rebuildRawData($key); - - $savedExport - ->setOptions($data); - - $this->entityManager->flush(); - - return $this->redirectToRoute('chill_main_export_saved_edit', ['id' => $savedExport->getId()]); - } - - #[\Symfony\Component\Routing\Attribute\Route(path: '/{_locale}/export/save-from-key/{alias}/{key}', name: 'chill_main_export_save_from_key')] - public function saveFromKey(string $alias, string $key, Request $request): Response - { - $this->denyAccessUnlessGranted('ROLE_USER'); - $user = $this->getUser(); - - if (!$user instanceof User) { - throw new AccessDeniedHttpException(); - } - - $data = $this->rebuildRawData($key); - - $savedExport = new SavedExport(); - $savedExport - ->setOptions($data) - ->setExportAlias($alias) - ->setUser($user); - - $form = $this->createForm(SavedExportType::class, $savedExport); - - $form->handleRequest($request); - - if ($form->isSubmitted() && $form->isValid()) { - $this->entityManager->persist($savedExport); - $this->entityManager->flush(); - - return $this->redirectToRoute('chill_main_export_index'); - } - - return $this->render( - '@ChillMain/SavedExport/new.html.twig', - [ - 'form' => $form, - 'saved_export' => $savedExport, - ] - ); - } - /** * create a form to show on different steps. * @@ -263,19 +114,26 @@ class ExportController extends AbstractController */ protected function createCreateFormExport(string $alias, string $step, array $data, ?SavedExport $savedExport): FormInterface { - /** @var ExportManager $exportManager */ $exportManager = $this->exportManager; $isGenerate = str_starts_with($step, 'generate_'); + $canEditFull = $this->security->isGranted(ChillExportVoter::COMPOSE_EXPORT); + + if (!$canEditFull && null === $savedExport) { + throw new AccessDeniedHttpException('The user is not allowed to edit all filter, it should edit only SavedExport'); + } $options = match ($step) { 'export', 'generate_export' => [ 'export_alias' => $alias, - 'picked_centers' => $exportManager->getPickedCenters($data['centers'] ?? []), + 'picked_centers' => $this->filterStatsByCenters ? $this->exportFormHelper->getPickedCenters($data) : [], + 'can_edit_full' => $canEditFull, + 'allowed_filters' => $canEditFull ? null : $this->exportConfigProcessor->retrieveUsedFilters($savedExport->getOptions()['filters']), + 'allowed_aggregators' => $canEditFull ? null : $this->exportConfigProcessor->retrieveUsedAggregators($savedExport->getOptions()['aggregators']), ], 'formatter', 'generate_formatter' => [ 'export_alias' => $alias, 'formatter_alias' => $exportManager->getFormatterAlias($data['export']), - 'aggregator_aliases' => $exportManager->getUsedAggregatorsAliases($data['export']), + 'aggregator_aliases' => $exportManager->getUsedAggregatorsAliases($data['export']['aggregators']), ], default => [ 'export_alias' => $alias, @@ -284,14 +142,14 @@ class ExportController extends AbstractController $defaultFormData = match ($savedExport) { null => $this->exportFormHelper->getDefaultData($step, $exportManager->getExport($alias), $options), - default => $this->exportFormHelper->savedExportDataToFormData($savedExport, $step, $options), + default => $this->exportFormHelper->savedExportDataToFormData($savedExport, $step), }; $builder = $this->formFactory ->createNamedBuilder( '', FormType::class, - $defaultFormData, + 'centers' === $step ? ['centers' => $defaultFormData] : $defaultFormData, [ 'method' => $isGenerate ? Request::METHOD_GET : Request::METHOD_POST, 'csrf_protection' => !$isGenerate, @@ -328,7 +186,7 @@ class ExportController extends AbstractController $exportManager = $this->exportManager; // check we have data from the previous step (export step) - $data = $this->requestStack->getSession()->get('centers_step', []); + $data = $this->session->get('centers_step', []); if (null === $data && true === $this->filterStatsByCenters) { return $this->redirectToRoute('chill_main_export_new', [ @@ -350,11 +208,11 @@ class ExportController extends AbstractController // store data for reusing in next steps $data = $form->getData(); - $this->requestStack->getSession()->set( + $this->session->set( 'export_step_raw', $request->request->all() ); - $this->requestStack->getSession()->set('export_step', $data); + $this->session->set('export_step', $data); // redirect to next step return $this->redirectToRoute('chill_main_export_new', [ @@ -368,7 +226,7 @@ class ExportController extends AbstractController } return $this->render('@ChillMain/Export/new.html.twig', [ - 'form' => $form, + 'form' => $form->createView(), 'export_alias' => $alias, 'export' => $export, 'export_group' => $this->getExportGroup($export), @@ -384,7 +242,7 @@ class ExportController extends AbstractController private function formatterFormStep(Request $request, DirectExportInterface|ExportInterface $export, string $alias, ?SavedExport $savedExport = null): Response { // check we have data from the previous step (export step) - $data = $this->requestStack->getSession()->get('export_step', null); + $data = $this->session->get('export_step', null); if (null === $data) { return $this->redirectToRoute('chill_main_export_new', [ @@ -400,8 +258,8 @@ class ExportController extends AbstractController if ($form->isValid()) { $dataFormatter = $form->getData(); - $this->requestStack->getSession()->set('formatter_step', $dataFormatter); - $this->requestStack->getSession()->set( + $this->session->set('formatter_step', $dataFormatter); + $this->session->set( 'formatter_step_raw', $request->request->all() ); @@ -418,7 +276,7 @@ class ExportController extends AbstractController return $this->render( '@ChillMain/Export/new_formatter_step.html.twig', [ - 'form' => $form, + 'form' => $form->createView(), 'export' => $export, 'export_group' => $this->getExportGroup($export), ] @@ -430,14 +288,17 @@ class ExportController extends AbstractController * and redirect to the `generate` action. * * The data from previous steps is removed from session. - * - * @param string $alias */ - private function forwardToGenerate(Request $request, DirectExportInterface|ExportInterface $export, $alias, ?SavedExport $savedExport): RedirectResponse + private function forwardToGenerate(Request $request, DirectExportInterface|ExportInterface $export, $alias, ?SavedExport $savedExport): Response { - $dataCenters = $this->requestStack->getSession()->get('centers_step_raw', null); - $dataFormatter = $this->requestStack->getSession()->get('formatter_step_raw', null); - $dataExport = $this->requestStack->getSession()->get('export_step_raw', null); + $user = $this->getUser(); + + if (!$user instanceof User) { + throw new AccessDeniedHttpException('only regular users can generate export'); + } + $dataCenters = $this->session->get('centers_step_raw', null); + $dataFormatter = $this->session->get('formatter_step_raw', null); + $dataExport = $this->session->get('export_step_raw', null); if (null === $dataFormatter && $export instanceof ExportInterface) { return $this->redirectToRoute('chill_main_export_new', [ @@ -447,60 +308,82 @@ class ExportController extends AbstractController ]); } - $parameters = [ - 'formatter' => $dataFormatter ?? [], - 'export' => $dataExport ?? [], - 'centers' => $dataCenters ?? [], - 'alias' => $alias, - ]; - unset($parameters['_token']); - $key = md5(uniqid((string) random_int(0, mt_getrandmax()), false)); + $dataToNormalize = $this->buildExportDataForNormalization( + $alias, + $dataCenters, + $dataExport, + $dataFormatter, + $savedExport, + ); - $this->redis->setEx($key, 3600, \serialize($parameters)); + $deleteAt = $this->clock->now()->add(new \DateInterval('P6M')); + $options = $this->exportConfigNormalizer->normalizeConfig($alias, $dataToNormalize); + $exportGeneration = match (null === $savedExport) { + true => new ExportGeneration($alias, $options, $deleteAt), + false => ExportGeneration::fromSavedExport($savedExport, $deleteAt, $options), + }; + + $this->entityManager->persist($exportGeneration); + $this->entityManager->flush(); + $this->messageBus->dispatch(new ExportRequestGenerationMessage($exportGeneration, $user)); // remove data from session - $this->requestStack->getSession()->remove('export_step_raw'); - $this->requestStack->getSession()->remove('export_step'); - $this->requestStack->getSession()->remove('formatter_step_raw'); - $this->requestStack->getSession()->remove('formatter_step'); + $this->session->remove('centers_step_raw'); + $this->session->remove('export_step_raw'); + $this->session->remove('export_step'); + $this->session->remove('formatter_step_raw'); + $this->session->remove('formatter_step'); - return $this->redirectToRoute('chill_main_export_download', [ - 'key' => $key, - 'alias' => $alias, - 'from_saved' => $savedExport?->getId(), - ]); + return $this->redirectToRoute('chill_main_export-generation_wait', ['id' => $exportGeneration->getId()]); } - private function rebuildData($key, ?SavedExport $savedExport) + /** + * Build the export form data into a way suitable for normalization. + * + * @param string $alias the export alias + * @param array $dataCenters Raw data from center step + * @param array $dataExport Raw data from export step + * @param array $dataFormatter Raw data from formatter step + */ + private function buildExportDataForNormalization(string $alias, ?array $dataCenters, array $dataExport, array $dataFormatter, ?SavedExport $savedExport): array { - $rawData = $this->rebuildRawData($key); - - $alias = $rawData['alias']; - if ($this->filterStatsByCenters) { - $formCenters = $this->createCreateFormExport($alias, 'generate_centers', [], $savedExport); - $formCenters->submit($rawData['centers']); - $dataCenters = $formCenters->getData(); + $formCenters = $this->createCreateFormExport($alias, 'generate_centers', [], null); + $formCenters->submit($dataCenters); + $dataAsCollection = $formCenters->getData()['centers']; + $centers = $dataAsCollection['centers']; + $regroupments = $dataAsCollection['regroupments'] ?? []; + $dataCenters = [ + 'centers' => $centers instanceof Collection ? $centers->toArray() : $centers, + 'regroupments' => $regroupments instanceof Collection ? $regroupments->toArray() : $regroupments, + ]; } else { - $dataCenters = ['centers' => []]; + $dataCenters = ['centers' => [], 'regroupments' => []]; } $formExport = $this->createCreateFormExport($alias, 'generate_export', $dataCenters, $savedExport); - $formExport->submit($rawData['export']); + $formExport->submit($dataExport); $dataExport = $formExport->getData(); - if (\count($rawData['formatter']) > 0) { + if (\count($dataFormatter) > 0) { $formFormatter = $this->createCreateFormExport( $alias, 'generate_formatter', $dataExport, $savedExport ); - $formFormatter->submit($rawData['formatter']); + $formFormatter->submit($dataFormatter); $dataFormatter = $formFormatter->getData(); } - return [$dataCenters, $dataExport, $dataFormatter ?? null]; + return [ + 'centers' => ['centers' => $dataCenters['centers'], 'regroupments' => $dataCenters['regroupments']], + 'export' => $dataExport['export']['export'] ?? [], + 'filters' => $dataExport['export']['filters'] ?? [], + 'aggregators' => $dataExport['export']['aggregators'] ?? [], + 'pick_formatter' => $dataExport['export']['pick_formatter']['alias'], + 'formatter' => $dataFormatter['formatter'] ?? [], + ]; } /** @@ -508,7 +391,7 @@ class ExportController extends AbstractController * * @return Response */ - private function selectCentersStep(Request $request, DirectExportInterface|ExportInterface $export, $alias, ?SavedExport $savedExport = null): RedirectResponse|Response + private function selectCentersStep(Request $request, DirectExportInterface|ExportInterface $export, $alias, ExportGeneration|SavedExport|null $savedExport = null) { if (!$this->filterStatsByCenters) { return $this->redirectToRoute('chill_main_export_new', [ @@ -521,7 +404,12 @@ class ExportController extends AbstractController /** @var ExportManager $exportManager */ $exportManager = $this->exportManager; - $form = $this->createCreateFormExport($alias, 'centers', [], $savedExport); + $form = $this->createCreateFormExport( + $alias, + 'centers', + $this->exportFormHelper->getDefaultData('centers', $export, []), + $savedExport + ); if (Request::METHOD_POST === $request->getMethod()) { $form->handleRequest($request); @@ -537,17 +425,17 @@ class ExportController extends AbstractController false === $exportManager->isGrantedForElement( $export, null, - $exportManager->getPickedCenters($data['centers']) + $this->exportFormHelper->getPickedCenters($data['centers']), ) ) { throw $this->createAccessDeniedException('you do not have access to this export for those centers'); } - $this->requestStack->getSession()->set( + $this->session->set( 'centers_step_raw', $request->request->all() ); - $this->requestStack->getSession()->set('centers_step', $data); + $this->session->set('centers_step', $data['centers']); return $this->redirectToRoute('chill_main_export_new', [ 'step' => $this->getNextStep('centers', $export), @@ -560,7 +448,7 @@ class ExportController extends AbstractController return $this->render( '@ChillMain/Export/new_centers_step.html.twig', [ - 'form' => $form, + 'form' => $form->createView(), 'export' => $export, 'export_group' => $this->getExportGroup($export), ] @@ -631,43 +519,15 @@ class ExportController extends AbstractController } } - private function rebuildRawData(?string $key): array - { - if (null === $key) { - throw $this->createNotFoundException('key does not exists'); - } - - if (1 !== $this->redis->exists($key)) { - $this->addFlash('error', $this->translator->trans('This report is not available any more')); - - throw $this->createNotFoundException('key does not exists'); - } - - $serialized = $this->redis->get($key); - - if (false === $serialized) { - throw new \LogicException('the key could not be reached from redis'); - } - - $rawData = \unserialize($serialized); - - $this->logger->notice('[export] choices for an export unserialized', [ - 'key' => $key, - 'rawData' => json_encode($rawData, JSON_THROW_ON_ERROR), - ]); - - return $rawData; - } - - private function getSavedExportFromRequest(Request $request): ?SavedExport + private function getSavedExportFromRequest(Request $request): SavedExport|ExportGeneration|null { $savedExport = match ($savedExportId = $request->query->get('from_saved', '')) { '' => null, - default => $this->savedExportRepository->find($savedExportId), + default => $this->savedExportOrExportGenerationRepository->findById($savedExportId), }; - if (null !== $savedExport && !$this->security->isGranted(SavedExportVoter::EDIT, $savedExport)) { - throw new AccessDeniedHttpException('saved export edition not allowed'); + if (null !== $savedExport && !$this->security->isGranted(SavedExportVoter::GENERATE, $savedExport)) { + throw new AccessDeniedHttpException('saved export generation not allowed'); } return $savedExport; diff --git a/src/Bundle/ChillMainBundle/Controller/ExportGenerationController.php b/src/Bundle/ChillMainBundle/Controller/ExportGenerationController.php new file mode 100644 index 000000000..0e1b3b8c4 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/ExportGenerationController.php @@ -0,0 +1,64 @@ +security->isGranted('ROLE_USER')) { + throw new AccessDeniedHttpException('Only users can download an export'); + } + + $export = $this->exportManager->getExport($exportGeneration->getExportAlias()); + + return new Response( + $this->twig->render('@ChillMain/ExportGeneration/wait.html.twig', ['exportGeneration' => $exportGeneration, 'export' => $export]), + ); + } + + #[Route('/api/1.0/main/export-generation/{id}/object', methods: ['GET'])] + public function objectStatus(ExportGeneration $exportGeneration): JsonResponse + { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedHttpException('Only users can download an export'); + } + + return new JsonResponse( + $this->serializer->serialize( + $exportGeneration, + 'json', + [AbstractNormalizer::GROUPS => ['read']], + ), + json: true, + ); + } +} diff --git a/src/Bundle/ChillMainBundle/Controller/ExportGenerationCreateFromSavedExportController.php b/src/Bundle/ChillMainBundle/Controller/ExportGenerationCreateFromSavedExportController.php new file mode 100644 index 000000000..199b9c92d --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/ExportGenerationCreateFromSavedExportController.php @@ -0,0 +1,62 @@ +security->isGranted(SavedExportVoter::GENERATE, $export)) { + throw new AccessDeniedHttpException('Not allowed to generate an export from this saved export'); + } + $user = $this->security->getUser(); + + if (!$user instanceof User) { + throw new AccessDeniedHttpException('Only users can create exports'); + } + + $exportGeneration = ExportGeneration::fromSavedExport($export, $this->clock->now()->add(new \DateInterval('P6M'))); + + $this->entityManager->persist($exportGeneration); + $this->entityManager->flush(); + + $this->messageBus->dispatch(new ExportRequestGenerationMessage($exportGeneration, $user)); + + return new JsonResponse( + $this->serializer->serialize($exportGeneration, 'json', ['groups' => ['read']]), + json: true, + ); + } +} diff --git a/src/Bundle/ChillMainBundle/Controller/ExportIndexController.php b/src/Bundle/ChillMainBundle/Controller/ExportIndexController.php new file mode 100644 index 000000000..4f3de7aef --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/ExportIndexController.php @@ -0,0 +1,62 @@ +security->getUser(); + if (!$user instanceof User) { + throw new AccessDeniedHttpException('Only regular user can see this page'); + } + + if (!$this->security->isGranted(ChillExportVoter::COMPOSE_EXPORT)) { + throw new AccessDeniedHttpException(sprintf('Require the %s role', ChillExportVoter::COMPOSE_EXPORT)); + } + + $exports = $this->exportManager->getExportsGrouped(true); + + $lastExecutions = []; + foreach ($this->exportManager->getExports() as $alias => $export) { + $lastExecutions[$alias] = $this->exportGenerationRepository->findExportGenerationByAliasAndUser($alias, $user, 5); + } + + return new Response( + $this->twig->render('@ChillMain/Export/layout.html.twig', [ + 'grouped_exports' => $exports, + 'last_executions' => $lastExecutions, + ]), + ); + } +} diff --git a/src/Bundle/ChillMainBundle/Controller/NotificationController.php b/src/Bundle/ChillMainBundle/Controller/NotificationController.php index 10fd32715..9e0e1d09e 100644 --- a/src/Bundle/ChillMainBundle/Controller/NotificationController.php +++ b/src/Bundle/ChillMainBundle/Controller/NotificationController.php @@ -16,6 +16,7 @@ use Chill\MainBundle\Entity\NotificationComment; use Chill\MainBundle\Form\NotificationCommentType; use Chill\MainBundle\Form\NotificationType; use Chill\MainBundle\Notification\Exception\NotificationHandlerNotFound; +use Chill\MainBundle\Notification\FlagProviders\NotificationByUserFlagProvider; use Chill\MainBundle\Notification\NotificationHandlerManager; use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Repository\NotificationRepository; @@ -58,7 +59,8 @@ class NotificationController extends AbstractController $notification ->setRelatedEntityClass($request->query->get('entityClass')) ->setRelatedEntityId($request->query->getInt('entityId')) - ->setSender($this->security->getUser()); + ->setSender($this->security->getUser()) + ->setType(NotificationByUserFlagProvider::FLAG); $tos = $request->query->all('tos'); diff --git a/src/Bundle/ChillMainBundle/Controller/SavedExportController.php b/src/Bundle/ChillMainBundle/Controller/SavedExportController.php index f8f92f181..cdaea5daf 100644 --- a/src/Bundle/ChillMainBundle/Controller/SavedExportController.php +++ b/src/Bundle/ChillMainBundle/Controller/SavedExportController.php @@ -11,13 +11,13 @@ declare(strict_types=1); namespace Chill\MainBundle\Controller; +use Chill\MainBundle\Entity\ExportGeneration; use Chill\MainBundle\Entity\SavedExport; use Chill\MainBundle\Entity\User; -use Chill\MainBundle\Export\ExportInterface; +use Chill\MainBundle\Export\ExportDescriptionHelper; use Chill\MainBundle\Export\ExportManager; -use Chill\MainBundle\Export\GroupedExportInterface; use Chill\MainBundle\Form\SavedExportType; -use Chill\MainBundle\Repository\SavedExportRepositoryInterface; +use Chill\MainBundle\Security\Authorization\ExportGenerationVoter; use Chill\MainBundle\Security\Authorization\SavedExportVoter; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bridge\Doctrine\Attribute\MapEntity; @@ -27,15 +27,28 @@ use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\Translation\TranslatableMessage; +use Symfony\Contracts\Translation\TranslatableInterface; use Symfony\Contracts\Translation\TranslatorInterface; -class SavedExportController +final readonly class SavedExportController { - public function __construct(private readonly \Twig\Environment $templating, private readonly EntityManagerInterface $entityManager, private readonly ExportManager $exportManager, private readonly FormFactoryInterface $formFactory, private readonly SavedExportRepositoryInterface $savedExportRepository, private readonly Security $security, private readonly RequestStack $requestStack, private readonly TranslatorInterface $translator, private readonly UrlGeneratorInterface $urlGenerator) {} + public function __construct( + private \Twig\Environment $templating, + private EntityManagerInterface $entityManager, + private ExportManager $exportManager, + private FormFactoryInterface $formFactory, + private Security $security, + private TranslatorInterface $translator, + private UrlGeneratorInterface $urlGenerator, + private ExportDescriptionHelper $exportDescriptionHelper, + private RequestStack $requestStack, + ) {} #[\Symfony\Component\Routing\Attribute\Route(path: '/{_locale}/exports/saved/{id}/delete', name: 'chill_main_export_saved_delete')] public function delete(#[MapEntity(id: 'id')] SavedExport $savedExport, Request $request): Response @@ -52,6 +65,10 @@ class SavedExportController $this->entityManager->remove($savedExport); $this->entityManager->flush(); + $session = $this->requestStack->getSession(); + if ($session instanceof Session) { + $session->getFlashBag()->add('success', new TranslatableMessage('saved_export.Export is deleted')); + } $this->requestStack->getSession()->getFlashBag()->add('success', $this->translator->trans('saved_export.Export is deleted')); return new RedirectResponse( @@ -70,6 +87,105 @@ class SavedExportController ); } + #[Route(path: '/exports/saved/create-from-export-generation/{id}/new', name: 'chill_main_export_saved_create_from_export_generation')] + public function createFromExportGeneration(ExportGeneration $exportGeneration, Request $request): Response + { + if (!$this->security->isGranted(ExportGenerationVoter::VIEW, $exportGeneration)) { + throw new AccessDeniedHttpException(); + } + + $user = $this->security->getUser(); + if (!$user instanceof User) { + throw new AccessDeniedHttpException('only regular user can create a saved export'); + } + + $export = $this->exportManager->getExport($exportGeneration->getExportAlias()); + $title = $export->getTitle() instanceof TranslatableInterface ? $export->getTitle()->trans($this->translator) : + $this->translator->trans($export->getTitle()); + + $savedExport = new SavedExport(); + $savedExport + ->setExportAlias($exportGeneration->getExportAlias()) + ->setUser($user) + ->setOptions($exportGeneration->getOptions()) + ->setTitle( + $request->query->has('title') ? $request->query->get('title') : $title + ); + + if ($exportGeneration->isLinkedToSavedExport()) { + $savedExport->setDescription($exportGeneration->getSavedExport()->getDescription()); + } else { + $savedExport->setDescription( + implode( + "\n", + array_map( + fn (string $item) => '- '.$item."\n", + $this->exportDescriptionHelper->describe($savedExport->getExportAlias(), $savedExport->getOptions(), includeExportTitle: false) + ) + ) + ); + } + + return $this->handleEdit($savedExport, $request, true); + } + + #[Route(path: '/exports/saved/duplicate-from-saved-export/{id}/new', name: 'chill_main_export_saved_duplicate')] + public function duplicate(SavedExport $previousSavedExport, Request $request): Response + { + $user = $this->security->getUser(); + if (!$user instanceof User) { + throw new AccessDeniedHttpException('only regular user can create a saved export'); + } + + if (!$this->security->isGranted(SavedExportVoter::EDIT, $previousSavedExport)) { + throw new AccessDeniedHttpException('Not allowed to edit this saved export'); + } + + $savedExport = new SavedExport(); + $savedExport + ->setExportAlias($previousSavedExport->getExportAlias()) + ->setUser($user) + ->setOptions($previousSavedExport->getOptions()) + ->setDescription($previousSavedExport->getDescription()) + ->setTitle( + $request->query->has('title') ? + $request->query->get('title') : + $previousSavedExport->getTitle().' ('.$this->translator->trans('saved_export.Duplicated').' '.(new \DateTimeImmutable('now'))->format('d-m-Y H:i:s').')' + ); + + return $this->handleEdit($savedExport, $request); + + } + + private function handleEdit(SavedExport $savedExport, Request $request, bool $showWarningAutoGeneratedDescription = false): Response + { + $form = $this->formFactory->create(SavedExportType::class, $savedExport); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->entityManager->persist($savedExport); + $this->entityManager->flush(); + + if (($session = $request->getSession()) instanceof Session) { + $session->getFlashBag()->add('success', new TranslatableMessage('saved_export.Saved export is saved!')); + } + + return new RedirectResponse( + $this->urlGenerator->generate('chill_main_export_saved_list_my'), + ); + } + + return new Response( + $this->templating->render( + '@ChillMain/SavedExport/new.html.twig', + [ + 'form' => $form->createView(), + 'showWarningAutoGeneratedDescription' => $showWarningAutoGeneratedDescription, + ], + ), + ); + } + #[\Symfony\Component\Routing\Attribute\Route(path: '/{_locale}/exports/saved/{id}/edit', name: 'chill_main_export_saved_edit')] public function edit(#[MapEntity(id: 'id')] SavedExport $savedExport, Request $request): Response { @@ -87,7 +203,7 @@ class SavedExportController $this->requestStack->getSession()->getFlashBag()->add('success', $this->translator->trans('saved_export.Saved export is saved!')); return new RedirectResponse( - $this->urlGenerator->generate('chill_main_export_saved_list_my') + $this->urlGenerator->generate('chill_main_export_saved_list_my'), ); } @@ -96,45 +212,37 @@ class SavedExportController '@ChillMain/SavedExport/edit.html.twig', [ 'form' => $form->createView(), - ] - ) + ], + ), ); } - #[\Symfony\Component\Routing\Attribute\Route(path: '/{_locale}/exports/saved/my', name: 'chill_main_export_saved_list_my')] - public function list(): Response + #[\Symfony\Component\Routing\Attribute\Route(path: '/{_locale}/exports/saved/{savedExport}/edit-options/{exportGeneration}', name: 'chill_main_export_saved_options_edit')] + public function updateOptionsFromGeneration(SavedExport $savedExport, ExportGeneration $exportGeneration, Request $request): Response { - $user = $this->security->getUser(); - - if (!$this->security->isGranted('ROLE_USER') || !$user instanceof User) { - throw new AccessDeniedHttpException(); + if (!$this->security->isGranted(SavedExportVoter::DUPLICATE, $savedExport)) { + throw new AccessDeniedHttpException('You are not allowed to access this saved export'); } - $exports = $this->savedExportRepository->findByUser($user, ['title' => 'ASC']); - - // group by center - /** @var array $exportsGrouped */ - $exportsGrouped = []; - - foreach ($exports as $savedExport) { - $export = $this->exportManager->getExport($savedExport->getExportAlias()); - - $exportsGrouped[ - $export instanceof GroupedExportInterface - ? $this->translator->trans($export->getGroup()) : '_' - ][] = ['saved' => $savedExport, 'export' => $export]; + if (!$this->security->isGranted(ExportGenerationVoter::VIEW, $exportGeneration)) { + throw new AccessDeniedHttpException('You are not allowed to access this export generation'); } - ksort($exportsGrouped); + if ($savedExport->getExportAlias() !== $exportGeneration->getExportAlias()) { + throw new UnprocessableEntityHttpException('export alias does not match'); + } - return new Response( - $this->templating->render( - '@ChillMain/SavedExport/index.html.twig', - [ - 'grouped_exports' => $exportsGrouped, - 'total' => \count($exports), - ] - ) + $savedExport->setOptions($exportGeneration->getOptions()); + + $this->entityManager->flush(); + + $session = $request->getSession(); + if ($session instanceof Session) { + $session->getFlashBag()->add('success', new TranslatableMessage('saved_export.Options updated successfully')); + } + + return new RedirectResponse( + $this->urlGenerator->generate('chill_main_export_saved_edit', ['id' => $savedExport->getId()]), ); } } diff --git a/src/Bundle/ChillMainBundle/Controller/SavedExportIndexController.php b/src/Bundle/ChillMainBundle/Controller/SavedExportIndexController.php new file mode 100644 index 000000000..82e9913da --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/SavedExportIndexController.php @@ -0,0 +1,104 @@ +security->getUser(); + + if (!$this->security->isGranted(ChillExportVoter::GENERATE_SAVED_EXPORT) || !$user instanceof User) { + throw new AccessDeniedHttpException(sprintf('Missing role: %s', ChillExportVoter::GENERATE_SAVED_EXPORT)); + } + + $filter = $this->buildFilter(); + + $filterParams = []; + if ('' !== $filter->getQueryString() && null !== $filter->getQueryString()) { + $filterParams[SavedExportRepositoryInterface::FILTER_DESCRIPTION | SavedExportRepositoryInterface::FILTER_TITLE] = $filter->getQueryString(); + } + + $exports = array_filter( + $this->savedExportRepository->findSharedWithUser($user, ['exportAlias' => 'ASC', 'title' => 'ASC'], filters: $filterParams), + fn (SavedExport $savedExport): bool => $this->security->isGranted(SavedExportVoter::GENERATE, $savedExport), + ); + + // group by center + /** @var array $exportsGrouped */ + $exportsGrouped = []; + + foreach ($exports as $savedExport) { + $export = $this->exportManager->getExport($savedExport->getExportAlias()); + + $exportsGrouped[$export instanceof GroupedExportInterface + ? $this->translator->trans($export->getGroup()) : '_'][] = ['saved' => $savedExport, 'export' => $export]; + } + + ksort($exportsGrouped); + + // get last executions + $lastExecutions = []; + foreach ($exports as $savedExport) { + $lastExecutions[$savedExport->getId()->toString()] = $this->exportGenerationRepository + ->findExportGenerationBySavedExportAndUser($savedExport, $user, 5); + } + + return new Response( + $this->templating->render( + '@ChillMain/SavedExport/index.html.twig', + [ + 'grouped_exports' => $exportsGrouped, + 'total' => \count($exports), + 'last_executions' => $lastExecutions, + 'filter' => $filter, + ], + ), + ); + } + + private function buildFilter(): FilterOrderHelper + { + $filter = $this->filterOrderHelperFactory->create('saved-export-index-filter'); + $filter->addSearchBox(); + + return $filter->build(); + } +} diff --git a/src/Bundle/ChillMainBundle/Controller/UserProfileController.php b/src/Bundle/ChillMainBundle/Controller/UserProfileController.php index 4ac9f0095..9a3e5dfa4 100644 --- a/src/Bundle/ChillMainBundle/Controller/UserProfileController.php +++ b/src/Bundle/ChillMainBundle/Controller/UserProfileController.php @@ -11,14 +11,11 @@ declare(strict_types=1); namespace Chill\MainBundle\Controller; -use Chill\MainBundle\Form\UserPhonenumberType; +use Chill\MainBundle\Form\UserProfileType; use Chill\MainBundle\Security\ChillSecurity; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\Form\Extension\Core\Type\SubmitType; -use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; -use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Component\Routing\Annotation\Route; @@ -41,16 +38,19 @@ final class UserProfileController extends AbstractController } $user = $this->security->getUser(); - $editForm = $this->createPhonenumberEditForm($user); + $editForm = $this->createForm(UserProfileType::class, $user); + + $editForm->get('notificationFlags')->setData($user->getNotificationFlags()); + $editForm->handleRequest($request); if ($editForm->isSubmitted() && $editForm->isValid()) { - $phonenumber = $editForm->get('phonenumber')->getData(); + $notificationFlagsData = $editForm->get('notificationFlags')->getData(); + $user->setNotificationFlags($notificationFlagsData); - $user->setPhonenumber($phonenumber); - - $this->managerRegistry->getManager()->flush(); - $this->addFlash('success', $this->translator->trans('user.profile.Phonenumber successfully updated!')); + $em = $this->managerRegistry->getManager(); + $em->flush(); + $this->addFlash('success', $this->translator->trans('user.profile.Profile successfully updated!')); return $this->redirectToRoute('chill_main_user_profile'); } @@ -60,13 +60,4 @@ final class UserProfileController extends AbstractController 'form' => $editForm, ]); } - - private function createPhonenumberEditForm(UserInterface $user): FormInterface - { - return $this->createForm( - UserPhonenumberType::class, - $user, - ) - ->add('submit', SubmitType::class, ['label' => $this->translator->trans('Save')]); - } } diff --git a/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadUsers.php b/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadUsers.php index 09de1eb20..f1479d360 100644 --- a/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadUsers.php +++ b/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadUsers.php @@ -11,7 +11,10 @@ declare(strict_types=1); namespace Chill\MainBundle\DataFixtures\ORM; +use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\GroupCenter; +use Chill\MainBundle\Entity\PermissionsGroup; +use Chill\MainBundle\Entity\RoleScope; use Chill\MainBundle\Entity\User; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Common\DataFixtures\OrderedFixtureInterface; @@ -57,6 +60,15 @@ class LoadUsers extends Fixture implements OrderedFixtureInterface public function load(ObjectManager $manager): void { + $roleScope = new RoleScope(); + $roleScope->setRole('CHILL_MAIN_COMPOSE_EXPORT'); + $permissionGroup = new PermissionsGroup(); + $permissionGroup->setName('export'); + $permissionGroup->addRoleScope($roleScope); + + $manager->persist($roleScope); + $manager->persist($permissionGroup); + foreach (self::$refs as $username => $params) { $user = new User(); @@ -76,7 +88,14 @@ class LoadUsers extends Fixture implements OrderedFixtureInterface ->setEmail(sprintf('%s@chill.social', \str_replace(' ', '', (string) $username))); foreach ($params['groupCenterRefs'] as $groupCenterRef) { - $user->addGroupCenter($this->getReference($groupCenterRef, GroupCenter::class)); + $user->addGroupCenter($gc = $this->getReference($groupCenterRef, GroupCenter::class)); + + $exportGroupCenter = new GroupCenter(); + $exportGroupCenter->setPermissionsGroup($permissionGroup); + $exportGroupCenter->setCenter($gc->getCenter()); + $manager->persist($exportGroupCenter); + + $user->addGroupCenter($exportGroupCenter); } echo 'Creating user '.$username."... \n"; diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index 4c5297f8d..21b450540 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -78,6 +78,7 @@ use Chill\MainBundle\Form\RegroupmentType; use Chill\MainBundle\Form\UserGroupType; use Chill\MainBundle\Form\UserJobType; use Chill\MainBundle\Form\UserType; +use Chill\MainBundle\Security\Authorization\ChillExportVoter; use Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType; use Ramsey\Uuid\Doctrine\UuidType; use Symfony\Component\Config\FileLocator; @@ -332,6 +333,9 @@ class ChillMainExtension extends Extension implements 'strategy' => 'unanimous', 'allow_if_all_abstain' => false, ], + 'role_hierarchy' => [ + ChillExportVoter::COMPOSE_EXPORT => ChillExportVoter::GENERATE_SAVED_EXPORT, + ], ]); // add crud api diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/ExportsCompilerPass.php b/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/ExportsCompilerPass.php deleted file mode 100644 index d8da384c8..000000000 --- a/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/ExportsCompilerPass.php +++ /dev/null @@ -1,102 +0,0 @@ -has(ExportManager::class)) { - throw new \LogicException('service '.ExportManager::class.' is not defined. It is required by ExportsCompilerPass'); - } - - $chillManagerDefinition = $container->findDefinition( - ExportManager::class - ); - - $this->compileFormatters($chillManagerDefinition, $container); - $this->compileExportElementsProvider($chillManagerDefinition, $container); - } - - private function compileExportElementsProvider( - Definition $chillManagerDefinition, - ContainerBuilder $container, - ): void { - $taggedServices = $container->findTaggedServiceIds( - 'chill.export_elements_provider' - ); - - $knownAliases = []; - - foreach ($taggedServices as $id => $tagAttributes) { - foreach ($tagAttributes as $attributes) { - if (!isset($attributes['prefix'])) { - throw new \LogicException("the 'prefix' attribute is missing in your service '{$id}' definition"); - } - - if (array_search($attributes['prefix'], $knownAliases, true)) { - throw new \LogicException('There is already a chill.export_elements_provider service with prefix '.$attributes['prefix'].'. Choose another prefix.'); - } - $knownAliases[] = $attributes['prefix']; - - $chillManagerDefinition->addMethodCall( - 'addExportElementsProvider', - [new Reference($id), $attributes['prefix']] - ); - } - } - } - - private function compileFormatters( - Definition $chillManagerDefinition, - ContainerBuilder $container, - ): void { - $taggedServices = $container->findTaggedServiceIds( - 'chill.export_formatter' - ); - - $knownAliases = []; - - foreach ($taggedServices as $id => $tagAttributes) { - foreach ($tagAttributes as $attributes) { - if (!isset($attributes['alias'])) { - throw new \LogicException("the 'alias' attribute is missing in your service '{$id}' definition"); - } - - if (array_search($attributes['alias'], $knownAliases, true)) { - throw new \LogicException('There is already a chill.export_formatter service with alias '.$attributes['alias'].'. Choose another alias.'); - } - $knownAliases[] = $attributes['alias']; - - $chillManagerDefinition->addMethodCall( - 'addFormatter', - [new Reference($id), $attributes['alias']] - ); - } - } - } -} diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/Unaccent.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/Unaccent.php index eacb0e3e7..3e1086409 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/Unaccent.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/Unaccent.php @@ -23,7 +23,7 @@ class Unaccent extends FunctionNode { private ?\Doctrine\ORM\Query\AST\Node $string = null; - public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) + public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker): string { return 'UNACCENT('.$this->string->dispatch($sqlWalker).')'; } diff --git a/src/Bundle/ChillMainBundle/Entity/Embeddable/PrivateCommentEmbeddable.php b/src/Bundle/ChillMainBundle/Entity/Embeddable/PrivateCommentEmbeddable.php index 0c2af22f7..5ada822c4 100644 --- a/src/Bundle/ChillMainBundle/Entity/Embeddable/PrivateCommentEmbeddable.php +++ b/src/Bundle/ChillMainBundle/Entity/Embeddable/PrivateCommentEmbeddable.php @@ -63,4 +63,28 @@ class PrivateCommentEmbeddable return $this; } + + /** + * Merges comments from the provided object into the current object. + * + * Identifies common user IDs between the current object's comments and the + * newComment's comments. If a user ID exists in both, their comments are + * concatenated with the provided separator. If a user ID exists only in the + * newComment, their comment is added to the current object directly. + * + * @param self $commentsToAppend the object containing the new comments to be merged + * @param string $separator the string used to separate concatenated comments + */ + public function concatenateComments(self $commentsToAppend, string $separator = "\n\n-----------\n\n"): void + { + $commonUserIds = array_intersect(array_keys($this->comments), array_keys($commentsToAppend->getComments())); + + foreach ($commentsToAppend->getComments() as $userId => $comment) { + if (in_array($userId, $commonUserIds, true)) { + $this->comments[$userId] = $this->comments[$userId].$separator.$commentsToAppend->getComments()[$userId]; + } else { + $this->comments[$userId] = $commentsToAppend->getComments()[$userId]; + } + } + } } diff --git a/src/Bundle/ChillMainBundle/Entity/ExportGeneration.php b/src/Bundle/ChillMainBundle/Entity/ExportGeneration.php new file mode 100644 index 000000000..4cfbda0f6 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Entity/ExportGeneration.php @@ -0,0 +1,150 @@ + ExportGeneration::class])] +class ExportGeneration implements TrackCreationInterface +{ + use TrackCreationTrait; + + #[ORM\Id] + #[ORM\Column(type: 'uuid', unique: true)] + #[Serializer\Groups(['read'])] + private UuidInterface $id; + + + #[ORM\ManyToOne(targetEntity: StoredObject::class, cascade: ['persist', 'refresh'])] + #[ORM\JoinColumn(nullable: false)] + #[Serializer\Groups(['read'])] + private StoredObject $storedObject; + + public function __construct( + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])] + #[Serializer\Groups(['read'])] + private string $exportAlias, + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])] + private array $options = [], + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true)] + private ?\DateTimeImmutable $deleteAt = null, + + /** + * The related saved export. + * + * Note that, in some case, the options of this ExportGeneration are not equals to the options of the saved export. + * This happens when the options of the saved export are updated. + */ + #[ORM\ManyToOne(targetEntity: SavedExport::class)] + #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')] + private ?SavedExport $savedExport = null, + ) { + $this->id = Uuid::uuid4(); + $this->storedObject = new StoredObject(StoredObject::STATUS_PENDING); + } + + public function setDeleteAt(?\DateTimeImmutable $deleteAt): ExportGeneration + { + $this->deleteAt = $deleteAt; + + return $this; + } + + public function getDeleteAt(): ?\DateTimeImmutable + { + return $this->deleteAt; + } + + public function getId(): UuidInterface + { + return $this->id; + } + + public function getStoredObject(): StoredObject + { + return $this->storedObject; + } + + public function getExportAlias(): string + { + return $this->exportAlias; + } + + public function getOptions(): array + { + return $this->options; + } + + public function getSavedExport(): ?SavedExport + { + return $this->savedExport; + } + + #[Serializer\Groups(['read'])] + #[Serializer\SerializedName('status')] + public function getStatus(): string + { + return $this->getStoredObject()->getStatus(); + } + + public function setSavedExport(SavedExport $savedExport): self + { + $this->savedExport = $savedExport; + + return $this; + } + + public function isLinkedToSavedExport(): bool + { + return null !== $this->savedExport; + } + + /** + * Compares the options of the saved export and the current export generation. + * + * Return false if the current export generation's options are not equal to the one in the saved export. This may + * happens when we update the configuration of a saved export. + */ + public function isConfigurationDifferentFromSavedExport(): bool + { + if (!$this->isLinkedToSavedExport()) { + return false; + } + + return $this->savedExport->getOptions() !== $this->getOptions(); + } + + public static function fromSavedExport(SavedExport $savedExport, ?\DateTimeImmutable $deletedAt = null, ?array $overrideOptions = null): self + { + $generation = new self($savedExport->getExportAlias(), $overrideOptions ?? $savedExport->getOptions(), $deletedAt, $savedExport); + $generation->getStoredObject()->setTitle($savedExport->getTitle()); + + return $generation; + } +} diff --git a/src/Bundle/ChillMainBundle/Entity/GeographicalUnit/SimpleGeographicalUnitDTO.php b/src/Bundle/ChillMainBundle/Entity/GeographicalUnit/SimpleGeographicalUnitDTO.php index 481b530d9..2d7028d21 100644 --- a/src/Bundle/ChillMainBundle/Entity/GeographicalUnit/SimpleGeographicalUnitDTO.php +++ b/src/Bundle/ChillMainBundle/Entity/GeographicalUnit/SimpleGeographicalUnitDTO.php @@ -50,4 +50,9 @@ class SimpleGeographicalUnitDTO #[Serializer\Groups(['read'])] public int $layerId, ) {} + + public function getId(): int + { + return $this->id; + } } diff --git a/src/Bundle/ChillMainBundle/Entity/Notification.php b/src/Bundle/ChillMainBundle/Entity/Notification.php index 321bfd65a..19bf300f9 100644 --- a/src/Bundle/ChillMainBundle/Entity/Notification.php +++ b/src/Bundle/ChillMainBundle/Entity/Notification.php @@ -15,6 +15,7 @@ use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface; use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Context\ExecutionContextInterface; @@ -22,10 +23,11 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; #[ORM\Entity] #[ORM\HasLifecycleCallbacks] #[ORM\Table(name: 'chill_main_notification')] -#[ORM\Index(name: 'chill_main_notification_related_entity_idx', columns: ['relatedentityclass', 'relatedentityid'])] +#[ORM\Index(columns: ['relatedentityclass', 'relatedentityid'], name: 'chill_main_notification_related_entity_idx')] class Notification implements TrackUpdateInterface { use TrackUpdateTrait; + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false)] private string $accessKey; @@ -38,12 +40,19 @@ class Notification implements TrackUpdateInterface #[ORM\JoinTable(name: 'chill_main_notification_addresses_user')] private Collection $addressees; + /** + * @var Collection + */ + #[ORM\ManyToMany(targetEntity: UserGroup::class)] + #[ORM\JoinTable(name: 'chill_main_notification_addressee_user_group')] + private Collection $addresseeUserGroups; + /** * a list of destinee which will receive notifications. * * @var array|string[] */ - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, options: ['default' => '[]', 'jsonb' => true])] + #[ORM\Column(type: Types::JSON, options: ['default' => '[]', 'jsonb' => true])] private array $addressesEmails = []; /** @@ -62,21 +71,21 @@ class Notification implements TrackUpdateInterface #[ORM\OrderBy(['createdAt' => \Doctrine\Common\Collections\Criteria::ASC])] private Collection $comments; - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE)] + #[ORM\Column(type: Types::DATETIME_IMMUTABLE)] private \DateTimeImmutable $date; #[ORM\Id] #[ORM\GeneratedValue] - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)] + #[ORM\Column(type: Types::INTEGER)] private ?int $id = null; - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)] + #[ORM\Column(type: Types::TEXT)] private string $message = ''; - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)] + #[ORM\Column(type: Types::STRING, length: 255)] private string $relatedEntityClass = ''; - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)] + #[ORM\Column(type: Types::INTEGER)] private int $relatedEntityId; private array $removedAddresses = []; @@ -86,7 +95,7 @@ class Notification implements TrackUpdateInterface private ?User $sender = null; #[Assert\NotBlank(message: 'notification.Title must be defined')] - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, options: ['default' => ''])] + #[ORM\Column(type: Types::TEXT, options: ['default' => ''])] private string $title = ''; /** @@ -96,26 +105,47 @@ class Notification implements TrackUpdateInterface #[ORM\JoinTable(name: 'chill_main_notification_addresses_unread')] private Collection $unreadBy; + #[ORM\Column(type: Types::DATETIME_IMMUTABLE)] + private ?\DateTimeImmutable $updatedAt = null; + + #[ORM\ManyToOne(targetEntity: User::class)] + private ?User $updatedBy = null; + + #[ORM\Column(name: 'type', type: Types::STRING, nullable: true)] + private string $type = ''; + public function __construct() { $this->addressees = new ArrayCollection(); + $this->addresseeUserGroups = new ArrayCollection(); $this->unreadBy = new ArrayCollection(); $this->comments = new ArrayCollection(); $this->setDate(new \DateTimeImmutable()); $this->accessKey = bin2hex(openssl_random_pseudo_bytes(24)); } - public function addAddressee(User $addressee): self + public function addAddressee(User|UserGroup $addressee): self { - if (!$this->addressees->contains($addressee)) { - $this->addressees[] = $addressee; - $this->addedAddresses[] = $addressee; + if ($addressee instanceof User) { + if (!$this->addressees->contains($addressee)) { + $this->addressees->add($addressee); + $this->addedAddresses[] = $addressee; + } + + return $this; + } + + if (!$this->addresseeUserGroups->contains($addressee)) { + $this->addresseeUserGroups->add($addressee); } return $this; } - public function addAddressesEmail(string $email): void + /** + * @deprecated + */ + public function addAddressesEmail(string $email) { if (!\in_array($email, $this->addressesEmails, true)) { $this->addressesEmails[] = $email; @@ -148,13 +178,23 @@ class Notification implements TrackUpdateInterface #[Assert\Callback] public function assertCountAddresses(ExecutionContextInterface $context, $payload): void { - if (0 === (\count($this->getAddressesEmails()) + \count($this->getAddressees()))) { + if (0 === (\count($this->getAddresseeUserGroups()) + \count($this->getAddressees()))) { $context->buildViolation('notification.At least one addressee') ->atPath('addressees') ->addViolation(); } } + public function getAddresseeUserGroups(): Collection + { + return $this->addresseeUserGroups; + } + + public function setAddresseeUserGroups(Collection $addresseeUserGroups): void + { + $this->addresseeUserGroups = $addresseeUserGroups; + } + public function getAccessKey(): string { return $this->accessKey; @@ -178,6 +218,23 @@ class Notification implements TrackUpdateInterface return $this->addressees; } + public function getAllAddressees(): array + { + $allUsers = []; + + foreach ($this->getAddressees() as $user) { + $allUsers[$user->getId()] = $user; + } + + foreach ($this->getAddresseeUserGroups() as $userGroup) { + foreach ($userGroup->getUsers() as $user) { + $allUsers[$user->getId()] = $user; + } + } + + return array_values($allUsers); + } + /** * @return array|string[] */ @@ -289,12 +346,18 @@ class Notification implements TrackUpdateInterface $this->addressesOnLoad = null; } - public function removeAddressee(User $addressee): self + public function removeAddressee(User|UserGroup $addressee): self { - if ($this->addressees->removeElement($addressee)) { - $this->removedAddresses[] = $addressee; + if ($addressee instanceof User) { + if ($this->addressees->contains($addressee)) { + $this->addressees->removeElement($addressee); + + return $this; + } } + $this->addresseeUserGroups->removeElement($addressee); + return $this; } @@ -361,4 +424,30 @@ class Notification implements TrackUpdateInterface return $this; } + + public function setUpdatedAt(\DateTimeInterface $datetime): self + { + $this->updatedAt = \DateTimeImmutable::createFromInterface($datetime); + + return $this; + } + + public function setUpdatedBy(User $user): self + { + $this->updatedBy = $user; + + return $this; + } + + public function setType(string $type): self + { + $this->type = $type; + + return $this; + } + + public function getType(): string + { + return $this->type; + } } diff --git a/src/Bundle/ChillMainBundle/Entity/Regroupment.php b/src/Bundle/ChillMainBundle/Entity/Regroupment.php index 389eceb31..d28784210 100644 --- a/src/Bundle/ChillMainBundle/Entity/Regroupment.php +++ b/src/Bundle/ChillMainBundle/Entity/Regroupment.php @@ -102,4 +102,22 @@ class Regroupment return $this; } + + /** + * Return true if the given center is contained into this regroupment. + */ + public function containsCenter(Center $center): bool + { + return $this->centers->contains($center); + } + + /** + * Return true if at least one of the given centers is contained into this regroupment. + * + * @param list
$centers + */ + public function containsAtLeastOneCenter(array $centers): bool + { + return array_reduce($centers, fn (bool $carry, Center $center) => $carry || $this->containsCenter($center), false); + } } diff --git a/src/Bundle/ChillMainBundle/Entity/SavedExport.php b/src/Bundle/ChillMainBundle/Entity/SavedExport.php index eec6b83e4..7e0306a7e 100644 --- a/src/Bundle/ChillMainBundle/Entity/SavedExport.php +++ b/src/Bundle/ChillMainBundle/Entity/SavedExport.php @@ -15,6 +15,9 @@ use Chill\MainBundle\Doctrine\Model\TrackCreationInterface; use Chill\MainBundle\Doctrine\Model\TrackCreationTrait; use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface; use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\Common\Collections\ReadableCollection; use Doctrine\ORM\Mapping as ORM; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; @@ -50,9 +53,25 @@ class SavedExport implements TrackCreationInterface, TrackUpdateInterface #[ORM\ManyToOne(targetEntity: User::class)] private User $user; + /** + * @var Collection + */ + #[ORM\ManyToMany(targetEntity: User::class)] + #[ORM\JoinTable(name: 'chill_main_saved_export_users')] + private Collection $sharedWithUsers; + + /** + * @var Collection + */ + #[ORM\ManyToMany(targetEntity: UserGroup::class)] + #[ORM\JoinTable(name: 'chill_main_saved_export_usergroups')] + private Collection $sharedWithGroups; + public function __construct() { $this->id = Uuid::uuid4(); + $this->sharedWithUsers = new ArrayCollection(); + $this->sharedWithGroups = new ArrayCollection(); } public function getDescription(): string @@ -119,4 +138,71 @@ class SavedExport implements TrackCreationInterface, TrackUpdateInterface return $this; } + + public function addShare(User|UserGroup $shareUser): SavedExport + { + if ($shareUser instanceof User) { + if (!$this->sharedWithUsers->contains($shareUser)) { + $this->sharedWithUsers->add($shareUser); + } + } else { + if (!$this->sharedWithGroups->contains($shareUser)) { + $this->sharedWithGroups->add($shareUser); + } + } + + return $this; + } + + public function removeShare(User|UserGroup $shareUser): SavedExport + { + if ($shareUser instanceof User) { + $this->sharedWithUsers->removeElement($shareUser); + } else { + $this->sharedWithGroups->removeElement($shareUser); + } + + return $this; + } + + /** + * @return ReadableCollection + */ + public function getShare(): ReadableCollection + { + return new ArrayCollection([ + ...$this->sharedWithUsers->toArray(), + ...$this->sharedWithGroups->toArray(), + ]); + } + + /** + * Return true if shared with at least one user or one group. + */ + public function isShared(): bool + { + return $this->sharedWithUsers->count() > 0 || $this->sharedWithGroups->count() > 0; + } + + /** + * Determines if the user is shared with either directly or through a group. + * + * @param User $user the user to check + * + * @return bool returns true if the user is shared with directly or via group, otherwise false + */ + public function isSharedWithUser(User $user): bool + { + if ($this->sharedWithUsers->contains($user)) { + return true; + } + + foreach ($this->sharedWithGroups as $group) { + if ($group->contains($user)) { + return true; + } + } + + return false; + } } diff --git a/src/Bundle/ChillMainBundle/Entity/User.php b/src/Bundle/ChillMainBundle/Entity/User.php index b8910ef8d..9bb4202d3 100644 --- a/src/Bundle/ChillMainBundle/Entity/User.php +++ b/src/Bundle/ChillMainBundle/Entity/User.php @@ -34,6 +34,9 @@ use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint; #[ORM\Table(name: 'users')] class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInterface { + public const NOTIF_FLAG_IMMEDIATE_EMAIL = 'immediate-email'; + public const NOTIF_FLAG_DAILY_DIGEST = 'daily-digest'; + #[ORM\Id] #[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)] #[ORM\GeneratedValue(strategy: 'AUTO')] @@ -116,6 +119,12 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter #[PhonenumberConstraint] private ?PhoneNumber $phonenumber = null; + /** + * @var array> + */ + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])] + private array $notificationFlags = []; + /** * User constructor. */ @@ -613,4 +622,57 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter return $this; } + + /** + * Check if the current object is an instance of User. + * + * @return bool returns true if the current object is an instance of User, false otherwise + */ + public function isUser(): bool + { + return true; + } + + public function getNotificationFlags(): array + { + return $this->notificationFlags; + } + + public function setNotificationFlags(array $notificationFlags) + { + $this->notificationFlags = $notificationFlags; + } + + public function getNotificationFlagData(string $flag): array + { + return $this->notificationFlags[$flag] ?? []; + } + + public function setNotificationFlagData(string $flag, array $data): void + { + $this->notificationFlags[$flag] = $data; + } + + public function isNotificationSendImmediately(string $type): bool + { + if ([] === $this->getNotificationFlagData($type) || in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $this->getNotificationFlagData($type), true)) { + return true; + } + + return false; + } + + public function isNotificationDailyDigest(string $type): bool + { + if (in_array(User::NOTIF_FLAG_DAILY_DIGEST, $this->getNotificationFlagData($type), true)) { + return true; + } + + return false; + } + + public function getLocale(): string + { + return 'fr'; + } } diff --git a/src/Bundle/ChillMainBundle/Entity/UserGroup.php b/src/Bundle/ChillMainBundle/Entity/UserGroup.php index d720d4ffc..39df04b31 100644 --- a/src/Bundle/ChillMainBundle/Entity/UserGroup.php +++ b/src/Bundle/ChillMainBundle/Entity/UserGroup.php @@ -21,6 +21,19 @@ use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\DiscriminatorMap; use Symfony\Component\Validator\Constraints as Assert; +/** + * Represents a user group entity in the system. + * + * This class is used for managing user groups, including their relationships + * with users, administrative users, and additional metadata such as colors and labels. + * + * Groups may be configured to have mutual exclusion properties based on an + * exclusion key. This ensures that groups sharing the same key cannot coexist + * in certain relationship contexts. + * + * Groups may be related to a UserJob. In that case, a cronjob task ensure that the members of the groups are + * automatically synced with this group. Such groups are also automatically created by the cronjob. + */ #[ORM\Entity] #[ORM\Table(name: 'chill_main_user_group')] // this discriminator key is required for automated denormalization @@ -71,6 +84,13 @@ class UserGroup #[Assert\Email] private string $email = ''; + /** + * UserJob to which the group is related. + */ + #[ORM\ManyToOne(targetEntity: UserJob::class)] + #[ORM\JoinColumn(nullable: true)] + private ?UserJob $userJob = null; + public function __construct() { $this->adminUsers = new ArrayCollection(); @@ -209,6 +229,21 @@ class UserGroup return '' !== $this->email; } + public function hasUserJob(): bool + { + return null !== $this->userJob; + } + + public function getUserJob(): ?UserJob + { + return $this->userJob; + } + + public function setUserJob(?UserJob $userJob): void + { + $this->userJob = $userJob; + } + /** * Checks if the current object is an instance of the UserGroup class. * diff --git a/src/Bundle/ChillMainBundle/Export/AggregatorInterface.php b/src/Bundle/ChillMainBundle/Export/AggregatorInterface.php index e849dec07..27ce09746 100644 --- a/src/Bundle/ChillMainBundle/Export/AggregatorInterface.php +++ b/src/Bundle/ChillMainBundle/Export/AggregatorInterface.php @@ -12,25 +12,42 @@ declare(strict_types=1); namespace Chill\MainBundle\Export; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Contracts\Translation\TranslatableInterface; /** * Interface for Aggregators. * * Aggregators gather result of a query. Most of the time, it will add * a GROUP BY clause. + * + * @template D of array */ interface AggregatorInterface extends ModifierInterface { /** * Add a form to collect data from the user. */ - public function buildForm(FormBuilderInterface $builder); + public function buildForm(FormBuilderInterface $builder): void; /** * Get the default data, that can be use as "data" for the form. + * + * @return D */ public function getFormDefaultData(): array; + /** + * @param D $formData + */ + public function normalizeFormData(array $formData): array; + + /** + * @return D + */ + public function denormalizeFormData(array $formData, int $fromVersion): array; + + public function getNormalizationVersion(): int; + /** * get a callable which will be able to transform the results into * viewable and understable string. @@ -74,9 +91,9 @@ interface AggregatorInterface extends ModifierInterface * @param string $key The column key, as added in the query * @param mixed[] $values The values from the result. if there are duplicates, those might be given twice. Example: array('FR', 'BE', 'CZ', 'FR', 'BE', 'FR') * - * @return \Closure where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }` + * @return callable(mixed $value): (string|int|\DateTimeInterface|TranslatableInterface) where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }` */ - public function getLabels($key, array $values, mixed $data); + public function getLabels(string $key, array $values, mixed $data): callable; /** * give the list of keys the current export added to the queryBuilder in @@ -85,7 +102,9 @@ interface AggregatorInterface extends ModifierInterface * Example: if your query builder will contains `SELECT count(id) AS count_id ...`, * this function will return `array('count_id')`. * - * @param mixed[] $data the data from the export's form (added by self::buildForm) + * @param D $data the data from the export's form (added by self::buildForm) + * + * @return list */ - public function getQueryKeys($data); + public function getQueryKeys(array $data): array; } diff --git a/src/Bundle/ChillMainBundle/Export/Cronjob/RemoveExpiredExportGenerationCronJob.php b/src/Bundle/ChillMainBundle/Export/Cronjob/RemoveExpiredExportGenerationCronJob.php new file mode 100644 index 000000000..2f15bf095 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/Cronjob/RemoveExpiredExportGenerationCronJob.php @@ -0,0 +1,52 @@ +getLastStart()->getTimestamp() < $this->clock->now()->sub(new \DateInterval('PT24H'))->getTimestamp(); + } + + public function getKey(): string + { + return self::KEY; + } + + public function run(array $lastExecutionData): ?array + { + $now = $this->clock->now(); + + foreach ($this->exportGenerationRepository->findExpiredExportGeneration($now) as $exportGeneration) { + $this->messageBus->dispatch(new Envelope(new RemoveExportGenerationMessage($exportGeneration))); + } + + return ['last-deletion' => $now->getTimestamp()]; + } +} diff --git a/src/Bundle/ChillMainBundle/Export/DirectExportInterface.php b/src/Bundle/ChillMainBundle/Export/DirectExportInterface.php index 0949b0be0..0ca0b4a43 100644 --- a/src/Bundle/ChillMainBundle/Export/DirectExportInterface.php +++ b/src/Bundle/ChillMainBundle/Export/DirectExportInterface.php @@ -28,8 +28,16 @@ interface DirectExportInterface extends ExportElementInterface /** * Generate the export. + * + * @return FormattedExportGeneration */ - public function generate(array $acl, array $data = []): Response; + public function generate(array $acl, array $data, ExportGenerationContext $context): Response|FormattedExportGeneration; + + public function normalizeFormData(array $formData): array; + + public function denormalizeFormData(array $formData, int $fromVersion): array; + + public function getNormalizationVersion(): int; /** * get a description, which will be used in UI (and translated). diff --git a/src/Bundle/ChillMainBundle/Export/Exception/ExportGenerationException.php b/src/Bundle/ChillMainBundle/Export/Exception/ExportGenerationException.php new file mode 100644 index 000000000..b8f12c6bf --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/Exception/ExportGenerationException.php @@ -0,0 +1,14 @@ +, regroupments: list}, export: array{form: array, version: int}, filters: array, version: int}>, aggregators: array, version: int}>, pick_formatter: string, formatter: array{form: array, version: int}} + */ +class ExportConfigNormalizer +{ + public function __construct( + private readonly ExportManager $exportManager, + private readonly CenterRepositoryInterface $centerRepository, + private readonly RegroupmentRepositoryInterface $regroupmentRepository, + ) {} + + /** + * @return NormalizedData + */ + public function normalizeConfig(string $exportAlias, array $formData): array + { + $exportData = $formData[ExportType::EXPORT_KEY]; + $export = $this->exportManager->getExport($exportAlias); + + $serialized = [ + 'export' => [ + 'form' => $export->normalizeFormData($exportData), + 'version' => $export->getNormalizationVersion(), + ], + ]; + + $serialized['centers'] = [ + 'centers' => array_values(array_map(static fn (Center $center) => $center->getId(), $formData['centers']['centers'] ?? [])), + 'regroupments' => array_values(array_map(static fn (Regroupment $group) => $group->getId(), $formData['centers']['regroupments'] ?? [])), + ]; + + $filtersSerialized = []; + foreach ($formData[ExportType::FILTER_KEY] as $alias => $filterData) { + $filter = $this->exportManager->getFilter($alias); + $filtersSerialized[$alias][FilterType::ENABLED_FIELD] = (bool) $filterData[FilterType::ENABLED_FIELD]; + if ($filterData[FilterType::ENABLED_FIELD]) { + $filtersSerialized[$alias]['form'] = $filter->normalizeFormData($filterData['form']); + $filtersSerialized[$alias]['version'] = $filter->getNormalizationVersion(); + } + } + $serialized['filters'] = $filtersSerialized; + + $aggregatorsSerialized = []; + foreach ($formData[ExportType::AGGREGATOR_KEY] as $alias => $aggregatorData) { + $aggregator = $this->exportManager->getAggregator($alias); + $aggregatorsSerialized[$alias][FilterType::ENABLED_FIELD] = (bool) $aggregatorData[AggregatorType::ENABLED_FIELD]; + if ($aggregatorData[AggregatorType::ENABLED_FIELD]) { + $aggregatorsSerialized[$alias]['form'] = $aggregator->normalizeFormData($aggregatorData['form']); + $aggregatorsSerialized[$alias]['version'] = $aggregator->getNormalizationVersion(); + } + } + $serialized['aggregators'] = $aggregatorsSerialized; + + $serialized['pick_formatter'] = $formData['pick_formatter']; + $formatter = $this->exportManager->getFormatter($formData['pick_formatter']); + $serialized['formatter']['form'] = $formatter->normalizeFormData($formData['formatter']); + $serialized['formatter']['version'] = $formatter->getNormalizationVersion(); + + return $serialized; + } + + /** + * @param NormalizedData $serializedData + * @param bool $replaceDisabledByDefaultData if true, when a filter is not enabled, the formDefaultData is set + */ + public function denormalizeConfig(string $exportAlias, array $serializedData, bool $replaceDisabledByDefaultData = false): array + { + $export = $this->exportManager->getExport($exportAlias); + $formater = $this->exportManager->getFormatter($serializedData['pick_formatter']); + + $filtersConfig = []; + foreach ($serializedData['filters'] as $alias => $filterData) { + $aggregator = $this->exportManager->getFilter($alias); + $filtersConfig[$alias]['enabled'] = $filterData['enabled']; + + if ($filterData['enabled']) { + $filtersConfig[$alias]['form'] = $aggregator->denormalizeFormData($filterData['form'], $filterData['version']); + } elseif ($replaceDisabledByDefaultData) { + $filtersConfig[$alias]['form'] = $aggregator->getFormDefaultData(); + } + } + + $aggregatorsConfig = []; + foreach ($serializedData['aggregators'] as $alias => $aggregatorData) { + $aggregator = $this->exportManager->getAggregator($alias); + $aggregatorsConfig[$alias]['enabled'] = $aggregatorData['enabled']; + + if ($aggregatorData['enabled']) { + $aggregatorsConfig[$alias]['form'] = $aggregator->denormalizeFormData($aggregatorData['form'], $aggregatorData['version']); + } elseif ($replaceDisabledByDefaultData) { + $aggregatorsConfig[$alias]['form'] = $aggregator->getFormDefaultData(); + } + } + + return [ + 'export' => $export->denormalizeFormData($serializedData['export']['form'], $serializedData['export']['version']), + 'filters' => $filtersConfig, + 'aggregators' => $aggregatorsConfig, + 'pick_formatter' => $serializedData['pick_formatter'], + 'formatter' => $formater->denormalizeFormData($serializedData['formatter']['form'], $serializedData['formatter']['version']), + 'centers' => [ + 'centers' => array_values(array_filter(array_map(fn (int $id) => $this->centerRepository->find($id), $serializedData['centers']['centers']), fn ($item) => null !== $item)), + 'regroupments' => array_values(array_filter(array_map(fn (int $id) => $this->regroupmentRepository->find($id), $serializedData['centers']['regroupments']), fn ($item) => null !== $item)), + ], + ]; + } +} diff --git a/src/Bundle/ChillMainBundle/Export/ExportConfigProcessor.php b/src/Bundle/ChillMainBundle/Export/ExportConfigProcessor.php new file mode 100644 index 000000000..d898ac1af --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/ExportConfigProcessor.php @@ -0,0 +1,49 @@ + + */ + public function retrieveUsedAggregators(mixed $data): iterable + { + if (null === $data) { + return []; + } + + foreach ($data as $alias => $aggregatorData) { + if ($this->exportManager->hasAggregator($alias) && true === $aggregatorData['enabled']) { + yield $alias => $this->exportManager->getAggregator($alias); + } + } + } + + /** + * @return iterable + */ + public function retrieveUsedFilters(mixed $data): iterable + { + if (null === $data) { + return []; + } + + foreach ($data as $alias => $filterData) { + if ($this->exportManager->hasFilter($alias) && true === $filterData['enabled']) { + yield $alias => $this->exportManager->getFilter($alias); + } + } + } +} diff --git a/src/Bundle/ChillMainBundle/Export/ExportDataNormalizerTrait.php b/src/Bundle/ChillMainBundle/Export/ExportDataNormalizerTrait.php new file mode 100644 index 000000000..147b5844f --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/ExportDataNormalizerTrait.php @@ -0,0 +1,189 @@ +|null $entity the entity or collection of entities to normalize + * + * @return array|int|string Returns the identifier(s) of the entity or entities. If an array of entities is provided, + * an array of their identifiers is returned. If a single entity is provided, its identifier + * is returned. If null, returns an empty value. + */ + private function normalizeDoctrineEntity(object|array|null $entity): array|int|string + { + if (is_array($entity)) { + return array_values(array_filter(array_map(static fn (object $entity) => $entity->getId(), $entity), fn ($value) => null !== $value)); + } + if ($entity instanceof Collection) { + return $this->normalizeDoctrineEntity($entity->toArray()); + } + + return $entity?->getId(); + } + + /** + * Denormalizes a Doctrine entity by fetching it from the provided repository based on the given ID(s). + * + * @param list|int|string $id the identifier(s) of the entity to find + * @param ObjectRepository $repository the Doctrine repository to query + * + * @return object|array the found entity or an array of entities if multiple IDs are provided + * + * @throws \UnexpectedValueException when the entity with the given ID does not exist + */ + private function denormalizeDoctrineEntity(array|int|string $id, ObjectRepository $repository): object|array + { + if (is_array($id)) { + if ([] === $id) { + return []; + } + + return $repository->findBy(['id' => $id]); + } + + if (null === $object = $repository->find($id)) { + throw new \UnexpectedValueException(sprintf('Object with id "%s" does not exist.', $id)); + } + + return $object; + } + + /** + * Normalizer the "user or me" values. + * + * @param 'me'|User|iterable<'me'|User> $user + * + * @return int|'me'|list<'me'|int> + */ + private function normalizeUserOrMe(string|User|iterable $user): int|string|array + { + if (is_iterable($user)) { + $users = []; + foreach ($user as $u) { + $users[] = $this->normalizeUserOrMe($u); + } + + return $users; + } + + if ('me' === $user) { + return $user; + } + + return $user->getId(); + } + + /** + * @param 'me'|int|iterable<'me'|int> $userId + * + * @return 'me'|User|array|null + */ + private function denormalizeUserOrMe(string|int|iterable $userId, UserRepositoryInterface $userRepository): string|User|array|null + { + if (is_iterable($userId)) { + $users = []; + foreach ($userId as $id) { + $users[] = $this->denormalizeUserOrMe($id, $userRepository); + } + + return $users; + } + + if ('me' === $userId) { + return 'me'; + } + + return $userRepository->find($userId); + } + + /** + * @param 'me'|User|iterable<'me'|User> $user + * + * @return User|list + */ + private function userOrMe(string|User|iterable $user, ExportGenerationContext $context): User|array + { + if (is_iterable($user)) { + $users = []; + foreach ($user as $u) { + $users[] = $this->userOrMe($u, $context); + } + + return array_values( + array_filter($users, static fn (?User $user) => null !== $user) + ); + } + + if ('me' === $user) { + return $context->byUser; + } + + return $user; + } + + /** + * Normalizes a provided date into a specific string format. + * + * @param \DateTimeImmutable|\DateTime $date the date instance to normalize + * + * @return string a formatted string containing the type and formatted date + */ + private function normalizeDate(\DateTimeImmutable|\DateTime $date): string + { + return sprintf( + '%s,%s', + $date instanceof \DateTimeImmutable ? 'imm1' : 'mut1', + $date->format('d-m-Y-H:i:s.u e'), + ); + } + + /** + * Denormalizes a string back into a DateTime instance. + * + * The string is expected to contain a kind selector (e.g., 'imm1' or 'mut1') + * to determine the type of DateTime object (immutable or mutable) followed by a date format. + * + * @param string $date the string to be denormalized, containing the kind selector and formatted date + * + * @return \DateTimeImmutable|\DateTime a DateTime instance created from the given string + * + * @throws \UnexpectedValueException if the kind selector or date format is invalid + */ + private function denormalizeDate(string $date): \DateTimeImmutable|\DateTime + { + $format = 'd-m-Y-H:i:s.u e'; + + $denormalized = match (substr($date, 0, 4)) { + 'imm1' => \DateTimeImmutable::createFromFormat($format, substr($date, 5)), + 'mut1' => \DateTime::createFromFormat($format, substr($date, 5)), + default => throw new \UnexpectedValueException(sprintf('Unexpected format for the kind selector: %s', substr($date, 0, 4))), + }; + + if (false === $denormalized) { + throw new \UnexpectedValueException(sprintf('Unexpected date format: %s', substr($date, 5))); + } + + return $denormalized; + } +} diff --git a/src/Bundle/ChillMainBundle/Export/ExportDescriptionHelper.php b/src/Bundle/ChillMainBundle/Export/ExportDescriptionHelper.php new file mode 100644 index 000000000..3016f2f79 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/ExportDescriptionHelper.php @@ -0,0 +1,74 @@ + + */ + public function describe(string $exportAlias, array $exportOptions, bool $includeExportTitle = true): array + { + $output = []; + $denormalized = $this->exportConfigNormalizer->denormalizeConfig($exportAlias, $exportOptions); + $user = $this->security->getUser(); + + if ($includeExportTitle) { + $output[] = $this->trans($this->exportManager->getExport($exportAlias)->getTitle()); + } + + if (!$user instanceof User) { + return $output; + } + $context = new ExportGenerationContext($user); + + foreach ($this->exportConfigProcessor->retrieveUsedFilters($denormalized['filters']) as $name => $filter) { + $output[] = $this->trans($filter->describeAction($denormalized['filters'][$name]['form'], $context)); + } + + foreach ($this->exportConfigProcessor->retrieveUsedAggregators($denormalized['aggregators']) as $name => $aggregator) { + $output[] = $this->trans($aggregator->getTitle()); + } + + return $output; + } + + private function trans(string|TranslatableInterface|array $translatable): string + { + if (is_string($translatable)) { + return $this->translator->trans($translatable); + } + + if ($translatable instanceof TranslatableInterface) { + return $translatable->trans($this->translator); + } + + // array case + return $this->translator->trans($translatable[0], $translatable[1] ?? []); + } +} diff --git a/src/Bundle/ChillMainBundle/Export/ExportElementInterface.php b/src/Bundle/ChillMainBundle/Export/ExportElementInterface.php index 49b2500f0..e8018bb1a 100644 --- a/src/Bundle/ChillMainBundle/Export/ExportElementInterface.php +++ b/src/Bundle/ChillMainBundle/Export/ExportElementInterface.php @@ -11,6 +11,8 @@ declare(strict_types=1); namespace Chill\MainBundle\Export; +use Symfony\Contracts\Translation\TranslatableInterface; + /** * The common methods between different object used to build export (i.e. : ExportInterface, * FilterInterface, AggregatorInterface). @@ -19,8 +21,6 @@ interface ExportElementInterface { /** * get a title, which will be used in UI (and translated). - * - * @return string */ - public function getTitle(); + public function getTitle(): string|TranslatableInterface; } diff --git a/src/Bundle/ChillMainBundle/Export/ExportElementValidatedInterface.php b/src/Bundle/ChillMainBundle/Export/ExportElementValidatedInterface.php index d4f58a570..d5c5b435c 100644 --- a/src/Bundle/ChillMainBundle/Export/ExportElementValidatedInterface.php +++ b/src/Bundle/ChillMainBundle/Export/ExportElementValidatedInterface.php @@ -31,5 +31,5 @@ interface ExportElementValidatedInterface * validate the form's data and, if required, build a contraint * violation on the data. */ - public function validateForm(mixed $data, ExecutionContextInterface $context); + public function validateForm(mixed $data, ExecutionContextInterface $context): void; } diff --git a/src/Bundle/ChillMainBundle/Export/ExportElementsProviderInterface.php b/src/Bundle/ChillMainBundle/Export/ExportElementsProviderInterface.php index 43e0e506a..f92e9bd50 100644 --- a/src/Bundle/ChillMainBundle/Export/ExportElementsProviderInterface.php +++ b/src/Bundle/ChillMainBundle/Export/ExportElementsProviderInterface.php @@ -21,7 +21,7 @@ namespace Chill\MainBundle\Export; interface ExportElementsProviderInterface { /** - * @return ExportElementInterface[] + * @return iterable */ - public function getExportElements(); + public function getExportElements(): iterable; } diff --git a/src/Bundle/ChillMainBundle/Export/ExportFormHelper.php b/src/Bundle/ChillMainBundle/Export/ExportFormHelper.php index ce43c230a..7bc7e9fc5 100644 --- a/src/Bundle/ChillMainBundle/Export/ExportFormHelper.php +++ b/src/Bundle/ChillMainBundle/Export/ExportFormHelper.php @@ -11,27 +11,28 @@ declare(strict_types=1); namespace Chill\MainBundle\Export; +use Chill\MainBundle\Entity\Center; +use Chill\MainBundle\Entity\ExportGeneration; use Chill\MainBundle\Entity\SavedExport; use Chill\MainBundle\Form\Type\Export\ExportType; use Chill\MainBundle\Form\Type\Export\FilterType; -use Chill\MainBundle\Form\Type\Export\FormatterType; -use Chill\MainBundle\Form\Type\Export\PickCenterType; use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface; -use Symfony\Component\Form\Extension\Core\Type\FormType; -use Symfony\Component\Form\FormFactoryInterface; +use Chill\MainBundle\Service\Regroupement\CenterRegroupementResolver; +use Doctrine\Common\Collections\Collection; final readonly class ExportFormHelper { public function __construct( private AuthorizationHelperForCurrentUserInterface $authorizationHelper, private ExportManager $exportManager, - private FormFactoryInterface $formFactory, + private ExportConfigNormalizer $configNormalizer, + private CenterRegroupementResolver $centerRegroupementResolver, ) {} public function getDefaultData(string $step, DirectExportInterface|ExportInterface $export, array $options = []): array { return match ($step) { - 'centers', 'generate_centers' => ['centers' => $this->authorizationHelper->getReachableCenters($export->requiredRole())], + 'centers', 'generate_centers' => ['centers' => $this->authorizationHelper->getReachableCenters($export->requiredRole()), 'regroupments' => []], 'export', 'generate_export' => ['export' => $this->getDefaultDataStepExport($export, $options)], 'formatter', 'generate_formatter' => ['formatter' => $this->getDefaultDataStepFormatter($options)], default => throw new \LogicException('step not allowed : '.$step), @@ -91,80 +92,68 @@ final readonly class ExportFormHelper } public function savedExportDataToFormData( - SavedExport $savedExport, + ExportGeneration|SavedExport $savedExport, string $step, - array $formOptions = [], ): array { return match ($step) { 'centers', 'generate_centers' => $this->savedExportDataToFormDataStepCenter($savedExport), - 'export', 'generate_export' => $this->savedExportDataToFormDataStepExport($savedExport, $formOptions), - 'formatter', 'generate_formatter' => $this->savedExportDataToFormDataStepFormatter($savedExport, $formOptions), + 'export', 'generate_export' => $this->savedExportDataToFormDataStepExport($savedExport), + 'formatter', 'generate_formatter' => $this->savedExportDataToFormDataStepFormatter($savedExport), default => throw new \LogicException('this step is not allowed: '.$step), }; } private function savedExportDataToFormDataStepCenter( - SavedExport $savedExport, + ExportGeneration|SavedExport $savedExport, ): array { - $builder = $this->formFactory - ->createBuilder( - FormType::class, - [], - [ - 'csrf_protection' => false, - ] - ); - $builder->add('centers', PickCenterType::class, [ - 'export_alias' => $savedExport->getExportAlias(), - ]); - $form = $builder->getForm(); - $form->submit($savedExport->getOptions()['centers']); - - return $form->getData(); + return [ + 'centers' => $this->configNormalizer->denormalizeConfig($savedExport->getExportAlias(), $savedExport->getOptions(), true)['centers'], + ]; } private function savedExportDataToFormDataStepExport( - SavedExport $savedExport, - array $formOptions, + ExportGeneration|SavedExport $savedExport, ): array { - $builder = $this->formFactory - ->createBuilder( - FormType::class, - [], - [ - 'csrf_protection' => false, - ] - ); + $data = $this->configNormalizer->denormalizeConfig($savedExport->getExportAlias(), $savedExport->getOptions(), true); - $builder->add('export', ExportType::class, [ - 'export_alias' => $savedExport->getExportAlias(), ...$formOptions, - ]); - $form = $builder->getForm(); - $form->submit($savedExport->getOptions()['export']); - - return $form->getData(); + return [ + 'export' => [ + 'export' => $data['export'], + 'filters' => $data['filters'], + 'pick_formatter' => ['alias' => $data['pick_formatter']], + 'aggregators' => $data['aggregators'], + ], + ]; } private function savedExportDataToFormDataStepFormatter( - SavedExport $savedExport, - array $formOptions, + ExportGeneration|SavedExport $savedExport, ): array { - $builder = $this->formFactory - ->createBuilder( - FormType::class, - [], - [ - 'csrf_protection' => false, - ] - ); + $data = $this->configNormalizer->denormalizeConfig($savedExport->getExportAlias(), $savedExport->getOptions(), true); - $builder->add('formatter', FormatterType::class, [ - 'export_alias' => $savedExport->getExportAlias(), ...$formOptions, - ]); - $form = $builder->getForm(); - $form->submit($savedExport->getOptions()['formatter']); + return [ + 'formatter' => $data['formatter'], + ]; + } - return $form->getData(); + /** + * Get the Center picked by the user for this export. The data are + * extracted from the PickCenterType data. + * + * @param array $data the data as given by the @see{Chill\MainBundle\Form\Type\Export\PickCenterType} + * + * @return list
+ */ + public function getPickedCenters(array $data): array + { + if (!array_key_exists('centers', $data)) { + throw new \RuntimeException('array has not the expected shape'); + } + + $centers = $data['centers'] instanceof Collection ? $data['centers']->toArray() : $data['centers']; + $regroupments = ($data['regroupments'] ?? []) instanceof Collection ? $data['regroupments']->toArray() : ($data['regroupments'] ?? []); + + return $this->centerRegroupementResolver->resolveCenters($regroupments, $centers); } } diff --git a/src/Bundle/ChillMainBundle/Export/ExportGenerationContext.php b/src/Bundle/ChillMainBundle/Export/ExportGenerationContext.php new file mode 100644 index 000000000..0ffb29c20 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/ExportGenerationContext.php @@ -0,0 +1,21 @@ +filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center']; + } + + public function generate(string $exportAlias, array $configuration, ?User $byUser = null): FormattedExportGeneration + { + $data = $this->configNormalizer->denormalizeConfig($exportAlias, $configuration); + $export = $this->exportManager->getExport($exportAlias); + + $centers = $this->filterCenters($byUser, $data['centers']['centers'], $data['centers']['regroupments'], $export); + + $context = new ExportGenerationContext($byUser); + + if ($export instanceof DirectExportInterface) { + $generatedExport = $export->generate( + $this->buildCenterReachableScopes($centers), + $data['export'], + $context, + ); + + if ($generatedExport instanceof Response) { + trigger_deprecation('chill-project/chill-bundles', '3.10', 'DirectExportInterface should not return a %s instance, but a %s instance', Response::class, FormattedExportGeneration::class); + + return new FormattedExportGeneration($generatedExport->getContent(), $generatedExport->headers->get('Content-Type')); + } + + return $generatedExport; + } + + $query = $export->initiateQuery( + $this->retrieveUsedModifiers($data), + $this->buildCenterReachableScopes($centers), + $data['export'], + $context, + ); + + if ($query instanceof \Doctrine\ORM\NativeQuery) { + // throw an error if the export require other modifier, which is + // not allowed when the export return a `NativeQuery` + if (\count($export->supportsModifiers()) > 0) { + throw new \LogicException("The export with alias `{$exportAlias}` return ".'a `\Doctrine\ORM\NativeQuery` and supports modifiers, which is not allowed. Either the method `supportsModifiers` should return an empty array, or return a `Doctrine\ORM\QueryBuilder`'); + } + } elseif ($query instanceof QueryBuilder) { + // handle filters + $this->handleFilters($query, $data[ExportType::FILTER_KEY], $context); + + // handle aggregators + $this->handleAggregators($query, $data[ExportType::AGGREGATOR_KEY], $context); + + $this->logger->notice('[export] will execute this qb in export', [ + 'dql' => $query->getDQL(), + ]); + $this->logger->debug('[export] will execute this sql qb in export', [ + 'sql' => $query->getQuery()->getSQL(), + ]); + } else { + throw new \UnexpectedValueException('The method `intiateQuery` should return a `\Doctrine\ORM\NativeQuery` or a `Doctrine\ORM\QueryBuilder` object.'); + } + + $result = $export->getResult($query, $data['export'], $context); + + $formatter = $this->exportManager->getFormatter($data['pick_formatter']); + $filtersData = []; + $aggregatorsData = []; + + if ($query instanceof QueryBuilder) { + foreach ($this->exportConfigProcessor->retrieveUsedAggregators($data[ExportType::AGGREGATOR_KEY]) as $alias => $aggregator) { + $aggregatorsData[$alias] = $data[ExportType::AGGREGATOR_KEY][$alias]['form']; + } + foreach ($this->exportConfigProcessor->retrieveUsedFilters($data[ExportType::FILTER_KEY]) as $alias => $filter) { + $filtersData[$alias] = $data[ExportType::FILTER_KEY][$alias]['form']; + } + } + + /* @phpstan-ignore-next-line the method "generate" is not yet implemented on all formatters */ + if (method_exists($formatter, 'generate')) { + return $formatter->generate( + $result, + $data['formatter'], + $exportAlias, + $data['export'], + $filtersData, + $aggregatorsData, + $context, + ); + } + + trigger_deprecation('chill-project/chill-bundles', '3.10', '%s should implements the "generate" method', FormatterInterface::class); + + /* @phpstan-ignore-next-line this is a deprecated method that we must still call */ + $generatedExport = $formatter->getResponse( + $result, + $data['formatter'], + $exportAlias, + $data['export'], + $filtersData, + $aggregatorsData, + $context, + ); + + return new FormattedExportGeneration($generatedExport->getContent(), $generatedExport->headers->get('content-type')); + } + + private function filterCenters(User $byUser, array $centers, array $regroupements, ExportInterface|DirectExportInterface $export): array + { + if (!$this->filterStatsByCenters) { + return $this->centerRepository->findActive(); + } + + $authorizedCenters = new ArrayCollection($this->authorizationHelper->getReachableCenters($byUser, $export->requiredRole())); + if ($authorizedCenters->isEmpty()) { + throw new UnauthorizedGenerationException('No authorized centers'); + } + + $wantedCenters = $this->centerRegroupementResolver->resolveCenters($regroupements, $centers); + + $resolvedCenters = []; + foreach ($wantedCenters as $wantedCenter) { + if ($authorizedCenters->contains($wantedCenter)) { + $resolvedCenters[] = $wantedCenter; + } + } + + if ([] == $resolvedCenters) { + throw new UnauthorizedGenerationException('No common centers between wanted centers and authorized centers'); + } + + return $resolvedCenters; + } + + /** + * parse the data to retrieve the used filters and aggregators. + * + * @return list + */ + private function retrieveUsedModifiers(mixed $data): array + { + if (null === $data) { + return []; + } + + $usedTypes = array_merge( + $this->retrieveUsedFiltersType($data[ExportType::FILTER_KEY]), + $this->retrieveUsedAggregatorsType($data[ExportType::AGGREGATOR_KEY]), + ); + + return array_values(array_unique($usedTypes)); + } + + /** + * Retrieve the filter used in this export. + * + * @return list an array with types + */ + private function retrieveUsedFiltersType(mixed $data): array + { + if (null === $data) { + return []; + } + + $usedTypes = []; + + foreach ($this->exportConfigProcessor->retrieveUsedFilters($data) as $filter) { + if (!\in_array($filter->applyOn(), $usedTypes, true)) { + $usedTypes[] = $filter->applyOn(); + } + } + + return $usedTypes; + } + + /** + * @return string[] + */ + private function retrieveUsedAggregatorsType(mixed $data): array + { + if (null === $data) { + return []; + } + + $usedTypes = []; + + foreach ($this->exportConfigProcessor->retrieveUsedAggregators($data) as $alias => $aggregator) { + if (!\in_array($aggregator->applyOn(), $usedTypes, true)) { + $usedTypes[] = $aggregator->applyOn(); + } + } + + return $usedTypes; + } + + /** + * Alter the query with selected aggregators. + */ + private function handleAggregators( + QueryBuilder $qb, + array $data, + ExportGenerationContext $context, + ): void { + foreach ($this->exportConfigProcessor->retrieveUsedAggregators($data) as $alias => $aggregator) { + $formData = $data[$alias]; + $aggregator->alterQuery($qb, $formData['form'], $context); + } + } + + /** + * alter the query with selected filters. + */ + private function handleFilters( + QueryBuilder $qb, + mixed $data, + ExportGenerationContext $context, + ): void { + foreach ($this->exportConfigProcessor->retrieveUsedFilters($data) as $alias => $filter) { + $formData = $data[$alias]; + + $filter->alterQuery($qb, $formData['form'], $context); + } + } + + /** + * build the array required for defining centers and circles in the initiate + * queries of ExportElementsInterfaces. + * + * @param list
$centers + */ + private function buildCenterReachableScopes(array $centers) + { + return array_map(static fn (Center $center) => ['center' => $center, 'circles' => []], $centers); + } +} diff --git a/src/Bundle/ChillMainBundle/Export/ExportInterface.php b/src/Bundle/ChillMainBundle/Export/ExportInterface.php index a9d3efd13..edb5a53d1 100644 --- a/src/Bundle/ChillMainBundle/Export/ExportInterface.php +++ b/src/Bundle/ChillMainBundle/Export/ExportInterface.php @@ -14,6 +14,7 @@ namespace Chill\MainBundle\Export; use Doctrine\ORM\NativeQuery; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Contracts\Translation\TranslatableInterface; /** * Interface for Export. @@ -27,6 +28,7 @@ use Symfony\Component\Form\FormBuilderInterface; * @example Chill\PersonBundle\Export\CountPerson an example of implementation * * @template Q of QueryBuilder|NativeQuery + * @template D of array */ interface ExportInterface extends ExportElementInterface { @@ -94,12 +96,12 @@ interface ExportInterface extends ExportElementInterface * which do not need to be translated, or value already translated in * database. But the header must be, in every case, translated. * - * @param string $key The column key, as added in the query - * @param mixed[] $values The values from the result. if there are duplicates, those might be given twice. Example: array('FR', 'BE', 'CZ', 'FR', 'BE', 'FR') + * @param string $key The column key, as added in the query + * @param list $values The values from the result. if there are duplicates, those might be given twice. Example: array('FR', 'BE', 'CZ', 'FR', 'BE', 'FR') * - * @return (callable(string|int|float|'_header'|null $value): string|int|\DateTimeInterface) where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }` + * @return (callable(string|int|float|'_header'|null $value): string|int|\DateTimeInterface|TranslatableInterface) where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }` */ - public function getLabels($key, array $values, mixed $data); + public function getLabels(string $key, array $values, mixed $data); /** * give the list of keys the current export added to the queryBuilder in @@ -108,29 +110,27 @@ interface ExportInterface extends ExportElementInterface * Example: if your query builder will contains `SELECT count(id) AS count_id ...`, * this function will return `array('count_id')`. * - * @param mixed[] $data the data from the export's form (added by self::buildForm) + * @param D $data the data from the export's form (added by self::buildForm) */ - public function getQueryKeys($data); + public function getQueryKeys(array $data): array; /** * Return the results of the query builder. * - * @param Q $query - * @param mixed[] $data the data from the export's fomr (added by self::buildForm) + * @param Q $query + * @param D $data the data from the export's fomr (added by self::buildForm) * * @return mixed[] an array of results */ - public function getResult($query, $data); + public function getResult(QueryBuilder|NativeQuery $query, array $data, ExportGenerationContext $context): array; /** * Return the Export's type. This will inform _on what_ export will apply. * Most of the type, it will be a string which references an entity. * * Example of types : Chill\PersonBundle\Export\Declarations::PERSON_TYPE - * - * @return string */ - public function getType(); + public function getType(): string; /** * The initial query, which will be modified by ModifiersInterface @@ -147,7 +147,21 @@ interface ExportInterface extends ExportElementInterface * * @return Q the query to execute */ - public function initiateQuery(array $requiredModifiers, array $acl, array $data = []); + public function initiateQuery(array $requiredModifiers, array $acl, array $data, ExportGenerationContext $context): QueryBuilder|NativeQuery; + + /** + * @param D $formData + */ + public function normalizeFormData(array $formData): array; + + /** + * @param array $formData the normalized data + * + * @return D + */ + public function denormalizeFormData(array $formData, int $fromVersion): array; + + public function getNormalizationVersion(): int; /** * Return the required Role to execute the Export. diff --git a/src/Bundle/ChillMainBundle/Export/ExportManager.php b/src/Bundle/ChillMainBundle/Export/ExportManager.php index 9bcf8d362..f5e9bfc4d 100644 --- a/src/Bundle/ChillMainBundle/Export/ExportManager.php +++ b/src/Bundle/ChillMainBundle/Export/ExportManager.php @@ -11,13 +11,9 @@ declare(strict_types=1); namespace Chill\MainBundle\Export; -use Chill\MainBundle\Entity\User; use Chill\MainBundle\Form\Type\Export\ExportType; use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface; -use Doctrine\ORM\QueryBuilder; use Psr\Log\LoggerInterface; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; @@ -63,14 +59,13 @@ class ExportManager iterable $exports, iterable $aggregators, iterable $filters, - // iterable $formatters, + iterable $formatters, // iterable $exportElementProvider ) { $this->exports = iterator_to_array($exports); $this->aggregators = iterator_to_array($aggregators); $this->filters = iterator_to_array($filters); - // NOTE: PHP crashes on the next line (exit error code 11). This is desactivated until further investigation - // $this->formatters = iterator_to_array($formatters); + $this->formatters = iterator_to_array($formatters); // foreach ($exportElementProvider as $prefix => $provider) { // $this->addExportElementsProvider($provider, $prefix); @@ -102,7 +97,7 @@ class ExportManager \in_array($filter->applyOn(), $export->supportsModifiers(), true) && $this->isGrantedForElement($filter, $export, $centers) ) { - $filters[$alias] = $filter; + $filters[$alias] = $this->getFilter($alias); } } @@ -136,125 +131,32 @@ class ExportManager return $aggregators; } - public function addExportElementsProvider(ExportElementsProviderInterface $provider, string $prefix): void - { - foreach ($provider->getExportElements() as $suffix => $element) { - $alias = $prefix.'_'.$suffix; - - if ($element instanceof ExportInterface) { - $this->exports[$alias] = $element; - } elseif ($element instanceof FilterInterface) { - $this->filters[$alias] = $element; - } elseif ($element instanceof AggregatorInterface) { - $this->aggregators[$alias] = $element; - } elseif ($element instanceof FormatterInterface) { - $this->addFormatter($element, $alias); - } else { - throw new \LogicException('This element '.$element::class.' is not an instance of export element'); - } - } - } - /** * add a formatter. * * @internal used by DI */ - public function addFormatter(FormatterInterface $formatter, string $alias): void + public function addFormatter(FormatterInterface $formatter, string $alias) { $this->formatters[$alias] = $formatter; } - /** - * Generate a response which contains the requested data. - */ - public function generate(string $exportAlias, array $pickedCentersData, array $data, array $formatterData): Response - { - $export = $this->getExport($exportAlias); - $centers = $this->getPickedCenters($pickedCentersData); - - if ($export instanceof DirectExportInterface) { - return $export->generate( - $this->buildCenterReachableScopes($centers, $export), - $data[ExportType::EXPORT_KEY] - ); - } - - $query = $export->initiateQuery( - $this->retrieveUsedModifiers($data), - $this->buildCenterReachableScopes($centers, $export), - $data[ExportType::EXPORT_KEY] - ); - - if ($query instanceof \Doctrine\ORM\NativeQuery) { - // throw an error if the export require other modifier, which is - // not allowed when the export return a `NativeQuery` - if (\count($export->supportsModifiers()) > 0) { - throw new \LogicException("The export with alias `{$exportAlias}` return ".'a `\Doctrine\ORM\NativeQuery` and supports modifiers, which is not allowed. Either the method `supportsModifiers` should return an empty array, or return a `Doctrine\ORM\QueryBuilder`'); - } - } elseif ($query instanceof QueryBuilder) { - // handle filters - $this->handleFilters($export, $query, $data[ExportType::FILTER_KEY], $centers); - - // handle aggregators - $this->handleAggregators($export, $query, $data[ExportType::AGGREGATOR_KEY], $centers); - - $this->logger->notice('[export] will execute this qb in export', [ - 'dql' => $query->getDQL(), - ]); - } else { - throw new \UnexpectedValueException('The method `intiateQuery` should return a `\Doctrine\ORM\NativeQuery` or a `Doctrine\ORM\QueryBuilder` object.'); - } - - $result = $export->getResult($query, $data[ExportType::EXPORT_KEY]); - - if (!is_iterable($result)) { - throw new \UnexpectedValueException(sprintf('The result of the export should be an iterable, %s given', \gettype($result))); - } - - /** @var FormatterInterface $formatter */ - $formatter = $this->getFormatter($this->getFormatterAlias($data)); - $filtersData = []; - $aggregatorsData = []; - - if ($query instanceof QueryBuilder) { - $aggregators = $this->retrieveUsedAggregators($data[ExportType::AGGREGATOR_KEY]); - - foreach ($aggregators as $alias => $aggregator) { - $aggregatorsData[$alias] = $data[ExportType::AGGREGATOR_KEY][$alias]['form']; - } - } - - $filters = $this->retrieveUsedFilters($data[ExportType::FILTER_KEY]); - - foreach ($filters as $alias => $filter) { - $filtersData[$alias] = $data[ExportType::FILTER_KEY][$alias]['form']; - } - - return $formatter->getResponse( - $result, - $formatterData, - $exportAlias, - $data[ExportType::EXPORT_KEY], - $filtersData, - $aggregatorsData - ); - } - /** * @param string $alias * - * @return AggregatorInterface - * * @throws \RuntimeException if the aggregator is not known */ - public function getAggregator($alias) + public function getAggregator($alias): AggregatorInterface { - if (!\array_key_exists($alias, $this->aggregators)) { + if (null === $aggregator = $this->aggregators[$alias] ?? null) { throw new \RuntimeException("The aggregator with alias {$alias} is not known."); } - return $this->aggregators[$alias]; + if ($aggregator instanceof ExportManagerAwareInterface) { + $aggregator->setExportManager($this); + } + + return $aggregator; } /** @@ -313,10 +215,10 @@ class ExportManager foreach ($this->exports as $alias => $export) { if ($whereUserIsGranted) { if ($this->isGrantedForElement($export, null, null)) { - yield $alias => $export; + yield $alias => $this->getExport($alias); } } else { - yield $alias => $export; + yield $alias => $this->getExport($alias); } } } @@ -332,9 +234,9 @@ class ExportManager foreach ($this->getExports($whereUserIsGranted) as $alias => $export) { if ($export instanceof GroupedExportInterface) { - $groups[$export->getGroup()][$alias] = $export; + $groups[$export->getGroup()][$alias] = $this->getExport($alias); } else { - $groups['_'][$alias] = $export; + $groups['_'][$alias] = $this->getExport($alias); } } @@ -346,11 +248,25 @@ class ExportManager */ public function getFilter(string $alias): FilterInterface { - if (!\array_key_exists($alias, $this->filters)) { + if (null === $filter = $this->filters[$alias] ?? null) { throw new \RuntimeException("The filter with alias {$alias} is not known."); } - return $this->filters[$alias]; + if ($filter instanceof ExportManagerAwareInterface) { + $filter->setExportManager($this); + } + + return $filter; + } + + public function hasFilter(string $alias): bool + { + return array_key_exists($alias, $this->filters); + } + + public function hasAggregator(string $alias): bool + { + return array_key_exists($alias, $this->aggregators); } public function getAllFilters(): array @@ -358,7 +274,7 @@ class ExportManager $filters = []; foreach ($this->filters as $alias => $filter) { - $filters[$alias] = $filter; + $filters[$alias] = $this->getFilter($alias); } return $filters; @@ -380,11 +296,15 @@ class ExportManager public function getFormatter(string $alias): FormatterInterface { - if (!\array_key_exists($alias, $this->formatters)) { + if (null === $formatter = $this->formatters[$alias] ?? null) { throw new \RuntimeException("The formatter with alias {$alias} is not known."); } - return $this->formatters[$alias]; + if ($formatter instanceof ExportManagerAwareInterface) { + $formatter->setExportManager($this); + } + + return $formatter; } /** @@ -412,26 +332,13 @@ class ExportManager { foreach ($this->formatters as $alias => $formatter) { if (\in_array($formatter->getType(), $types, true)) { - yield $alias => $formatter; + yield $alias => $this->getFormatter($alias); } } } /** - * Get the Center picked by the user for this export. The data are - * extracted from the PickCenterType data. - * - * @param array $data the data from a PickCenterType - * - * @return \Chill\MainBundle\Entity\Center[] the picked center - */ - public function getPickedCenters(array $data): array - { - return $data; - } - - /** - * get the aggregators typse used in the form export data. + * get the aggregators types used in the form export data. * * @param array $data the data from the export form * @@ -439,9 +346,15 @@ class ExportManager */ public function getUsedAggregatorsAliases(array $data): array { - $aggregators = $this->retrieveUsedAggregators($data[ExportType::AGGREGATOR_KEY]); + $keys = []; - return array_keys(iterator_to_array($aggregators)); + foreach ($data as $alias => $aggregatorData) { + if (true === $aggregatorData['enabled']) { + $keys[] = $alias; + } + } + + return array_values(array_unique($keys)); } /** @@ -490,190 +403,4 @@ class ExportManager return true; } - - /** - * build the array required for defining centers and circles in the initiate - * queries of ExportElementsInterfaces. - * - * @param \Chill\MainBundle\Entity\Center[] $centers - */ - private function buildCenterReachableScopes(array $centers, ExportElementInterface $element) - { - $r = []; - - $user = $this->tokenStorage->getToken()->getUser(); - - if (!$user instanceof User) { - return []; - } - - foreach ($centers as $center) { - $r[] = [ - 'center' => $center, - 'circles' => $this->authorizationHelper->getReachableScopes( - $user, - $element->requiredRole(), - $center - ), - ]; - } - - return $r; - } - - /** - * Alter the query with selected aggregators. - * - * Check for acl. If an user is not authorized to see an aggregator, throw an - * UnauthorizedException. - * - * @throw UnauthorizedHttpException if the user is not authorized - */ - private function handleAggregators( - ExportInterface $export, - QueryBuilder $qb, - array $data, - array $center, - ): void { - $aggregators = $this->retrieveUsedAggregators($data); - - foreach ($aggregators as $alias => $aggregator) { - if (false === $this->isGrantedForElement($aggregator, $export, $center)) { - throw new UnauthorizedHttpException('You are not authorized to use the aggregator'.$aggregator->getTitle()); - } - - $formData = $data[$alias]; - $aggregator->alterQuery($qb, $formData['form']); - } - } - - /** - * alter the query with selected filters. - * - * This function check the acl. - * - * @param \Chill\MainBundle\Entity\Center[] $centers the picked centers - * - * @throw UnauthorizedHttpException if the user is not authorized - */ - private function handleFilters( - ExportInterface $export, - QueryBuilder $qb, - mixed $data, - array $centers, - ): void { - $filters = $this->retrieveUsedFilters($data); - - foreach ($filters as $alias => $filter) { - if (false === $this->isGrantedForElement($filter, $export, $centers)) { - throw new UnauthorizedHttpException('You are not authorized to use the filter '.$filter->getTitle()); - } - - $formData = $data[$alias]; - - $this->logger->debug('alter query by filter '.$alias, [ - 'class' => self::class, 'function' => __FUNCTION__, - ]); - $filter->alterQuery($qb, $formData['form']); - } - } - - /** - * @return iterable - */ - private function retrieveUsedAggregators(mixed $data): iterable - { - if (null === $data) { - return []; - } - - foreach ($data as $alias => $aggregatorData) { - if (true === $aggregatorData['enabled']) { - yield $alias => $this->getAggregator($alias); - } - } - } - - /** - * @return string[] - */ - private function retrieveUsedAggregatorsType(mixed $data) - { - if (null === $data) { - return []; - } - - $usedTypes = []; - - foreach ($this->retrieveUsedAggregators($data) as $alias => $aggregator) { - if (!\in_array($aggregator->applyOn(), $usedTypes, true)) { - $usedTypes[] = $aggregator->applyOn(); - } - } - - return $usedTypes; - } - - private function retrieveUsedFilters(mixed $data): iterable - { - if (null === $data) { - return []; - } - - foreach ($data as $alias => $filterData) { - if (true === $filterData['enabled']) { - yield $alias => $this->getFilter($alias); - } - } - } - - /** - * Retrieve the filter used in this export. - * - * @return array an array with types - */ - private function retrieveUsedFiltersType(mixed $data): iterable - { - if (null === $data) { - return []; - } - - $usedTypes = []; - - foreach ($data as $alias => $filterData) { - if (true === $filterData['enabled']) { - $filter = $this->getFilter($alias); - - if (!\in_array($filter->applyOn(), $usedTypes, true)) { - $usedTypes[] = $filter->applyOn(); - } - } - } - - return $usedTypes; - } - - /** - * parse the data to retrieve the used filters and aggregators. - * - * @return string[] - */ - private function retrieveUsedModifiers(mixed $data): array - { - if (null === $data) { - return []; - } - - $usedTypes = array_merge( - $this->retrieveUsedFiltersType($data[ExportType::FILTER_KEY]), - $this->retrieveUsedAggregatorsType($data[ExportType::AGGREGATOR_KEY]) - ); - - $this->logger->debug( - 'Required types are '.implode(', ', $usedTypes), - ['class' => self::class, 'function' => __FUNCTION__] - ); - - return array_unique($usedTypes); - } } diff --git a/src/Bundle/ChillMainBundle/Export/ExportManagerAwareInterface.php b/src/Bundle/ChillMainBundle/Export/ExportManagerAwareInterface.php new file mode 100644 index 000000000..f4e43d206 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/ExportManagerAwareInterface.php @@ -0,0 +1,20 @@ + 'good news'], 'mydomain', 'mylocale')` * - * @param array $data - * @param string $format the format + * @param D $data * - * @return array|string a string with the data or, if translatable, an array where first element is string, second elements is an array of arguments + * @return array|string|TranslatableInterface a string with the data or, if translatable, an array where first element is string, second elements is an array of arguments */ - public function describeAction($data, $format = 'string'); + public function describeAction(array $data, ExportGenerationContext $context): array|string|TranslatableInterface; } diff --git a/src/Bundle/ChillMainBundle/Export/FormattedExportGeneration.php b/src/Bundle/ChillMainBundle/Export/FormattedExportGeneration.php new file mode 100644 index 000000000..678bb8f31 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/FormattedExportGeneration.php @@ -0,0 +1,20 @@ +translator = $translatorInterface; - $this->exportManager = $exportManager; - } - - /** - * build a form, which will be used to collect data required for the execution - * of this formatter. - * - * @uses appendAggregatorForm - * - * @param type $exportAlias - */ - public function buildForm( - FormBuilderInterface $builder, - $exportAlias, - array $aggregatorAliases, - ): void { - $builder->add('numerotation', ChoiceType::class, [ - 'choices' => [ - 'yes' => true, - 'no' => false, - ], - 'expanded' => true, - 'multiple' => false, - 'label' => 'Add a number on first column', - ]); - } - - public function getFormDefaultData(array $aggregatorAliases): array - { - return ['numerotation' => true]; - } - - public function getName() - { - return 'CSV vertical list'; - } - - /** - * Generate a response from the data collected on differents ExportElementInterface. - * - * @param mixed[] $result The result, as given by the ExportInterface - * @param mixed[] $formatterData collected from the current form - * @param string $exportAlias the id of the current export - * @param array $filtersData an array containing the filters data. The key are the filters id, and the value are the data - * @param array $aggregatorsData an array containing the aggregators data. The key are the filters id, and the value are the data - * - * @return Response The response to be shown - */ - public function getResponse( - $result, - $formatterData, - $exportAlias, - array $exportData, - array $filtersData, - array $aggregatorsData, - ): \Symfony\Component\HttpFoundation\Response { - $this->result = $result; - $this->exportAlias = $exportAlias; - $this->exportData = $exportData; - $this->formatterData = $formatterData; - - $output = fopen('php://output', 'wb'); - - $this->prepareHeaders($output); - - $i = 1; - - foreach ($result as $row) { - $line = []; - - if (true === $this->formatterData['numerotation']) { - $line[] = $i; - } - - foreach ($row as $key => $value) { - $line[] = $this->getLabel($key, $value); - } - - fputcsv($output, $line); - - ++$i; - } - - $csvContent = stream_get_contents($output); - fclose($output); - - $response = new Response(); - $response->setStatusCode(200); - $response->headers->set('Content-Type', 'text/csv; charset=utf-8'); - // $response->headers->set('Content-Disposition','attachment; filename="export.csv"'); - - $response->setContent($csvContent); - - return $response; - } - - public function getType() - { - return FormatterInterface::TYPE_LIST; - } - - /** - * Give the label corresponding to the given key and value. - * - * @param string $key - * @param string $value - * - * @return string - * - * @throws \LogicException if the label is not found - */ - protected function getLabel($key, $value) - { - if (null === $this->labelsCache) { - $this->prepareCacheLabels(); - } - - if (!\array_key_exists($key, $this->labelsCache)) { - throw new \OutOfBoundsException(sprintf('The key "%s" is not present in the list of keys handled by this query. Check your `getKeys` and `getLabels` methods. Available keys are %s.', $key, \implode(', ', \array_keys($this->labelsCache)))); - } - - return $this->labelsCache[$key]($value); - } - - /** - * Prepare the label cache which will be used by getLabel. This function - * should be called only once in the generation lifecycle. - */ - protected function prepareCacheLabels() - { - $export = $this->exportManager->getExport($this->exportAlias); - $keys = $export->getQueryKeys($this->exportData); - - foreach ($keys as $key) { - // get an array with all values for this key if possible - $values = \array_map(static fn ($v) => $v[$key], $this->result); - // store the label in the labelsCache property - $this->labelsCache[$key] = $export->getLabels($key, $values, $this->exportData); - } - } - - /** - * add the headers to the csv file. - * - * @param resource $output - */ - protected function prepareHeaders($output) - { - $keys = $this->exportManager->getExport($this->exportAlias)->getQueryKeys($this->exportData); - // we want to keep the order of the first row. So we will iterate on the first row of the results - $first_row = \count($this->result) > 0 ? $this->result[0] : []; - $header_line = []; - - if (true === $this->formatterData['numerotation']) { - $header_line[] = $this->translator->trans('Number'); - } - - foreach ($first_row as $key => $value) { - $header_line[] = $this->translator->trans( - $this->getLabel($key, '_header') - ); - } - - if (\count($header_line) > 0) { - fputcsv($output, $header_line); - } - } -} diff --git a/src/Bundle/ChillMainBundle/Export/Formatter/CSVPivotedListFormatter.php b/src/Bundle/ChillMainBundle/Export/Formatter/CSVPivotedListFormatter.php deleted file mode 100644 index d830bf22a..000000000 --- a/src/Bundle/ChillMainBundle/Export/Formatter/CSVPivotedListFormatter.php +++ /dev/null @@ -1,217 +0,0 @@ -translator = $translatorInterface; - $this->exportManager = $exportManager; - } - - /** - * build a form, which will be used to collect data required for the execution - * of this formatter. - * - * @uses appendAggregatorForm - * - * @param type $exportAlias - */ - public function buildForm( - FormBuilderInterface $builder, - $exportAlias, - array $aggregatorAliases, - ): void { - $builder->add('numerotation', ChoiceType::class, [ - 'choices' => [ - 'yes' => true, - 'no' => false, - ], - 'expanded' => true, - 'multiple' => false, - 'label' => 'Add a number on first column', - 'data' => true, - ]); - } - - public function getFormDefaultData(array $aggregatorAliases): array - { - return ['numerotation' => true]; - } - - public function getName() - { - return 'CSV horizontal list'; - } - - /** - * Generate a response from the data collected on differents ExportElementInterface. - * - * @param mixed[] $result The result, as given by the ExportInterface - * @param mixed[] $formatterData collected from the current form - * @param string $exportAlias the id of the current export - * @param array $filtersData an array containing the filters data. The key are the filters id, and the value are the data - * @param array $aggregatorsData an array containing the aggregators data. The key are the filters id, and the value are the data - * - * @return Response The response to be shown - */ - public function getResponse( - $result, - $formatterData, - $exportAlias, - array $exportData, - array $filtersData, - array $aggregatorsData, - ): \Symfony\Component\HttpFoundation\Response { - $this->result = $result; - $this->exportAlias = $exportAlias; - $this->exportData = $exportData; - $this->formatterData = $formatterData; - - $output = fopen('php://output', 'wb'); - - $i = 1; - $lines = []; - $this->prepareHeaders($lines); - - foreach ($result as $row) { - $j = 0; - - if (true === $this->formatterData['numerotation']) { - $lines[$j][] = $i; - ++$j; - } - - foreach ($row as $key => $value) { - $lines[$j][] = $this->getLabel($key, $value); - ++$j; - } - ++$i; - } - - // adding the lines to the csv output - foreach ($lines as $line) { - fputcsv($output, $line); - } - - $csvContent = stream_get_contents($output); - fclose($output); - - $response = new Response(); - $response->setStatusCode(200); - $response->headers->set('Content-Type', 'text/csv; charset=utf-8'); - $response->headers->set('Content-Disposition', 'attachment; filename="export.csv"'); - - $response->setContent($csvContent); - - return $response; - } - - public function getType() - { - return FormatterInterface::TYPE_LIST; - } - - /** - * Give the label corresponding to the given key and value. - * - * @param string $key - * @param string $value - * - * @return string - * - * @throws \LogicException if the label is not found - */ - protected function getLabel($key, $value) - { - if (null === $this->labelsCache) { - $this->prepareCacheLabels(); - } - - return $this->labelsCache[$key]($value); - } - - /** - * Prepare the label cache which will be used by getLabel. This function - * should be called only once in the generation lifecycle. - */ - protected function prepareCacheLabels() - { - $export = $this->exportManager->getExport($this->exportAlias); - $keys = $export->getQueryKeys($this->exportData); - - foreach ($keys as $key) { - // get an array with all values for this key if possible - $values = \array_map(static fn ($v) => $v[$key], $this->result); - // store the label in the labelsCache property - $this->labelsCache[$key] = $export->getLabels($key, $values, $this->exportData); - } - } - - /** - * add the headers to lines array. - * - * @param array $lines the lines where the header will be added - */ - protected function prepareHeaders(array &$lines) - { - $keys = $this->exportManager->getExport($this->exportAlias)->getQueryKeys($this->exportData); - // we want to keep the order of the first row. So we will iterate on the first row of the results - $first_row = \count($this->result) > 0 ? $this->result[0] : []; - $header_line = []; - - if (true === $this->formatterData['numerotation']) { - $lines[] = [$this->translator->trans('Number')]; - } - - foreach ($first_row as $key => $value) { - $lines[] = [$this->getLabel($key, '_header')]; - } - } -} diff --git a/src/Bundle/ChillMainBundle/Export/Formatter/SpreadSheetFormatter.php b/src/Bundle/ChillMainBundle/Export/Formatter/SpreadSheetFormatter.php index cf15f5bbd..87c46dfbb 100644 --- a/src/Bundle/ChillMainBundle/Export/Formatter/SpreadSheetFormatter.php +++ b/src/Bundle/ChillMainBundle/Export/Formatter/SpreadSheetFormatter.php @@ -11,120 +11,26 @@ declare(strict_types=1); namespace Chill\MainBundle\Export\Formatter; -use Chill\MainBundle\Export\ExportManager; +use Chill\MainBundle\Export\ExportGenerationContext; +use Chill\MainBundle\Export\ExportInterface; +use Chill\MainBundle\Export\ExportManagerAwareInterface; +use Chill\MainBundle\Export\FormattedExportGeneration; use Chill\MainBundle\Export\FormatterInterface; +use Chill\MainBundle\Export\Helper\ExportManagerAwareTrait; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\Response; +use Symfony\Contracts\Translation\TranslatableInterface; use Symfony\Contracts\Translation\TranslatorInterface; -class SpreadSheetFormatter implements FormatterInterface +final class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInterface { - /** - * an array where keys are the aggregators aliases and - * values are the data. - * - * replaced when `getResponse` is called. - * - * @var type - */ - protected $aggregatorsData; + use ExportManagerAwareTrait; - /** - * The export. - * - * replaced when `getResponse` is called. - * - * @var \Chill\MainBundle\Export\ExportInterface - */ - protected $export; - - /** - * replaced when `getResponse` is called. - * - * @var type - */ - // protected $aggregators; - - /** - * array containing value of export form. - * - * replaced when `getResponse` is called. - * - * @var array - */ - protected $exportData; - - /** - * @var ExportManager - */ - protected $exportManager; - - /** - * replaced when `getResponse` is called. - * - * @var array - */ - protected $filtersData; - - /** - * replaced when `getResponse` is called. - * - * @var type - */ - protected $formatterData; - - /** - * The result, as returned by the export. - * - * replaced when `getResponse` is called. - * - * @var type - */ - protected $result; - - /** - * replaced when `getResponse` is called. - * - * @var array - */ - // protected $labels; - - /** - * temporary file to store spreadsheet. - * - * @var string - */ - protected $tempfile; - - /** - * @var TranslatorInterface - */ - protected $translator; - - /** - * cache for displayable result. - * - * This cache is reset when `getResponse` is called. - * - * The array's keys are the keys in the raw result, and - * values are the callable which will transform the raw result to - * displayable result. - */ - private ?array $cacheDisplayableResult = null; - - /** - * Whethe `cacheDisplayableResult` is initialized or not. - */ - private bool $cacheDisplayableResultIsInitialized = false; - - public function __construct(TranslatorInterface $translatorInterface, ExportManager $exportManager) - { - $this->translator = $translatorInterface; - $this->exportManager = $exportManager; - } + public function __construct(private readonly TranslatorInterface $translator) {} public function buildForm( FormBuilderInterface $builder, @@ -142,7 +48,7 @@ class SpreadSheetFormatter implements FormatterInterface ]); // ordering aggregators - $aggregators = $this->exportManager->getAggregators($aggregatorAliases); + $aggregators = $this->getExportManager()->getAggregators($aggregatorAliases); $nb = \count($aggregatorAliases); foreach ($aggregators as $alias => $aggregator) { @@ -155,11 +61,26 @@ class SpreadSheetFormatter implements FormatterInterface } } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return $formData; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return $formData; + } + public function getFormDefaultData(array $aggregatorAliases): array { $data = ['format' => 'xlsx']; - $aggregators = iterator_to_array($this->exportManager->getAggregators($aggregatorAliases)); + $aggregators = iterator_to_array($this->getExportManager()->getAggregators($aggregatorAliases)); foreach (array_keys($aggregators) as $index => $alias) { $data[$alias] = ['order' => $index + 1]; } @@ -172,6 +93,51 @@ class SpreadSheetFormatter implements FormatterInterface return 'SpreadSheet (xlsx, ods)'; } + public function generate( + $result, + $formatterData, + string $exportAlias, + array $exportData, + array $filtersData, + array $aggregatorsData, + ExportGenerationContext $context, + ) { + // Initialize local variables instead of class properties + /** @var ExportInterface $export */ + $export = $this->getExportManager()->getExport($exportAlias); + + // Initialize cache variables + $cacheDisplayableResult = $this->initializeDisplayable($result, $export, $exportData, $aggregatorsData); + + $tempfile = \tempnam(\sys_get_temp_dir(), ''); + + if (false === $tempfile) { + throw new \RuntimeException('Unable to create temporary file'); + } + + $this->generateContent( + $context, + $tempfile, + $result, + $formatterData, + $export, + $exportData, + $filtersData, + $aggregatorsData, + $cacheDisplayableResult, + ); + + $result = new FormattedExportGeneration( + file_get_contents($tempfile), + $this->getContentType($formatterData['format']), + ); + + // remove the temp file from disk + \unlink($tempfile); + + return $result; + } + public function getResponse( $result, $formatterData, @@ -179,44 +145,22 @@ class SpreadSheetFormatter implements FormatterInterface array $exportData, array $filtersData, array $aggregatorsData, + ExportGenerationContext $context, ): Response { - // store all data when the process is initiated - $this->result = $result; - $this->formatterData = $formatterData; - $this->export = $this->exportManager->getExport($exportAlias); - $this->exportData = $exportData; - $this->filtersData = $filtersData; - $this->aggregatorsData = $aggregatorsData; + $formattedResult = $this->generate($result, $formatterData, $exportAlias, $exportData, $filtersData, $aggregatorsData, $context); - // reset cache - $this->cacheDisplayableResult = []; - $this->cacheDisplayableResultIsInitialized = false; - - $response = new Response(); - $response->headers->set( - 'Content-Type', - $this->getContentType($this->formatterData['format']) - ); - - $this->tempfile = \tempnam(\sys_get_temp_dir(), ''); - $this->generateContent(); - - $f = \fopen($this->tempfile, 'rb'); - $response->setContent(\stream_get_contents($f)); - fclose($f); - - // remove the temp file from disk - \unlink($this->tempfile); + $response = new BinaryFileResponse($formattedResult->content); + $response->headers->set('Content-Type', $formattedResult->contentType); return $response; } - public function getType() + public function getType(): string { return 'tabular'; } - protected function addContentTable( + private function addContentTable( Worksheet $worksheet, $sortedResults, $line, @@ -238,20 +182,21 @@ class SpreadSheetFormatter implements FormatterInterface * * @return int the line number after the last description */ - protected function addFiltersDescription(Worksheet &$worksheet) + private function addFiltersDescription(Worksheet &$worksheet, ExportGenerationContext $context, array $filtersData) { $line = 3; - foreach ($this->filtersData as $alias => $data) { - $filter = $this->exportManager->getFilter($alias); - $description = $filter->describeAction($data, 'string'); - + foreach ($filtersData as $alias => $data) { + $filter = $this->getExportManager()->getFilter($alias); + $description = $filter->describeAction($data, $context); if (\is_array($description)) { $description = $this->translator ->trans( $description[0], - $description[1] ?? [] + $description[1] ?? [], ); + } elseif ($description instanceof TranslatableInterface) { + $description = $description->trans($this->translator, $this->translator->getLocale()); } $worksheet->setCellValue('A'.$line, $description); @@ -266,23 +211,23 @@ class SpreadSheetFormatter implements FormatterInterface * * return the line number where the next content (i.e. result) should * be appended. - * - * @param int $line - * - * @return int */ - protected function addHeaders( + private function addHeaders( Worksheet &$worksheet, array $globalKeys, - $line, - ) { + int $line, + array $cacheDisplayableResult = [], + ): int { // get the displayable form of headers $displayables = []; - foreach ($globalKeys as $key) { - $displayables[] = $this->translator->trans( - $this->getDisplayableResult($key, '_header') - ); + $displayable = $this->getDisplayableResult($key, '_header', $cacheDisplayableResult); + + if ($displayable instanceof TranslatableInterface) { + $displayables[] = $displayable->trans($this->translator, $this->translator->getLocale()); + } else { + $displayables[] = $this->translator->trans($this->getDisplayableResult($key, '_header', $cacheDisplayableResult)); + } } // add headers on worksheet @@ -299,9 +244,9 @@ class SpreadSheetFormatter implements FormatterInterface * Add the title to the worksheet and merge the cell containing * the title. */ - protected function addTitleToWorkSheet(Worksheet &$worksheet) + private function addTitleToWorkSheet(Worksheet &$worksheet, $export) { - $worksheet->setCellValue('A1', $this->getTitle()); + $worksheet->setCellValue('A1', $this->getTitle($export)); $worksheet->mergeCells('A1:G1'); } @@ -310,14 +255,14 @@ class SpreadSheetFormatter implements FormatterInterface * * @return array where 1st member is spreadsheet, 2nd is worksheet */ - protected function createSpreadsheet() + private function createSpreadsheet($export) { $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); $worksheet = $spreadsheet->getActiveSheet(); // setting the worksheet title and code name $worksheet - ->setTitle($this->getTitle()) + ->setTitle($this->getTitle($export)) ->setCodeName('result'); return [$spreadsheet, $worksheet]; @@ -326,29 +271,38 @@ class SpreadSheetFormatter implements FormatterInterface /** * Generate the content and write it to php://temp. */ - protected function generateContent() - { - [$spreadsheet, $worksheet] = $this->createSpreadsheet(); + private function generateContent( + ExportGenerationContext $context, + string $tempfile, + $result, + $formatterData, + $export, + array $exportData, + array $filtersData, + array $aggregatorsData, + array $cacheDisplayableResult, + ) { + [$spreadsheet, $worksheet] = $this->createSpreadsheet($export); - $this->addTitleToWorkSheet($worksheet); - $line = $this->addFiltersDescription($worksheet); + $this->addTitleToWorkSheet($worksheet, $export); + $line = $this->addFiltersDescription($worksheet, $context, $filtersData); - // at this point, we are going to sort retsults for an easier manipulation + // at this point, we are going to sort results for an easier manipulation [$sortedResult, $exportKeys, $aggregatorKeys, $globalKeys] = - $this->sortResult(); + $this->sortResult($result, $export, $exportData, $aggregatorsData, $formatterData, $cacheDisplayableResult); - $line = $this->addHeaders($worksheet, $globalKeys, $line); + $line = $this->addHeaders($worksheet, $globalKeys, $line, $cacheDisplayableResult); - $line = $this->addContentTable($worksheet, $sortedResult, $line); + $this->addContentTable($worksheet, $sortedResult, $line); - $writer = match ($this->formatterData['format']) { + $writer = match ($formatterData['format']) { 'ods' => \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Ods'), 'xlsx' => \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xlsx'), 'csv' => \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Csv'), default => throw new \LogicException(), }; - $writer->save($this->tempfile); + $writer->save($tempfile); } /** @@ -357,7 +311,7 @@ class SpreadSheetFormatter implements FormatterInterface * * @return string[] an array containing the keys of aggregators */ - protected function getAggregatorKeysSorted() + private function getAggregatorKeysSorted(array $aggregatorsData, array $formatterData) { // empty array for aggregators keys $keys = []; @@ -365,7 +319,7 @@ class SpreadSheetFormatter implements FormatterInterface // during sorting $aggregatorKeyAssociation = []; - foreach ($this->aggregatorsData as $alias => $data) { + foreach ($aggregatorsData as $alias => $data) { $aggregator = $this->exportManager->getAggregator($alias); $aggregatorsKeys = $aggregator->getQueryKeys($data); // append the keys from aggregator to the $keys existing array @@ -377,9 +331,9 @@ class SpreadSheetFormatter implements FormatterInterface } // sort the result using the form - usort($keys, function ($a, $b) use ($aggregatorKeyAssociation) { - $A = $this->formatterData[$aggregatorKeyAssociation[$a]]['order']; - $B = $this->formatterData[$aggregatorKeyAssociation[$b]]['order']; + usort($keys, function ($a, $b) use ($aggregatorKeyAssociation, $formatterData) { + $A = $formatterData[$aggregatorKeyAssociation[$a]]['order']; + $B = $formatterData[$aggregatorKeyAssociation[$b]]['order']; if ($A === $B) { return 0; @@ -395,7 +349,7 @@ class SpreadSheetFormatter implements FormatterInterface return $keys; } - protected function getContentType($format) + private function getContentType($format) { switch ($format) { case 'csv': @@ -412,25 +366,26 @@ class SpreadSheetFormatter implements FormatterInterface /** * Get the displayable result. - * - * @param string $key - * - * @return string */ - protected function getDisplayableResult($key, mixed $value): mixed - { - if (false === $this->cacheDisplayableResultIsInitialized) { - $this->initializeCache($key); - } - + private function getDisplayableResult( + string $key, + mixed $value, + array $cacheDisplayableResult, + ): string|TranslatableInterface|\DateTimeInterface|int|float|bool { $value ??= ''; - return \call_user_func($this->cacheDisplayableResult[$key], $value); + return \call_user_func($cacheDisplayableResult[$key], $value); } - protected function getTitle() + private function getTitle($export): string { - $title = $this->translator->trans($this->export->getTitle()); + $original = $export->getTitle(); + + if ($original instanceof TranslatableInterface) { + $title = $original->trans($this->translator, $this->translator->getLocale()); + } else { + $title = $this->translator->trans($original); + } if (30 < strlen($title)) { return substr($title, 0, 30).'…'; @@ -439,8 +394,13 @@ class SpreadSheetFormatter implements FormatterInterface return $title; } - protected function initializeCache($key) - { + private function initializeDisplayable( + $result, + ExportInterface $export, + array $exportData, + array $aggregatorsData, + ): array { + $cacheDisplayableResult = []; /* * this function follows the following steps : * @@ -453,13 +413,12 @@ class SpreadSheetFormatter implements FormatterInterface // 1. create an associative array with key and export / aggregator $keysExportElementAssociation = []; // keys for export - foreach ($this->export->getQueryKeys($this->exportData) as $key) { - $keysExportElementAssociation[$key] = [$this->export, - $this->exportData, ]; + foreach ($export->getQueryKeys($exportData) as $key) { + $keysExportElementAssociation[$key] = [$export, $exportData]; } // keys for aggregator - foreach ($this->aggregatorsData as $alias => $data) { - $aggregator = $this->exportManager->getAggregator($alias); + foreach ($aggregatorsData as $alias => $data) { + $aggregator = $this->getExportManager()->getAggregator($alias); foreach ($aggregator->getQueryKeys($data) as $key) { $keysExportElementAssociation[$key] = [$aggregator, $data]; @@ -471,7 +430,7 @@ class SpreadSheetFormatter implements FormatterInterface $allValues = []; // store all the values in an array - foreach ($this->result as $row) { + foreach ($result as $row) { foreach ($keys as $key) { $allValues[$key][] = $row[$key]; } @@ -482,15 +441,14 @@ class SpreadSheetFormatter implements FormatterInterface foreach ($keysExportElementAssociation as $key => [$element, $data]) { // handle the case when there is not results lines (query is empty) if ([] === $allValues) { - $this->cacheDisplayableResult[$key] = $element->getLabels($key, ['_header'], $data); + $cacheDisplayableResult[$key] = $element->getLabels($key, ['_header'], $data); } else { - $this->cacheDisplayableResult[$key] = + $cacheDisplayableResult[$key] = $element->getLabels($key, \array_unique($allValues[$key]), $data); } } - // the cache is initialized ! - $this->cacheDisplayableResultIsInitialized = true; + return $cacheDisplayableResult; } /** @@ -528,23 +486,28 @@ class SpreadSheetFormatter implements FormatterInterface * ) * ``` */ - protected function sortResult() - { + private function sortResult( + $result, + ExportInterface $export, + array $exportData, + array $aggregatorsData, + array $formatterData, + array $cacheDisplayableResult, + ) { // get the keys for each row - $exportKeys = $this->export->getQueryKeys($this->exportData); - $aggregatorKeys = $this->getAggregatorKeysSorted(); - + $exportKeys = $export->getQueryKeys($exportData); + $aggregatorKeys = $this->getAggregatorKeysSorted($aggregatorsData, $formatterData); $globalKeys = \array_merge($aggregatorKeys, $exportKeys); - $sortedResult = \array_map(function ($row) use ($globalKeys) { + $sortedResult = \array_map(function ($row) use ($globalKeys, $cacheDisplayableResult) { $newRow = []; foreach ($globalKeys as $key) { - $newRow[] = $this->getDisplayableResult($key, $row[$key]); + $newRow[] = $this->getDisplayableResult($key, $row[$key], $cacheDisplayableResult); } return $newRow; - }, $this->result); + }, $result); \array_multisort($sortedResult); diff --git a/src/Bundle/ChillMainBundle/Export/Formatter/SpreadsheetListFormatter.php b/src/Bundle/ChillMainBundle/Export/Formatter/SpreadsheetListFormatter.php index 1bb004012..27ee7f44c 100644 --- a/src/Bundle/ChillMainBundle/Export/Formatter/SpreadsheetListFormatter.php +++ b/src/Bundle/ChillMainBundle/Export/Formatter/SpreadsheetListFormatter.php @@ -11,67 +11,40 @@ declare(strict_types=1); namespace Chill\MainBundle\Export\Formatter; -use Chill\MainBundle\Export\ExportManager; +use Chill\MainBundle\Export\ExportGenerationContext; +use Chill\MainBundle\Export\ExportManagerAwareInterface; +use Chill\MainBundle\Export\FormattedExportGeneration; use Chill\MainBundle\Export\FormatterInterface; +use Chill\MainBundle\Export\Helper\ExportManagerAwareTrait; use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\Response; +use Symfony\Contracts\Translation\TranslatableInterface; use Symfony\Contracts\Translation\TranslatorInterface; -use function count; - -// command to get the report with curl : curl --user "center a_social:password" "http://localhost:8000/fr/exports/generate/count_person?export[filters][person_gender_filter][enabled]=&export[filters][person_nationality_filter][enabled]=&export[filters][person_nationality_filter][form][nationalities]=&export[aggregators][person_nationality_aggregator][order]=1&export[aggregators][person_nationality_aggregator][form][group_by_level]=country&export[submit]=&export[_token]=RHpjHl389GrK-bd6iY5NsEqrD5UKOTHH40QKE9J1edU" --globoff /** * Create a CSV List for the export. */ -class SpreadsheetListFormatter implements FormatterInterface +class SpreadsheetListFormatter implements FormatterInterface, ExportManagerAwareInterface { - protected $exportAlias; + use ExportManagerAwareTrait; - protected $exportData; - - /** - * @var ExportManager - */ - protected $exportManager; - - protected $formatterData; - - /** - * This variable cache the labels internally. - * - * @var string[] - */ - protected $labelsCache; - - protected $result; - - /** - * @var TranslatorInterface - */ - protected $translator; - - public function __construct(TranslatorInterface $translatorInterface, ExportManager $exportManager) - { - $this->translator = $translatorInterface; - $this->exportManager = $exportManager; - } + public function __construct(private readonly TranslatorInterface $translator) {} /** * build a form, which will be used to collect data required for the execution * of this formatter. * * @uses appendAggregatorForm - * - * @param string $exportAlias */ public function buildForm( FormBuilderInterface $builder, - $exportAlias, + string $exportAlias, array $aggregatorAliases, ): void { $builder @@ -93,58 +66,52 @@ class SpreadsheetListFormatter implements FormatterInterface ]); } + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return ['format' => $formData['format'], 'numerotation' => $formData['numerotation']]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return ['format' => $formData['format'], 'numerotation' => $formData['numerotation']]; + } + public function getFormDefaultData(array $aggregatorAliases): array { return ['numerotation' => true, 'format' => 'xlsx']; } - public function getName() + public function getName(): string|TranslatableInterface { return 'Spreadsheet list formatter (.xlsx, .ods)'; } - /** - * Generate a response from the data collected on differents ExportElementInterface. - * - * @param mixed[] $result The result, as given by the ExportInterface - * @param mixed[] $formatterData collected from the current form - * @param string $exportAlias the id of the current export - * @param array $filtersData an array containing the filters data. The key are the filters id, and the value are the data - * @param array $aggregatorsData an array containing the aggregators data. The key are the filters id, and the value are the data - * - * @return Response The response to be shown - */ - public function getResponse( - $result, - $formatterData, - $exportAlias, - array $exportData, - array $filtersData, - array $aggregatorsData, - ): \Symfony\Component\HttpFoundation\Response { - $this->result = $result; - $this->exportAlias = $exportAlias; - $this->exportData = $exportData; - $this->formatterData = $formatterData; - + public function generate($result, $formatterData, string $exportAlias, array $exportData, array $filtersData, array $aggregatorsData, ExportGenerationContext $context): FormattedExportGeneration + { $spreadsheet = new Spreadsheet(); $worksheet = $spreadsheet->getActiveSheet(); + $cacheLabels = $this->prepareCacheLabels($result, $exportAlias, $exportData); - $this->prepareHeaders($worksheet); + $this->prepareHeaders($cacheLabels, $worksheet, $result, $formatterData, $exportAlias, $exportData); $i = 1; foreach ($result as $row) { - if (true === $this->formatterData['numerotation']) { + if (true === $formatterData['numerotation']) { $worksheet->setCellValue('A'.($i + 1), (string) $i); } - $a = $this->formatterData['numerotation'] ? 'B' : 'A'; + $a = $formatterData['numerotation'] ? 'B' : 'A'; foreach ($row as $key => $value) { $row = $a.($i + 1); - $formattedValue = $this->getLabel($key, $value); + $formattedValue = $this->getLabel($cacheLabels, $key, $value, $result, $exportAlias, $exportData); if ($formattedValue instanceof \DateTimeInterface) { $worksheet->setCellValue($row, Date::PHPToExcel($formattedValue)); @@ -158,6 +125,8 @@ class SpreadsheetListFormatter implements FormatterInterface ->getNumberFormat() ->setFormatCode(NumberFormat::FORMAT_DATE_DATETIME); } + } elseif ($formattedValue instanceof TranslatableInterface) { + $worksheet->setCellValue($row, $formattedValue->trans($this->translator)); } else { $worksheet->setCellValue($row, $formattedValue); } @@ -167,7 +136,7 @@ class SpreadsheetListFormatter implements FormatterInterface ++$i; } - switch ($this->formatterData['format']) { + switch ($formatterData['format']) { case 'ods': $writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Ods'); $contentType = 'application/vnd.oasis.opendocument.spreadsheet'; @@ -190,26 +159,52 @@ class SpreadsheetListFormatter implements FormatterInterface default: // this should not happen // throw an exception to ensure that the error is catched - throw new \OutOfBoundsException('The format '.$this->formatterData['format'].' is not supported'); + throw new \OutOfBoundsException('The format '.$formatterData['format'].' is not supported'); } - $response = new Response(); - $response->headers->set('content-type', $contentType); - $tempfile = \tempnam(\sys_get_temp_dir(), ''); $writer->save($tempfile); - $f = \fopen($tempfile, 'rb'); - $response->setContent(\stream_get_contents($f)); - fclose($f); + $generated = new FormattedExportGeneration( + file_get_contents($tempfile), + $contentType, + ); // remove the temp file from disk \unlink($tempfile); + return $generated; + } + + /** + * Generate a response from the data collected on differents ExportElementInterface. + * + * @param mixed[] $result The result, as given by the ExportInterface + * @param mixed[] $formatterData collected from the current form + * @param string $exportAlias the id of the current export + * @param array $aggregatorsData an array containing the aggregators data. The key are the filters id, and the value are the data + * @param array $filtersData an array containing the filters data. The key are the filters id, and the value are the data + * + * @return Response The response to be shown + */ + public function getResponse( + $result, + $formatterData, + $exportAlias, + array $exportData, + array $filtersData, + array $aggregatorsData, + ExportGenerationContext $context, + ) { + $generated = $this->generate($result, $formatterData, $exportAlias, $exportData, $filtersData, $aggregatorsData, $context); + + $response = new BinaryFileResponse($generated->content); + $response->headers->set('Content-Type', $generated->contentType); + return $response; } - public function getType() + public function getType(): string { return FormatterInterface::TYPE_LIST; } @@ -217,34 +212,29 @@ class SpreadsheetListFormatter implements FormatterInterface /** * Give the label corresponding to the given key and value. * - * @param string $key - * @param string $value - * - * @return string + * @return string|\DateTimeInterface|int|float|TranslatableInterface|null * * @throws \LogicException if the label is not found */ - protected function getLabel($key, $value) + private function getLabel(array $labelsCache, $key, $value, array $result, string $exportAlias, array $exportData) { - if (null === $this->labelsCache) { - $this->prepareCacheLabels(); + if (!\array_key_exists($key, $labelsCache)) { + throw new \OutOfBoundsException(sprintf('The key "%s" is not present in the list of keys handled by this query. Check your `getKeys` and `getLabels` methods. Available keys are %s.', $key, \implode(', ', \array_keys($labelsCache)))); } - if (!\array_key_exists($key, $this->labelsCache)) { - throw new \OutOfBoundsException(sprintf('The key "%s" is not present in the list of keys handled by this query. Check your `getKeys` and `getLabels` methods. Available keys are %s.', $key, \implode(', ', \array_keys($this->labelsCache)))); - } - - return $this->labelsCache[$key]($value); + return $labelsCache[$key]($value); } /** - * Prepare the label cache which will be used by getLabel. This function - * should be called only once in the generation lifecycle. + * Prepare the label cache which will be used by getLabel. + * + * @return array The labels cache */ - protected function prepareCacheLabels() + private function prepareCacheLabels(array $result, string $exportAlias, array $exportData): array { - $export = $this->exportManager->getExport($this->exportAlias); - $keys = $export->getQueryKeys($this->exportData); + $labelsCache = []; + $export = $this->getExportManager()->getExport($exportAlias); + $keys = $export->getQueryKeys($exportData); foreach ($keys as $key) { // get an array with all values for this key if possible @@ -254,29 +244,31 @@ class SpreadsheetListFormatter implements FormatterInterface } return $v[$key]; - }, $this->result); - // store the label in the labelsCache property - $this->labelsCache[$key] = $export->getLabels($key, $values, $this->exportData); + }, $result); + // store the label in the labelsCache + $labelsCache[$key] = $export->getLabels($key, $values, $exportData); } + + return $labelsCache; } /** * add the headers to the csv file. */ - protected function prepareHeaders(Worksheet $worksheet) + protected function prepareHeaders(array $labelsCache, Worksheet $worksheet, array $result, array $formatterData, string $exportAlias, array $exportData) { - $keys = $this->exportManager->getExport($this->exportAlias)->getQueryKeys($this->exportData); + $keys = $this->getExportManager()->getExport($exportAlias)->getQueryKeys($exportData); // we want to keep the order of the first row. So we will iterate on the first row of the results - $first_row = \count($this->result) > 0 ? $this->result[0] : []; + $first_row = \count($result) > 0 ? $result[0] : []; $header_line = []; - if (true === $this->formatterData['numerotation']) { + if (true === $formatterData['numerotation']) { $header_line[] = $this->translator->trans('Number'); } foreach ($first_row as $key => $value) { $header_line[] = $this->translator->trans( - $this->getLabel($key, '_header') + $this->getLabel($labelsCache, $key, '_header', $result, $exportAlias, $exportData) ); } diff --git a/src/Bundle/ChillMainBundle/Export/FormatterInterface.php b/src/Bundle/ChillMainBundle/Export/FormatterInterface.php index 37ed788f4..ac9c3e903 100644 --- a/src/Bundle/ChillMainBundle/Export/FormatterInterface.php +++ b/src/Bundle/ChillMainBundle/Export/FormatterInterface.php @@ -12,7 +12,11 @@ declare(strict_types=1); namespace Chill\MainBundle\Export; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Contracts\Translation\TranslatableInterface; +/** + * @method generate($result, $formatterData, string $exportAlias, array $exportData, array $filtersData, array $aggregatorsData, ExportGenerationContext $context): FormattedExportGeneration + */ interface FormatterInterface { public const TYPE_LIST = 'list'; @@ -30,16 +34,16 @@ interface FormatterInterface */ public function buildForm( FormBuilderInterface $builder, - $exportAlias, + string $exportAlias, array $aggregatorAliases, - ); + ): void; /** * get the default data for the form build by buildForm. */ public function getFormDefaultData(array $aggregatorAliases): array; - public function getName(); + public function getName(): string|TranslatableInterface; /** * Generate a response from the data collected on differents ExportElementInterface. @@ -47,19 +51,28 @@ interface FormatterInterface * @param mixed[] $result The result, as given by the ExportInterface * @param mixed[] $formatterData collected from the current form * @param string $exportAlias the id of the current export - * @param array $filtersData an array containing the filters data. The key are the filters id, and the value are the data * @param array $aggregatorsData an array containing the aggregators data. The key are the filters id, and the value are the data + * @param array $filtersData an array containing the filters data. The key are the filters id, and the value are the data * * @return \Symfony\Component\HttpFoundation\Response The response to be shown + * + * @deprecated use generate instead */ public function getResponse( - $result, - $formatterData, - $exportAlias, + array $result, + array $formatterData, + string $exportAlias, array $exportData, array $filtersData, array $aggregatorsData, + ExportGenerationContext $context, ); - public function getType(); + public function getType(): string; + + public function normalizeFormData(array $formData): array; + + public function denormalizeFormData(array $formData, int $fromVersion): array; + + public function getNormalizationVersion(): int; } diff --git a/src/Bundle/ChillMainBundle/Export/Helper/ExportManagerAwareTrait.php b/src/Bundle/ChillMainBundle/Export/Helper/ExportManagerAwareTrait.php new file mode 100644 index 000000000..eb89f7970 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/Helper/ExportManagerAwareTrait.php @@ -0,0 +1,34 @@ +exportManager = $exportManager; + } + + public function getExportManager(): ExportManager + { + if (null === $this->exportManager) { + throw new ExportRuntimeException('ExportManager not set'); + } + + return $this->exportManager; + } +} diff --git a/src/Bundle/ChillMainBundle/Export/ListInterface.php b/src/Bundle/ChillMainBundle/Export/ListInterface.php index 9b88525ca..3fe9315ae 100644 --- a/src/Bundle/ChillMainBundle/Export/ListInterface.php +++ b/src/Bundle/ChillMainBundle/Export/ListInterface.php @@ -11,6 +11,9 @@ declare(strict_types=1); namespace Chill\MainBundle\Export; +use Doctrine\ORM\NativeQuery; +use Doctrine\ORM\QueryBuilder; + /** * Define methods to export list. * @@ -19,5 +22,10 @@ namespace Chill\MainBundle\Export; * (and list does not support aggregation on their data). * * When used, the `ExportManager` will not handle aggregator for this class. + * + * @template Q of QueryBuilder|NativeQuery + * @template D of array + * + * @template-extends ExportInterface */ interface ListInterface extends ExportInterface {} diff --git a/src/Bundle/ChillMainBundle/Export/Messenger/ExportRequestGenerationMessage.php b/src/Bundle/ChillMainBundle/Export/Messenger/ExportRequestGenerationMessage.php new file mode 100644 index 000000000..cb6c757b8 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/Messenger/ExportRequestGenerationMessage.php @@ -0,0 +1,31 @@ +id = $exportGeneration->getId(); + $this->userId = $user->getId(); + } +} diff --git a/src/Bundle/ChillMainBundle/Export/Messenger/ExportRequestGenerationMessageHandler.php b/src/Bundle/ChillMainBundle/Export/Messenger/ExportRequestGenerationMessageHandler.php new file mode 100644 index 000000000..94610a995 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/Messenger/ExportRequestGenerationMessageHandler.php @@ -0,0 +1,77 @@ +logger->info( + self::LOG_PREFIX.'Handle generation message', + [ + 'exportId' => (string) $exportRequestGenerationMessage->id, + ] + ); + + if (null === $exportGeneration = $this->repository->find($exportRequestGenerationMessage->id)) { + throw new \UnexpectedValueException('ExportRequestGenerationMessage not found'); + } + + if (null === $user = $this->userRepository->find($exportRequestGenerationMessage->userId)) { + throw new \UnexpectedValueException('User not found'); + } + + if (StoredObject::STATUS_PENDING !== $exportGeneration->getStatus()) { + throw new UnrecoverableMessageHandlingException('object already generated'); + } + + $generated = $this->exportGenerator->generate($exportGeneration->getExportAlias(), $exportGeneration->getOptions(), $user); + $this->storedObjectManager->write($exportGeneration->getStoredObject(), $generated->content, $generated->contentType); + $exportGeneration->getStoredObject()->setStatus(StoredObject::STATUS_READY); + + $this->entityManager->flush(); + + $end = microtime(true); + + $this->logger->notice(self::LOG_PREFIX.'Export generation successfully finished', [ + 'exportId' => (string) $exportRequestGenerationMessage->id, + 'exportAlias' => $exportGeneration->getExportAlias(), + 'full_generation_duration' => $end - $exportGeneration->getCreatedAt()->getTimestamp(), + 'message_handler_duration' => $end - $start, + ]); + } +} diff --git a/src/Bundle/ChillMainBundle/Export/Messenger/OnExportGenerationFails.php b/src/Bundle/ChillMainBundle/Export/Messenger/OnExportGenerationFails.php new file mode 100644 index 000000000..04c07f5cf --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/Messenger/OnExportGenerationFails.php @@ -0,0 +1,74 @@ + 'onMessageFailed', + ]; + } + + public function onMessageFailed(WorkerMessageFailedEvent $event): void + { + if ($event->willRetry()) { + return; + } + + $message = $event->getEnvelope()->getMessage(); + + if (!$message instanceof ExportRequestGenerationMessage) { + return; + } + + if (null === $exportGeneration = $this->repository->find($message->id)) { + throw new \UnexpectedValueException('ExportRequestGenerationMessage not found'); + } + + $this->logger->error(self::LOG_PREFIX.'ExportRequestGenerationMessage failed to execute generation', [ + 'exportId' => (string) $message->id, + 'userId' => $message->userId, + 'alias' => $exportGeneration->getExportAlias(), + 'throwable_message' => $event->getThrowable()->getMessage(), + 'throwable_trace' => $event->getThrowable()->getTraceAsString(), + 'throwable' => $event->getThrowable()::class, + 'full_generation_duration_failure' => microtime(true) - $exportGeneration->getCreatedAt()->getTimestamp(), + ]); + + $this->markObjectAsFailed($event, $exportGeneration); + $this->entityManager->flush(); + } + + private function markObjectAsFailed(WorkerMessageFailedEvent $event, ExportGeneration $exportGeneration): void + { + $exportGeneration->getStoredObject()->addGenerationErrors($event->getThrowable()->getMessage()); + $exportGeneration->getStoredObject()->setStatus(StoredObject::STATUS_FAILURE); + } +} diff --git a/src/Bundle/ChillMainBundle/Export/Messenger/RemoveExportGenerationMessage.php b/src/Bundle/ChillMainBundle/Export/Messenger/RemoveExportGenerationMessage.php new file mode 100644 index 000000000..31e68a82d --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/Messenger/RemoveExportGenerationMessage.php @@ -0,0 +1,24 @@ +exportGenerationId = $exportGeneration->getId()->toString(); + } +} diff --git a/src/Bundle/ChillMainBundle/Export/Messenger/RemoveExportGenerationMessageHandler.php b/src/Bundle/ChillMainBundle/Export/Messenger/RemoveExportGenerationMessageHandler.php new file mode 100644 index 000000000..c1f59a8fd --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/Messenger/RemoveExportGenerationMessageHandler.php @@ -0,0 +1,49 @@ +exportGenerationRepository->find($message->exportGenerationId); + + if (null === $exportGeneration) { + $this->logger->error(self::LOG_PREFIX.'ExportGeneration not found'); + throw new UnrecoverableMessageHandlingException(self::LOG_PREFIX.'ExportGeneration not found'); + } + + $storedObject = $exportGeneration->getStoredObject(); + $storedObject->setDeleteAt($this->clock->now()); + + $this->entityManager->remove($exportGeneration); + $this->entityManager->flush(); + } +} diff --git a/src/Bundle/ChillMainBundle/Export/Migrator/SavedExportOptionsMigrator.php b/src/Bundle/ChillMainBundle/Export/Migrator/SavedExportOptionsMigrator.php new file mode 100644 index 000000000..36e9c5f7d --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/Migrator/SavedExportOptionsMigrator.php @@ -0,0 +1,124 @@ + array_map( + self::mapFormData(...), + array_filter( + $fromOptions['export']['export']['export'] ?? [], + static fn (string $key) => !in_array($key, ['filters', 'aggregators', 'pick_formatter'], true), + ARRAY_FILTER_USE_KEY, + ), + ), + 'version' => 1, + ]; + + $to['pick_formatter'] = $fromOptions['export']['export']['pick_formatter']['alias'] ?? null; + $to['centers'] = [ + 'centers' => array_values(array_map(static fn ($id) => (int) $id, $fromOptions['centers']['centers']['c'] ?? $fromOptions['centers']['centers']['center'] ?? [])), + 'regroupments' => array_values(array_map(static fn ($id) => (int) $id, $fromOptions['centers']['centers']['regroupment'] ?? [])), + ]; + $to['formatter'] = [ + 'form' => $fromOptions['formatter']['formatter'] ?? [], + 'version' => 1, + ]; + + return $to; + } + + private static function mapEnabledStatus(array $modifiersData): array + { + if ('1' === ($modifiersData['enabled'] ?? '0')) { + return [ + 'form' => array_map(self::mapFormData(...), $modifiersData['form'] ?? []), + 'version' => 1, + 'enabled' => true, + ]; + } + + return ['enabled' => false]; + } + + private static function mapFormData(array|string $formData): array|string|null + { + if (is_array($formData) && array_key_exists('roll', $formData)) { + return self::refactorRollingDate($formData); + } + + if (is_string($formData)) { + // we try different date formats + if (false !== \DateTimeImmutable::createFromFormat('d-m-Y', $formData)) { + return $formData; + } + if (false !== \DateTimeImmutable::createFromFormat('Y-m-d', $formData)) { + return $formData; + } + + // we try json content + try { + $data = json_decode($formData, true, 512, JSON_THROW_ON_ERROR); + + if (is_array($data)) { + if (array_key_exists('type', $data) && array_key_exists('id', $data) && in_array($data['type'], ['person', 'thirdParty', 'user'], true)) { + return $data['id']; + } + $response = []; + foreach ($data as $item) { + if (array_key_exists('type', $item) && array_key_exists('id', $item) && in_array($item['type'], ['person', 'thirdParty', 'user'], true)) { + $response[] = $item['id']; + } + } + if ([] !== $response) { + return $response; + } + } + } catch (\JsonException) { + return $formData; + } + } + + return $formData; + } + + private static function refactorRollingDate(array $formData): ?array + { + if ('' === $formData['roll']) { + return null; + } + + $fixedDate = null !== ($formData['fixedDate'] ?? null) && '' !== $formData['fixedDate'] ? + \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', sprintf('%s 00:00:00', $formData['fixedDate']), new \DateTimeZone(date_default_timezone_get())) : null; + + return (new RollingDate( + $formData['roll'], + $fixedDate, + ))->normalize(); + } +} diff --git a/src/Bundle/ChillMainBundle/Export/ModifierInterface.php b/src/Bundle/ChillMainBundle/Export/ModifierInterface.php index d3ec9de83..9596e026b 100644 --- a/src/Bundle/ChillMainBundle/Export/ModifierInterface.php +++ b/src/Bundle/ChillMainBundle/Export/ModifierInterface.php @@ -37,12 +37,12 @@ interface ModifierInterface extends ExportElementInterface * @param QueryBuilder $qb the QueryBuilder initiated by the Export (and eventually modified by other Modifiers) * @param mixed[] $data the data from the Form (builded by buildForm) */ - public function alterQuery(QueryBuilder $qb, $data); + public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void; /** * On which type of Export this ModifiersInterface may apply. * * @return string the type on which the Modifiers apply */ - public function applyOn(); + public function applyOn(): string; } diff --git a/src/Bundle/ChillMainBundle/Export/SortExportElement.php b/src/Bundle/ChillMainBundle/Export/SortExportElement.php index 6228109ed..7fdac9be1 100644 --- a/src/Bundle/ChillMainBundle/Export/SortExportElement.php +++ b/src/Bundle/ChillMainBundle/Export/SortExportElement.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Export; +use Symfony\Contracts\Translation\TranslatableInterface; use Symfony\Contracts\Translation\TranslatorInterface; final readonly class SortExportElement @@ -19,12 +20,21 @@ final readonly class SortExportElement private TranslatorInterface $translator, ) {} + private function trans(string|TranslatableInterface $message): string + { + if ($message instanceof TranslatableInterface) { + return $message->trans($this->translator, $this->translator->getLocale()); + } + + return $this->translator->trans($message); + } + /** * @param array $elements */ public function sortFilters(array &$elements): void { - uasort($elements, fn (FilterInterface $a, FilterInterface $b) => $this->translator->trans($a->getTitle()) <=> $this->translator->trans($b->getTitle())); + uasort($elements, fn (FilterInterface $a, FilterInterface $b) => $this->trans($a->getTitle()) <=> $this->trans($b->getTitle())); } /** @@ -32,6 +42,6 @@ final readonly class SortExportElement */ public function sortAggregators(array &$elements): void { - uasort($elements, fn (AggregatorInterface $a, AggregatorInterface $b) => $this->translator->trans($a->getTitle()) <=> $this->translator->trans($b->getTitle())); + uasort($elements, fn (AggregatorInterface $a, AggregatorInterface $b) => $this->trans($a->getTitle()) <=> $this->trans($b->getTitle())); } } diff --git a/src/Bundle/ChillMainBundle/Form/DataMapper/ExportPickCenterDataMapper.php b/src/Bundle/ChillMainBundle/Form/DataMapper/ExportPickCenterDataMapper.php deleted file mode 100644 index 2a19c8e0b..000000000 --- a/src/Bundle/ChillMainBundle/Form/DataMapper/ExportPickCenterDataMapper.php +++ /dev/null @@ -1,56 +0,0 @@ - $form */ - $form = iterator_to_array($forms); - - $form['center']->setData($viewData); - - // NOTE: we do not map back the regroupments - } - - public function mapFormsToData(\Traversable $forms, &$viewData): void - { - /** @var array $forms */ - $forms = iterator_to_array($forms); - - $centers = []; - - foreach ($forms['center']->getData() as $center) { - $centers[spl_object_hash($center)] = $center; - } - - if (\array_key_exists('regroupment', $forms)) { - /** @var Regroupment $regroupment */ - foreach ($forms['regroupment']->getData() as $regroupment) { - foreach ($regroupment->getCenters() as $center) { - $centers[spl_object_hash($center)] = $center; - } - } - } - - $viewData = array_values($centers); - } -} diff --git a/src/Bundle/ChillMainBundle/Form/DataMapper/NotificationFlagDataMapper.php b/src/Bundle/ChillMainBundle/Form/DataMapper/NotificationFlagDataMapper.php new file mode 100644 index 000000000..d904ed5b5 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Form/DataMapper/NotificationFlagDataMapper.php @@ -0,0 +1,75 @@ +notificationFlagProviders as $flagProvider) { + $flag = $flagProvider->getFlag(); + + if (isset($formsArray[$flag])) { + $flagForm = $formsArray[$flag]; + + $immediateEmailChecked = in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $viewData[$flag] ?? [], true) + || !array_key_exists($flag, $viewData); + $dailyEmailChecked = in_array(User::NOTIF_FLAG_DAILY_DIGEST, $viewData[$flag] ?? [], true); + + if ($flagForm->has('immediate_email')) { + $flagForm->get('immediate_email')->setData($immediateEmailChecked); + } + if ($flagForm->has('daily_email')) { + $flagForm->get('daily_email')->setData($dailyEmailChecked); + } + } + } + } + + public function mapFormsToData($forms, &$viewData): void + { + $formsArray = iterator_to_array($forms); + $viewData = []; + + foreach ($this->notificationFlagProviders as $flagProvider) { + $flag = $flagProvider->getFlag(); + + if (isset($formsArray[$flag])) { + $flagForm = $formsArray[$flag]; + $viewData[$flag] = []; + + if (true === $flagForm['immediate_email']->getData()) { + $viewData[$flag][] = User::NOTIF_FLAG_IMMEDIATE_EMAIL; + } + + if (true === $flagForm['daily_email']->getData()) { + $viewData[$flag][] = User::NOTIF_FLAG_DAILY_DIGEST; + } + + if ([] === $viewData[$flag]) { + $viewData[$flag][] = User::NOTIF_FLAG_IMMEDIATE_EMAIL; + } + } + } + } +} diff --git a/src/Bundle/ChillMainBundle/Form/NotificationType.php b/src/Bundle/ChillMainBundle/Form/NotificationType.php index 79207cc04..0cc7e6aac 100644 --- a/src/Bundle/ChillMainBundle/Form/NotificationType.php +++ b/src/Bundle/ChillMainBundle/Form/NotificationType.php @@ -12,17 +12,12 @@ declare(strict_types=1); namespace Chill\MainBundle\Form; use Chill\MainBundle\Entity\Notification; -use Chill\MainBundle\Form\Type\ChillCollectionType; use Chill\MainBundle\Form\Type\ChillTextareaType; -use Chill\MainBundle\Form\Type\PickUserDynamicType; +use Chill\MainBundle\Form\Type\PickUserGroupOrUserDynamicType; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Constraints\Email; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\NotNull; class NotificationType extends AbstractType { @@ -33,29 +28,14 @@ class NotificationType extends AbstractType 'label' => 'Title', 'required' => true, ]) - ->add('addressees', PickUserDynamicType::class, [ + ->add('addressees', PickUserGroupOrUserDynamicType::class, [ 'multiple' => true, - 'required' => false, + 'label' => 'notification.Pick user or user group', + 'empty_data' => '[]', + 'required' => true, ]) ->add('message', ChillTextareaType::class, [ 'required' => false, - ]) - ->add('addressesEmails', ChillCollectionType::class, [ - 'label' => 'notification.dest by email', - 'help' => 'notification.dest by email help', - 'by_reference' => false, - 'allow_add' => true, - 'allow_delete' => true, - 'entry_type' => EmailType::class, - 'button_add_label' => 'notification.Add an email', - 'button_remove_label' => 'notification.Remove an email', - 'empty_collection_explain' => 'notification.Any email', - 'entry_options' => [ - 'constraints' => [ - new NotNull(), new NotBlank(), new Email(), - ], - 'label' => 'Email', - ], ]); } diff --git a/src/Bundle/ChillMainBundle/Form/SavedExportType.php b/src/Bundle/ChillMainBundle/Form/SavedExportType.php index b60d60e89..c7fa21f8b 100644 --- a/src/Bundle/ChillMainBundle/Form/SavedExportType.php +++ b/src/Bundle/ChillMainBundle/Form/SavedExportType.php @@ -13,6 +13,9 @@ namespace Chill\MainBundle\Form; use Chill\MainBundle\Entity\SavedExport; use Chill\MainBundle\Form\Type\ChillTextareaType; +use Chill\MainBundle\Form\Type\PickUserGroupOrUserDynamicType; +use Chill\MainBundle\Security\Authorization\SavedExportVoter; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; @@ -20,8 +23,12 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class SavedExportType extends AbstractType { + public function __construct(private readonly Security $security) {} + public function buildForm(FormBuilderInterface $builder, array $options): void { + $savedExport = $options['data']; + $builder ->add('title', TextType::class, [ 'required' => true, @@ -29,6 +36,14 @@ class SavedExportType extends AbstractType ->add('description', ChillTextareaType::class, [ 'required' => false, ]); + + if ($this->security->isGranted(SavedExportVoter::SHARE, $savedExport)) { + $builder->add('share', PickUserGroupOrUserDynamicType::class, [ + 'multiple' => true, + 'required' => false, + 'label' => 'saved_export.Share', + ]); + } } public function configureOptions(OptionsResolver $resolver): void diff --git a/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/EntityToJsonTransformer.php b/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/EntityToJsonTransformer.php index ee1de004b..cf78cd33d 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/EntityToJsonTransformer.php +++ b/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/EntityToJsonTransformer.php @@ -26,7 +26,7 @@ class EntityToJsonTransformer implements DataTransformerInterface { public function __construct(private readonly DenormalizerInterface $denormalizer, private readonly SerializerInterface $serializer, private readonly bool $multiple, private readonly string $type) {} - public function reverseTransform($value): mixed + public function reverseTransform($value) { if ('' === $value) { return $this->multiple ? [] : null; @@ -66,8 +66,11 @@ class EntityToJsonTransformer implements DataTransformerInterface ]); } - private function denormalizeOne(array $item): mixed + private function denormalizeOne(array|string $item) { + if ('me' === $item) { + return $item; + } if (!\array_key_exists('type', $item)) { throw new TransformationFailedException('the key "type" is missing on element'); } @@ -98,5 +101,6 @@ class EntityToJsonTransformer implements DataTransformerInterface 'json', $context, ); + } } diff --git a/src/Bundle/ChillMainBundle/Form/Type/Export/AggregatorType.php b/src/Bundle/ChillMainBundle/Form/Type/Export/AggregatorType.php index 72e501108..cc6e583d5 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Export/AggregatorType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Export/AggregatorType.php @@ -21,15 +21,18 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class AggregatorType extends AbstractType { + public const ENABLED_FIELD = 'enabled'; + public function buildForm(FormBuilderInterface $builder, array $options): void { $exportManager = $options['export_manager']; $aggregator = $exportManager->getAggregator($options['aggregator_alias']); $builder - ->add('enabled', CheckboxType::class, [ + ->add(self::ENABLED_FIELD, CheckboxType::class, [ 'value' => true, 'required' => false, + 'disabled' => $options['disable_enable_field'], ]); $aggregatorFormBuilder = $builder->create('form', FormType::class, [ @@ -53,6 +56,7 @@ class AggregatorType extends AbstractType { $resolver->setRequired('aggregator_alias') ->setRequired('export_manager') + ->setDefault('disable_enable_field', false) ->setDefault('compound', true) ->setDefault('error_bubbling', false); } diff --git a/src/Bundle/ChillMainBundle/Form/Type/Export/ExportType.php b/src/Bundle/ChillMainBundle/Form/Type/Export/ExportType.php index 7f523ab32..48d8da3fc 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Export/ExportType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Export/ExportType.php @@ -35,7 +35,7 @@ class ExportType extends AbstractType public function __construct( private readonly ExportManager $exportManager, private readonly SortExportElement $sortExportElement, - protected ParameterBagInterface $parameterBag, + ParameterBagInterface $parameterBag, ) { $this->personFieldsConfig = $parameterBag->get('chill_person.person_fields'); } @@ -43,6 +43,8 @@ class ExportType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { $export = $this->exportManager->getExport($options['export_alias']); + /** @var bool $canEditFull */ + $canEditFull = $options['can_edit_full']; $exportOptions = [ 'compound' => true, @@ -59,8 +61,18 @@ class ExportType extends AbstractType if ($export instanceof \Chill\MainBundle\Export\ExportInterface) { // add filters - $filters = $this->exportManager->getFiltersApplyingOn($export, $options['picked_centers']); + $filterAliases = $options['allowed_filters']; + $filters = []; + if (is_iterable($filterAliases)) { + foreach ($filterAliases as $alias => $filter) { + $filters[$alias] = $filter; + } + } else { + $filters = $this->exportManager->getFiltersApplyingOn($export, $options['picked_centers']); + } + $this->sortExportElement->sortFilters($filters); + $filterBuilder = $builder->create(self::FILTER_KEY, FormType::class, ['compound' => true]); foreach ($filters as $alias => $filter) { @@ -70,15 +82,26 @@ class ExportType extends AbstractType 'constraints' => [ new ExportElementConstraint(['element' => $filter]), ], + 'disable_enable_field' => !$canEditFull, ]); } $builder->add($filterBuilder); // add aggregators - $aggregators = $this->exportManager - ->getAggregatorsApplyingOn($export, $options['picked_centers']); + $aggregatorsAliases = $options['allowed_aggregators']; + $aggregators = []; + if (is_iterable($aggregatorsAliases)) { + foreach ($aggregatorsAliases as $alias => $aggregator) { + $aggregators[$alias] = $aggregator; + } + } else { + $aggregators = $this->exportManager + ->getAggregatorsApplyingOn($export, $options['picked_centers']); + } + $this->sortExportElement->sortAggregators($aggregators); + $aggregatorBuilder = $builder->create( self::AGGREGATOR_KEY, FormType::class, @@ -96,11 +119,11 @@ class ExportType extends AbstractType } } - $aggregatorBuilder->add($alias, AggregatorType::class, [ 'aggregator_alias' => $alias, 'export_manager' => $this->exportManager, 'label' => $aggregator->getTitle(), + 'disable_enable_field' => !$canEditFull, 'constraints' => [ new ExportElementConstraint(['element' => $aggregator]), ], @@ -125,8 +148,13 @@ class ExportType extends AbstractType public function configureOptions(OptionsResolver $resolver): void { - $resolver->setRequired(['export_alias', 'picked_centers']) + $resolver->setRequired(['export_alias', 'picked_centers', 'can_edit_full']) ->setAllowedTypes('export_alias', ['string']) + ->setAllowedValues('can_edit_full', [true, false]) + ->setDefault('allowed_filters', null) + ->setAllowedTypes('allowed_filters', ['iterable', 'null']) + ->setDefault('allowed_aggregators', null) + ->setAllowedTypes('allowed_aggregators', ['iterable', 'null']) ->setDefault('compound', true) ->setDefault('constraints', [ // new \Chill\MainBundle\Validator\Constraints\Export\ExportElementConstraint() diff --git a/src/Bundle/ChillMainBundle/Form/Type/Export/FilterType.php b/src/Bundle/ChillMainBundle/Form/Type/Export/FilterType.php index aa54e2e50..df956b543 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Export/FilterType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Export/FilterType.php @@ -34,6 +34,7 @@ class FilterType extends AbstractType ->add(self::ENABLED_FIELD, CheckboxType::class, [ 'value' => true, 'required' => false, + 'disabled' => $options['disable_enable_field'], ]); $filterFormBuilder = $builder->create('form', FormType::class, [ @@ -58,6 +59,7 @@ class FilterType extends AbstractType $resolver ->setRequired('filter') ->setAllowedTypes('filter', [FilterInterface::class]) + ->setDefault('disable_enable_field', false) ->setDefault('compound', true) ->setDefault('error_bubbling', false); } diff --git a/src/Bundle/ChillMainBundle/Form/Type/Export/PickCenterType.php b/src/Bundle/ChillMainBundle/Form/Type/Export/PickCenterType.php index 942101f89..348ea415c 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Export/PickCenterType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Export/PickCenterType.php @@ -14,9 +14,9 @@ namespace Chill\MainBundle\Form\Type\Export; use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\Regroupment; use Chill\MainBundle\Export\ExportManager; -use Chill\MainBundle\Form\DataMapper\ExportPickCenterDataMapper; use Chill\MainBundle\Repository\RegroupmentRepository; use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface; +use Chill\MainBundle\Service\Regroupement\RegroupementFiltering; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; @@ -27,27 +27,26 @@ use Symfony\Component\OptionsResolver\OptionsResolver; */ final class PickCenterType extends AbstractType { - public const CENTERS_IDENTIFIERS = 'c'; - public function __construct( private readonly ExportManager $exportManager, private readonly RegroupmentRepository $regroupmentRepository, private readonly AuthorizationHelperForCurrentUserInterface $authorizationHelper, + private readonly RegroupementFiltering $regroupementFiltering, ) {} public function buildForm(FormBuilderInterface $builder, array $options): void { $export = $this->exportManager->getExport($options['export_alias']); $centers = $this->authorizationHelper->getReachableCenters( - $export->requiredRole() + $export->requiredRole(), ); $centersActive = array_filter($centers, fn (Center $c) => $c->getIsActive()); // order alphabetically - usort($centersActive, fn (Center $a, Center $b) => $a->getCenter() <=> $b->getName()); + usort($centersActive, fn (Center $a, Center $b) => $a->getName() <=> $b->getName()); - $builder->add('center', EntityType::class, [ + $builder->add('centers', EntityType::class, [ 'class' => Center::class, 'choices' => $centersActive, 'label' => 'center', @@ -56,18 +55,22 @@ final class PickCenterType extends AbstractType 'choice_label' => static fn (Center $c) => $c->getName(), ]); - if (\count($this->regroupmentRepository->findAllActive()) > 0) { - $builder->add('regroupment', EntityType::class, [ + $groups = $this->regroupementFiltering + ->filterContainsAtLeastOneCenter($this->regroupmentRepository->findAllActive(), $centersActive); + + // order alphabetically + usort($groups, fn (Regroupment $a, Regroupment $b) => $a->getName() <=> $b->getName()); + + if (\count($groups) > 0) { + $builder->add('regroupments', EntityType::class, [ 'class' => Regroupment::class, 'label' => 'regroupment', 'multiple' => true, 'expanded' => true, - 'choices' => $this->regroupmentRepository->findAllActive(), + 'choices' => $groups, 'choice_label' => static fn (Regroupment $r) => $r->getName(), ]); } - - $builder->setDataMapper(new ExportPickCenterDataMapper()); } public function configureOptions(OptionsResolver $resolver): void diff --git a/src/Bundle/ChillMainBundle/Form/Type/NotificationFlagsType.php b/src/Bundle/ChillMainBundle/Form/Type/NotificationFlagsType.php new file mode 100644 index 000000000..4535a4815 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Form/Type/NotificationFlagsType.php @@ -0,0 +1,63 @@ +notificationFlagProviders = $notificationFlagManager->getAllNotificationFlagProviders(); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->setDataMapper(new NotificationFlagDataMapper($this->notificationFlagProviders)); + + foreach ($this->notificationFlagProviders as $flagProvider) { + $flag = $flagProvider->getFlag(); + $builder->add($flag, FormType::class, [ + 'label' => $flagProvider->getLabel(), + 'required' => false, + ]); + + $builder->get($flag) + ->add('immediate_email', CheckboxType::class, [ + 'label' => false, + 'required' => false, + 'mapped' => false, + ]) + ->add('daily_email', CheckboxType::class, [ + 'label' => false, + 'required' => false, + 'mapped' => false, + ]) + ; + } + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => null, + ]); + } +} diff --git a/src/Bundle/ChillMainBundle/Form/Type/PickUserOrMeDynamicType.php b/src/Bundle/ChillMainBundle/Form/Type/PickUserOrMeDynamicType.php new file mode 100644 index 000000000..93ecd3f4e --- /dev/null +++ b/src/Bundle/ChillMainBundle/Form/Type/PickUserOrMeDynamicType.php @@ -0,0 +1,82 @@ +addViewTransformer(new EntityToJsonTransformer($this->denormalizer, $this->serializer, $options['multiple'], 'user')); + } + + public function buildView(FormView $view, FormInterface $form, array $options) + { + $view->vars['multiple'] = $options['multiple']; + $view->vars['types'] = ['user']; + $view->vars['uniqid'] = uniqid('pick_user_or_me_dyn'); + $view->vars['suggested'] = []; + $view->vars['as_id'] = true === $options['as_id'] ? '1' : '0'; + $view->vars['submit_on_adding_new_entity'] = true === $options['submit_on_adding_new_entity'] ? '1' : '0'; + + foreach ($options['suggested'] as $user) { + $view->vars['suggested'][] = $this->normalizer->normalize($user, 'json', ['groups' => 'read']); + } + // $user = /* should come from context */ $options['context']; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver + ->setDefault('multiple', false) + ->setAllowedTypes('multiple', ['bool']) + ->setDefault('compound', false) + ->setDefault('suggested', []) + // if set to true, only the id will be set inside the content. The denormalization will not work. + ->setDefault('as_id', false) + ->setAllowedTypes('as_id', ['bool']) + ->setDefault('submit_on_adding_new_entity', false) + ->setAllowedTypes('submit_on_adding_new_entity', ['bool']); + } + + public function getBlockPrefix() + { + return 'pick_entity_dynamic'; + } +} diff --git a/src/Bundle/ChillMainBundle/Form/UserGroupType.php b/src/Bundle/ChillMainBundle/Form/UserGroupType.php index 3bf06af1c..e64c37f81 100644 --- a/src/Bundle/ChillMainBundle/Form/UserGroupType.php +++ b/src/Bundle/ChillMainBundle/Form/UserGroupType.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Form; +use Chill\MainBundle\Entity\UserGroup; use Chill\MainBundle\Form\Type\PickUserDynamicType; use Chill\MainBundle\Form\Type\TranslatableStringFormType; use Symfony\Component\Form\AbstractType; @@ -23,6 +24,9 @@ class UserGroupType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { + /** @var UserGroup $userGroup */ + $userGroup = $options['data']; + $builder ->add('label', TranslatableStringFormType::class, [ 'label' => 'user_group.Label', @@ -46,20 +50,25 @@ class UserGroupType extends AbstractType 'help' => 'user_group.ExcludeKeyHelp', 'required' => false, 'empty_data' => '', - ]) - ->add('users', PickUserDynamicType::class, [ - 'label' => 'user_group.Users', - 'multiple' => true, - 'required' => false, - 'empty_data' => [], - ]) - ->add('adminUsers', PickUserDynamicType::class, [ - 'label' => 'user_group.adminUsers', - 'multiple' => true, - 'required' => false, - 'empty_data' => [], - 'help' => 'user_group.adminUsersHelp', - ]) - ; + ]); + + if (!$userGroup->hasUserJob()) { + $builder + ->add('users', PickUserDynamicType::class, [ + 'label' => 'user_group.Users', + 'multiple' => true, + 'required' => false, + 'empty_data' => [], + ]) + ->add('adminUsers', PickUserDynamicType::class, [ + 'label' => 'user_group.adminUsers', + 'multiple' => true, + 'required' => false, + 'empty_data' => [], + 'help' => 'user_group.adminUsersHelp', + ]) + ; + } + } } diff --git a/src/Bundle/ChillMainBundle/Form/UserProfileType.php b/src/Bundle/ChillMainBundle/Form/UserProfileType.php new file mode 100644 index 000000000..f9fa65991 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Form/UserProfileType.php @@ -0,0 +1,41 @@ +add('phonenumber', ChillPhoneNumberType::class, [ + 'required' => false, + ]) + ->add('notificationFlags', NotificationFlagsType::class, [ + 'label' => false, + 'mapped' => false, + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => \Chill\MainBundle\Entity\User::class, + ]); + } +} diff --git a/src/Bundle/ChillMainBundle/Notification/Email/DailyNotificationDigestCronjob.php b/src/Bundle/ChillMainBundle/Notification/Email/DailyNotificationDigestCronjob.php new file mode 100644 index 000000000..5ed6696f7 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Notification/Email/DailyNotificationDigestCronjob.php @@ -0,0 +1,102 @@ +clock->now(); + + if (null !== $cronJobExecution && $now->sub(new \DateInterval('PT23H45M')) < $cronJobExecution->getLastStart()) { + return false; + } + + // Run between 6 and 9 AM + return in_array((int) $now->format('H'), [6, 7, 8], true); + } + + public function getKey(): string + { + return 'daily-notification-digest'; + } + + /** + * @throws \DateInvalidOperationException + * @throws Exception + */ + public function run(array $lastExecutionData): ?array + { + $now = $this->clock->now(); + if (isset($lastExecutionData['last_execution'])) { + $lastExecution = \DateTimeImmutable::createFromFormat( + \DateTimeImmutable::ATOM, + $lastExecutionData['last_execution'] + ); + } else { + $lastExecution = $now->sub(new \DateInterval('P1D')); + } + + // Get distinct users who received notifications since the last execution + $sql = <<<'SQL' + SELECT DISTINCT cmnau.user_id + FROM chill_main_notification cmn + JOIN chill_main_notification_addresses_user cmnau ON cmnau.notification_id = cmn.id + WHERE cmn.date >= :lastExecution AND cmn.date <= :now + SQL; + + $sqlStatement = $this->connection->prepare($sql); + $sqlStatement->bindValue('lastExecution', $lastExecution->format(\DateTimeInterface::RFC3339)); + $sqlStatement->bindValue('now', $now->format(\DateTimeInterface::RFC3339)); + $result = $sqlStatement->executeQuery(); + + $count = 0; + foreach ($result->fetchAllAssociative() as $row) { + $userId = (int) $row['user_id']; + + $message = new ScheduleDailyNotificationDigestMessage( + $userId, + $lastExecution, + $now + ); + + $this->messageBus->dispatch($message); + ++$count; + } + + $this->logger->info('[DailyNotificationDigestCronjob] Dispatched daily digest messages', [ + 'user_count' => $count, + 'last_execution' => $lastExecution->format('Y-m-d-H:i:s.u e'), + 'current_time' => $now->format('Y-m-d-H:i:s.u e'), + ]); + + return [ + 'last_execution' => $now->format('Y-m-d-H:i:s.u e'), + ]; + } +} diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/ScheduleDailyNotificationDigestHandler.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/ScheduleDailyNotificationDigestHandler.php new file mode 100644 index 000000000..0a6aef393 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/ScheduleDailyNotificationDigestHandler.php @@ -0,0 +1,75 @@ +getUserId(); + $lastExecutionDate = $message->getLastExecutionDateTime(); + $currentDate = $message->getCurrentDateTime(); + + $user = $this->userRepository->find($userId); + if (null === $user) { + $this->logger->warning('[ScheduleDailyNotificationDigestHandler] User not found', [ + 'user_id' => $userId, + ]); + + throw new \InvalidArgumentException(sprintf('User with ID %s not found', $userId)); + } + + // Get all notifications for this user between last execution and current date + $notifications = $this->notificationRepository->findNotificationsForUserBetweenDates( + $userId, + $lastExecutionDate, + $currentDate + ); + + // Filter out notifications that should be sent in a daily digest + $dailyNotifications = array_filter($notifications, fn ($notification) => $user->isNotificationDailyDigest($notification->getType())); + + if ([] === $dailyNotifications) { + $this->logger->info('[ScheduleDailyNotificationDigestHandler] No daily notifications found for user', [ + 'user_id' => $userId, + ]); + + return; + } + + $this->notificationMailer->sendDailyDigest($user, $dailyNotifications); + + $this->logger->info('[ScheduleDailyNotificationDigestHandler] Sent daily digest', [ + 'user_id' => $userId, + 'notification_count' => count($dailyNotifications), + ]); + } +} diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/SendImmediateNotificationEmailHandler.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/SendImmediateNotificationEmailHandler.php new file mode 100644 index 000000000..b27f16423 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/SendImmediateNotificationEmailHandler.php @@ -0,0 +1,68 @@ +notificationRepository->find($message->getNotificationId()); + $addressee = $this->userRepository->find($message->getAddresseeId()); + + if (null === $notification) { + $this->logger->error('[SendImmediateNotificationEmailHandler] Notification not found', [ + 'notification_id' => $message->getNotificationId(), + ]); + + throw new \InvalidArgumentException(sprintf('Notification with ID %s not found', $message->getNotificationId())); + } + + if (null === $addressee) { + $this->logger->error('[SendImmediateNotificationEmailHandler] Addressee not found', [ + 'addressee_id' => $message->getAddresseeId(), + ]); + + throw new \InvalidArgumentException(sprintf('User with ID %s not found', $message->getAddresseeId())); + } + + try { + $this->notificationMailer->sendEmailToAddressee($notification, $addressee); + } catch (\Exception $e) { + $this->logger->error('[SendImmediateNotificationEmailHandler] Failed to send email', [ + 'notification_id' => $message->getNotificationId(), + 'addressee_id' => $message->getAddresseeId(), + 'stacktrace' => $e->getTraceAsString(), + ]); + throw $e; + } + } +} diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/ScheduleDailyNotificationDigestMessage.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/ScheduleDailyNotificationDigestMessage.php new file mode 100644 index 000000000..335185503 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/ScheduleDailyNotificationDigestMessage.php @@ -0,0 +1,36 @@ +userId; + } + + public function getLastExecutionDateTime(): \DateTimeInterface + { + return $this->lastExecutionDate; + } + + public function getCurrentDateTime(): \DateTimeInterface + { + return $this->currentDate; + } +} diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/SendImmediateNotificationEmailMessage.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/SendImmediateNotificationEmailMessage.php new file mode 100644 index 000000000..fb9908b21 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/SendImmediateNotificationEmailMessage.php @@ -0,0 +1,30 @@ +notificationId; + } + + public function getAddresseeId(): int + { + return $this->addresseeId; + } +} diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php index 7b535f1a7..2f888ffd5 100644 --- a/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php @@ -13,22 +13,32 @@ namespace Chill\MainBundle\Notification\Email; use Chill\MainBundle\Entity\Notification; use Chill\MainBundle\Entity\NotificationComment; +use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage; use Doctrine\ORM\Event\PostPersistEventArgs; -use Doctrine\ORM\Event\PostUpdateEventArgs; use Psr\Log\LoggerInterface; use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mime\Email; +use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Contracts\Translation\TranslatorInterface; -class NotificationMailer +readonly class NotificationMailer { - public function __construct(private readonly MailerInterface $mailer, private readonly LoggerInterface $logger, private readonly TranslatorInterface $translator) {} + public function __construct( + private MailerInterface $mailer, + private LoggerInterface $logger, + private MessageBusInterface $messageBus, + private TranslatorInterface $translator, + ) {} public function postPersistComment(NotificationComment $comment, PostPersistEventArgs $eventArgs): void { - $dests = [$comment->getNotification()->getSender(), ...$comment->getNotification()->getAddressees()->toArray()]; + $dests = [ + $comment->getNotification()->getSender(), + ...$comment->getNotification()->getAddressees()->toArray(), + ]; $uniqueDests = []; foreach ($dests as $dest) { @@ -69,55 +79,147 @@ class NotificationMailer */ public function postPersistNotification(Notification $notification, PostPersistEventArgs $eventArgs): void { - $this->sendNotificationEmailsToAddresses($notification); + $this->sendNotificationEmailsToAddressees($notification); $this->sendNotificationEmailsToAddressesEmails($notification); } - public function postUpdateNotification(Notification $notification, PostUpdateEventArgs $eventArgs): void + private function sendNotificationEmailsToAddressees(Notification $notification): void { - $this->sendNotificationEmailsToAddressesEmails($notification); - } + if ('' === $notification->getType()) { + $this->logger->warning('[NotificationMailer] Notification has no type, skipping email processing', [ + 'notification_id' => $notification->getId(), + ]); - private function sendNotificationEmailsToAddresses(Notification $notification): void - { - foreach ($notification->getAddressees() as $addressee) { + return; + } + + foreach ($notification->getAllAddressees() as $addressee) { if (null === $addressee->getEmail()) { continue; } - if ($notification->isSystem()) { - $email = new Email(); - $email - ->text($notification->getMessage()); - } else { - $email = new TemplatedEmail(); - $email - ->textTemplate('@ChillMain/Notification/email_non_system_notification_content.fr.md.twig') - ->context([ - 'notification' => $notification, - 'dest' => $addressee, - ]); - } + $this->processNotificationForAddressee($notification, $addressee); + } + } + private function processNotificationForAddressee(Notification $notification, User $addressee): void + { + $notificationType = $notification->getType(); + + if ($addressee->isNotificationSendImmediately($notificationType)) { + $this->scheduleImmediateEmail($notification, $addressee); + } + } + + private function scheduleImmediateEmail(Notification $notification, User $addressee): void + { + $message = new SendImmediateNotificationEmailMessage( + $notification->getId(), + $addressee->getId() + ); + + $this->messageBus->dispatch($message); + + $this->logger->info('[NotificationMailer] Scheduled immediate email', [ + 'notification_id' => $notification->getId(), + 'addressee_email' => $addressee->getEmail(), + ]); + } + + /** + * This method sends the email but is now called by the immediate notification email message handler. + * + * @throws TransportExceptionInterface + */ + public function sendEmailToAddressee(Notification $notification, User $addressee): void + { + if (null === $addressee->getEmail()) { + return; + } + + if ($notification->isSystem()) { + $email = new Email(); + $email->text($notification->getMessage()); + } else { + $email = new TemplatedEmail(); $email - ->subject($notification->getTitle()) - ->to($addressee->getEmail()); - - try { - $this->mailer->send($email); - } catch (TransportExceptionInterface $e) { - $this->logger->warning('[NotificationMailer] could not send an email notification', [ - 'to' => $addressee->getEmail(), - 'error_message' => $e->getMessage(), - 'error_trace' => $e->getTraceAsString(), + ->textTemplate('@ChillMain/Notification/email_non_system_notification_content.fr.md.twig') + ->context([ + 'notification' => $notification, + 'dest' => $addressee, ]); - } + } + + $email + ->subject($notification->getTitle()) + ->to($addressee->getEmail()); + + try { + $this->mailer->send($email); + $this->logger->info('[NotificationMailer] Email sent successfully', [ + 'notification_id' => $notification->getId(), + 'addressee_email' => $addressee->getEmail(), + ]); + } catch (TransportExceptionInterface $e) { + $this->logger->warning('[NotificationMailer] Could not send an email notification', [ + 'to' => $addressee->getEmail(), + 'notification_id' => $notification->getId(), + 'error_message' => $e->getMessage(), + 'error_trace' => $e->getTraceAsString(), + ]); + throw $e; + } + } + + /** + * Send daily digest email with multiple notifications to a user. + * + * @throws TransportExceptionInterface + */ + public function sendDailyDigest(User $user, array $notifications): void + { + if (null === $user->getEmail() || [] === $notifications) { + return; + } + + $email = new TemplatedEmail(); + $email + ->htmlTemplate('@ChillMain/Notification/email_daily_digest.fr.md.twig') + ->context([ + 'user' => $user, + 'notifications' => $notifications, + 'notification_count' => count($notifications), + ]) + ->subject($this->translator->trans('notification.Daily Notification Digest')) + ->to($user->getEmail()); + + try { + $this->mailer->send($email); + $this->logger->info('[NotificationMailer] Daily digest email sent successfully', [ + 'user_email' => $user->getEmail(), + 'notification_count' => count($notifications), + ]); + } catch (TransportExceptionInterface $e) { + $this->logger->warning('[NotificationMailer] Could not send daily digest email', [ + 'to' => $user->getEmail(), + 'notification_count' => count($notifications), + 'error_message' => $e->getMessage(), + 'error_trace' => $e->getTraceAsString(), + ]); + throw $e; } } private function sendNotificationEmailsToAddressesEmails(Notification $notification): void { - foreach ($notification->getAddressesEmailsAdded() as $emailAddress) { + foreach ($notification->getAddresseeUserGroups() as $userGroup) { + + if (!$userGroup->hasEmail()) { + continue; + } + + $emailAddress = $userGroup->getEmail(); + $email = new TemplatedEmail(); $email ->textTemplate('@ChillMain/Notification/email_non_system_notification_content_to_email.fr.md.twig') diff --git a/src/Bundle/ChillMainBundle/Notification/FlagProviders/NotificationByUserFlagProvider.php b/src/Bundle/ChillMainBundle/Notification/FlagProviders/NotificationByUserFlagProvider.php new file mode 100644 index 000000000..887d7f3d1 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Notification/FlagProviders/NotificationByUserFlagProvider.php @@ -0,0 +1,30 @@ + + */ + private array $notificationFlagProviders; + + public function __construct( + iterable $notificationFlagProviders, + ) { + $this->notificationFlagProviders = iterator_to_array($notificationFlagProviders); + } + + public function getAllNotificationFlagProviders(): array + { + return $this->notificationFlagProviders; + } +} diff --git a/src/Bundle/ChillMainBundle/Repository/ExportGenerationRepository.php b/src/Bundle/ChillMainBundle/Repository/ExportGenerationRepository.php new file mode 100644 index 000000000..bb70a22ba --- /dev/null +++ b/src/Bundle/ChillMainBundle/Repository/ExportGenerationRepository.php @@ -0,0 +1,85 @@ + + * + * @implements AssociatedEntityToStoredObjectInterface + */ +class ExportGenerationRepository extends ServiceEntityRepository implements AssociatedEntityToStoredObjectInterface +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, ExportGeneration::class); + } + + public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?ExportGeneration + { + return $this->createQueryBuilder('e') + ->where('e.storedObject = :storedObject') + ->setParameter('storedObject', $storedObject) + ->getQuery() + ->getOneOrNullResult(); + } + + /** + * @return list + */ + public function findExportGenerationByAliasAndUser(string $alias, User $user, int $limit = 100, int $offset = 0): array + { + return $this->createQueryBuilder('e') + ->where('e.createdBy = :user') + ->andWhere('e.exportAlias LIKE :alias') + ->orderBy('e.createdAt', 'DESC') + ->setParameter('user', $user) + ->setParameter('alias', $alias) + ->setFirstResult($offset) + ->setMaxResults($limit) + ->getQuery() + ->getResult(); + } + + /** + * @return list + */ + public function findExportGenerationBySavedExportAndUser(SavedExport $savedExport, User $user, int $limit = 100, int $offset = 0): array + { + return $this->createQueryBuilder('e') + ->where('e.createdBy = :user') + ->andWhere('e.savedExport = :savedExport') + ->orderBy('e.createdAt', 'DESC') + ->setParameter('user', $user) + ->setParameter('savedExport', $savedExport) + ->setFirstResult($offset) + ->setMaxResults($limit) + ->getQuery() + ->getResult(); + } + + public function findExpiredExportGeneration(\DateTimeImmutable $atDate): iterable + { + return $this->createQueryBuilder('e') + ->where('e.deleteAt < :atDate') + ->setParameter('atDate', $atDate) + ->getQuery() + ->toIterable(); + } +} diff --git a/src/Bundle/ChillMainBundle/Repository/GeographicalUnitRepository.php b/src/Bundle/ChillMainBundle/Repository/GeographicalUnitRepository.php index 35ec31ff3..ec24421f8 100644 --- a/src/Bundle/ChillMainBundle/Repository/GeographicalUnitRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/GeographicalUnitRepository.php @@ -13,6 +13,7 @@ namespace Chill\MainBundle\Repository; use Chill\MainBundle\Entity\Address; use Chill\MainBundle\Entity\GeographicalUnit; +use Chill\MainBundle\Entity\GeographicalUnit\SimpleGeographicalUnitDTO; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Query\Expr\Join; @@ -42,7 +43,7 @@ final readonly class GeographicalUnitRepository implements GeographicalUnitRepos $qb = $this->buildQueryGeographicalUnitContainingAddress($address); return $qb - ->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', GeographicalUnit\SimpleGeographicalUnitDTO::class)) + ->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', SimpleGeographicalUnitDTO::class)) ->addOrderBy('IDENTITY(gu.layer)') ->addOrderBy('gu.unitName') ->getQuery() @@ -58,7 +59,7 @@ final readonly class GeographicalUnitRepository implements GeographicalUnitRepos ; return $qb - ->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', GeographicalUnit\SimpleGeographicalUnitDTO::class)) + ->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', SimpleGeographicalUnitDTO::class)) ->innerJoin(Address::class, 'address', Join::WITH, 'ST_CONTAINS(gu.geom, address.point) = TRUE') ->where($qb->expr()->eq('address', ':address')) ->setParameter('address', $address) @@ -70,6 +71,19 @@ final readonly class GeographicalUnitRepository implements GeographicalUnitRepos return $this->repository->find($id); } + public function findSimpleGeographicalUnit(int $id): ?SimpleGeographicalUnitDTO + { + $qb = $this->repository + ->createQueryBuilder('gu'); + + return $qb + ->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', SimpleGeographicalUnitDTO::class)) + ->where('gu.id = :id') + ->setParameter('id', $id) + ->getQuery() + ->getOneOrNullResult(); + } + /** * Will return only partial object, where the @see{GeographicalUnit::geom} property is not loaded. * @@ -79,7 +93,7 @@ final readonly class GeographicalUnitRepository implements GeographicalUnitRepos { return $this->repository ->createQueryBuilder('gu') - ->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', GeographicalUnit\SimpleGeographicalUnitDTO::class)) + ->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', SimpleGeographicalUnitDTO::class)) ->addOrderBy('IDENTITY(gu.layer)') ->addOrderBy('gu.unitName') ->getQuery() diff --git a/src/Bundle/ChillMainBundle/Repository/GeographicalUnitRepositoryInterface.php b/src/Bundle/ChillMainBundle/Repository/GeographicalUnitRepositoryInterface.php index b00b0cd98..695474036 100644 --- a/src/Bundle/ChillMainBundle/Repository/GeographicalUnitRepositoryInterface.php +++ b/src/Bundle/ChillMainBundle/Repository/GeographicalUnitRepositoryInterface.php @@ -27,4 +27,6 @@ interface GeographicalUnitRepositoryInterface extends ObjectRepository public function findGeographicalUnitContainingAddress(Address $address, int $offset = 0, int $limit = 50): array; public function countGeographicalUnitContainingAddress(Address $address): int; + + public function findSimpleGeographicalUnit(int $id): ?SimpleGeographicalUnitDTO; } diff --git a/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php b/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php index fb79a7397..99fb57094 100644 --- a/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php @@ -290,12 +290,19 @@ final class NotificationRepository implements ObjectRepository return $qb; } - private function queryByAddressee(User $addressee, bool $countQuery = false): QueryBuilder + private function queryByAddressee(User $addressee): QueryBuilder { $qb = $this->repository->createQueryBuilder('n'); $qb - ->where($qb->expr()->isMemberOf(':addressee', 'n.addressees')) + ->leftJoin('n.addresseeUserGroups', 'aug') + ->leftJoin('aug.users', 'ugu') + ->where( + $qb->expr()->orX( + $qb->expr()->isMemberOf(':addressee', 'n.addressees'), + $qb->expr()->eq('ugu.id', ':addressee') + ) + ) ->setParameter('addressee', $addressee); return $qb; @@ -393,4 +400,30 @@ final class NotificationRepository implements ObjectRepository return $nq->getResult(); } + + /** + * Find all notifications for a user that were created between two dates. + * + * @return array|Notification[] + */ + public function findNotificationsForUserBetweenDates(int $userId, \DateTimeInterface $startDate, \DateTimeInterface $endDate): array + { + $rsm = new Query\ResultSetMappingBuilder($this->em); + $rsm->addRootEntityFromClassMetadata(Notification::class, 'cmn'); + + $sql = 'SELECT '.$rsm->generateSelectClause(['cmn' => 'cmn']).' '. + 'FROM chill_main_notification cmn '. + 'JOIN chill_main_notification_addresses_user cmnau ON cmnau.notification_id = cmn.id '. + 'WHERE cmnau.user_id = :userId '. + 'AND cmn.date >= :startDate '. + 'AND cmn.date <= :endDate '. + 'ORDER BY cmn.date DESC'; + + $nq = $this->em->createNativeQuery($sql, $rsm) + ->setParameter('userId', $userId) + ->setParameter('startDate', $startDate, Types::DATETIME_MUTABLE) + ->setParameter('endDate', $endDate, Types::DATETIME_MUTABLE); + + return $nq->getResult(); + } } diff --git a/src/Bundle/ChillMainBundle/Repository/RegroupmentRepository.php b/src/Bundle/ChillMainBundle/Repository/RegroupmentRepository.php index 793015f5b..b78e5ccc1 100644 --- a/src/Bundle/ChillMainBundle/Repository/RegroupmentRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/RegroupmentRepository.php @@ -16,9 +16,8 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; -use Doctrine\Persistence\ObjectRepository; -final readonly class RegroupmentRepository implements ObjectRepository +final readonly class RegroupmentRepository implements RegroupmentRepositoryInterface { private EntityRepository $repository; diff --git a/src/Bundle/ChillMainBundle/Repository/RegroupmentRepositoryInterface.php b/src/Bundle/ChillMainBundle/Repository/RegroupmentRepositoryInterface.php new file mode 100644 index 000000000..501cee8c5 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Repository/RegroupmentRepositoryInterface.php @@ -0,0 +1,34 @@ + + */ +interface RegroupmentRepositoryInterface extends ObjectRepository +{ + /** + * @throws NonUniqueResultException + * @throws NoResultException + */ + public function findOneByName(string $name): ?Regroupment; + + /** + * @return array + */ + public function findRegroupmentAssociatedToNoCenter(): array; +} diff --git a/src/Bundle/ChillMainBundle/Repository/SavedExportOrExportGenerationRepository.php b/src/Bundle/ChillMainBundle/Repository/SavedExportOrExportGenerationRepository.php new file mode 100644 index 000000000..81174fd1e --- /dev/null +++ b/src/Bundle/ChillMainBundle/Repository/SavedExportOrExportGenerationRepository.php @@ -0,0 +1,32 @@ +savedExportRepository->find($uuid)) { + return $savedExport; + } + + return $this->exportGenerationRepository->find($uuid); + } +} diff --git a/src/Bundle/ChillMainBundle/Repository/SavedExportRepository.php b/src/Bundle/ChillMainBundle/Repository/SavedExportRepository.php index 702fdf7d6..9b0d5a2ef 100644 --- a/src/Bundle/ChillMainBundle/Repository/SavedExportRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/SavedExportRepository.php @@ -13,9 +13,12 @@ namespace Chill\MainBundle\Repository; use Chill\MainBundle\Entity\SavedExport; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\UserGroup; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; +use Symfony\Component\String\UnicodeString; /** * @implements ObjectRepository @@ -55,6 +58,51 @@ class SavedExportRepository implements SavedExportRepositoryInterface ->where($qb->expr()->eq('se.user', ':user')) ->setParameter('user', $user); + return $this->prepareResult($qb, $orderBy, $limit, $offset); + } + + public function findSharedWithUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null, array $filters = []): array + { + $qb = $this->repository->createQueryBuilder('se'); + + $qb + ->where( + $qb->expr()->orX( + $qb->expr()->eq('se.user', ':user'), + $qb->expr()->isMemberOf(':user', 'se.sharedWithUsers'), + $qb->expr()->exists( + sprintf('SELECT 1 FROM %s ug WHERE ug MEMBER OF se.sharedWithGroups AND :user MEMBER OF ug.users', UserGroup::class) + ) + ) + ) + ->setParameter('user', $user); + + foreach ($filters as $key => $filter) { + if (self::FILTER_TITLE === ($key & self::FILTER_TITLE) + || self::FILTER_DESCRIPTION === ($key & self::FILTER_DESCRIPTION)) { + $filter = new UnicodeString($filter); + + $i = 0; + foreach ($filter->split(' ') as $word) { + $orx = $qb->expr()->orX(); + if (self::FILTER_TITLE === ($key & self::FILTER_TITLE)) { + $orx->add($qb->expr()->like('LOWER(se.title)', 'LOWER(:qs'.$i.')')); + } + if (self::FILTER_DESCRIPTION === ($key & self::FILTER_DESCRIPTION)) { + $orx->add($qb->expr()->like('LOWER(se.description)', 'LOWER(:qs'.$i.')')); + } + $qb->andWhere($orx); + $qb->setParameter('qs'.$i, '%'.$word->trim().'%'); + ++$i; + } + } + } + + return $this->prepareResult($qb, $orderBy, $limit, $offset); + } + + private function prepareResult(QueryBuilder $qb, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array + { if (null !== $limit) { $qb->setMaxResults($limit); } diff --git a/src/Bundle/ChillMainBundle/Repository/SavedExportRepositoryInterface.php b/src/Bundle/ChillMainBundle/Repository/SavedExportRepositoryInterface.php index 3b168505f..c62e0f29c 100644 --- a/src/Bundle/ChillMainBundle/Repository/SavedExportRepositoryInterface.php +++ b/src/Bundle/ChillMainBundle/Repository/SavedExportRepositoryInterface.php @@ -20,6 +20,9 @@ use Doctrine\Persistence\ObjectRepository; */ interface SavedExportRepositoryInterface extends ObjectRepository { + public const FILTER_TITLE = 0x01; + public const FILTER_DESCRIPTION = 0x10; + public function find($id): ?SavedExport; /** @@ -34,6 +37,15 @@ interface SavedExportRepositoryInterface extends ObjectRepository */ public function findByUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array; + /** + * Get the saved export created by and the user and the ones shared with the user. + * + * @param array $filters filters where keys are one of the constant starting with FILTER_ + * + * @return list + */ + public function findSharedWithUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null, array $filters = []): array; + public function findOneBy(array $criteria): ?SavedExport; public function getClassName(): string; diff --git a/src/Bundle/ChillMainBundle/Repository/UserJobRepository.php b/src/Bundle/ChillMainBundle/Repository/UserJobRepository.php index 6b793a2c8..ff8e5ffd2 100644 --- a/src/Bundle/ChillMainBundle/Repository/UserJobRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/UserJobRepository.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Repository; +use Chill\MainBundle\Entity\UserGroup; use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Doctrine\ORM\EntityManagerInterface; @@ -30,9 +31,6 @@ readonly class UserJobRepository implements UserJobRepositoryInterface return $this->repository->find($id); } - /** - * @return array|UserJob[] - */ public function findAll(): array { return $this->repository->findAll(); @@ -56,12 +54,20 @@ readonly class UserJobRepository implements UserJobRepositoryInterface return $jobs; } - /** - * @param mixed|null $limit - * @param mixed|null $offset - * - * @return array|object[]|UserJob[] - */ + public function findAllNotAssociatedWithUserGroup(): array + { + $qb = $this->repository->createQueryBuilder('u'); + $qb->select('u'); + + $qb->where( + $qb->expr()->not( + $qb->expr()->exists(sprintf('SELECT 1 FROM %s ug WHERE ug.userJob = u', UserGroup::class)) + ) + ); + + return $qb->getQuery()->getResult(); + } + public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null) { return $this->repository->findBy($criteria, $orderBy, $limit, $offset); diff --git a/src/Bundle/ChillMainBundle/Repository/UserJobRepositoryInterface.php b/src/Bundle/ChillMainBundle/Repository/UserJobRepositoryInterface.php index 75c2a5671..df042101c 100644 --- a/src/Bundle/ChillMainBundle/Repository/UserJobRepositoryInterface.php +++ b/src/Bundle/ChillMainBundle/Repository/UserJobRepositoryInterface.php @@ -14,18 +14,15 @@ namespace Chill\MainBundle\Repository; use Chill\MainBundle\Entity\UserJob; use Doctrine\Persistence\ObjectRepository; +/** + * @template-extends ObjectRepository + */ interface UserJobRepositoryInterface extends ObjectRepository { public function find($id): ?UserJob; - /** - * @return array|UserJob[] - */ public function findAll(): array; - /** - * @return array|UserJob[] - */ public function findAllActive(): array; /** @@ -36,11 +33,14 @@ interface UserJobRepositoryInterface extends ObjectRepository public function findAllOrderedByName(): array; /** - * @param mixed|null $limit - * @param mixed|null $offset + * Find all the user job which are not related to a UserGroup. * - * @return array|object[]|UserJob[] + * This is useful for synchronizing UserGroups with jobs. + * + * @return list */ + public function findAllNotAssociatedWithUserGroup(): array; + public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null); public function findOneBy(array $criteria): ?UserJob; diff --git a/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts b/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts index b50bb5534..e8256b348 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts @@ -61,6 +61,9 @@ export interface ConflictHttpExceptionInterface /** * Generic api method that can be adapted to any fetch request + * + * This method is suitable make a single fetch. When performing a GET to fetch a list of elements, always consider pagination + * and use of the @link{fetchResults} method. */ export const makeFetch = ( method: "POST" | "GET" | "PUT" | "PATCH" | "DELETE", diff --git a/src/Bundle/ChillMainBundle/Resources/public/lib/api/export.ts b/src/Bundle/ChillMainBundle/Resources/public/lib/api/export.ts new file mode 100644 index 000000000..7e350b102 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/lib/api/export.ts @@ -0,0 +1,18 @@ +import { makeFetch } from "ChillMainAssets/lib/api/apiMethods"; +import { ExportGeneration } from "ChillMainAssets/types"; + +export const fetchExportGenerationStatus = async ( + exportGenerationId: string, +): Promise => + makeFetch( + "GET", + `/api/1.0/main/export-generation/${exportGenerationId}/object`, + ); + +export const generateFromSavedExport = async ( + savedExportUuid: string, +): Promise => + makeFetch( + "POST", + `/api/1.0/main/export/export-generation/create-from-saved-export/${savedExportUuid}`, + ); diff --git a/src/Bundle/ChillMainBundle/Resources/public/lib/api/return_path.ts b/src/Bundle/ChillMainBundle/Resources/public/lib/api/return_path.ts new file mode 100644 index 000000000..e444aa2e5 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/lib/api/return_path.ts @@ -0,0 +1,3 @@ +export function buildReturnPath(location: Location): string { + return location.pathname + location.search; +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/module/pick-entity/index.js b/src/Bundle/ChillMainBundle/Resources/public/module/pick-entity/index.js index 1b3ee4594..9f6bb753e 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/module/pick-entity/index.js +++ b/src/Bundle/ChillMainBundle/Resources/public/module/pick-entity/index.js @@ -12,6 +12,11 @@ function loadDynamicPicker(element) { let apps = element.querySelectorAll('[data-module="pick-dynamic"]'); apps.forEach(function (el) { + let suggested; + let as_id; + let submit_on_adding_new_entity; + let label; + let isCurrentUserPicker; const isMultiple = parseInt(el.dataset.multiple) === 1, uniqId = el.dataset.uniqid, input = element.querySelector( @@ -22,12 +27,13 @@ function loadDynamicPicker(element) { ? JSON.parse(input.value) : input.value === "[]" || input.value === "" ? null - : [JSON.parse(input.value)], - suggested = JSON.parse(el.dataset.suggested), - as_id = parseInt(el.dataset.asId) === 1, - submit_on_adding_new_entity = - parseInt(el.dataset.submitOnAddingNewEntity) === 1, - label = el.dataset.label; + : [JSON.parse(input.value)]; + suggested = JSON.parse(el.dataset.suggested); + as_id = parseInt(el.dataset.asId) === 1; + submit_on_adding_new_entity = + parseInt(el.dataset.submitOnAddingNewEntity) === 1; + label = el.dataset.label; + isCurrentUserPicker = uniqId.startsWith("pick_user_or_me_dyn"); if (!isMultiple) { if (input.value === "[]") { @@ -44,6 +50,7 @@ function loadDynamicPicker(element) { ':uniqid="uniqid" ' + ':suggested="notPickedSuggested" ' + ':label="label" ' + + ':isCurrentUserPicker="isCurrentUserPicker" ' + '@addNewEntity="addNewEntity" ' + '@removeEntity="removeEntity" ' + '@addNewEntityProcessEnded="addNewEntityProcessEnded"' + @@ -61,6 +68,7 @@ function loadDynamicPicker(element) { as_id, submit_on_adding_new_entity, label, + isCurrentUserPicker, }; }, computed: { @@ -89,7 +97,8 @@ function loadDynamicPicker(element) { const ids = this.picked.map((el) => el.id); input.value = ids.join(","); } - console.log(entity); + console.log(this.picked); + // console.log(entity); } } else { if ( diff --git a/src/Bundle/ChillMainBundle/Resources/public/page/export/download-export.js b/src/Bundle/ChillMainBundle/Resources/public/page/export/download-export.js deleted file mode 100644 index 164e8e97e..000000000 --- a/src/Bundle/ChillMainBundle/Resources/public/page/export/download-export.js +++ /dev/null @@ -1,14 +0,0 @@ -import { download_report } from "../../lib/download-report/download-report"; - -window.addEventListener("DOMContentLoaded", function (e) { - const export_generate_url = window.export_generate_url; - - if (typeof export_generate_url === "undefined") { - console.error("Alias not found!"); - throw new Error("Alias not found!"); - } - - const query = window.location.search, - container = document.querySelector("#download_container"); - download_report(export_generate_url + query.toString(), container); -}); diff --git a/src/Bundle/ChillMainBundle/Resources/public/types.ts b/src/Bundle/ChillMainBundle/Resources/public/types.ts index 90ddacf22..2cd83bc64 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/types.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/types.ts @@ -1,4 +1,5 @@ import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc"; +import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types"; export interface DateTime { datetime: string; @@ -200,3 +201,17 @@ export interface WorkflowAttachment { updatedBy: User | null; genericDoc: null | GenericDoc; } + +export interface ExportGeneration { + id: string; + type: "export_generation"; + exportAlias: string; + createdBy: User | null; + createdAt: DateTime | null; + status: StoredObjectStatus; + storedObject: StoredObject; +} + +export interface PrivateCommentEmbeddable { + comments: Record; +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/DownloadExport/App.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/DownloadExport/App.vue new file mode 100644 index 000000000..033eb6cac --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/DownloadExport/App.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/DownloadExport/index.ts b/src/Bundle/ChillMainBundle/Resources/public/vuejs/DownloadExport/index.ts new file mode 100644 index 000000000..e970e51b9 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/DownloadExport/index.ts @@ -0,0 +1,15 @@ +import { createApp } from "vue"; +import App from "./App.vue"; + +const el = document.getElementById("app"); + +if (null === el) { + console.error("div element app was not found"); + throw new Error("div element app was not found"); +} + +const exportGenerationId = el?.dataset.exportGenerationId as string; +const title = el?.dataset.exportTitle as string; +const createdDate = el?.dataset.exportGenerationDate as string; + +createApp(App, { exportGenerationId, title, createdDate }).mount(el); diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/PickEntity.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/PickEntity.vue index 56e324133..dde8c2dca 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/PickEntity.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/PickEntity.vue @@ -1,10 +1,26 @@ - + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/i18n.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/i18n.js index 8502bf53e..31e13bc88 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/i18n.js +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/i18n.js @@ -11,10 +11,12 @@ const appMessages = { user: "Utilisateurs", person: "Usagers", thirdparty: "Tiers", + acpw: "Action d'accompagnements", modal_title_one: "Indiquer un ", user_one: "Utilisateur", thirdparty_one: "Tiers", person_one: "Usager", + acpw_one: "Action d'accompagnement", }, }, }; diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/SavedExportButtons/App.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/SavedExportButtons/App.vue new file mode 100644 index 000000000..6f021ac80 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/SavedExportButtons/App.vue @@ -0,0 +1,18 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/SavedExportButtons/Component/GenerateButton.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/SavedExportButtons/Component/GenerateButton.vue new file mode 100644 index 000000000..f4dafd006 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/SavedExportButtons/Component/GenerateButton.vue @@ -0,0 +1,186 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/SavedExportButtons/index.ts b/src/Bundle/ChillMainBundle/Resources/public/vuejs/SavedExportButtons/index.ts new file mode 100644 index 000000000..518746dea --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/SavedExportButtons/index.ts @@ -0,0 +1,13 @@ +import { createApp } from "vue"; + +import App from "./App.vue"; + +const buttons = document.querySelectorAll( + "[data-generate-export-button]", +); + +buttons.forEach((button) => { + const savedExportUuid = button.dataset.savedExportUuid as string; + + createApp(App, { savedExportUuid, savedExportAlias: "" }).mount(button); +}); diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Entity/UserRenderBoxBadge.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Entity/UserRenderBoxBadge.vue index 4645f322d..63c43c37f 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Entity/UserRenderBoxBadge.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Entity/UserRenderBoxBadge.vue @@ -1,11 +1,11 @@