diff --git a/.changes/v2.16.2.md b/.changes/v2.16.2.md new file mode 100644 index 000000000..3aa01ff16 --- /dev/null +++ b/.changes/v2.16.2.md @@ -0,0 +1,3 @@ +## v2.16.2 - 2024-02-21 +### Fixed +* Check for null values in closing motive of parcours d'accompagnement for correct rendering of template diff --git a/.changes/v2.16.3.md b/.changes/v2.16.3.md new file mode 100644 index 000000000..7bb143382 --- /dev/null +++ b/.changes/v2.16.3.md @@ -0,0 +1,5 @@ +## v2.16.3 - 2024-02-26 +### Fixed +* ([#236](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/236)) Fix translation of user job -> 'service' must be 'métier' +### UX +* ([#232](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/232)) Order user jobs and services alphabetically in export filters diff --git a/.changes/v2.17.0.md b/.changes/v2.17.0.md new file mode 100644 index 000000000..52c916bcd --- /dev/null +++ b/.changes/v2.17.0.md @@ -0,0 +1,9 @@ +## v2.17.0 - 2024-03-19 +### Feature +* ([#237](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/237)) New export filter for social actions with an evaluation created between two dates +* ([#258](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/258)) In the list of accompangying period, add the list of person's centers and the duration of the course +* ([#238](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/238)) Allow to customize list person with new fields +* ([#159](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/159)) Admin can publish news on the homepage +### Fixed +* ([#264](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/264)) Fix languages: load the languages in all availables languages configured for Chill +* ([#259](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/259)) Keep a consistent behaviour between the filtering of activities within the document generation (model "accompanying period with activities"), and the same filter in the list of activities for an accompanying period diff --git a/.changes/v2.18.0.md b/.changes/v2.18.0.md new file mode 100644 index 000000000..ad52d1d87 --- /dev/null +++ b/.changes/v2.18.0.md @@ -0,0 +1,5 @@ +## v2.18.0 - 2024-03-26 +### Feature +* ([#268](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/268)) Improve admin UX to configure document templates for document generation +### Fixed +* ([#267](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/267)) Fix the join between job and user in the user list (admin): show only the current user job diff --git a/.changes/v2.18.1.md b/.changes/v2.18.1.md new file mode 100644 index 000000000..f0d68925d --- /dev/null +++ b/.changes/v2.18.1.md @@ -0,0 +1,3 @@ +## v2.18.1 - 2024-03-26 +### Fixed +* Fix layout issue in document generation for admin (minor) diff --git a/.editorconfig b/.editorconfig index a3e5a0fc1..bede621e3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -23,3 +23,7 @@ max_line_length = 0 indent_size = 2 indent_style = space +[.rst] +ident_size = 3 +ident_style = space + diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 63519eb9d..e355d95de 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -91,7 +91,7 @@ rector_tests: stage: Tests image: gitea.champs-libres.be/chill-project/chill-skeleton-basic/base-image:php82 script: - - tests/console cache:clear --env=test + - tests/console cache:clear - bin/rector process --dry-run cache: paths: diff --git a/CHANGELOG.md b/CHANGELOG.md index 3986a1e64..8c0a6b008 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,36 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). +## v2.18.1 - 2024-03-26 +### Fixed +* Fix layout issue in document generation for admin (minor) + +## v2.18.0 - 2024-03-26 +### Feature +* ([#268](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/268)) Improve admin UX to configure document templates for document generation +### Fixed +* ([#267](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/267)) Fix the join between job and user in the user list (admin): show only the current user job + +## v2.17.0 - 2024-03-19 +### Feature +* ([#237](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/237)) New export filter for social actions with an evaluation created between two dates +* ([#258](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/258)) In the list of accompangying period, add the list of person's centers and the duration of the course +* ([#238](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/238)) Allow to customize list person with new fields +* ([#159](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/159)) Admin can publish news on the homepage +### Fixed +* ([#264](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/264)) Fix languages: load the languages in all availables languages configured for Chill +* ([#259](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/259)) Keep a consistent behaviour between the filtering of activities within the document generation (model "accompanying period with activities"), and the same filter in the list of activities for an accompanying period + +## v2.16.3 - 2024-02-26 +### Fixed +* ([#236](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/236)) Fix translation of user job -> 'service' must be 'métier' +### UX +* ([#232](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/232)) Order user jobs and services alphabetically in export filters + +## v2.16.2 - 2024-02-21 +### Fixed +* Check for null values in closing motive of parcours d'accompagnement for correct rendering of template + ## v2.16.1 - 2024-02-09 ### Fixed * Force bootstrap version to avoid error in builds with newer version diff --git a/docs/source/development/exports.rst b/docs/source/development/exports.rst index 3b01f9e0f..7cb04f32e 100644 --- a/docs/source/development/exports.rst +++ b/docs/source/development/exports.rst @@ -242,3 +242,129 @@ This is an example of the *filter by birthdate*. This filter asks some informati Continue to explain the export framework .. _main bundle: https://git.framasoft.org/Chill-project/Chill-Main + + +With many-to-* relationship, why should we set WHERE clauses in an EXISTS subquery instead of a JOIN ? +`````````````````````````````````````````````````````````````````````````````````````````````````````` + +As we described above, the doctrine builder is converted into a sql query. Let's see how to compute the "number of course +which count at least one activity type with the id 7". For the purpose of this demonstration, we will restrict this on +two accompanying period only: the ones with id 329 and 334. + +Let's see the list of activities associated with those accompanying period: + +.. code-block:: sql + + SELECT id, accompanyingperiod_id, type_id FROM activity WHERE accompanyingperiod_id IN (329, 334) AND type_id = 7 + ORDER BY accompanyingperiod_id; + +We see that we have 6 activities for the accompanying period with id 329, and only one for the 334's one. + +.. csv-table:: + :header: id, accompanyingperiod_id, type_id + + 990,329,7 + 986,329,7 + 987,329,7 + 993,329,7 + 991,329,7 + 992,329,7 + 1000,334,7 + +Let's calculate the average duration for those accompanying periods, and the number of period: + +.. code-block:: sql + + SELECT AVG(age(COALESCE(closingdate, CURRENT_DATE), openingdate)), COUNT(id) from chill_person_accompanying_period WHERE id IN (329, 334); + +The result of this query is: + +.. csv-table:: + :header: AVG, COUNT + + 2 years 2 mons 21 days 12 hours 0 mins 0.0 secs,2 + +Now, we count the number of accompanying period, adding a :code:`JOIN` clause which make a link to the :code:`activity` table, and add a :code:`WHERE` clause to keep +only the accompanying period which contains the given activity type: + +.. code-block:: sql + + SELECT COUNT(chill_person_accompanying_period.id) from chill_person_accompanying_period + JOIN activity ON chill_person_accompanying_period.id = activity.accompanyingperiod_id + WHERE chill_person_accompanying_period.id IN (329, 334) AND activity.type_id = 7; + +What are the results here ? + +.. csv-table:: + :header: COUNT + + 7 + +:code:`7` ! Why this result ? Because the number of lines is duplicated for each activity. Let's see the list of rows which +are taken into account for the computation: + +.. code-block:: sql + + SELECT chill_person_accompanying_period.id, activity.id from chill_person_accompanying_period + JOIN activity ON chill_person_accompanying_period.id = activity.accompanyingperiod_id + WHERE chill_person_accompanying_period.id IN (329, 334) AND activity.type_id = 7; + +.. csv-table:: + :header: accompanyingperiod.id, activity.id + + 329,993 + 334,1000 + 329,987 + 329,990 + 329,991 + 329,992 + 329,986 + +For each activity, a row is created and, as we count the number of non-null :code:`accompanyingperiod.id` columns, we +count one entry for each activity (actually, we count the number of activities). + +So, let's use the :code:`DISTINCT` keyword to count only once the equal ids: + +.. code-block:: + + SELECT COUNT(DISTINCT chill_person_accompanying_period.id) from chill_person_accompanying_period + JOIN activity ON chill_person_accompanying_period.id = activity.accompanyingperiod_id + WHERE chill_person_accompanying_period.id IN (329, 334) AND activity.type_id = 7; + +Now, it works again... + +.. csv-table:: + :header: COUNT + + 2 + +But, for the average duration, this won't work: the duration which are equals (because the :code:`openingdate` is the same and +:code:`closingdate` is still :code:`NULL`, for instance) will be counted only once, which will give unexpected result. + +The solution is to move the condition "having an activity with activity type with id 7" in a :code:`EXISTS` clause: + +.. code-block:: sql + + SELECT COUNT(chill_person_accompanying_period.id) from chill_person_accompanying_period + WHERE chill_person_accompanying_period.id IN (329, 334) AND EXISTS (SELECT 1 FROM activity WHERE type_id = 7 AND accompanyingperiod_id = chill_person_accompanying_period.id); + +The result is correct without :code:`DISTINCT` keyword: + +.. csv-table:: + :header: COUNT + + 2 + +And we can now compute the average duration without fear: + +.. code-block:: sql + + SELECT AVG(age(COALESCE(closingdate, CURRENT_DATE), openingdate)) from chill_person_accompanying_period + WHERE chill_person_accompanying_period.id IN (329, 334) AND EXISTS (SELECT 1 FROM activity WHERE type_id = 7 AND accompanyingperiod_id = chill_person_accompanying_period.id); + +Give the result: + +.. csv-table:: + :header: AVG + + 2 years 2 mons 21 days 12 hours 0 mins 0.0 secs diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/CreatorJobFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/CreatorJobFilter.php index 4288920e9..3b805d4ff 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/CreatorJobFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/CreatorJobFilter.php @@ -79,7 +79,7 @@ final readonly class CreatorJobFilter implements FilterInterface { $builder ->add('jobs', EntityType::class, [ - 'choices' => $this->userJobRepository->findAllOrderedByName(), + 'choices' => $this->userJobRepository->findAllActive(), 'class' => UserJob::class, 'choice_label' => fn (UserJob $s) => $this->translatableStringHelper->localize( $s->getLabel() diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/CreatorScopeFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/CreatorScopeFilter.php index e5da96554..36b827e9b 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/CreatorScopeFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/CreatorScopeFilter.php @@ -15,6 +15,7 @@ use Chill\ActivityBundle\Export\Declarations; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User\UserScopeHistory; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Repository\ScopeRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelper; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; @@ -26,7 +27,8 @@ class CreatorScopeFilter implements FilterInterface private const PREFIX = 'acp_act_filter_creator_scope'; public function __construct( - private readonly TranslatableStringHelper $translatableStringHelper + private readonly TranslatableStringHelper $translatableStringHelper, + private readonly ScopeRepositoryInterface $scopeRepository, ) {} public function addRole(): ?string @@ -75,6 +77,7 @@ class CreatorScopeFilter implements FilterInterface $builder ->add('scopes', EntityType::class, [ 'class' => Scope::class, + 'choices' => $this->scopeRepository->findAllActive(), 'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize( $s->getName() ), diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/UsersJobFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/UsersJobFilter.php index 23bd4f84c..0987e7ae9 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/UsersJobFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/UsersJobFilter.php @@ -16,6 +16,7 @@ use Chill\ActivityBundle\Export\Declarations; use Chill\MainBundle\Entity\User\UserJobHistory; use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Repository\UserJobRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\QueryBuilder; @@ -27,8 +28,10 @@ class UsersJobFilter implements FilterInterface private const PREFIX = 'act_filter_user_job'; public function __construct( - private readonly TranslatableStringHelperInterface $translatableStringHelper - ) {} + private readonly TranslatableStringHelperInterface $translatableStringHelper, + private readonly UserJobRepositoryInterface $userJobRepository + ) { + } public function addRole(): ?string { @@ -68,6 +71,7 @@ class UsersJobFilter implements FilterInterface $builder ->add('jobs', EntityType::class, [ 'class' => UserJob::class, + 'choices' => $this->userJobRepository->findAllActive(), 'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()), 'multiple' => true, 'expanded' => true, diff --git a/src/Bundle/ChillActivityBundle/Form/ActivityType.php b/src/Bundle/ChillActivityBundle/Form/ActivityType.php index d322089c8..fbcf60bf0 100644 --- a/src/Bundle/ChillActivityBundle/Form/ActivityType.php +++ b/src/Bundle/ChillActivityBundle/Form/ActivityType.php @@ -95,7 +95,7 @@ class ActivityType extends AbstractType ]); } - /** @var \Chill\PersonBundle\Entity\AccompanyingPeriod|null $accompanyingPeriod */ + /** @var AccompanyingPeriod|null $accompanyingPeriod */ $accompanyingPeriod = null; if ($options['accompanyingPeriod'] instanceof AccompanyingPeriod) { diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php index 231ad5432..34ddfe432 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php @@ -242,7 +242,8 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos thirdparties.thirdpartyids, persons.personids, actions.socialactionids, - issues.socialissueids + issues.socialissueids, + a.user_id FROM activity a LEFT JOIN chill_main_location location ON a.location_id = location.id @@ -282,6 +283,7 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos ->addJoinedEntityResult(ActivityPresence::class, 'activityPresence', 'a', 'attendee') ->addFieldResult('activityPresence', 'presence_id', 'id') ->addFieldResult('activityPresence', 'presence_name', 'name') + ->addScalarResult('user_id', 'userId', Types::INTEGER) // results which cannot be mapped into entity ->addScalarResult('comment_comment', 'comment', Types::TEXT) diff --git a/src/Bundle/ChillActivityBundle/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContext.php b/src/Bundle/ChillActivityBundle/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContext.php index 221d1f4b2..9298983c2 100644 --- a/src/Bundle/ChillActivityBundle/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContext.php +++ b/src/Bundle/ChillActivityBundle/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContext.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Service\DocGenerator; +use Chill\ActivityBundle\Entity\Activity; use Chill\ActivityBundle\Entity\ActivityPresence; use Chill\ActivityBundle\Entity\ActivityType; use Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface; @@ -111,7 +112,7 @@ class ListActivitiesByAccompanyingPeriodContext implements } /** - * @return list + * @return list */ private function filterActivitiesByUser(array $activities, User $user): array { @@ -119,6 +120,12 @@ class ListActivitiesByAccompanyingPeriodContext implements array_filter( $activities, function ($activity) use ($user) { + $u = $activity['user']; + + if (null !== $u && $u['username'] === $user->getUsername()) { + return true; + } + $activityUsernames = array_map(static fn ($user) => $user['username'], $activity['users'] ?? []); return \in_array($user->getUsername(), $activityUsernames, true); @@ -128,7 +135,7 @@ class ListActivitiesByAccompanyingPeriodContext implements } /** - * @return list + * @return list */ private function filterWorksByUser(array $works, User $user): array { @@ -215,6 +222,15 @@ class ListActivitiesByAccompanyingPeriodContext implements foreach ($activities as $row) { $activity = $row[0]; + $user = match (null === $row['userId']) { + false => $this->userRepository->find($row['userId']), + true => null, + }; + + $activity['user'] = $this->normalizer->normalize($user, 'docgen', [ + AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => User::class, + ]); + $activity['date'] = $this->normalizer->normalize($activity['date'], 'docgen', [ AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => \DateTime::class, ]); diff --git a/src/Bundle/ChillActivityBundle/Tests/Repository/ActivityACLAwareRepositoryTest.php b/src/Bundle/ChillActivityBundle/Tests/Repository/ActivityACLAwareRepositoryTest.php index f0036612d..de4a3312d 100644 --- a/src/Bundle/ChillActivityBundle/Tests/Repository/ActivityACLAwareRepositoryTest.php +++ b/src/Bundle/ChillActivityBundle/Tests/Repository/ActivityACLAwareRepositoryTest.php @@ -91,6 +91,29 @@ class ActivityACLAwareRepositoryTest extends KernelTestCase self::assertIsArray($actual); } + /** + * @dataProvider provideDataFindByAccompanyingPeriod + */ + public function testfindByAccompanyingPeriodSimplified(AccompanyingPeriod $period, User $user, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): void + { + $security = $this->prophesize(Security::class); + $security->isGranted($role, $period)->willReturn(true); + $security->getUser()->willReturn($user); + + $repository = new ActivityACLAwareRepository( + $this->authorizationHelperForCurrentUser, + $this->centerResolverManager, + $this->activityRepository, + $this->entityManager, + $security->reveal(), + $this->requestStack + ); + + $actual = $repository->findByAccompanyingPeriodSimplified($period); + + self::assertIsArray($actual); + } + /** * @dataProvider provideDataFindByAccompanyingPeriod */ @@ -301,7 +324,10 @@ class ActivityACLAwareRepositoryTest extends KernelTestCase ->getQuery() ->getResult() ) { - throw new \RuntimeException('no jobs found'); + $job = new UserJob(); + $job->setLabel(['fr' => 'test']); + $this->entityManager->persist($job); + $this->entityManager->flush(); } if (null === $user = $this->entityManager diff --git a/src/Bundle/ChillActivityBundle/Tests/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContextTest.php b/src/Bundle/ChillActivityBundle/Tests/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContextTest.php new file mode 100644 index 000000000..ff9f30c30 --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Tests/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContextTest.php @@ -0,0 +1,139 @@ +listActivitiesByAccompanyingPeriodContext = self::$container->get(ListActivitiesByAccompanyingPeriodContext::class); + $this->accompanyingPeriodRepository = self::$container->get(AccompanyingPeriodRepository::class); + $this->userRepository = self::$container->get(UserRepositoryInterface::class); + } + + /** + * @dataProvider provideAccompanyingPeriod + */ + public function testGetDataWithoutFilteringActivityNorWorks(int $accompanyingPeriodId, int $userId): void + { + $context = $this->getContext(); + $template = new DocGeneratorTemplate(); + $template->setOptions([ + 'mainPerson' => false, + 'person1' => false, + 'person2' => false, + 'thirdParty' => false, + ]); + + $data = $context->getData( + $template, + $this->accompanyingPeriodRepository->find($accompanyingPeriodId), + ['myActivitiesOnly' => false, 'myWorksOnly' => false] + ); + + self::assertIsArray($data); + self::assertArrayHasKey('activities', $data); + self::assertIsArray($data['activities']); + self::assertGreaterThan(0, count($data['activities'])); + self::assertIsArray($data['activities'][0]); + self::assertArrayHasKey('user', $data['activities'][0]); + self::assertIsArray($data['activities'][0]['user']); + } + + /** + * @dataProvider provideAccompanyingPeriod + */ + public function testGetDataWithoutFilteringActivityByUser(int $accompanyingPeriodId, int $userId): void + { + $context = $this->getContext(); + $template = new DocGeneratorTemplate(); + $template->setOptions([ + 'mainPerson' => false, + 'person1' => false, + 'person2' => false, + 'thirdParty' => false, + ]); + + $data = $context->getData( + $template, + $this->accompanyingPeriodRepository->find($accompanyingPeriodId), + ['myActivitiesOnly' => true, 'myWorksOnly' => false, 'creator' => $this->userRepository->find($userId)] + ); + + self::assertIsArray($data); + self::assertArrayHasKey('activities', $data); + self::assertIsArray($data['activities']); + self::assertGreaterThan(0, count($data['activities'])); + self::assertIsArray($data['activities'][0]); + self::assertArrayHasKey('user', $data['activities'][0]); + self::assertIsArray($data['activities'][0]['user']); + } + + public static function provideAccompanyingPeriod(): array + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + if (null === $period = $em->createQuery('SELECT a FROM '.AccompanyingPeriod::class.' a') + ->setMaxResults(1) + ->getSingleResult()) { + throw new \RuntimeException('no period found'); + } + + if (null === $user = $em->createQuery('SELECT u FROM '.User::class.' u') + ->setMaxResults(1) + ->getSingleResult() + ) { + throw new \RuntimeException('no user found'); + } + + $activity = new Activity(); + $activity + ->setAccompanyingPeriod($period) + ->setUser($user) + ->setDate(new \DateTime()); + + $em->persist($activity); + $em->flush(); + + self::ensureKernelShutdown(); + + return [ + [$period->getId(), $user->getId()], + ]; + } + + private function getContext(): ListActivitiesByAccompanyingPeriodContext + { + return $this->listActivitiesByAccompanyingPeriodContext; + } +} diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml index 6d447ea21..441c23eeb 100644 --- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml @@ -396,7 +396,7 @@ export: by_creator_job: job_form_label: Métiers Filter activity by user job: Filtrer les échanges par métier du créateur de l'échange - 'Filtered activity by user job: only %jobs%': "Filtré par service du créateur de l'échange: uniquement %jobs%" + 'Filtered activity by user job: only %jobs%': "Filtré par métier du créateur de l'échange: uniquement %jobs%" by_persons: Filter activity by persons: Filtrer les échanges par usager participant 'Filtered activity by persons: only %persons%': 'Échanges filtrés par usagers participants: seulement %persons%' diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserJobFilter.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserJobFilter.php index d7255d9fa..ec2e3d220 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserJobFilter.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserJobFilter.php @@ -16,6 +16,7 @@ use Chill\AsideActivityBundle\Export\Declarations; use Chill\MainBundle\Entity\User\UserJobHistory; use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Repository\UserJobRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\QueryBuilder; @@ -27,7 +28,8 @@ class ByUserJobFilter implements FilterInterface private const PREFIX = 'aside_act_filter_user_job'; public function __construct( - private readonly TranslatableStringHelperInterface $translatableStringHelper + private readonly TranslatableStringHelperInterface $translatableStringHelper, + private readonly UserJobRepositoryInterface $userJobRepository ) {} public function addRole(): ?string @@ -68,6 +70,7 @@ class ByUserJobFilter implements FilterInterface $builder ->add('jobs', EntityType::class, [ 'class' => UserJob::class, + 'choices' => $this->userJobRepository->findAllActive(), 'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()), 'multiple' => true, 'expanded' => true, diff --git a/src/Bundle/ChillCalendarBundle/Export/Filter/JobFilter.php b/src/Bundle/ChillCalendarBundle/Export/Filter/JobFilter.php index c122a298d..b2b5e6a12 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Filter/JobFilter.php +++ b/src/Bundle/ChillCalendarBundle/Export/Filter/JobFilter.php @@ -15,6 +15,7 @@ use Chill\CalendarBundle\Export\Declarations; use Chill\MainBundle\Entity\User\UserJobHistory; use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Repository\UserJobRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelper; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; @@ -26,8 +27,10 @@ final readonly class JobFilter implements FilterInterface private const PREFIX = 'cal_filter_job'; public function __construct( - private TranslatableStringHelper $translatableStringHelper - ) {} + private TranslatableStringHelper $translatableStringHelper, + private UserJobRepositoryInterface $userJobRepository + ) { + } public function addRole(): ?string { @@ -73,6 +76,7 @@ final readonly class JobFilter implements FilterInterface $builder ->add('job', EntityType::class, [ 'class' => UserJob::class, + 'choices' => $this->userJobRepository->findAllActive(), 'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize( $j->getLabel() ), diff --git a/src/Bundle/ChillCalendarBundle/Export/Filter/ScopeFilter.php b/src/Bundle/ChillCalendarBundle/Export/Filter/ScopeFilter.php index 93edc1b3a..179258e41 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Filter/ScopeFilter.php +++ b/src/Bundle/ChillCalendarBundle/Export/Filter/ScopeFilter.php @@ -15,6 +15,7 @@ use Chill\CalendarBundle\Export\Declarations; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User\UserScopeHistory; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Repository\ScopeRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelper; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; @@ -28,8 +29,10 @@ class ScopeFilter implements FilterInterface public function __construct( protected TranslatorInterface $translator, - private readonly TranslatableStringHelper $translatableStringHelper - ) {} + private readonly TranslatableStringHelper $translatableStringHelper, + private readonly ScopeRepositoryInterface $scopeRepository + ) { + } public function addRole(): ?string { @@ -75,6 +78,7 @@ class ScopeFilter implements FilterInterface $builder ->add('scope', EntityType::class, [ 'class' => Scope::class, + 'choices' => $this->scopeRepository->findAllActive(), 'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize( $s->getName() ), diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php index 64a86cccc..4acc9de46 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php @@ -32,7 +32,7 @@ final readonly class MSUserAbsenceReader implements MSUserAbsenceReaderInterface /** * @throw UserAbsenceSyncException when the data cannot be reached or is not valid from microsoft */ - public function isUserAbsent(User $user): bool|null + public function isUserAbsent(User $user): ?bool { $id = $this->mapCalendarToUser->getUserId($user); diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderInterface.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderInterface.php index a918bb7ea..7c3fd69d6 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderInterface.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderInterface.php @@ -18,5 +18,5 @@ interface MSUserAbsenceReaderInterface /** * @throw UserAbsenceSyncException when the data cannot be reached or is not valid from microsoft */ - public function isUserAbsent(User $user): bool|null; + public function isUserAbsent(User $user): ?bool; } diff --git a/src/Bundle/ChillDocGeneratorBundle/Controller/DocGeneratorTemplateController.php b/src/Bundle/ChillDocGeneratorBundle/Controller/DocGeneratorTemplateController.php index 5f8d99f7c..1a716f17e 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Controller/DocGeneratorTemplateController.php +++ b/src/Bundle/ChillDocGeneratorBundle/Controller/DocGeneratorTemplateController.php @@ -16,25 +16,30 @@ use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface; use Chill\DocGeneratorBundle\Context\Exception\ContextNotFoundException; use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository; -use Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface; use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage; use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Form\Type\PickUserDynamicType; use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Security\ChillSecurity; use Chill\MainBundle\Serializer\Model\Collection; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\Clock\ClockInterface; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; -use Symfony\Component\Form\Extension\Core\Type\FileType; +use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; -// TODO à mettre dans services use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Security\Core\Security; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; +use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Constraints\NotNull; final class DocGeneratorTemplateController extends AbstractController { @@ -45,6 +50,7 @@ final class DocGeneratorTemplateController extends AbstractController private readonly MessageBusInterface $messageBus, private readonly PaginatorFactory $paginatorFactory, private readonly EntityManagerInterface $entityManager, + private readonly ClockInterface $clock, private readonly ChillSecurity $security ) {} @@ -170,9 +176,7 @@ final class DocGeneratorTemplateController extends AbstractController throw new NotFoundHttpException(sprintf('Entity with classname %s and id %s is not found', $context->getEntityClass(), $entityId)); } - $contextGenerationData = [ - 'test_file' => null, - ]; + $contextGenerationData = []; if ( $context instanceof DocGeneratorContextWithPublicFormInterface @@ -182,25 +186,39 @@ final class DocGeneratorTemplateController extends AbstractController $builder = $this->createFormBuilder( array_merge( $context->getFormData($template, $entity), - $isTest ? ['test_file' => null, 'show_data' => false] : [] + $isTest ? ['creator' => null, 'dump_only' => false, 'send_result_to' => ''] : [] ) ); $context->buildPublicForm($builder, $template, $entity); } else { $builder = $this->createFormBuilder( - ['test_file' => null, 'show_data' => false] + ['creator' => null, 'show_data' => false, 'send_result_to' => ''] ); } if ($isTest) { - $builder->add('test_file', FileType::class, [ - 'label' => 'Template file', + $builder->add('dump_only', CheckboxType::class, [ + 'label' => 'docgen.Show data instead of generating', 'required' => false, ]); - $builder->add('show_data', CheckboxType::class, [ - 'label' => 'Show data instead of generating', - 'required' => false, + $builder->add('send_result_to', EmailType::class, [ + 'label' => 'docgen.Send report to', + 'help' => 'docgen.Send report errors to this email address', + 'empty_data' => '', + 'required' => true, + 'constraints' => [ + new NotBlank(), + new NotNull(), + ], + ]); + $builder->add('creator', PickUserDynamicType::class, [ + 'label' => 'docgen.Generate as creator', + 'help' => 'docgen.The document will be generated as the given creator', + 'multiple' => false, + 'constraints' => [ + new NotNull(), + ], ]); } @@ -211,8 +229,10 @@ final class DocGeneratorTemplateController extends AbstractController } elseif (!$form->isSubmitted() || ($form->isSubmitted() && !$form->isValid())) { $templatePath = '@ChillDocGenerator/Generator/basic_form.html.twig'; $templateOptions = [ - 'entity' => $entity, 'form' => $form->createView(), - 'template' => $template, 'context' => $context, + 'entity' => $entity, + 'form' => $form->createView(), + 'template' => $template, + 'context' => $context, ]; return $this->render($templatePath, $templateOptions); @@ -225,60 +245,57 @@ final class DocGeneratorTemplateController extends AbstractController $context->contextGenerationDataNormalize($template, $entity, $contextGenerationData) : []; - // if is test, render the data or generate the doc - if ($isTest && isset($form) && true === $form['show_data']->getData()) { - return $this->render('@ChillDocGenerator/Generator/debug_value.html.twig', [ - 'datas' => json_encode($context->getData($template, $entity, $contextGenerationData), \JSON_PRETTY_PRINT), - ]); - } - if ($isTest) { - $generated = $this->generator->generateDocFromTemplate( - $template, - $entityId, - $contextGenerationDataSanitized, - null, - true, - isset($form) ? $form['test_file']->getData() : null - ); - - return new Response( - $generated, - Response::HTTP_OK, - [ - 'Content-Transfer-Encoding', 'binary', - 'Content-Type' => 'application/vnd.oasis.opendocument.text', - 'Content-Disposition' => 'attachment; filename="generated.odt"', - 'Content-Length' => \strlen($generated), - ], - ); - } - - // this is not a test // we prepare the object to store the document $storedObject = (new StoredObject()) ->setStatus(StoredObject::STATUS_PENDING) ; + if ($isTest) { + // document will be stored during 15 days, if generation is a test + $storedObject->setDeleteAt($this->clock->now()->add(new \DateInterval('P15D'))); + } + $this->entityManager->persist($storedObject); - // we store the generated document - $context - ->storeGenerated( - $template, - $storedObject, - $entity, - $contextGenerationData - ); + // we store the generated document (associate with the original entity, etc.) + // but only if this is not a test + if (!$isTest) { + $context + ->storeGenerated( + $template, + $storedObject, + $entity, + $contextGenerationData + ); + } $this->entityManager->flush(); + if ($isTest) { + $creator = $contextGenerationData['creator']; + $sendResultTo = ($form ?? null)?->get('send_result_to')?->getData() ?? null; + $dumpOnly = ($form ?? null)?->get('dump_only')?->getData() ?? false; + } else { + $creator = $this->security->getUser(); + + if (!$creator instanceof User) { + throw new AccessDeniedHttpException('only authenticated user can request a generation'); + } + + $sendResultTo = null; + $dumpOnly = false; + } + $this->messageBus->dispatch( new RequestGenerationMessage( - $this->security->getUser(), + $creator, $template, $entityId, $storedObject, $contextGenerationDataSanitized, + $isTest, + $sendResultTo, + $dumpOnly, ) ); diff --git a/src/Bundle/ChillDocGeneratorBundle/Entity/DocGeneratorTemplate.php b/src/Bundle/ChillDocGeneratorBundle/Entity/DocGeneratorTemplate.php index 2438e66d9..e2524e5ba 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Entity/DocGeneratorTemplate.php +++ b/src/Bundle/ChillDocGeneratorBundle/Entity/DocGeneratorTemplate.php @@ -69,7 +69,7 @@ class DocGeneratorTemplate * * @Serializer\Groups({"read"}) */ - private int $id; + private ?int $id = null; /** * @ORM\Column(type="json") diff --git a/src/Bundle/ChillDocGeneratorBundle/Repository/DocGeneratorTemplateRepository.php b/src/Bundle/ChillDocGeneratorBundle/Repository/DocGeneratorTemplateRepository.php index 0f2771b4e..b5f409032 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Repository/DocGeneratorTemplateRepository.php +++ b/src/Bundle/ChillDocGeneratorBundle/Repository/DocGeneratorTemplateRepository.php @@ -14,10 +14,9 @@ namespace Chill\DocGeneratorBundle\Repository; use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; -use Doctrine\Persistence\ObjectRepository; use Symfony\Component\HttpFoundation\RequestStack; -final class DocGeneratorTemplateRepository implements ObjectRepository +final class DocGeneratorTemplateRepository implements DocGeneratorTemplateRepositoryInterface { private readonly EntityRepository $repository; diff --git a/src/Bundle/ChillDocGeneratorBundle/Repository/DocGeneratorTemplateRepositoryInterface.php b/src/Bundle/ChillDocGeneratorBundle/Repository/DocGeneratorTemplateRepositoryInterface.php new file mode 100644 index 000000000..e5071e76a --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/Repository/DocGeneratorTemplateRepositoryInterface.php @@ -0,0 +1,23 @@ + + */ +interface DocGeneratorTemplateRepositoryInterface extends ObjectRepository +{ + public function countByEntity(string $entity): int; +} diff --git a/src/Bundle/ChillDocGeneratorBundle/Resources/views/DocGeneratorTemplate/index.html.twig b/src/Bundle/ChillDocGeneratorBundle/Resources/views/DocGeneratorTemplate/index.html.twig index 1adb6872b..ee167bcf7 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Resources/views/DocGeneratorTemplate/index.html.twig +++ b/src/Bundle/ChillDocGeneratorBundle/Resources/views/DocGeneratorTemplate/index.html.twig @@ -1,36 +1,62 @@ {% extends '@ChillMain/CRUD/Admin/index.html.twig' %} +{% block js %} + {{ parent() }} + {{ encore_entry_script_tags('mod_document_action_buttons_group') }} +{% endblock %} + +{% block css %} + {{ parent() }} + {{ encore_entry_link_tags('mod_document_action_buttons_group') }} +{% endblock %} + + {% block admin_content %} {% embed '@ChillMain/CRUD/_index.html.twig' %} {% block table_entities_thead_tr %} - - {{ 'Title'|trans }} - {{ 'docgen.Context'|trans }} - {{ 'docgen.test generate'|trans }} - {{ 'Edit'|trans }} {% endblock %} {% block table_entities_tbody %} - {% for entity in entities %} - - {{ entity.id }} - {{ entity.name|localize_translatable_string}} - {{ contextManager.getContextByKey(entity.context).name|trans }} - -
- - - - + {% if entities|length == 0 %} +

{{ 'docgen.Any template configured'|trans }}

+ {% else %} +
+ {% for entity in entities %} +
+
+
+

{{ entity.name|localize_translatable_string }}

+
+
+
+

{{ contextManager.getContextByKey(entity.context).name|trans }}

+
+
+
+
    +
  • + + + + + + + +
  • + +
  • + {{ entity.file|chill_document_button_group('Template file', true) }} +
  • +
  • + +
  • +
+
+
+ {% endfor %} +
+ {% endif %} - - - - - - - - {% endfor %} {% endblock %} {% block actions_before %} diff --git a/src/Bundle/ChillDocGeneratorBundle/Resources/views/DocGeneratorTemplate/pick-context.html.twig b/src/Bundle/ChillDocGeneratorBundle/Resources/views/DocGeneratorTemplate/pick-context.html.twig index c2f4a9a95..f61725765 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Resources/views/DocGeneratorTemplate/pick-context.html.twig +++ b/src/Bundle/ChillDocGeneratorBundle/Resources/views/DocGeneratorTemplate/pick-context.html.twig @@ -6,18 +6,20 @@

{{ block('title') }}

-
+
{% for key, context in contexts %} -
-
+
+ -
- {{ context.description|trans|nl2br }} +
+
+ {{ context.description|trans|nl2br }} +
{% endfor %} diff --git a/src/Bundle/ChillDocGeneratorBundle/Resources/views/Email/on_generation_failed_email.txt.twig b/src/Bundle/ChillDocGeneratorBundle/Resources/views/Email/on_generation_failed_email.txt.twig index c4ca7079d..594785bfc 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Resources/views/Email/on_generation_failed_email.txt.twig +++ b/src/Bundle/ChillDocGeneratorBundle/Resources/views/Email/on_generation_failed_email.txt.twig @@ -1,6 +1,6 @@ -{{ creator.label }}, +{% if creator is not same as null %}{{ creator.label }},{% endif %} -{{ 'docgen.failure_email.The generation of the document {template_name} failed'|trans({'{template_name}': template.name|localize_translatable_string}) }} +{{ 'docgen.failure_email.The generation of the document %template_name% failed'|trans({'%template_name%': template.name|localize_translatable_string}) }} {{ 'docgen.failure_email.Forward this email to your administrator for solving'|trans }} diff --git a/src/Bundle/ChillDocGeneratorBundle/Resources/views/Email/send_data_dump_to_admin.txt.twig b/src/Bundle/ChillDocGeneratorBundle/Resources/views/Email/send_data_dump_to_admin.txt.twig new file mode 100644 index 000000000..566bfbfa3 --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/Resources/views/Email/send_data_dump_to_admin.txt.twig @@ -0,0 +1,7 @@ +{{ 'docgen.data_dump_email.Dear'|trans }} + +{{ 'docgen.data_dump_email.data_dump_ready_and_link'|trans }} + +{{ link }} + +{{ 'docgen.data_dump_email.link_valid_until'|trans({validity: validity}) }} diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Generator/Generator.php b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/Generator.php index 5931899a3..702d7f114 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Service/Generator/Generator.php +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/Generator.php @@ -17,52 +17,88 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface; use Chill\DocGeneratorBundle\GeneratorDriver\Exception\TemplateException; use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Exception\StoredObjectManagerException; use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; use Chill\MainBundle\Entity\User; -use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\ManagerRegistry; use Psr\Log\LoggerInterface; -use Symfony\Component\HttpFoundation\File\File; +use Symfony\Component\Yaml\Yaml; class Generator implements GeneratorInterface { private const LOG_PREFIX = '[docgen generator] '; - public function __construct(private readonly ContextManagerInterface $contextManager, private readonly DriverInterface $driver, private readonly EntityManagerInterface $entityManager, private readonly LoggerInterface $logger, private readonly StoredObjectManagerInterface $storedObjectManager) {} + public function __construct( + private readonly ContextManagerInterface $contextManager, + private readonly DriverInterface $driver, + private readonly ManagerRegistry $objectManagerRegistry, + private readonly LoggerInterface $logger, + private readonly StoredObjectManagerInterface $storedObjectManager + ) { + } + + public function generateDataDump( + DocGeneratorTemplate $template, + int $entityId, + array $contextGenerationDataNormalized, + StoredObject $destinationStoredObject, + User $creator, + bool $clearEntityManagerDuringProcess = true, + ): StoredObject { + return $this->generateFromTemplate( + $template, + $entityId, + $contextGenerationDataNormalized, + $destinationStoredObject, + $creator, + $clearEntityManagerDuringProcess, + true, + ); + } - /** - * @template T of File|null - * @template B of bool - * - * @param B $isTest - * @param (B is true ? T : null) $testFile - * - * @psalm-return (B is true ? string : null) - * - * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface|\Throwable - */ public function generateDocFromTemplate( DocGeneratorTemplate $template, int $entityId, array $contextGenerationDataNormalized, - ?StoredObject $destinationStoredObject = null, - bool $isTest = false, - ?File $testFile = null, - ?User $creator = null - ): ?string { - if ($destinationStoredObject instanceof StoredObject && StoredObject::STATUS_PENDING !== $destinationStoredObject->getStatus()) { + StoredObject $destinationStoredObject, + User $creator, + bool $clearEntityManagerDuringProcess = true, + ): StoredObject { + return $this->generateFromTemplate( + $template, + $entityId, + $contextGenerationDataNormalized, + $destinationStoredObject, + $creator, + $clearEntityManagerDuringProcess, + false, + ); + } + + private function generateFromTemplate( + DocGeneratorTemplate $template, + int $entityId, + array $contextGenerationDataNormalized, + StoredObject $destinationStoredObject, + User $creator, + bool $clearEntityManagerDuringProcess = true, + bool $generateDumpOnly = false, + ): StoredObject { + if (StoredObject::STATUS_PENDING !== $destinationStoredObject->getStatus()) { $this->logger->info(self::LOG_PREFIX.'Aborting generation of an already generated document'); throw new ObjectReadyException(); } $this->logger->info(self::LOG_PREFIX.'Starting generation of a document', [ 'entity_id' => $entityId, - 'destination_stored_object' => null === $destinationStoredObject ? null : $destinationStoredObject->getId(), + 'destination_stored_object' => $destinationStoredObject->getId(), ]); $context = $this->contextManager->getContextByDocGeneratorTemplate($template); $entity = $this - ->entityManager + ->objectManagerRegistry + ->getManagerForClass($context->getEntityClass()) ->find($context->getEntityClass(), $entityId) ; @@ -80,17 +116,47 @@ class Generator implements GeneratorInterface $data = $context->getData($template, $entity, $contextGenerationDataNormalized); - $destinationStoredObjectId = $destinationStoredObject instanceof StoredObject ? $destinationStoredObject->getId() : null; - $this->entityManager->clear(); - gc_collect_cycles(); - if (null !== $destinationStoredObjectId) { - $destinationStoredObject = $this->entityManager->find(StoredObject::class, $destinationStoredObjectId); + $destinationStoredObjectId = $destinationStoredObject->getId(); + + if ($clearEntityManagerDuringProcess) { + // we clean the entity manager + $this->objectManagerRegistry->getManagerForClass($context->getEntityClass())?->clear(); + + // this will force php to clean the memory + gc_collect_cycles(); } - if ($isTest && ($testFile instanceof File)) { - $templateDecrypted = file_get_contents($testFile->getPathname()); - } else { + // as we potentially deleted the storedObject from memory, we have to restore it + $destinationStoredObject = $this->objectManagerRegistry + ->getManagerForClass(StoredObject::class) + ->find(StoredObject::class, $destinationStoredObjectId); + + if ($generateDumpOnly) { + $content = Yaml::dump($data, 6); + /* @var StoredObject $destinationStoredObject */ + $destinationStoredObject + ->setType('application/yaml') + ->setFilename(sprintf('%s_yaml', uniqid('doc_', true))) + ->setStatus(StoredObject::STATUS_READY) + ; + + try { + $this->storedObjectManager->write($destinationStoredObject, $content); + } catch (StoredObjectManagerException $e) { + $destinationStoredObject->addGenerationErrors($e->getMessage()); + + throw new GeneratorException([$e->getMessage()], $e); + } + + return $destinationStoredObject; + } + + try { $templateDecrypted = $this->storedObjectManager->read($template->getFile()); + } catch (StoredObjectManagerException $e) { + $destinationStoredObject->addGenerationErrors($e->getMessage()); + + throw new GeneratorException([$e->getMessage()], $e); } try { @@ -103,19 +169,10 @@ class Generator implements GeneratorInterface $template->getFile()->getFilename() ); } catch (TemplateException $e) { + $destinationStoredObject->addGenerationErrors(implode("\n", $e->getErrors())); throw new GeneratorException($e->getErrors(), $e); } - if (true === $isTest) { - $this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [ - 'is_test' => true, - 'entity_id' => $entityId, - 'destination_stored_object' => null === $destinationStoredObject ? null : $destinationStoredObject->getId(), - ]); - - return $generatedResource; - } - /* @var StoredObject $destinationStoredObject */ $destinationStoredObject ->setType($template->getFile()->getType()) @@ -123,15 +180,19 @@ class Generator implements GeneratorInterface ->setStatus(StoredObject::STATUS_READY) ; - $this->storedObjectManager->write($destinationStoredObject, $generatedResource); + try { + $this->storedObjectManager->write($destinationStoredObject, $generatedResource); + } catch (StoredObjectManagerException $e) { + $destinationStoredObject->addGenerationErrors($e->getMessage()); - $this->entityManager->flush(); + throw new GeneratorException([$e->getMessage()], $e); + } $this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [ 'entity_id' => $entityId, 'destination_stored_object' => $destinationStoredObject->getId(), ]); - return null; + return $destinationStoredObject; } } diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorInterface.php b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorInterface.php index c4ff38ac5..e7db7ba53 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorInterface.php +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorInterface.php @@ -13,29 +13,48 @@ namespace Chill\DocGeneratorBundle\Service\Generator; use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Exception\StoredObjectManagerException; use Chill\MainBundle\Entity\User; -use Symfony\Component\HttpFoundation\File\File; interface GeneratorInterface { /** - * @template T of File|null - * @template B of bool + * Generate a document and store the document on disk. * - * @param B $isTest - * @param (B is true ? T : null) $testFile + * The given $destinationStoredObject will be updated with filename, status, and eventually errors will be stored + * into the object. The number of generation trial will also be incremented. * - * @psalm-return (B is true ? string : null) + * This process requires a huge amount of data. For this reason, the entity manager will be cleaned during the process, + * unless the paarameter `$clearEntityManagerDuringProcess` is set on false. * - * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface|\Throwable + * As the entity manager might be cleaned, the new instance of the stored object will be returned by this method. + * + * Ensure to store change in the database after each generation trial (call `EntityManagerInterface::flush`). + * + * @phpstan-impure + * + * @param StoredObject $destinationStoredObject will be update with filename, status and incremented of generation trials + * + * @throws StoredObjectManagerException if unable to decrypt the template or store the document */ public function generateDocFromTemplate( DocGeneratorTemplate $template, int $entityId, array $contextGenerationDataNormalized, - ?StoredObject $destinationStoredObject = null, - bool $isTest = false, - ?File $testFile = null, - ?User $creator = null - ): ?string; + StoredObject $destinationStoredObject, + User $creator, + bool $clearEntityManagerDuringProcess = true, + ): StoredObject; + + /** + * Generate a data dump, and store it within the `$destinationStoredObject`. + */ + public function generateDataDump( + DocGeneratorTemplate $template, + int $entityId, + array $contextGenerationDataNormalized, + StoredObject $destinationStoredObject, + User $creator, + bool $clearEntityManagerDuringProcess = true, + ): StoredObject; } diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/OnAfterMessageHandledClearStoredObjectCache.php b/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/OnAfterMessageHandledClearStoredObjectCache.php new file mode 100644 index 000000000..a558f5fde --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/OnAfterMessageHandledClearStoredObjectCache.php @@ -0,0 +1,64 @@ + [ + ['afterHandling', 0], + ], + WorkerMessageFailedEvent::class => [ + ['afterFails', 0], + ], + ]; + } + + public function afterHandling(WorkerMessageHandledEvent $event): void + { + if ($event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) { + $this->clearStoredObjectCache(); + } + } + + public function afterFails(WorkerMessageFailedEvent $event): void + { + if ($event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) { + $this->clearStoredObjectCache(); + } + } + + private function clearStoredObjectCache(): void + { + $this->logger->debug('clear the cache after generation of a document'); + + $this->storedObjectManager->clearCache(); + } +} diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/OnGenerationFails.php b/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/OnGenerationFails.php index b9c29aff9..e1bd20ac8 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/OnGenerationFails.php +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/OnGenerationFails.php @@ -11,10 +11,11 @@ declare(strict_types=1); namespace Chill\DocGeneratorBundle\Service\Messenger; -use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository; +use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface; use Chill\DocGeneratorBundle\Service\Generator\GeneratorException; +use Chill\DocGeneratorBundle\tests\Service\Messenger\OnGenerationFailsTest; use Chill\DocStoreBundle\Entity\StoredObject; -use Chill\DocStoreBundle\Repository\StoredObjectRepository; +use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface; use Chill\MainBundle\Repository\UserRepositoryInterface; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; @@ -24,11 +25,23 @@ use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; use Symfony\Contracts\Translation\TranslatorInterface; +/** + * @see OnGenerationFailsTest for test suite + */ final readonly class OnGenerationFails implements EventSubscriberInterface { public const LOG_PREFIX = '[docgen failed] '; - public function __construct(private DocGeneratorTemplateRepository $docGeneratorTemplateRepository, private EntityManagerInterface $entityManager, private LoggerInterface $logger, private MailerInterface $mailer, private StoredObjectRepository $storedObjectRepository, private TranslatorInterface $translator, private UserRepositoryInterface $userRepository) {} + public function __construct( + private DocGeneratorTemplateRepositoryInterface $docGeneratorTemplateRepository, + private EntityManagerInterface $entityManager, + private LoggerInterface $logger, + private MailerInterface $mailer, + private StoredObjectRepositoryInterface $storedObjectRepository, + private TranslatorInterface $translator, + private UserRepositoryInterface $userRepository + ) { + } public static function getSubscribedEvents() { @@ -43,13 +56,12 @@ final readonly class OnGenerationFails implements EventSubscriberInterface return; } - if (!$event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) { + $message = $event->getEnvelope()->getMessage(); + + if (!$message instanceof RequestGenerationMessage) { return; } - /** @var RequestGenerationMessage $message */ - $message = $event->getEnvelope()->getMessage(); - $this->logger->error(self::LOG_PREFIX.'Docgen failed', [ 'stored_object_id' => $message->getDestinationStoredObjectId(), 'entity_id' => $message->getEntityId(), @@ -77,16 +89,8 @@ final readonly class OnGenerationFails implements EventSubscriberInterface private function warnCreator(RequestGenerationMessage $message, WorkerMessageFailedEvent $event): void { - $creatorId = $message->getCreatorId(); - - if (null === $creator = $this->userRepository->find($creatorId)) { - $this->logger->error(self::LOG_PREFIX.'Creator not found with given id', ['creator_id', $creatorId]); - - return; - } - - if (null === $creator->getEmail() || '' === $creator->getEmail()) { - $this->logger->info(self::LOG_PREFIX.'Creator does not have any email', ['user' => $creator->getUsernameCanonical()]); + if (null === $message->getSendResultToEmail() || '' === $message->getSendResultToEmail()) { + $this->logger->info(self::LOG_PREFIX.'No email associated with this request generation'); return; } @@ -94,7 +98,7 @@ final readonly class OnGenerationFails implements EventSubscriberInterface // if the exception is not a GeneratorException, we try the previous one... $throwable = $event->getThrowable(); if (!$throwable instanceof GeneratorException) { - $throwable = $throwable->getPrevious(); + $throwable = $throwable->getPrevious() ?? $throwable; } if ($throwable instanceof GeneratorException) { @@ -109,8 +113,14 @@ final readonly class OnGenerationFails implements EventSubscriberInterface return; } + if (null === $creator = $this->userRepository->find($message->getCreatorId())) { + $this->logger->error(self::LOG_PREFIX.'Creator not found'); + + return; + } + $email = (new TemplatedEmail()) - ->to($creator->getEmail()) + ->to($message->getSendResultToEmail()) ->subject($this->translator->trans('docgen.failure_email.The generation of a document failed')) ->textTemplate('@ChillDocGenerator/Email/on_generation_failed_email.txt.twig') ->context([ diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationHandler.php b/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationHandler.php index f6723c617..c20971f27 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationHandler.php +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationHandler.php @@ -11,15 +11,21 @@ declare(strict_types=1); namespace Chill\DocGeneratorBundle\Service\Messenger; +use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface; use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository; use Chill\DocGeneratorBundle\Service\Generator\Generator; +use Chill\DocGeneratorBundle\Service\Generator\GeneratorException; use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Exception\StoredObjectManagerException; use Chill\DocStoreBundle\Repository\StoredObjectRepository; use Chill\MainBundle\Repository\UserRepositoryInterface; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; +use Symfony\Bridge\Twig\Mime\TemplatedEmail; +use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * Handle the request of document generation. @@ -30,7 +36,18 @@ class RequestGenerationHandler implements MessageHandlerInterface private const LOG_PREFIX = '[docgen message handler] '; - public function __construct(private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository, private readonly EntityManagerInterface $entityManager, private readonly Generator $generator, private readonly LoggerInterface $logger, private readonly StoredObjectRepository $storedObjectRepository, private readonly UserRepositoryInterface $userRepository) {} + public function __construct( + private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository, + private readonly EntityManagerInterface $entityManager, + private readonly Generator $generator, + private readonly LoggerInterface $logger, + private readonly StoredObjectRepository $storedObjectRepository, + private readonly UserRepositoryInterface $userRepository, + private readonly MailerInterface $mailer, + private readonly TempUrlGeneratorInterface $tempUrlGenerator, + private readonly TranslatorInterface $translator, + ) { + } public function __invoke(RequestGenerationMessage $message) { @@ -43,25 +60,59 @@ class RequestGenerationHandler implements MessageHandlerInterface } if ($destinationStoredObject->getGenerationTrialsCounter() >= self::AUTHORIZED_TRIALS) { + $this->logger->error(self::LOG_PREFIX.'Request generation abandoned: maximum number of retry reached', [ + 'template_id' => $message->getTemplateId(), + 'destination_stored_object' => $message->getDestinationStoredObjectId(), + 'trial' => $destinationStoredObject->getGenerationTrialsCounter(), + ]); + throw new UnrecoverableMessageHandlingException('maximum number of retry reached'); } $creator = $this->userRepository->find($message->getCreatorId()); + // we increase the number of generation trial in the object, and, in the same time, update the counter + // on the database side. This ensure that, if the script fails for any reason (memory limit reached), the + // counter is inscreased $destinationStoredObject->addGenerationTrial(); $this->entityManager->createQuery('UPDATE '.StoredObject::class.' s SET s.generationTrialsCounter = s.generationTrialsCounter + 1 WHERE s.id = :id') ->setParameter('id', $destinationStoredObject->getId()) ->execute(); - $this->generator->generateDocFromTemplate( - $template, - $message->getEntityId(), - $message->getContextGenerationData(), - $destinationStoredObject, - false, - null, - $creator - ); + try { + if ($message->isDumpOnly()) { + $destinationStoredObject = $this->generator->generateDataDump( + $template, + $message->getEntityId(), + $message->getContextGenerationData(), + $destinationStoredObject, + $creator + ); + + $this->sendDataDump($destinationStoredObject, $message); + } else { + $destinationStoredObject = $this->generator->generateDocFromTemplate( + $template, + $message->getEntityId(), + $message->getContextGenerationData(), + $destinationStoredObject, + $creator + ); + } + } catch (StoredObjectManagerException|GeneratorException $e) { + $this->entityManager->flush(); + + $this->logger->error(self::LOG_PREFIX.'Request generation failed', [ + 'template_id' => $message->getTemplateId(), + 'destination_stored_object' => $message->getDestinationStoredObjectId(), + 'trial' => $destinationStoredObject->getGenerationTrialsCounter(), + 'error' => $e->getTraceAsString(), + ]); + + throw $e; + } + + $this->entityManager->flush(); $this->logger->info(self::LOG_PREFIX.'Request generation finished', [ 'template_id' => $message->getTemplateId(), @@ -69,4 +120,23 @@ class RequestGenerationHandler implements MessageHandlerInterface 'duration_int' => (new \DateTimeImmutable('now'))->getTimestamp() - $message->getCreatedAt()->getTimestamp(), ]); } + + private function sendDataDump(StoredObject $destinationStoredObject, RequestGenerationMessage $message): void + { + $url = $this->tempUrlGenerator->generate('GET', $destinationStoredObject->getFilename(), 3600); + $parts = []; + parse_str(parse_url((string) $url->url)['query'], $parts); + $validity = \DateTimeImmutable::createFromFormat('U', $parts['temp_url_expires']); + + $email = (new TemplatedEmail()) + ->to($message->getSendResultToEmail()) + ->textTemplate('@ChillDocGenerator/Email/send_data_dump_to_admin.txt.twig') + ->context([ + 'link' => $url->url, + 'validity' => $validity, + ]) + ->subject($this->translator->trans('docgen.data_dump_email.subject')); + + $this->mailer->send($email); + } } diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationMessage.php b/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationMessage.php index 092073817..d41485346 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationMessage.php +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationMessage.php @@ -15,27 +15,33 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\MainBundle\Entity\User; -class RequestGenerationMessage +final readonly class RequestGenerationMessage { - private readonly int $creatorId; + private int $creatorId; - private readonly int $templateId; + private int $templateId; - private readonly int $destinationStoredObjectId; + private int $destinationStoredObjectId; - private readonly \DateTimeImmutable $createdAt; + private \DateTimeImmutable $createdAt; + + private ?string $sendResultToEmail; public function __construct( User $creator, DocGeneratorTemplate $template, - private readonly int $entityId, + private int $entityId, StoredObject $destinationStoredObject, - private readonly array $contextGenerationData + private array $contextGenerationData, + private bool $isTest = false, + ?string $sendResultToEmail = null, + private bool $dumpOnly = false, ) { $this->creatorId = $creator->getId(); $this->templateId = $template->getId(); $this->destinationStoredObjectId = $destinationStoredObject->getId(); $this->createdAt = new \DateTimeImmutable('now'); + $this->sendResultToEmail = $sendResultToEmail ?? $creator->getEmail(); } public function getCreatorId(): int @@ -67,4 +73,19 @@ class RequestGenerationMessage { return $this->createdAt; } + + public function isTest(): bool + { + return $this->isTest; + } + + public function getSendResultToEmail(): ?string + { + return $this->sendResultToEmail; + } + + public function isDumpOnly(): bool + { + return $this->dumpOnly; + } } diff --git a/src/Bundle/ChillDocGeneratorBundle/tests/Service/Context/Generator/GeneratorTest.php b/src/Bundle/ChillDocGeneratorBundle/tests/Service/Context/Generator/GeneratorTest.php index 2272f343e..0bb274228 100644 --- a/src/Bundle/ChillDocGeneratorBundle/tests/Service/Context/Generator/GeneratorTest.php +++ b/src/Bundle/ChillDocGeneratorBundle/tests/Service/Context/Generator/GeneratorTest.php @@ -20,7 +20,9 @@ use Chill\DocGeneratorBundle\Service\Generator\ObjectReadyException; use Chill\DocGeneratorBundle\Service\Generator\RelatedEntityNotFoundException; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; +use Chill\MainBundle\Entity\User; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\ManagerRegistry; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -66,7 +68,11 @@ class GeneratorTest extends TestCase $entityManager->find('DummyClass', Argument::type('int')) ->willReturn($entity); $entityManager->clear()->shouldBeCalled(); - $entityManager->flush()->shouldBeCalled(); + $entityManager->flush()->shouldNotBeCalled(); + + $managerRegistry = $this->prophesize(ManagerRegistry::class); + $managerRegistry->getManagerForClass('DummyClass')->willReturn($entityManager->reveal()); + $managerRegistry->getManagerForClass(StoredObject::class)->willReturn($entityManager->reveal()); $storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class); $storedObjectManager->read($templateStoredObject)->willReturn('template'); @@ -75,7 +81,7 @@ class GeneratorTest extends TestCase $generator = new Generator( $contextManagerInterface->reveal(), $driver->reveal(), - $entityManager->reveal(), + $managerRegistry->reveal(), new NullLogger(), $storedObjectManager->reveal() ); @@ -84,7 +90,8 @@ class GeneratorTest extends TestCase $template, 1, [], - $destinationStoredObject + $destinationStoredObject, + new User() ); } @@ -95,7 +102,7 @@ class GeneratorTest extends TestCase $generator = new Generator( $this->prophesize(ContextManagerInterface::class)->reveal(), $this->prophesize(DriverInterface::class)->reveal(), - $this->prophesize(EntityManagerInterface::class)->reveal(), + $this->prophesize(ManagerRegistry::class)->reveal(), new NullLogger(), $this->prophesize(StoredObjectManagerInterface::class)->reveal() ); @@ -108,7 +115,8 @@ class GeneratorTest extends TestCase $template, 1, [], - $destinationStoredObject + $destinationStoredObject, + new User() ); } @@ -136,10 +144,14 @@ class GeneratorTest extends TestCase $entityManager->find(Argument::type('string'), Argument::type('int')) ->willReturn(null); + $managerRegistry = $this->prophesize(ManagerRegistry::class); + $managerRegistry->getManagerForClass('DummyClass')->willReturn($entityManager->reveal()); + $managerRegistry->getManagerForClass(StoredObject::class)->willReturn($entityManager->reveal()); + $generator = new Generator( $contextManagerInterface->reveal(), $this->prophesize(DriverInterface::class)->reveal(), - $entityManager->reveal(), + $managerRegistry->reveal(), new NullLogger(), $this->prophesize(StoredObjectManagerInterface::class)->reveal() ); @@ -148,7 +160,8 @@ class GeneratorTest extends TestCase $template, 1, [], - $destinationStoredObject + $destinationStoredObject, + new User() ); } } diff --git a/src/Bundle/ChillDocGeneratorBundle/tests/Service/Messenger/OnAfterMessageHandledClearStoredObjectCacheTest.php b/src/Bundle/ChillDocGeneratorBundle/tests/Service/Messenger/OnAfterMessageHandledClearStoredObjectCacheTest.php new file mode 100644 index 000000000..9996b9b86 --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/tests/Service/Messenger/OnAfterMessageHandledClearStoredObjectCacheTest.php @@ -0,0 +1,107 @@ +prophesize(StoredObjectManagerInterface::class); + $storedObjectManager->clearCache()->shouldNotBeCalled(); + + $eventSubscriber = $this->buildEventSubscriber($storedObjectManager->reveal()); + + $eventSubscriber->afterHandling($this->buildEventSuccess(new \stdClass())); + $eventSubscriber->afterFails($this->buildEventFailed(new \stdClass())); + } + + public function testThatConcernedEventCallAClearCache(): void + { + $storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class); + $storedObjectManager->clearCache()->shouldBeCalledTimes(2); + + $eventSubscriber = $this->buildEventSubscriber($storedObjectManager->reveal()); + + $eventSubscriber->afterHandling($this->buildEventSuccess($this->buildRequestGenerationMessage())); + $eventSubscriber->afterFails($this->buildEventFailed($this->buildRequestGenerationMessage())); + } + + private function buildRequestGenerationMessage( + ): RequestGenerationMessage { + $creator = new User(); + $creator->setEmail('fake@example.com'); + + $class = new \ReflectionClass($creator); + $property = $class->getProperty('id'); + $property->setAccessible(true); + $property->setValue($creator, 1); + + $template ??= new DocGeneratorTemplate(); + $class = new \ReflectionClass($template); + $property = $class->getProperty('id'); + $property->setAccessible(true); + $property->setValue($template, 2); + + $destinationStoredObject = new StoredObject(); + $class = new \ReflectionClass($destinationStoredObject); + $property = $class->getProperty('id'); + $property->setAccessible(true); + $property->setValue($destinationStoredObject, 3); + + return new RequestGenerationMessage( + $creator, + $template, + 1, + $destinationStoredObject, + [], + ); + } + + private function buildEventSubscriber(StoredObjectManagerInterface $storedObjectManager): OnAfterMessageHandledClearStoredObjectCache + { + return new OnAfterMessageHandledClearStoredObjectCache($storedObjectManager, new NullLogger()); + } + + private function buildEventFailed(object $message): WorkerMessageFailedEvent + { + $envelope = new Envelope($message); + + return new WorkerMessageFailedEvent($envelope, 'testing', new \RuntimeException()); + } + + private function buildEventSuccess(object $message): WorkerMessageHandledEvent + { + $envelope = new Envelope($message); + + return new WorkerMessageHandledEvent($envelope, 'test_receiver'); + } +} diff --git a/src/Bundle/ChillDocGeneratorBundle/tests/Service/Messenger/OnGenerationFailsTest.php b/src/Bundle/ChillDocGeneratorBundle/tests/Service/Messenger/OnGenerationFailsTest.php new file mode 100644 index 000000000..f96b03020 --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/tests/Service/Messenger/OnGenerationFailsTest.php @@ -0,0 +1,226 @@ +prophesize(EntityManagerInterface::class); + $entityManager->flush()->shouldNotBeCalled(); + + $mailer = $this->prophesize(MailerInterface::class); + $mailer->send()->shouldNotBeCalled(); + + $eventSubscriber = $this->buildOnGenerationFailsEventSubscriber( + entityManager: $entityManager->reveal(), + mailer: $mailer->reveal() + ); + + $event = $this->buildEvent(new \stdClass()); + + $eventSubscriber->onMessageFailed($event); + } + + public function testMessageThatWillBeRetriedAreNotHandled(): void + { + $storedObject = new StoredObject(); + + $entityManager = $this->prophesize(EntityManagerInterface::class); + $entityManager->flush()->shouldNotBeCalled(); + + $mailer = $this->prophesize(MailerInterface::class); + $mailer->send()->shouldNotBeCalled(); + + $eventSubscriber = $this->buildOnGenerationFailsEventSubscriber( + entityManager: $entityManager->reveal(), + mailer: $mailer->reveal() + ); + + $event = $this->buildEvent($this->buildRequestGenerationMessage($storedObject)); + $event->setForRetry(); + + $eventSubscriber->onMessageFailed($event); + } + + public function testThatANotRetriyableEventWillMarkObjectAsFailed(): void + { + $storedObject = new StoredObject(); + + $entityManager = $this->prophesize(EntityManagerInterface::class); + $entityManager->flush()->shouldBeCalled(); + + $mailer = $this->prophesize(MailerInterface::class); + $mailer->send(Argument::type(RawMessage::class), Argument::any())->shouldBeCalled(); + + $eventSubscriber = $this->buildOnGenerationFailsEventSubscriber( + entityManager: $entityManager->reveal(), + mailer: $mailer->reveal(), + storedObject: $storedObject + ); + + $event = $this->buildEvent($this->buildRequestGenerationMessage($storedObject)); + + $eventSubscriber->onMessageFailed($event); + + self::assertEquals(StoredObject::STATUS_FAILURE, $storedObject->getStatus()); + } + + public function testThatANonRetryableEventSendAnEmail(): void + { + $storedObject = new StoredObject(); + + $entityManager = $this->prophesize(EntityManagerInterface::class); + $entityManager->flush()->shouldBeCalled(); + + $mailer = $this->prophesize(MailerInterface::class); + $mailer->send( + Argument::that(function ($arg): bool { + if (!$arg instanceof Email) { + return false; + } + + foreach ($arg->getTo() as $to) { + if ('test@test.com' === $to->getAddress()) { + return true; + } + } + + return false; + }), + Argument::any() + ) + ->shouldBeCalled(); + + $eventSubscriber = $this->buildOnGenerationFailsEventSubscriber( + entityManager: $entityManager->reveal(), + mailer: $mailer->reveal(), + storedObject: $storedObject + ); + + $event = $this->buildEvent($this->buildRequestGenerationMessage($storedObject, sendResultToEmail: 'test@test.com')); + + $eventSubscriber->onMessageFailed($event); + } + + private function buildRequestGenerationMessage( + StoredObject $destinationStoredObject, + ?User $creator = null, + ?DocGeneratorTemplate $template = null, + array $contextGenerationData = [], + bool $isTest = false, + ?string $sendResultToEmail = null, + ): RequestGenerationMessage { + if (null === $creator) { + $creator = new User(); + $creator->setEmail('fake@example.com'); + } + + if (null === $creator->getId()) { + $class = new \ReflectionClass($creator); + $property = $class->getProperty('id'); + $property->setAccessible(true); + $property->setValue($creator, 1); + } + + $template ??= new DocGeneratorTemplate(); + $class = new \ReflectionClass($template); + $property = $class->getProperty('id'); + $property->setAccessible(true); + $property->setValue($template, 2); + + $class = new \ReflectionClass($destinationStoredObject); + $property = $class->getProperty('id'); + $property->setAccessible(true); + $property->setValue($destinationStoredObject, 3); + + return new RequestGenerationMessage( + $creator, + $template, + 1, + $destinationStoredObject, + $contextGenerationData, + $isTest, + $sendResultToEmail + ); + } + + private function buildOnGenerationFailsEventSubscriber( + ?StoredObject $storedObject = null, + ?EntityManagerInterface $entityManager = null, + ?MailerInterface $mailer = null, + ): OnGenerationFails { + $storedObjectRepository = $this->prophesize(StoredObjectRepositoryInterface::class); + $storedObjectRepository->find(Argument::type('int'))->willReturn($storedObject ?? new StoredObject()); + + if (null === $entityManager) { + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + } + + if (null === $mailer) { + $mailerProphecy = $this->prophesize(MailerInterface::class); + } + + $translator = $this->prophesize(TranslatorInterface::class); + $translator->trans(Argument::type('string'))->will(fn ($args) => $args[0]); + + $userRepository = $this->prophesize(UserRepositoryInterface::class); + $userRepository->find(Argument::type('int'))->willReturn(new User()); + + $docGeneratorTemplateRepository = $this->prophesize(DocGeneratorTemplateRepositoryInterface::class); + $docGeneratorTemplateRepository->find(Argument::type('int'))->willReturn(new DocGeneratorTemplate()); + + return new OnGenerationFails( + $docGeneratorTemplateRepository->reveal(), + $entityManager ?? $entityManagerProphecy->reveal(), + new NullLogger(), + $mailer ?? $mailerProphecy->reveal(), + $storedObjectRepository->reveal(), + $translator->reveal(), + $userRepository->reveal() + ); + } + + private function buildEvent(object $message): WorkerMessageFailedEvent + { + $envelope = new Envelope($message); + + return new WorkerMessageFailedEvent($envelope, 'testing', new \RuntimeException()); + } +} diff --git a/src/Bundle/ChillDocGeneratorBundle/translations/messages+intl-icu.fr.yml b/src/Bundle/ChillDocGeneratorBundle/translations/messages+intl-icu.fr.yml new file mode 100644 index 000000000..df20907c9 --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/translations/messages+intl-icu.fr.yml @@ -0,0 +1,4 @@ +docgen: + data_dump_email: + link_valid_until: >- + Ce lien est valide jusqu'au {validity, date, full}, {validity, time, medium} diff --git a/src/Bundle/ChillDocGeneratorBundle/translations/messages.fr.yml b/src/Bundle/ChillDocGeneratorBundle/translations/messages.fr.yml index 5e55d6df8..1fdf14c02 100644 --- a/src/Bundle/ChillDocGeneratorBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillDocGeneratorBundle/translations/messages.fr.yml @@ -14,13 +14,31 @@ docgen: Doc generation is pending: La génération de ce document est en cours Come back later: Revenir plus tard + Send report to: Envoyer le rapport à + Send report errors to this email address: Les rapports d'erreurs seront envoyés à l'adresse email indiquée + Generate as creator: Générer en tant que + The document will be generated as the given creator: Le document sera généré à la place de l'utilisateur indiqué + Show data instead of generating: Montrer les données au lieu de générer le document + + Any template configured: Aucun gabarit de document configuré + + entity_id_placeholder: Identifiant de l'entité + failure_email: The generation of a document failed: La génération d'un document a échoué - The generation of the document {template_name} failed: La génération d'un document à partir du modèle {{ template_name }} a échoué. + The generation of the document %template_name% failed: La génération d'un document à partir du modèle {{ template_name }} a échoué. The following errors were encoutered: Les erreurs suivantes ont été rencontrées Forward this email to your administrator for solving: Faites suivre ce message vers votre administrateur pour la résolution du problème. References: Références + data_dump_email: + subject: Contenu des données de génération de document disponible + Dear: Cher + data_dump_ready_and_link: >- + Le contenu des données est disponible. Vous pouvez le télécharger à l'aide du lien suivant: + + + crud: docgen_template: index: @@ -28,5 +46,4 @@ crud: add_new: Créer -Show data instead of generating: Montrer les données au lieu de générer le document Template file: Fichier modèle diff --git a/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php b/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php index e937b2539..52fc35951 100644 --- a/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php +++ b/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php @@ -24,6 +24,11 @@ use Symfony\Component\Serializer\Annotation as Serializer; /** * Represent a document stored in an object store. * + * StoredObjects 's content should be read and written using the @see{StoredObjectManagerInterface}. + * + * The property `$deleteAt` allow a deletion of the document after the given date. But this property should + * be set before the document is actually written by the StoredObjectManager. + * * @ORM\Entity * * @ORM\Table("chill_doc.stored_object") @@ -116,6 +121,16 @@ class StoredObject implements Document, TrackCreationInterface */ private int $generationTrialsCounter = 0; + /** + * @ORM\Column(type="datetime_immutable", nullable=true, options={"default": null}) + */ + private ?\DateTimeImmutable $deleteAt = null; + + /** + * @ORM\Column(type="text", nullable=false, options={"default": ""}) + */ + private string $generationErrors = ''; + /** * @param StoredObject::STATUS_* $status */ @@ -143,6 +158,11 @@ class StoredObject implements Document, TrackCreationInterface */ public function getCreationDate(): \DateTime { + if (null === $this->createdAt) { + // this scenario will quite never happens + return new \DateTime('now'); + } + return \DateTime::createFromImmutable($this->createdAt); } @@ -302,4 +322,37 @@ class StoredObject implements Document, TrackCreationInterface { return self::STATUS_FAILURE === $this->getStatus(); } + + public function getDeleteAt(): ?\DateTimeImmutable + { + return $this->deleteAt; + } + + public function setDeleteAt(?\DateTimeImmutable $deleteAt): StoredObject + { + $this->deleteAt = $deleteAt; + + return $this; + } + + public function getGenerationErrors(): string + { + return $this->generationErrors; + } + + /** + * Adds generation errors to the stored object. + * + * The existing generation errors are not removed + * + * @param string $generationErrors the generation errors to be added + * + * @return StoredObject the modified StoredObject instance + */ + public function addGenerationErrors(string $generationErrors): StoredObject + { + $this->generationErrors = $this->generationErrors.$generationErrors."\n"; + + return $this; + } } diff --git a/src/Bundle/ChillDocStoreBundle/Repository/StoredObjectRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/StoredObjectRepository.php index d2e715f7e..84bc7d4cb 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/StoredObjectRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/StoredObjectRepository.php @@ -14,11 +14,10 @@ namespace Chill\DocStoreBundle\Repository; use Chill\DocStoreBundle\Entity\StoredObject; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; -use Doctrine\Persistence\ObjectRepository; -final class StoredObjectRepository implements ObjectRepository +final readonly class StoredObjectRepository implements StoredObjectRepositoryInterface { - private readonly EntityRepository $repository; + private EntityRepository $repository; public function __construct(EntityManagerInterface $entityManager) { diff --git a/src/Bundle/ChillDocStoreBundle/Repository/StoredObjectRepositoryInterface.php b/src/Bundle/ChillDocStoreBundle/Repository/StoredObjectRepositoryInterface.php new file mode 100644 index 000000000..df2202b4f --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Repository/StoredObjectRepositoryInterface.php @@ -0,0 +1,22 @@ + + */ +interface StoredObjectRepositoryInterface extends ObjectRepository +{ +} diff --git a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php index e5f7c4ee4..95aa1198c 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php +++ b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php @@ -105,6 +105,12 @@ final class StoredObjectManager implements StoredObjectManagerInterface ) : $clearContent; + $headers = []; + + if (null !== $document->getDeleteAt()) { + $headers['X-Delete-At'] = $document->getDeleteAt()->getTimestamp(); + } + try { $response = $this ->client @@ -119,6 +125,7 @@ final class StoredObjectManager implements StoredObjectManagerInterface ->url, [ 'body' => $encryptedContent, + 'headers' => $headers, ] ); } catch (TransportExceptionInterface $exception) { @@ -130,6 +137,11 @@ final class StoredObjectManager implements StoredObjectManagerInterface } } + public function clearCache(): void + { + $this->inMemory = []; + } + private function extractLastModifiedFromResponse(ResponseInterface $response): \DateTimeImmutable { $lastModifiedString = (($response->getHeaders()['last-modified'] ?? [])[0] ?? ''); diff --git a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManagerInterface.php b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManagerInterface.php index cee6586ea..d55f68023 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManagerInterface.php +++ b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManagerInterface.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Service; use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Exception\StoredObjectManagerException; interface StoredObjectManagerInterface { @@ -23,6 +24,8 @@ interface StoredObjectManagerInterface * @param StoredObject $document the document * * @return string the retrieved content in clear + * + * @throws StoredObjectManagerException if unable to read or decrypt the content */ public function read(StoredObject $document): string; @@ -31,6 +34,10 @@ interface StoredObjectManagerInterface * * @param StoredObject $document the document * @param $clearContent The content to store in clear + * + * @throws StoredObjectManagerException */ public function write(StoredObject $document, string $clearContent): void; + + public function clearCache(): void; } diff --git a/src/Bundle/ChillDocStoreBundle/Tests/StoredObjectManagerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Service/StoredObjectManagerTest.php similarity index 82% rename from src/Bundle/ChillDocStoreBundle/Tests/StoredObjectManagerTest.php rename to src/Bundle/ChillDocStoreBundle/Tests/Service/StoredObjectManagerTest.php index 94558c1b0..531d19592 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/StoredObjectManagerTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Service/StoredObjectManagerTest.php @@ -9,7 +9,7 @@ declare(strict_types=1); * the LICENSE file that was distributed with this source code. */ -namespace Chill\DocStoreBundle\Tests; +namespace Chill\DocStoreBundle\Tests\Service; use Chill\DocStoreBundle\AsyncUpload\SignedUrl; use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface; @@ -118,6 +118,41 @@ final class StoredObjectManagerTest extends TestCase self::assertEquals($clearContent, $storedObjectManager->read($storedObject)); } + public function testWriteWithDeleteAt() + { + $storedObject = new StoredObject(); + + $expectedRequests = [ + function ($method, $url, $options): MockResponse { + self::assertEquals('PUT', $method); + self::assertArrayHasKey('headers', $options); + self::assertIsArray($options['headers']); + self::assertCount(0, array_filter($options['headers'], fn (string $header) => str_contains($header, 'X-Delete-At'))); + + return new MockResponse('', ['http_code' => 201]); + }, + + function ($method, $url, $options): MockResponse { + self::assertEquals('PUT', $method); + self::assertArrayHasKey('headers', $options); + self::assertIsArray($options['headers']); + self::assertCount(1, array_filter($options['headers'], fn (string $header) => str_contains($header, 'X-Delete-At'))); + self::assertContains('X-Delete-At: 1711014260', $options['headers']); + + return new MockResponse('', ['http_code' => 201]); + }, + ]; + $client = new MockHttpClient($expectedRequests); + + $manager = new StoredObjectManager($client, $this->getTempUrlGenerator($storedObject)); + + $manager->write($storedObject, 'ok'); + + // with a deletedAt date + $storedObject->setDeleteAt(\DateTimeImmutable::createFromFormat('U', '1711014260')); + $manager->write($storedObject, 'ok'); + } + private function getHttpClient(string $encodedContent): HttpClientInterface { $callback = static function ($method, $url, $options) use ($encodedContent) { diff --git a/src/Bundle/ChillDocStoreBundle/migrations/Version20240322100107.php b/src/Bundle/ChillDocStoreBundle/migrations/Version20240322100107.php new file mode 100644 index 000000000..5f7a92b48 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/migrations/Version20240322100107.php @@ -0,0 +1,36 @@ +addSql('ALTER TABLE chill_doc.stored_object ADD deleteAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_doc.stored_object ADD generationErrors TEXT DEFAULT \'\' NOT NULL'); + $this->addSql('COMMENT ON COLUMN chill_doc.stored_object.deleteAt IS \'(DC2Type:datetime_immutable)\''); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_doc.stored_object DROP deleteAt'); + $this->addSql('ALTER TABLE chill_doc.stored_object DROP generationErrors'); + } +} diff --git a/src/Bundle/ChillEventBundle/Controller/EventController.php b/src/Bundle/ChillEventBundle/Controller/EventController.php index 3c80ccee9..c652ddecf 100644 --- a/src/Bundle/ChillEventBundle/Controller/EventController.php +++ b/src/Bundle/ChillEventBundle/Controller/EventController.php @@ -433,6 +433,7 @@ final class EventController extends AbstractController $builder->add('event_id', HiddenType::class, [ 'data' => $event->getId(), ]); + dump($event->getId()); return $builder->getForm(); } diff --git a/src/Bundle/ChillEventBundle/Entity/Event.php b/src/Bundle/ChillEventBundle/Entity/Event.php index 44f716edf..486a67c5c 100644 --- a/src/Bundle/ChillEventBundle/Entity/Event.php +++ b/src/Bundle/ChillEventBundle/Entity/Event.php @@ -197,7 +197,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter return $this->id; } - public function getModerator(): User|null + public function getModerator(): ?User { return $this->moderator; } diff --git a/src/Bundle/ChillEventBundle/Entity/Participation.php b/src/Bundle/ChillEventBundle/Entity/Participation.php index ded8ae8da..9b4664f0d 100644 --- a/src/Bundle/ChillEventBundle/Entity/Participation.php +++ b/src/Bundle/ChillEventBundle/Entity/Participation.php @@ -91,7 +91,7 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa /** * Get event. */ - public function getEvent(): Event|null + public function getEvent(): ?Event { return $this->event; } @@ -127,7 +127,7 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa /** * Get role. */ - public function getRole(): Role|null + public function getRole(): ?Role { return $this->role; } @@ -147,7 +147,7 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa /** * Get status. */ - public function getStatus(): Status|null + public function getStatus(): ?Status { return $this->status; } diff --git a/src/Bundle/ChillMainBundle/Command/LoadAndUpdateLanguagesCommand.php b/src/Bundle/ChillMainBundle/Command/LoadAndUpdateLanguagesCommand.php index 860f8c82e..e4b3ae673 100644 --- a/src/Bundle/ChillMainBundle/Command/LoadAndUpdateLanguagesCommand.php +++ b/src/Bundle/ChillMainBundle/Command/LoadAndUpdateLanguagesCommand.php @@ -12,11 +12,12 @@ declare(strict_types=1); namespace Chill\MainBundle\Command; use Chill\MainBundle\Entity\Language; -use Doctrine\ORM\EntityManager; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\Intl\Languages; /* @@ -39,7 +40,7 @@ class LoadAndUpdateLanguagesCommand extends Command /** * LoadCountriesCommand constructor. */ - public function __construct(private readonly EntityManager $entityManager, private $availableLanguages) + public function __construct(private readonly EntityManagerInterface $entityManager, private readonly ParameterBagInterface $parameterBag) { parent::__construct(); } @@ -79,7 +80,7 @@ class LoadAndUpdateLanguagesCommand extends Command protected function execute(InputInterface $input, OutputInterface $output): int { $em = $this->entityManager; - $chillAvailableLanguages = $this->availableLanguages; + $chillAvailableLanguages = $this->parameterBag->get('chill_main.available_languages'); $languages = []; foreach ($chillAvailableLanguages as $avLang) { @@ -113,7 +114,7 @@ class LoadAndUpdateLanguagesCommand extends Command $avLangNames = []; foreach ($chillAvailableLanguages as $avLang) { - $avLangNames[$avLang] = Languages::getName($code, $avLang); + $avLangNames[$avLang] = ucfirst(Languages::getName($code, $avLang)); } $languageDB->setName($avLangNames); diff --git a/src/Bundle/ChillMainBundle/Controller/AdminController.php b/src/Bundle/ChillMainBundle/Controller/AdminController.php index 7d3826823..46fbfb351 100644 --- a/src/Bundle/ChillMainBundle/Controller/AdminController.php +++ b/src/Bundle/ChillMainBundle/Controller/AdminController.php @@ -47,4 +47,12 @@ class AdminController extends AbstractController { return $this->render('@ChillMain/Admin/indexUser.html.twig'); } + + /** + * @Route("/{_locale}/admin/dashboard", name="chill_main_dashboard_admin") + */ + public function indexDashboardAction() + { + return $this->render('@ChillMain/Admin/indexDashboard.html.twig'); + } } diff --git a/src/Bundle/ChillMainBundle/Controller/DashboardApiController.php b/src/Bundle/ChillMainBundle/Controller/DashboardApiController.php new file mode 100644 index 000000000..8a6027ff3 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/DashboardApiController.php @@ -0,0 +1,52 @@ +newsItemRepository->countCurrentNews()) { + // show news only if we have news + // NOTE: maybe this should be done in the frontend... + $data[] = + [ + 'position' => 'top-left', + 'id' => 1, + 'type' => 'news', + 'metadata' => [ + // arbitrary data that will be store "some time" + 'only_unread' => false, + ], + ]; + } + + return new JsonResponse($data, JsonResponse::HTTP_OK, []); + } +} diff --git a/src/Bundle/ChillMainBundle/Controller/NewsItemApiController.php b/src/Bundle/ChillMainBundle/Controller/NewsItemApiController.php new file mode 100644 index 000000000..786a7e0f1 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/NewsItemApiController.php @@ -0,0 +1,53 @@ +newsItemRepository->countCurrentNews(); + $paginator = $this->paginatorFactory->create($total); + $newsItems = $this->newsItemRepository->findCurrentNews( + $paginator->getItemsPerPage(), + $paginator->getCurrentPage()->getFirstItemNumber() + ); + + return new JsonResponse($this->serializer->serialize( + new Collection(array_values($newsItems), $paginator), + 'json', + [ + AbstractNormalizer::GROUPS => ['read'], + ] + ), JsonResponse::HTTP_OK, [], true); + } +} diff --git a/src/Bundle/ChillMainBundle/Controller/NewsItemController.php b/src/Bundle/ChillMainBundle/Controller/NewsItemController.php new file mode 100644 index 000000000..a94dd55b9 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/NewsItemController.php @@ -0,0 +1,27 @@ +addOrderBy('e.startDate', 'DESC'); + $query->addOrderBy('e.id', 'DESC'); + + return parent::orderQuery($action, $query, $request, $paginator); + } +} diff --git a/src/Bundle/ChillMainBundle/Controller/NewsItemHistoryController.php b/src/Bundle/ChillMainBundle/Controller/NewsItemHistoryController.php new file mode 100644 index 000000000..cf1f4922b --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/NewsItemHistoryController.php @@ -0,0 +1,73 @@ +buildFilterOrder(); + $total = $this->newsItemRepository->countAllFilteredBySearchTerm($filter->getQueryString()); + $newsItems = $this->newsItemRepository->findAllFilteredBySearchTerm($filter->getQueryString()); + + $pagination = $this->paginatorFactory->create($total); + + return new Response($this->environment->render('@ChillMain/NewsItem/news_items_history.html.twig', [ + 'entities' => $newsItems, + 'paginator' => $pagination, + 'filter_order' => $filter, + ])); + } + + /** + * @Route("/{_locale}/news-items/{id}", name="chill_main_single_news_item") + */ + public function showSingleItem(NewsItem $newsItem, Request $request): Response + { + return new Response($this->environment->render( + '@ChillMain/NewsItem/show.html.twig', + [ + 'entity' => $newsItem, + ] + )); + } + + private function buildFilterOrder(): FilterOrderHelper + { + $filterBuilder = $this->filterOrderHelperFactory + ->create(self::class) + ->addSearchBox(); + + return $filterBuilder->build(); + } +} diff --git a/src/Bundle/ChillMainBundle/Cron/CronJobInterface.php b/src/Bundle/ChillMainBundle/Cron/CronJobInterface.php index dcc19f01c..050b777e9 100644 --- a/src/Bundle/ChillMainBundle/Cron/CronJobInterface.php +++ b/src/Bundle/ChillMainBundle/Cron/CronJobInterface.php @@ -28,5 +28,5 @@ interface CronJobInterface * * @return array|null optionally return an array with the same data than the previous execution */ - public function run(array $lastExecutionData): array|null; + public function run(array $lastExecutionData): ?array; } diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index b62f1f2d7..6dfae2fbd 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -19,6 +19,7 @@ use Chill\MainBundle\Controller\CountryController; use Chill\MainBundle\Controller\LanguageController; use Chill\MainBundle\Controller\LocationController; use Chill\MainBundle\Controller\LocationTypeController; +use Chill\MainBundle\Controller\NewsItemController; use Chill\MainBundle\Controller\RegroupmentController; use Chill\MainBundle\Controller\UserController; use Chill\MainBundle\Controller\UserJobApiController; @@ -53,6 +54,7 @@ use Chill\MainBundle\Entity\GeographicalUnitLayer; use Chill\MainBundle\Entity\Language; use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Entity\LocationType; +use Chill\MainBundle\Entity\NewsItem; use Chill\MainBundle\Entity\Regroupment; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\UserJob; @@ -62,6 +64,7 @@ use Chill\MainBundle\Form\CountryType; use Chill\MainBundle\Form\LanguageType; use Chill\MainBundle\Form\LocationFormType; use Chill\MainBundle\Form\LocationTypeType; +use Chill\MainBundle\Form\NewsItemType; use Chill\MainBundle\Form\RegroupmentType; use Chill\MainBundle\Form\UserJobType; use Chill\MainBundle\Form\UserType; @@ -544,6 +547,35 @@ class ChillMainExtension extends Extension implements ], ], ], + [ + 'class' => NewsItem::class, + 'name' => 'news_item', + 'base_path' => '/admin/news_item', + 'form_class' => NewsItemType::class, + 'controller' => NewsItemController::class, + 'actions' => [ + 'index' => [ + 'role' => 'ROLE_ADMIN', + 'template' => '@ChillMain/NewsItem/index.html.twig', + ], + 'new' => [ + 'role' => 'ROLE_ADMIN', + 'template' => '@ChillMain/NewsItem/new.html.twig', + ], + 'view' => [ + 'role' => 'ROLE_ADMIN', + 'template' => '@ChillMain/NewsItem/view_admin.html.twig', + ], + 'edit' => [ + 'role' => 'ROLE_ADMIN', + 'template' => '@ChillMain/NewsItem/edit.html.twig', + ], + 'delete' => [ + 'role' => 'ROLE_ADMIN', + 'template' => '@ChillMain/NewsItem/delete.html.twig', + ], + ], + ], ], 'apis' => [ [ diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/Age.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/Age.php index dd28a8efe..cce2f9ba4 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/Age.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/Age.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -40,15 +39,15 @@ class Age extends FunctionNode public function parse(Parser $parser) { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->value1 = $parser->SimpleArithmeticExpression(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->value2 = $parser->SimpleArithmeticExpression(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/Extract.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/Extract.php index 93000be9c..bc6134c60 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/Extract.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/Extract.php @@ -14,7 +14,6 @@ namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\DateDiffFunction; use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\PathExpression; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -45,17 +44,17 @@ class Extract extends FunctionNode public function parse(Parser $parser) { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); - $parser->match(Lexer::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); $this->field = $parser->getLexer()->token['value']; - $parser->match(Lexer::T_FROM); + $parser->match(\Doctrine\ORM\Query\TokenType::T_FROM); // $this->value = $parser->ScalarExpression(); $this->value = $parser->ArithmeticPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/GetJsonFieldByKey.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/GetJsonFieldByKey.php index 94d2da2e8..56b2a9cda 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/GetJsonFieldByKey.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/GetJsonFieldByKey.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -33,11 +32,11 @@ class GetJsonFieldByKey extends FunctionNode public function parse(Parser $parser) { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->expr1 = $parser->StringPrimary(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->expr2 = $parser->StringPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/Greatest.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/Greatest.php index 924831eb1..6467b93c5 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/Greatest.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/Greatest.php @@ -13,7 +13,6 @@ namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Node; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -41,15 +40,15 @@ class Greatest extends FunctionNode $this->exprs = []; $lexer = $parser->getLexer(); - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->exprs[] = $parser->ArithmeticPrimary(); - while (Lexer::T_COMMA === $lexer->lookahead['type']) { - $parser->match(Lexer::T_COMMA); + while (\Doctrine\ORM\Query\TokenType::T_COMMA === $lexer->lookahead['type']) { + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->exprs[] = $parser->ArithmeticPrimary(); } - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonAggregate.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonAggregate.php index 03809572d..171b069d3 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonAggregate.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonAggregate.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -33,9 +32,9 @@ class JsonAggregate extends FunctionNode public function parse(Parser $parser) { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->expr = $parser->StringPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonBuildObject.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonBuildObject.php index 675935867..ea6ea4c53 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonBuildObject.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonBuildObject.php @@ -13,7 +13,6 @@ namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Node; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -38,16 +37,16 @@ class JsonBuildObject extends FunctionNode public function parse(Parser $parser) { $lexer = $parser->getLexer(); - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->exprs[] = $parser->ArithmeticPrimary(); - while (Lexer::T_COMMA === $lexer->lookahead['type']) { - $parser->match(Lexer::T_COMMA); + while (\Doctrine\ORM\Query\TokenType::T_COMMA === $lexer->lookahead['type']) { + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->exprs[] = $parser->ArithmeticPrimary(); } - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonExtract.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonExtract.php index 95d851790..7abf15972 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonExtract.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonExtract.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -29,15 +28,15 @@ class JsonExtract extends FunctionNode public function parse(Parser $parser) { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->element = $parser->ArithmeticPrimary(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->keyToExtract = $parser->ArithmeticExpression(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonbArrayLength.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonbArrayLength.php index 4b3b75dfd..84f991fe8 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonbArrayLength.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonbArrayLength.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -33,9 +32,9 @@ class JsonbArrayLength extends FunctionNode public function parse(Parser $parser): void { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->expr1 = $parser->StringPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonbExistsInArray.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonbExistsInArray.php index 6ca3da89c..4c6d901d5 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonbExistsInArray.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonbExistsInArray.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -33,11 +32,11 @@ class JsonbExistsInArray extends FunctionNode public function parse(Parser $parser): void { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->expr1 = $parser->StringPrimary(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->expr2 = $parser->InputParameter(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/Least.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/Least.php index 7497e042e..aa6844e88 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/Least.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/Least.php @@ -13,7 +13,6 @@ namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Node; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -41,15 +40,15 @@ class Least extends FunctionNode $this->exprs = []; $lexer = $parser->getLexer(); - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->exprs[] = $parser->ArithmeticPrimary(); - while (Lexer::T_COMMA === $lexer->lookahead['type']) { - $parser->match(Lexer::T_COMMA); + while (\Doctrine\ORM\Query\TokenType::T_COMMA === $lexer->lookahead['type']) { + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->exprs[] = $parser->ArithmeticPrimary(); } - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/OverlapsI.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/OverlapsI.php index 2495199b1..edf00243f 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/OverlapsI.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/OverlapsI.php @@ -13,7 +13,6 @@ namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\PathExpression; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; /** @@ -45,29 +44,29 @@ class OverlapsI extends FunctionNode public function parse(Parser $parser): void { - $parser->match(Lexer::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->firstPeriodStart = $parser->StringPrimary(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->firstPeriodEnd = $parser->StringPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->secondPeriodStart = $parser->StringPrimary(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->secondPeriodEnd = $parser->StringPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } protected function makeCase($sqlWalker, $part, string $position): string diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/Replace.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/Replace.php index 7caf2c62c..d4c317f39 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/Replace.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/Replace.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; class Replace extends FunctionNode { @@ -35,19 +34,19 @@ class Replace extends FunctionNode public function parse(\Doctrine\ORM\Query\Parser $parser): void { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->string = $parser->StringPrimary(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->from = $parser->StringPrimary(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->to = $parser->StringPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/STContains.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/STContains.php index aa9bd29a4..9f3d1b861 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/STContains.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/STContains.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; /** * Geometry function 'ST_CONTAINS', added by postgis. @@ -31,15 +30,15 @@ class STContains extends FunctionNode public function parse(\Doctrine\ORM\Query\Parser $parser) { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->firstPart = $parser->StringPrimary(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->secondPart = $parser->StringPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/STX.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/STX.php index 2841bb729..f0d99d837 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/STX.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/STX.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -27,11 +26,11 @@ class STX extends FunctionNode public function parse(Parser $parser) { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->field = $parser->ArithmeticExpression(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/STY.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/STY.php index 9457a9357..42842605d 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/STY.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/STY.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -27,11 +26,11 @@ class STY extends FunctionNode public function parse(Parser $parser) { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->field = $parser->ArithmeticExpression(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/Similarity.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/Similarity.php index 2aa97fb9c..477e0c542 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/Similarity.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/Similarity.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; class Similarity extends FunctionNode { @@ -28,15 +27,15 @@ class Similarity extends FunctionNode public function parse(\Doctrine\ORM\Query\Parser $parser) { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->firstPart = $parser->StringPrimary(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->secondPart = $parser->StringPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/StrictWordSimilarityOPS.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/StrictWordSimilarityOPS.php index 7d0ef2acf..0096825a4 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/StrictWordSimilarityOPS.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/StrictWordSimilarityOPS.php @@ -11,7 +11,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -29,15 +28,15 @@ class StrictWordSimilarityOPS extends \Doctrine\ORM\Query\AST\Functions\Function public function parse(Parser $parser) { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->firstPart = $parser->StringPrimary(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->secondPart = $parser->StringPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/ToChar.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/ToChar.php index ef150867e..dc73aea92 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/ToChar.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/ToChar.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -36,11 +35,11 @@ class ToChar extends FunctionNode public function parse(Parser $parser) { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->datetime = $parser->ArithmeticExpression(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->fmt = $parser->StringExpression(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/Unaccent.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/Unaccent.php index 5813c546f..12a745a0c 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/Unaccent.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/Unaccent.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; /** * Unaccent string using postgresql extension unaccent : @@ -31,11 +30,11 @@ class Unaccent extends FunctionNode public function parse(\Doctrine\ORM\Query\Parser $parser) { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->string = $parser->StringPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Entity/DashboardConfigItem.php b/src/Bundle/ChillMainBundle/Entity/DashboardConfigItem.php new file mode 100644 index 000000000..ed9cc07bf --- /dev/null +++ b/src/Bundle/ChillMainBundle/Entity/DashboardConfigItem.php @@ -0,0 +1,112 @@ +id; + } + + public function getType(): string + { + return $this->type; + } + + public function setType(string $type): self + { + $this->type = $type; + + return $this; + } + + public function getPosition(): string + { + return $this->position; + } + + public function setPosition(string $position): void + { + $this->position = $position; + } + + public function getUser(): User + { + return $this->user; + } + + public function setUser(User $user): void + { + $this->user = $user; + } + + public function getMetadata(): array + { + return $this->metadata; + } + + public function setMetadata(array $metadata): void + { + $this->metadata = $metadata; + } +} diff --git a/src/Bundle/ChillMainBundle/Entity/NewsItem.php b/src/Bundle/ChillMainBundle/Entity/NewsItem.php new file mode 100644 index 000000000..604c58c5f --- /dev/null +++ b/src/Bundle/ChillMainBundle/Entity/NewsItem.php @@ -0,0 +1,128 @@ +title; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function getContent(): string + { + return $this->content; + } + + public function setContent(string $content): void + { + $this->content = $content; + } + + public function getStartDate(): ?\DateTimeImmutable + { + return $this->startDate; + } + + public function setStartDate(?\DateTimeImmutable $startDate): void + { + $this->startDate = $startDate; + } + + public function getEndDate(): ?\DateTimeImmutable + { + return $this->endDate; + } + + public function setEndDate(?\DateTimeImmutable $endDate): void + { + $this->endDate = $endDate; + } + + public function getId(): ?int + { + return $this->id; + } +} diff --git a/src/Bundle/ChillMainBundle/Entity/User.php b/src/Bundle/ChillMainBundle/Entity/User.php index fe02c1491..4ea96bb2d 100644 --- a/src/Bundle/ChillMainBundle/Entity/User.php +++ b/src/Bundle/ChillMainBundle/Entity/User.php @@ -550,7 +550,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter $this->scopeHistories[] = $newScope; $criteria = new Criteria(); - $criteria->orderBy(['startDate' => Criteria::ASC, 'id' => Criteria::ASC]); + $criteria->orderBy(['startDate' => \Doctrine\Common\Collections\Order::Ascending, 'id' => \Doctrine\Common\Collections\Order::Ascending]); /** @var \Iterator $scopes */ $scopes = $this->scopeHistories->matching($criteria)->getIterator(); @@ -606,7 +606,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter $this->jobHistories[] = $newJob; $criteria = new Criteria(); - $criteria->orderBy(['startDate' => Criteria::ASC, 'id' => Criteria::ASC]); + $criteria->orderBy(['startDate' => \Doctrine\Common\Collections\Order::Ascending, 'id' => \Doctrine\Common\Collections\Order::Ascending]); /** @var \Iterator $jobs */ $jobs = $this->jobHistories->matching($criteria)->getIterator(); diff --git a/src/Bundle/ChillMainBundle/Form/NewsItemType.php b/src/Bundle/ChillMainBundle/Form/NewsItemType.php new file mode 100644 index 000000000..b6a93a0a0 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Form/NewsItemType.php @@ -0,0 +1,56 @@ +add('title', TextType::class, [ + 'required' => true, + ]) + ->add('content', ChillTextareaType::class, [ + 'required' => false, + ]) + ->add( + 'startDate', + ChillDateType::class, + [ + 'required' => true, + 'input' => 'datetime_immutable', + 'label' => 'news.startDate', + ] + ) + ->add('endDate', ChillDateType::class, [ + 'required' => false, + 'input' => 'datetime_immutable', + 'label' => 'news.endDate', + ]); + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefault('data_class', NewsItem::class); + } +} diff --git a/src/Bundle/ChillMainBundle/Repository/NewsItemRepository.php b/src/Bundle/ChillMainBundle/Repository/NewsItemRepository.php new file mode 100644 index 000000000..4552e2a92 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Repository/NewsItemRepository.php @@ -0,0 +1,145 @@ +repository = $entityManager->getRepository(NewsItem::class); + } + + public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder + { + return $this->repository->createQueryBuilder($alias, $indexBy); + } + + public function find($id) + { + return $this->repository->find($id); + } + + public function findAll() + { + return $this->repository->findAll(); + } + + public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null) + { + return $this->repository->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria) + { + return $this->repository->findOneBy($criteria); + } + + public function getClassName() + { + return NewsItem::class; + } + + private function buildBaseQuery( + ?string $pattern = null + ): QueryBuilder { + $qb = $this->createQueryBuilder('n'); + + $qb->where('n.startDate <= :now'); + $qb->setParameter('now', $this->clock->now()); + + if (null !== $pattern && '' !== $pattern) { + $qb->andWhere($qb->expr()->like('LOWER(UNACCENT(n.title))', 'LOWER(UNACCENT(:pattern))')) + ->orWhere($qb->expr()->like('LOWER(UNACCENT(n.content))', 'LOWER(UNACCENT(:pattern))')) + ->setParameter('pattern', '%'.$pattern.'%'); + } + + return $qb; + } + + public function findAllFilteredBySearchTerm(?string $pattern = null) + { + $qb = $this->buildBaseQuery($pattern); + $qb + ->addOrderBy('n.startDate', 'DESC') + ->addOrderBy('n.id', 'DESC'); + + return $qb->getQuery()->getResult(); + } + + /** + * @return list + */ + public function findCurrentNews(?int $limit = null, ?int $offset = null): array + { + $qb = $this->buildQueryCurrentNews(); + $qb->addOrderBy('n.startDate', 'DESC'); + + if (null !== $limit) { + $qb->setMaxResults($limit); + } + + if (null !== $offset) { + $qb->setFirstResult($offset); + } + + return $qb + ->getQuery() + ->getResult(); + } + + public function countAllFilteredBySearchTerm(?string $pattern = null) + { + $qb = $this->buildBaseQuery($pattern); + + return $qb + ->select('COUNT(n)') + ->getQuery() + ->getSingleScalarResult(); + } + + public function countCurrentNews() + { + return $this->buildQueryCurrentNews() + ->select('COUNT(n)') + ->getQuery() + ->getSingleScalarResult(); + } + + private function buildQueryCurrentNews(): QueryBuilder + { + $now = $this->clock->now(); + + $qb = $this->createQueryBuilder('n'); + $qb + ->where( + $qb->expr()->andX( + $qb->expr()->lte('n.startDate', ':now'), + $qb->expr()->orX( + $qb->expr()->gt('n.endDate', ':now'), + $qb->expr()->isNull('n.endDate') + ) + ) + ) + ->setParameter('now', $now); + + return $qb; + } +} diff --git a/src/Bundle/ChillMainBundle/Repository/ScopeRepository.php b/src/Bundle/ChillMainBundle/Repository/ScopeRepository.php index ba4ddae9b..aa3c78270 100644 --- a/src/Bundle/ChillMainBundle/Repository/ScopeRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/ScopeRepository.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Repository; use Chill\MainBundle\Entity\Scope; +use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\QueryBuilder; @@ -20,7 +21,7 @@ final class ScopeRepository implements ScopeRepositoryInterface { private readonly EntityRepository $repository; - public function __construct(EntityManagerInterface $entityManager) + public function __construct(EntityManagerInterface $entityManager, private readonly TranslatableStringHelperInterface $translatableStringHelper) { $this->repository = $entityManager->getRepository(Scope::class); } @@ -45,11 +46,11 @@ final class ScopeRepository implements ScopeRepositoryInterface public function findAllActive(): array { - $qb = $this->repository->createQueryBuilder('s'); + $scopes = $this->repository->findBy(['active' => true]); - $qb->where('s.active = \'TRUE\''); + usort($scopes, fn (Scope $a, Scope $b) => $this->translatableStringHelper->localize($a->getName()) <=> $this->translatableStringHelper->localize($b->getName())); - return $qb->getQuery()->getResult(); + return $scopes; } /** diff --git a/src/Bundle/ChillMainBundle/Repository/UserJobRepository.php b/src/Bundle/ChillMainBundle/Repository/UserJobRepository.php index 3c92c7f60..6b793a2c8 100644 --- a/src/Bundle/ChillMainBundle/Repository/UserJobRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/UserJobRepository.php @@ -40,7 +40,11 @@ readonly class UserJobRepository implements UserJobRepositoryInterface public function findAllActive(): array { - return $this->repository->findBy(['active' => true]); + $jobs = $this->repository->findBy(['active' => true]); + + usort($jobs, fn (UserJob $a, UserJob $b) => $this->translatableStringHelper->localize($a->getLabel()) <=> $this->translatableStringHelper->localize($b->getLabel())); + + return $jobs; } public function findAllOrderedByName(): array diff --git a/src/Bundle/ChillMainBundle/Repository/UserRepository.php b/src/Bundle/ChillMainBundle/Repository/UserRepository.php index dc5a9adfe..5c5dff4d0 100644 --- a/src/Bundle/ChillMainBundle/Repository/UserRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/UserRepository.php @@ -106,8 +106,8 @@ final readonly class UserRepository implements UserRepositoryInterface FROM users u LEFT JOIN chill_main_civility civility ON u.civility_id = civility.id LEFT JOIN centers mainCenter ON u.maincenter_id = mainCenter.id - LEFT JOIN chill_main_user_job_history userJobHistory ON u.id = userJobHistory.user_id - LEFT JOIN chill_main_user_job userJob ON userJobHistory.job_id = userJob.id AND tstzrange(userJobHistory.startdate, userJobHistory.enddate) @> NOW() + LEFT JOIN chill_main_user_job_history userJobHistory ON u.id = userJobHistory.user_id AND tstzrange(userJobHistory.startdate, userJobHistory.enddate) @> NOW() + LEFT JOIN chill_main_user_job userJob ON userJobHistory.job_id = userJob.id LEFT JOIN chill_main_user_scope_history userScopeHistory ON u.id = userScopeHistory.user_id AND tstzrange(userScopeHistory.startdate, userScopeHistory.enddate) @> NOW() LEFT JOIN scopes mainScope ON userScopeHistory.scope_id = mainScope.id LEFT JOIN chill_main_location currentLocation ON u.currentlocation_id = currentLocation.id diff --git a/src/Bundle/ChillMainBundle/Resources/public/module/news/index.js b/src/Bundle/ChillMainBundle/Resources/public/module/news/index.js new file mode 100644 index 000000000..67aac616f --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/module/news/index.js @@ -0,0 +1 @@ +import './index.scss'; diff --git a/src/Bundle/ChillMainBundle/Resources/public/module/news/index.scss b/src/Bundle/ChillMainBundle/Resources/public/module/news/index.scss new file mode 100644 index 000000000..7b65cda80 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/module/news/index.scss @@ -0,0 +1,7 @@ +div.flex-table { + .news-content { + p { + margin-top: 1rem; + } + } +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/types.ts b/src/Bundle/ChillMainBundle/Resources/public/types.ts index b31b70897..2e33b8248 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/types.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/types.ts @@ -160,3 +160,11 @@ export interface LocationType { contactData: "optional" | "required"; title: TranslatableString; } + +export interface NewsItemType { + id: number; + title: string; + content: string; + startDate: DateTime; + endDate: DateTime | null; +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/App.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/App.vue index 315fd863f..02763221a 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/App.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/App.vue @@ -97,6 +97,8 @@ import MyNotifications from './MyNotifications'; import MyWorkflows from './MyWorkflows.vue'; import TabCounter from './TabCounter'; import { mapState } from "vuex"; +import { makeFetch } from "ChillMainAssets/lib/api/apiMethods"; + export default { name: "App", @@ -112,7 +114,7 @@ export default { }, data() { return { - activeTab: 'MyCustoms' + activeTab: 'MyCustoms', } }, computed: { @@ -126,8 +128,11 @@ export default { }, methods: { selectTab(tab) { - this.$store.dispatch('getByTab', { tab: tab }); + if (tab !== 'MyCustoms') { + this.$store.dispatch('getByTab', { tab: tab }); + } this.activeTab = tab; + console.log(this.activeTab) } }, mounted() { diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/DashboardWidgets/News.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/DashboardWidgets/News.vue new file mode 100644 index 000000000..ff1cae89b --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/DashboardWidgets/News.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/DashboardWidgets/NewsItem.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/DashboardWidgets/NewsItem.vue new file mode 100644 index 000000000..bbad4315c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/DashboardWidgets/NewsItem.vue @@ -0,0 +1,183 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyCustoms.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyCustoms.vue index 5e9cb79df..43ad31c45 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyCustoms.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyCustoms.vue @@ -1,76 +1,73 @@ @@ -98,4 +103,10 @@ span.counter { background-color: unset; } } - \ No newline at end of file + +div.news { + max-height: 22rem; + overflow: hidden; + overflow-y: scroll; +} + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/i18n.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/i18n.js index c4c97e5c6..ddc1b55ef 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/i18n.js +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/i18n.js @@ -63,7 +63,15 @@ const appMessages = { }, emergency: "Urgent", confidential: "Confidentiel", - automatic_notification: "Notification automatique" + automatic_notification: "Notification automatique", + widget: { + news: { + title: "Actualités", + readMore: "Lire la suite", + date: "Date", + none: "Aucune actualité" + } + } } }; diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/store.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/store.js index 088cb93b7..1579a3d0c 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/store.js +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/store.js @@ -96,13 +96,11 @@ const store = createStore({ }, catchError(state, error) { state.errorMsg.push(error); - } + }, }, actions: { getByTab({ commit, getters }, { tab, param }) { switch (tab) { - case 'MyCustoms': - break; // case 'MyWorks': // if (!getters.isWorksLoaded) { // commit('setLoading', true); @@ -221,8 +219,8 @@ const store = createStore({ default: throw 'tab '+ tab; } - } + }, }, }); -export { store }; \ No newline at end of file +export { store }; diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsButton.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsButton.vue index f5e914d27..9a1550d7d 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsButton.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsButton.vue @@ -15,18 +15,20 @@ import AddressModal from "./AddressModal.vue"; export interface AddressModalContentProps { address_id: number; - address_ref_status: AddressRefStatus | null; + address_ref_status: AddressRefStatus; } -const data = reactive<{ - loading: boolean, - working_address: Address | null, - working_ref_status: AddressRefStatus | null, -}>({ +interface AddressModalData { + loading: boolean, + working_address: Address | null, + working_ref_status: AddressRefStatus | null, +} + +const data: AddressModalData = reactive({ loading: false, working_address: null, working_ref_status: null, -}); +} as AddressModalData); const props = defineProps(); diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.ts b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.ts index c509ac10f..f81699a7c 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.ts @@ -51,7 +51,7 @@ const messages = { years_old: "1 an | {n} an | {n} ans", residential_address: "Adresse de résidence", located_at: "réside chez" - } + }, } }; diff --git a/src/Bundle/ChillMainBundle/Resources/views/Admin/indexDashboard.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Admin/indexDashboard.html.twig new file mode 100644 index 000000000..9c7513d4c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Admin/indexDashboard.html.twig @@ -0,0 +1,13 @@ +{% extends "@ChillMain/Admin/layoutWithVerticalMenu.html.twig" %} + +{% block vertical_menu_content %} + {{ chill_menu('admin_news_item', { + 'layout': '@ChillMain/Admin/menu_admin_section.html.twig', + }) }} +{% endblock %} + +{% block layout_wvm_content %} + {% block admin_content %} +

{{ 'admin.dashboard.description' | trans }}

+ {% endblock %} +{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_title.html.twig b/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_title.html.twig index 3473dd298..00688ecd9 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_title.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_title.html.twig @@ -1 +1 @@ -{{ 'crud.%crud_name%.title_view'|trans({'%crud_name%' : crud_name }) }} \ No newline at end of file +{{ ('crud.' ~ crud_name ~ '.title_view')|trans({'%crud_name%' : crud_name }) }} diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/_list.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/_list.html.twig new file mode 100644 index 000000000..11ca95995 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/_list.html.twig @@ -0,0 +1,30 @@ +
+
+

+ {{ entity.title }} +

+
+
+

+ {% if entity.startDate %} + {{ entity.startDate|format_date('long') }} + {% endif %} + {% if entity.endDate %} + - {{ entity.endDate|format_date('long') }} + {% endif %} +

+
+
+
+ {{ entity.content|u.truncate(350, '… [' ~ ('news.read_more'|trans) ~ '](' ~ chill_path_add_return_path('chill_main_single_news_item', { 'id': entity.id } ) ~ ')', false)|chill_markdown_to_html }} +
+
+
+
    +
  • + +
  • +
+
+
+ diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/_show.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/_show.html.twig new file mode 100644 index 000000000..5cd830bbc --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/_show.html.twig @@ -0,0 +1,15 @@ +
+
+

+ {{ entity.startDate|format_date('long') }} + {% if entity.endDate is not null %} + - {{ entity.endDate|format_date('long') }} + {% endif %} +

+
+
+
+ {{ entity.content|chill_markdown_to_html }} +
+
+
diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/delete.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/delete.html.twig new file mode 100644 index 000000000..28efd4748 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/delete.html.twig @@ -0,0 +1,6 @@ +{% extends '@ChillMain/CRUD/Admin/index.html.twig' %} + +{% block admin_content %} + {% embed '@ChillMain/CRUD/_delete_content.html.twig' %} + {% endembed %} +{% endblock admin_content %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/edit.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/edit.html.twig new file mode 100644 index 000000000..4d55c480c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/edit.html.twig @@ -0,0 +1,11 @@ +{% extends '@ChillMain/CRUD/Admin/index.html.twig' %} + +{% block title %} + {% include('@ChillMain/CRUD/_edit_title.html.twig') %} +{% endblock %} + +{% block admin_content %} + {% embed '@ChillMain/CRUD/_edit_content.html.twig' %} + {% block content_form_actions_save_and_show %}{% endblock %} + {% endembed %} +{% endblock admin_content %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/index.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/index.html.twig new file mode 100644 index 000000000..0a197353b --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/index.html.twig @@ -0,0 +1,43 @@ +{% extends '@ChillMain/CRUD/Admin/index.html.twig' %} + +{% block admin_content %} + {% embed '@ChillMain/CRUD/_index.html.twig' %} + {% block table_entities_thead_tr %} + {{ 'Title'|trans }} + {{ 'news.startDate'|trans }} + {{ 'news.endDate'|trans }} + {% endblock %} + {% block table_entities_tbody %} + {% for entity in entities %} + + {{ entity.title }} + {{ entity.startDate|format_date('long') }} + {% if entity.endDate is not null %} + {{ entity.endDate|format_date('long') }} + {% else %} + {{ 'news.noDate'|trans }} + {% endif %} + +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+ + + {% endfor %} + {% endblock %} + + {% block actions_before %} +
  • + {{'Back to the admin'|trans}} +
  • + {% endblock %} + {% endembed %} +{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/new.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/new.html.twig new file mode 100644 index 000000000..7c204dddd --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/new.html.twig @@ -0,0 +1,11 @@ +{% extends '@ChillMain/CRUD/Admin/index.html.twig' %} + +{% block title %} + {% include('@ChillMain/CRUD/_new_title.html.twig') %} +{% endblock %} + +{% block admin_content %} + {% embed '@ChillMain/CRUD/_new_content.html.twig' %} + {% block content_form_actions_save_and_show %}{% endblock %} + {% endembed %} +{% endblock admin_content %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/news_items_history.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/news_items_history.html.twig new file mode 100644 index 000000000..ff93a9e09 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/news_items_history.html.twig @@ -0,0 +1,70 @@ +{% extends "@ChillMain/layout.html.twig" %} + +{% block title %} + {{ 'news.title'|trans }} +{% endblock title %} + +{% block css %} + {{ parent() }} + {{ encore_entry_link_tags('mod_news') }} +{% endblock %} + +{% block content %} +
    +

    {{ 'news.title'|trans }}

    + + {{ filter_order|chill_render_filter_order_helper }} + + {% if entities|length == 0 %} +

    + {{ "news.no_data"|trans }} +

    + {% else %} + +
    + + {% for entity in entities %} + +
    +
    +
    +
    +
    +

    {{ entity.title }}

    +
    +
    +

    + {% if entity.startDate %} + {{ entity.startDate|format_date('long') }} + {% endif %} + {% if entity.endDate %} + - {{ entity.endDate|format_date('long') }} + {% endif %} +

    +
    +
    +
    +
    + +
    +
    + {{ entity.content|u.truncate(350, '…', false)|chill_markdown_to_html }} +
    +
    + {% if entity.content|length > 350 %} + + {% endif %} +
    + {% endfor %} +
    + + {{ chill_pagination(paginator) }} + {% endif %} +
    +{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/show.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/show.html.twig new file mode 100644 index 000000000..a718a2121 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/show.html.twig @@ -0,0 +1,24 @@ +{% extends '@ChillMain/layout.html.twig' %} + +{% block title 'news.show_details'|trans %} + +{% block css %} + {{ parent() }} + {{ encore_entry_link_tags('mod_news') }} +{% endblock %} + +{% block content %} +
    +
    +

    {{ entity.title }}

    + + {% include '@ChillMain/NewsItem/_show.html.twig' with { 'entity': entity } %} + + +
    +
    +{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/view_admin.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/view_admin.html.twig new file mode 100644 index 000000000..92ed2c235 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/view_admin.html.twig @@ -0,0 +1,34 @@ +{% extends '@ChillMain/CRUD/Admin/index.html.twig' %} + +{% block title %} + {% include('@ChillMain/CRUD/_view_title.html.twig') %} +{% endblock %} + +{% block css %} + {{ parent() }} + {{ encore_entry_link_tags('mod_news') }} +{% endblock %} + +{% block admin_content %} + +
    +
    +

    {{ entity.title }}

    + + {% include '@ChillMain/NewsItem/_show.html.twig' with { 'entity': entity } %} + + +
    +
    + +{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Routing/MenuBuilder/AdminNewsMenuBuilder.php b/src/Bundle/ChillMainBundle/Routing/MenuBuilder/AdminNewsMenuBuilder.php new file mode 100644 index 000000000..0ce6a9824 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Routing/MenuBuilder/AdminNewsMenuBuilder.php @@ -0,0 +1,47 @@ +authorizationChecker->isGranted('ROLE_ADMIN')) { + return; + } + + $menu->addChild('admin.dashboard.title', [ + 'route' => 'chill_main_dashboard_admin', + ]) + ->setAttribute('class', 'list-group-item-header') + ->setExtras([ + 'order' => 9000, + ]); + + $menu->addChild('admin.dashboard.news', [ + 'route' => 'chill_crud_news_item_index', + ])->setExtras(['order' => 9000]); + } + + public static function getMenuIds(): array + { + return ['admin_section', 'admin_news_item']; + } +} diff --git a/src/Bundle/ChillMainBundle/Routing/MenuBuilder/SectionMenuBuilder.php b/src/Bundle/ChillMainBundle/Routing/MenuBuilder/SectionMenuBuilder.php index 37c47e1bd..ab73932b5 100644 --- a/src/Bundle/ChillMainBundle/Routing/MenuBuilder/SectionMenuBuilder.php +++ b/src/Bundle/ChillMainBundle/Routing/MenuBuilder/SectionMenuBuilder.php @@ -58,6 +58,14 @@ class SectionMenuBuilder implements LocalMenuBuilderInterface 'order' => 20, ]); } + + $menu->addChild($this->translator->trans('news.menu'), [ + 'route' => 'chill_main_news_items_history', + ]) + ->setExtras([ + 'icons' => ['newspaper-o'], + 'order' => 5, + ]); } public static function getMenuIds(): array diff --git a/src/Bundle/ChillMainBundle/Service/AddressGeographicalUnit/CollateAddressWithReferenceOrPostalCodeCronJob.php b/src/Bundle/ChillMainBundle/Service/AddressGeographicalUnit/CollateAddressWithReferenceOrPostalCodeCronJob.php index 603265682..4e64a1f9f 100644 --- a/src/Bundle/ChillMainBundle/Service/AddressGeographicalUnit/CollateAddressWithReferenceOrPostalCodeCronJob.php +++ b/src/Bundle/ChillMainBundle/Service/AddressGeographicalUnit/CollateAddressWithReferenceOrPostalCodeCronJob.php @@ -40,7 +40,7 @@ final readonly class CollateAddressWithReferenceOrPostalCodeCronJob implements C return 'collate-address'; } - public function run(array $lastExecutionData): array|null + public function run(array $lastExecutionData): ?array { $maxId = ($this->collateAddressWithReferenceOrPostalCode)($lastExecutionData[self::LAST_MAX_ID] ?? 0); diff --git a/src/Bundle/ChillMainBundle/Service/AddressGeographicalUnit/RefreshAddressToGeographicalUnitMaterializedViewCronJob.php b/src/Bundle/ChillMainBundle/Service/AddressGeographicalUnit/RefreshAddressToGeographicalUnitMaterializedViewCronJob.php index 6e39b6f92..489e75151 100644 --- a/src/Bundle/ChillMainBundle/Service/AddressGeographicalUnit/RefreshAddressToGeographicalUnitMaterializedViewCronJob.php +++ b/src/Bundle/ChillMainBundle/Service/AddressGeographicalUnit/RefreshAddressToGeographicalUnitMaterializedViewCronJob.php @@ -45,7 +45,7 @@ final readonly class RefreshAddressToGeographicalUnitMaterializedViewCronJob imp return 'refresh-materialized-view-address-to-geog-units'; } - public function run(array $lastExecutionData): array|null + public function run(array $lastExecutionData): ?array { $this->connection->executeQuery('REFRESH MATERIALIZED VIEW view_chill_main_address_geographical_unit'); diff --git a/src/Bundle/ChillMainBundle/Templating/Entity/NewsItemRender.php b/src/Bundle/ChillMainBundle/Templating/Entity/NewsItemRender.php new file mode 100644 index 000000000..fd2e2e211 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Templating/Entity/NewsItemRender.php @@ -0,0 +1,35 @@ + + */ +final readonly class NewsItemRender implements ChillEntityRenderInterface +{ + public function renderBox($entity, array $options): string + { + return ''; + } + + public function renderString($entity, array $options): string + { + return $entity->getTitle(); + } + + public function supports($newsItem, array $options): bool + { + return $newsItem instanceof NewsItem; + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemApiControllerTest.php b/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemApiControllerTest.php new file mode 100644 index 000000000..f8f7aafea --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemApiControllerTest.php @@ -0,0 +1,39 @@ +getClientAuthenticated(); + + $client->request('GET', '/api/1.0/main/news/current.json'); + $this->assertResponseIsSuccessful('Testing whether the GET request to the news item Api endpoint was successful'); + + $responseContent = json_decode($client->getResponse()->getContent(), true, 512, JSON_THROW_ON_ERROR); + + if (!empty($responseContent['data'][0])) { + $this->assertArrayHasKey('title', $responseContent['data'][0]); + } + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemControllerTest.php b/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemControllerTest.php new file mode 100644 index 000000000..5aa515fd1 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemControllerTest.php @@ -0,0 +1,96 @@ + + */ + private static array $entitiesToDelete = []; + + private readonly EntityManagerInterface $em; + + protected function tearDown(): void + { + self::ensureKernelShutdown(); + } + + public static function tearDownAfterClass(): void + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + foreach (self::$entitiesToDelete as [$class, $id]) { + $entity = $em->find($class, $id); + + if (null !== $entity) { + $em->remove($entity); + } + } + + $em->flush(); + } + + public static function generateNewsItemIds(): iterable + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + $newsItem = new NewsItem(); + $newsItem->setTitle('Lorem Ipsum'); + $newsItem->setContent('some text'); + $newsItem->setStartDate(new \DateTimeImmutable('now')); + + $em->persist($newsItem); + $em->flush(); + + self::$entitiesToDelete[] = [NewsItem::class, $newsItem]; + + self::ensureKernelShutdown(); + + yield [$newsItem]; + } + + public function testList() + { + $client = $this->getClientAuthenticated('admin', 'password'); + $client->request('GET', '/fr/admin/news_item'); + + self::assertResponseIsSuccessful('News item admin page shows'); + } + + /** + * @dataProvider generateNewsItemIds + */ + public function testShowSingleItem(NewsItem $newsItem) + { + $client = $this->getClientAuthenticated('admin', 'password'); + $client->request('GET', "/fr/admin/news_item/{$newsItem->getId()}/view"); + + self::assertResponseIsSuccessful('Single news item admin page loads successfully'); + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemsHistoryControllerTest.php b/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemsHistoryControllerTest.php new file mode 100644 index 000000000..19da9ac18 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemsHistoryControllerTest.php @@ -0,0 +1,97 @@ + + */ + private static array $toDelete = []; + + public static function tearDownAfterClass(): void + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + foreach (static::$toDelete as [$class, $entity]) { + $query = $em->createQuery(sprintf('DELETE FROM %s e WHERE e.id = :id', $class)) + ->setParameter('id', $entity->getId()); + $query->execute(); + } + + static::$toDelete = []; + + self::ensureKernelShutdown(); + } + + protected function tearDown(): void + { + self::ensureKernelShutdown(); + } + + public static function generateNewsItemIds(): iterable + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + $news = new NewsItem(); + + $news->setContent('test content'); + $news->setTitle('Title'); + $news->setStartDate(new \DateTimeImmutable('yesterday')); + + $em->persist($news); + $em->flush(); + + static::$toDelete[] = [NewsItem::class, $news]; + + self::ensureKernelShutdown(); + + yield [$news->getId()]; + } + + public function testList() + { + self::ensureKernelShutdown(); + $client = $this->getClientAuthenticated(); + + $client->request('GET', '/fr/news-items/history'); + + self::assertResponseIsSuccessful('Test that /fr/news-items history shows'); + } + + /** + * @dataProvider generateNewsItemIds + */ + public function testShowSingleItem(int $newsItemId) + { + self::ensureKernelShutdown(); + $client = $this->getClientAuthenticated(); + + $client->request('GET', "/fr/news-items/{$newsItemId}"); + + $this->assertResponseIsSuccessful('test that single news item page loads successfully'); + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Cron/CronJobDatabaseInteractionTest.php b/src/Bundle/ChillMainBundle/Tests/Cron/CronJobDatabaseInteractionTest.php index 88f46497b..278cecd1e 100644 --- a/src/Bundle/ChillMainBundle/Tests/Cron/CronJobDatabaseInteractionTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Cron/CronJobDatabaseInteractionTest.php @@ -91,7 +91,7 @@ class JobWithReturn implements CronJobInterface return 'with-data'; } - public function run(array $lastExecutionData): array|null + public function run(array $lastExecutionData): ?array { return ['data' => 'test']; } diff --git a/src/Bundle/ChillMainBundle/Tests/Cron/CronManagerTest.php b/src/Bundle/ChillMainBundle/Tests/Cron/CronManagerTest.php index 9d0a15f6d..548017c90 100644 --- a/src/Bundle/ChillMainBundle/Tests/Cron/CronManagerTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Cron/CronManagerTest.php @@ -173,7 +173,7 @@ class JobCanRun implements CronJobInterface return $this->key; } - public function run(array $lastExecutionData): array|null + public function run(array $lastExecutionData): ?array { return null; } @@ -191,7 +191,7 @@ class JobCannotRun implements CronJobInterface return 'job-b'; } - public function run(array $lastExecutionData): array|null + public function run(array $lastExecutionData): ?array { return null; } diff --git a/src/Bundle/ChillMainBundle/Tests/Repository/NewsItemRepositoryTest.php b/src/Bundle/ChillMainBundle/Tests/Repository/NewsItemRepositoryTest.php new file mode 100644 index 000000000..7aab97e48 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Repository/NewsItemRepositoryTest.php @@ -0,0 +1,108 @@ + + */ + private array $toDelete = []; + + protected function setUp(): void + { + self::bootKernel(); + $this->entityManager = self::$container->get(EntityManagerInterface::class); + } + + protected function tearDown(): void + { + foreach ($this->toDelete as [$class, $entity]) { + $query = $this->entityManager->createQuery(sprintf('DELETE FROM %s e WHERE e.id = :id', $class)) + ->setParameter('id', $entity->getId()); + $query->execute(); + } + + $this->toDelete = []; + } + + private function getNewsItemsRepository(ClockInterface $clock): NewsItemRepository + { + return new NewsItemRepository($this->entityManager, $clock); + } + + public function testFindCurrentNews() + { + $clock = new MockClock($now = new \DateTimeImmutable('2023-01-10')); + $repository = $this->getNewsItemsRepository($clock); + + $newsItem1 = new NewsItem(); + $newsItem1->setTitle('This is a mock news item'); + $newsItem1->setContent('We are testing that the repository returns the correct news items'); + $newsItem1->setStartDate(new \DateTimeImmutable('2023-01-01')); + $newsItem1->setEndDate(new \DateTimeImmutable('2023-01-05')); + + $newsItem2 = new NewsItem(); + $newsItem2->setTitle('This is a mock news item'); + $newsItem2->setContent('We are testing that the repository returns the correct news items'); + $newsItem2->setStartDate(new \DateTimeImmutable('2023-01-01')); + $newsItem2->setEndDate($now->add(new \DateInterval('P1D'))); + + $newsItem3 = new NewsItem(); + $newsItem3->setTitle('This is a mock news item'); + $newsItem3->setContent('We are testing that the repository returns the correct news items'); + $newsItem3->setStartDate(new \DateTimeImmutable('2033-11-03')); + $newsItem3->setEndDate(null); + + $newsItem4 = new NewsItem(); + $newsItem4->setTitle('This is a mock news item'); + $newsItem4->setContent('We are testing that the repository returns the correct news items'); + $newsItem4->setStartDate(new \DateTimeImmutable('2023-01-03')); + $newsItem4->setEndDate(null); + + $this->entityManager->persist($newsItem1); + $this->entityManager->persist($newsItem2); + $this->entityManager->persist($newsItem3); + $this->entityManager->persist($newsItem4); + $this->entityManager->flush(); + + $this->toDelete = [ + [NewsItem::class, $newsItem1], + [NewsItem::class, $newsItem2], + [NewsItem::class, $newsItem3], + [NewsItem::class, $newsItem4], + ]; + + // Call the method to test + $result = $repository->findCurrentNews(); + + // Assertions + $this->assertCount(2, $result); + $this->assertInstanceOf(NewsItem::class, $result[0]); + $this->assertContains($newsItem2, $result); + $this->assertContains($newsItem4, $result); + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/UserNormalizerTest.php b/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/UserNormalizerTest.php index ab59e0a63..1f0f8d5bf 100644 --- a/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/UserNormalizerTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/UserNormalizerTest.php @@ -117,7 +117,7 @@ final class UserNormalizerTest extends TestCase * * @throws ExceptionInterface */ - public function testNormalize(User|null $user, mixed $format, mixed $context, mixed $expected) + public function testNormalize(?User $user, mixed $format, mixed $context, mixed $expected) { $userRender = $this->prophesize(UserRender::class); $userRender->renderString(Argument::type(User::class), Argument::type('array'))->willReturn($user ? $user->getLabel() : ''); diff --git a/src/Bundle/ChillMainBundle/Validation/Constraint/RoleScopeScopePresenceConstraint.php b/src/Bundle/ChillMainBundle/Validation/Constraint/RoleScopeScopePresenceConstraint.php index a379b9539..7528908b7 100644 --- a/src/Bundle/ChillMainBundle/Validation/Constraint/RoleScopeScopePresenceConstraint.php +++ b/src/Bundle/ChillMainBundle/Validation/Constraint/RoleScopeScopePresenceConstraint.php @@ -21,7 +21,7 @@ class RoleScopeScopePresenceConstraint extends Constraint public $messageNullRequired = 'The role "%role%" should not be associated with a scope.'; public $messagePresenceRequired = 'The role "%role%" require to be associated with ' - .'a scope.'; + .'a scope.'; public function getTargets() { diff --git a/src/Bundle/ChillMainBundle/chill.api.specs.yaml b/src/Bundle/ChillMainBundle/chill.api.specs.yaml index 98e0e915e..f37ee723d 100644 --- a/src/Bundle/ChillMainBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillMainBundle/chill.api.specs.yaml @@ -10,6 +10,12 @@ servers: components: schemas: + Date: + type: object + properties: + datetime: + type: string + format: date-time User: type: object properties: @@ -131,6 +137,35 @@ components: id: type: integer + DashboardConfigItem: + type: object + properties: + id: + type: integer + type: + type: string + metadata: + type: object + userId: + type: integer + position: + type: string + + NewsItem: + type: object + properties: + id: + type: integer + title: + type: string + content: + type: string + startDate: + $ref: "#/components/schemas/Date" + endDate: + $ref: "#/components/schemas/Date" + + paths: /1.0/search.json: get: @@ -842,4 +877,34 @@ paths: $ref: '#/components/schemas/Workflow' 403: description: "Unauthorized" + /1.0/main/dashboard-config-item.json: + get: + tags: + - dashboard config item + summary: Returns the dashboard configuration for the current user. + responses: + 200: + description: "ok" + content: + application/json: + schema: + $ref: '#/components/schemas/DashboardConfigItem' + 403: + description: "Unauthorized" + /1.0/main/news/current.json: + get: + tags: + - news items + summary: Returns a list of news items which are valid + responses: + 200: + description: "ok" + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/NewsItem' + 403: + description: "Unauthorized" diff --git a/src/Bundle/ChillMainBundle/chill.webpack.config.js b/src/Bundle/ChillMainBundle/chill.webpack.config.js index 792d0a27e..7e0060892 100644 --- a/src/Bundle/ChillMainBundle/chill.webpack.config.js +++ b/src/Bundle/ChillMainBundle/chill.webpack.config.js @@ -76,6 +76,7 @@ module.exports = function(encore, entries) encore.addEntry('mod_pick_postal_code', __dirname + '/Resources/public/module/pick-postal-code/index.js'); encore.addEntry('mod_pick_rolling_date', __dirname + '/Resources/public/module/pick-rolling-date/index.js'); encore.addEntry('mod_address_details', __dirname + '/Resources/public/module/address-details/index'); + encore.addEntry('mod_news', __dirname + '/Resources/public/module/news/index.js'); // Vue entrypoints encore.addEntry('vue_address', __dirname + '/Resources/public/vuejs/Address/index.js'); diff --git a/src/Bundle/ChillMainBundle/config/services/command.yaml b/src/Bundle/ChillMainBundle/config/services/command.yaml index 29d6d4bf1..8a2327c0b 100644 --- a/src/Bundle/ChillMainBundle/config/services/command.yaml +++ b/src/Bundle/ChillMainBundle/config/services/command.yaml @@ -17,9 +17,6 @@ services: - { name: console.command } Chill\MainBundle\Command\LoadAndUpdateLanguagesCommand: - arguments: - $entityManager: '@doctrine.orm.entity_manager' - $availableLanguages: '%chill_main.available_languages%' tags: - { name: console.command } diff --git a/src/Bundle/ChillMainBundle/config/services/templating.yaml b/src/Bundle/ChillMainBundle/config/services/templating.yaml index e69700732..0baa91b69 100644 --- a/src/Bundle/ChillMainBundle/config/services/templating.yaml +++ b/src/Bundle/ChillMainBundle/config/services/templating.yaml @@ -47,6 +47,8 @@ services: Chill\MainBundle\Templating\Entity\AddressRender: ~ + Chill\MainBundle\Templating\Entity\NewsItemRender: ~ + Chill\MainBundle\Templating\Entity\UserRender: ~ Chill\MainBundle\Templating\Listing\: diff --git a/src/Bundle/ChillMainBundle/migrations/Version20231108141141.php b/src/Bundle/ChillMainBundle/migrations/Version20231108141141.php new file mode 100644 index 000000000..d4fe9b561 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20231108141141.php @@ -0,0 +1,55 @@ +addSql('CREATE SEQUENCE chill_main_dashboard_config_item_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE chill_main_news_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE chill_main_dashboard_config_item (id INT NOT NULL, user_id INT DEFAULT NULL, type VARCHAR(255) NOT NULL, position VARCHAR(255) NOT NULL, metadata JSONB DEFAULT \'{}\'::jsonb, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_CF59DFD6A76ED395 ON chill_main_dashboard_config_item (user_id)'); + $this->addSql('CREATE TABLE chill_main_news (id INT NOT NULL, title TEXT NOT NULL, content TEXT NOT NULL, startDate DATE NOT NULL, endDate DATE DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_96922AFB3174800F ON chill_main_news (createdBy_id)'); + $this->addSql('CREATE INDEX IDX_96922AFB65FF1AEC ON chill_main_news (updatedBy_id)'); + $this->addSql('COMMENT ON COLUMN chill_main_news.startDate IS \'(DC2Type:date_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_main_news.endDate IS \'(DC2Type:date_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_main_news.createdAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_main_news.updatedAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE chill_main_dashboard_config_item ADD CONSTRAINT FK_CF59DFD6A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_news ADD CONSTRAINT FK_96922AFB3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_news ADD CONSTRAINT FK_96922AFB65FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP SEQUENCE chill_main_dashboard_config_item_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE chill_main_news_id_seq CASCADE'); + $this->addSql('ALTER TABLE chill_main_dashboard_config_item DROP CONSTRAINT FK_CF59DFD6A76ED395'); + $this->addSql('ALTER TABLE chill_main_news DROP CONSTRAINT FK_96922AFB3174800F'); + $this->addSql('ALTER TABLE chill_main_news DROP CONSTRAINT FK_96922AFB65FF1AEC'); + $this->addSql('DROP TABLE chill_main_dashboard_config_item'); + $this->addSql('DROP TABLE chill_main_news'); + } +} diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index 91068275f..421cac473 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -82,7 +82,6 @@ Comment: Commentaire Comments: Commentaires Pinned comment: Commentaire épinglé Any comment: Aucun commentaire -Read more: Lire la suite (more...): (suite...) # comment embeddable @@ -438,6 +437,16 @@ crud: add_new: Ajouter un centre title_new: Nouveau centre title_edit: Modifier un centre + news_item: + index: + title: Liste des actualités + add_new: Créer une nouvelle actualité + title_new: Nouvelle actualité + title_view: Voir l'actualité + title_edit: Modifier une actualité + title_delete: Supprimer une actualité + button_delete: Supprimer + confirm_message_delete: Êtes-vous sûr de vouloir supprimer l'actualité, "%as_string%" ? No entities: Aucun élément @@ -679,3 +688,20 @@ admin: undefined: non défini user: Utilisateur scope: Service + dashboard: + title: Tableau de bord + news: Actualités + description: Configuration du tableau de bord + + +news: + noDate: Pas de date de fin + startDate: Date de début de publication + endDate: Date de fin de publication sur la page d'accueil + title: Historique des actualités + menu: Actualités + no_data: Aucune actualité + read_more: Lire la suite + show_details: Voir l'actualité + + diff --git a/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Lifecycle/AccompanyingPeriodStepChangeCronjob.php b/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Lifecycle/AccompanyingPeriodStepChangeCronjob.php index 4a0e922d2..27553444b 100644 --- a/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Lifecycle/AccompanyingPeriodStepChangeCronjob.php +++ b/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Lifecycle/AccompanyingPeriodStepChangeCronjob.php @@ -38,7 +38,7 @@ readonly class AccompanyingPeriodStepChangeCronjob implements CronJobInterface return 'accompanying-period-step-change'; } - public function run(array $lastExecutionData): array|null + public function run(array $lastExecutionData): ?array { ($this->requestor)(); diff --git a/src/Bundle/ChillPersonBundle/ChillPersonBundle.php b/src/Bundle/ChillPersonBundle/ChillPersonBundle.php index 8a48cccba..89477a47b 100644 --- a/src/Bundle/ChillPersonBundle/ChillPersonBundle.php +++ b/src/Bundle/ChillPersonBundle/ChillPersonBundle.php @@ -13,6 +13,7 @@ namespace Chill\PersonBundle; use Chill\PersonBundle\Actions\Remove\PersonMoveSqlHandlerInterface; use Chill\PersonBundle\DependencyInjection\CompilerPass\AccompanyingPeriodTimelineCompilerPass; +use Chill\PersonBundle\Export\Helper\CustomizeListPersonHelperInterface; use Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoUnionQueryPartInterface; use Chill\PersonBundle\Widget\PersonListWidgetFactory; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -32,5 +33,7 @@ class ChillPersonBundle extends Bundle ->addTag('chill_person.accompanying_period_info_part'); $container->registerForAutoconfiguration(PersonMoveSqlHandlerInterface::class) ->addTag('chill_person.person_move_handler'); + $container->registerForAutoconfiguration(CustomizeListPersonHelperInterface::class) + ->addTag('chill_person.list_person_customizer'); } } diff --git a/src/Bundle/ChillPersonBundle/DependencyInjection/Configuration.php b/src/Bundle/ChillPersonBundle/DependencyInjection/Configuration.php index 3579407f2..0d935e8c0 100644 --- a/src/Bundle/ChillPersonBundle/DependencyInjection/Configuration.php +++ b/src/Bundle/ChillPersonBundle/DependencyInjection/Configuration.php @@ -22,8 +22,8 @@ use Symfony\Component\Config\Definition\ConfigurationInterface; class Configuration implements ConfigurationInterface { private string $validationBirthdateNotAfterInfos = 'The period before today during which' - .' any birthdate is not allowed. The birthdate is expressed as ISO8601 : ' - .'https://en.wikipedia.org/wiki/ISO_8601#Durations'; + .' any birthdate is not allowed. The birthdate is expressed as ISO8601 : ' + .'https://en.wikipedia.org/wiki/ISO_8601#Durations'; public function getConfigTreeBuilder() { diff --git a/src/Bundle/ChillPersonBundle/Doctrine/DQL/AddressPart.php b/src/Bundle/ChillPersonBundle/Doctrine/DQL/AddressPart.php index 229a69f2c..2ebc1a859 100644 --- a/src/Bundle/ChillPersonBundle/Doctrine/DQL/AddressPart.php +++ b/src/Bundle/ChillPersonBundle/Doctrine/DQL/AddressPart.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\PersonBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -73,13 +72,13 @@ abstract class AddressPart extends FunctionNode public function parse(Parser $parser) { - $a = $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $a = $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); // person id $this->pid = $parser->SingleValuedPathExpression(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); // date $this->date = $parser->ArithmeticPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php index d555dec42..a38b7055c 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php @@ -555,7 +555,7 @@ class AccompanyingPeriod implements // ensure continuity of histories $criteria = new Criteria(); - $criteria->orderBy(['startDate' => Criteria::ASC, 'id' => Criteria::ASC]); + $criteria->orderBy(['startDate' => \Doctrine\Common\Collections\Order::Ascending, 'id' => \Doctrine\Common\Collections\Order::Ascending]); /** @var \Iterator $locations */ $locations = $this->getLocationHistories()->matching($criteria)->getIterator(); @@ -1536,7 +1536,7 @@ class AccompanyingPeriod implements { // ensure continuity of histories $criteria = new Criteria(); - $criteria->orderBy(['startDate' => Criteria::ASC, 'id' => Criteria::ASC]); + $criteria->orderBy(['startDate' => \Doctrine\Common\Collections\Order::Ascending, 'id' => \Doctrine\Common\Collections\Order::Ascending]); /** @var \Iterator $steps */ $steps = $this->getStepHistories()->matching($criteria)->getIterator(); diff --git a/src/Bundle/ChillPersonBundle/Entity/Person.php b/src/Bundle/ChillPersonBundle/Entity/Person.php index d337d5d7f..3ee30ac87 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Person.php +++ b/src/Bundle/ChillPersonBundle/Entity/Person.php @@ -1205,7 +1205,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI ->where( $expr->eq('shareHousehold', false) ) - ->orderBy(['startDate' => Criteria::DESC]); + ->orderBy(['startDate' => \Doctrine\Common\Collections\Order::Descending]); return $this->getHouseholdParticipations() ->matching($criteria); @@ -1227,7 +1227,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI ->where( $expr->eq('shareHousehold', true) ) - ->orderBy(['startDate' => Criteria::DESC, 'id' => Criteria::DESC]); + ->orderBy(['startDate' => \Doctrine\Common\Collections\Order::Descending, 'id' => \Doctrine\Common\Collections\Order::Descending]); return $this->getHouseholdParticipations() ->matching($criteria); diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ClosingDateAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ClosingDateAggregator.php index 6a9cce351..ab6bb6170 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ClosingDateAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ClosingDateAggregator.php @@ -48,7 +48,7 @@ final readonly class ClosingDateAggregator implements AggregatorInterface public function getLabels($key, array $values, mixed $data) { - return function (string|null $value): string { + return function (?string $value): string { if ('_header' === $value) { return 'export.aggregator.course.by_closing_date.header'; } diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/OpeningDateAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/OpeningDateAggregator.php index 6f9ea4859..d0d121c2a 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/OpeningDateAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/OpeningDateAggregator.php @@ -48,7 +48,7 @@ final readonly class OpeningDateAggregator implements AggregatorInterface public function getLabels($key, array $values, mixed $data) { - return function (string|null $value): string { + return function (?string $value): string { if ('_header' === $value) { return 'export.aggregator.course.by_opening_date.header'; } diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListPerson.php b/src/Bundle/ChillPersonBundle/Export/Export/ListPerson.php index 8529fac10..37b7ca7d7 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/ListPerson.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListPerson.php @@ -14,10 +14,8 @@ namespace Chill\PersonBundle\Export\Export; use Chill\CustomFieldsBundle\CustomFields\CustomFieldChoice; use Chill\CustomFieldsBundle\Entity\CustomField; use Chill\CustomFieldsBundle\Service\CustomFieldProvider; -use Chill\MainBundle\Export\ExportElementValidatedInterface; use Chill\MainBundle\Export\FormatterInterface; use Chill\MainBundle\Export\GroupedExportInterface; -use Chill\MainBundle\Export\Helper\ExportAddressHelper; use Chill\MainBundle\Export\ListInterface; use Chill\MainBundle\Form\Type\ChillDateType; use Chill\MainBundle\Templating\TranslatableStringHelper; @@ -30,21 +28,17 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Query; use PhpOffice\PhpSpreadsheet\Shared\Date; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; -use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Validator\Constraints\Callback; -use Symfony\Component\Validator\Context\ExecutionContextInterface; /** * Render a list of people. */ -class ListPerson implements ExportElementValidatedInterface, ListInterface, GroupedExportInterface +class ListPerson implements ListInterface, GroupedExportInterface { private array $slugs = []; private readonly bool $filterStatsByCenters; public function __construct( - private readonly ExportAddressHelper $addressHelper, private readonly CustomFieldProvider $customFieldProvider, private readonly ListPersonHelper $listPersonHelper, private readonly EntityManagerInterface $entityManager, @@ -56,39 +50,6 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou public function buildForm(FormBuilderInterface $builder) { - $choices = array_combine(ListPersonHelper::FIELDS, ListPersonHelper::FIELDS); - - foreach ($this->getCustomFields() as $cf) { - $choices[$this->translatableStringHelper->localize($cf->getName())] - = - $cf->getSlug(); - } - - // Add a checkbox to select fields - $builder->add('fields', ChoiceType::class, [ - 'multiple' => true, - 'expanded' => true, - 'choices' => $choices, - 'label' => 'Fields to include in export', - 'choice_attr' => static function (string $val): array { - // add a 'data-display-target' for address fields - if (str_starts_with($val, 'address') || 'center' === $val || 'household' === $val) { - return ['data-display-target' => 'address_date']; - } - - return []; - }, - 'constraints' => [new Callback([ - 'callback' => static function ($selected, ExecutionContextInterface $context) { - if (0 === \count($selected)) { - $context->buildViolation('You must select at least one element') - ->atPath('fields') - ->addViolation(); - } - }, - ])], - ]); - // add a date field for addresses $builder->add('address_date', ChillDateType::class, [ 'label' => 'Data valid at this date', @@ -99,15 +60,7 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou public function getFormDefaultData(): array { - $choices = array_combine(ListPersonHelper::FIELDS, ListPersonHelper::FIELDS); - - foreach ($this->getCustomFields() as $cf) { - $choices[$this->translatableStringHelper->localize($cf->getName())] - = - $cf->getSlug(); - } - - return ['fields' => array_values($choices), 'address_date' => new \DateTimeImmutable()]; + return ['address_date' => new \DateTimeImmutable()]; } public function getAllowedFormattersTypes() @@ -127,7 +80,7 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou public function getLabels($key, array $values, $data) { - if (\in_array($key, $this->listPersonHelper->getAllPossibleFields(), true)) { + if (\in_array($key, $this->listPersonHelper->getAllKeys(), true)) { return $this->listPersonHelper->getLabels($key, $values, $data); } @@ -138,28 +91,12 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou { $fields = []; - foreach (ListPersonHelper::FIELDS as $key) { - if (!\in_array($key, $data['fields'], true)) { - continue; - } - - if (str_starts_with($key, 'address_fields')) { - $fields = \array_merge($fields, $this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields')); - - continue; - } - - if ('lifecycleUpdate' === $key) { - $fields = \array_merge($fields, ['createdAt', 'createdBy', 'updatedAt', 'updatedBy']); - - continue; - } - + foreach ($this->listPersonHelper->getAllKeys() as $key) { $fields[] = $key; } // add the key from slugs and return - return \array_merge($fields, \array_keys($this->slugs)); + return [...$fields, ...\array_keys($this->slugs)]; } public function getResult($query, $data) @@ -184,11 +121,6 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou { $centers = array_map(static fn ($el) => $el['center'], $acl); - // throw an error if any fields are present - if (!\array_key_exists('fields', $data)) { - throw new \Doctrine\DBAL\Exception\InvalidArgumentException('any fields have been checked'); - } - $qb = $this->entityManager->createQueryBuilder() ->from(Person::class, 'person'); @@ -202,15 +134,9 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou ->setParameter('authorized_centers', $centers); } - $fields = $data['fields']; - - $this->listPersonHelper->addSelect($qb, $fields, $data['address_date']); + $this->listPersonHelper->addSelect($qb, $data['address_date']); foreach ($this->getCustomFields() as $cf) { - if (!\in_array($cf->getSlug(), $fields, true)) { - continue; - } - $cfType = $this->customFieldProvider->getCustomFieldByType($cf->getType()); if ($cfType instanceof CustomFieldChoice && $cfType->isMultiple($cf)) { @@ -251,26 +177,6 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou return [Declarations::PERSON_TYPE]; } - public function validateForm($data, ExecutionContextInterface $context) - { - // get the field starting with address_ - $addressFields = array_filter( - ListPersonHelper::FIELDS, - static fn (string $el): bool => str_starts_with($el, 'address_') - ); - - // check if there is one field starting with address in data - if (\count(array_intersect($data['fields'], $addressFields)) > 0) { - // if a field address is checked, the date must not be empty - if (!$data['address_date'] instanceof \DateTimeImmutable) { - $context - ->buildViolation('You must set this date if an address is checked') - ->atPath('address_date') - ->addViolation(); - } - } - } - private function DQLToSlug($cleanedSlug) { return $this->slugs[$cleanedSlug]['slug']; @@ -293,9 +199,9 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou { return $this->entityManager ->createQuery('SELECT cf ' - .'FROM ChillCustomFieldsBundle:CustomField cf ' + .'FROM '.CustomField::class.' cf ' .'JOIN cf.customFieldGroup g ' - .'WHERE cf.type != :title AND g.entity LIKE :entity') + .'WHERE cf.type != :title AND g.entity LIKE :entity AND cf.active = TRUE') ->setParameters([ 'title' => 'title', 'entity' => \addcslashes(Person::class, '\\'), diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonHavingAccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonHavingAccompanyingPeriod.php index e638838fb..4a44f2dc5 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonHavingAccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonHavingAccompanyingPeriod.php @@ -12,10 +12,8 @@ declare(strict_types=1); namespace Chill\PersonBundle\Export\Export; use Chill\MainBundle\Export\AccompanyingCourseExportHelper; -use Chill\MainBundle\Export\ExportElementValidatedInterface; use Chill\MainBundle\Export\FormatterInterface; use Chill\MainBundle\Export\GroupedExportInterface; -use Chill\MainBundle\Export\Helper\ExportAddressHelper; use Chill\MainBundle\Export\ListInterface; use Chill\MainBundle\Form\Type\PickRollingDateType; use Chill\MainBundle\Service\RollingDate\RollingDate; @@ -30,22 +28,18 @@ use DateTimeImmutable; use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; -use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Validator\Constraints\Callback; -use Symfony\Component\Validator\Context\ExecutionContextInterface; /** * List the persons, having an accompanying period. * * Details of the accompanying period are not included */ -final readonly class ListPersonHavingAccompanyingPeriod implements ExportElementValidatedInterface, ListInterface, GroupedExportInterface +final readonly class ListPersonHavingAccompanyingPeriod implements ListInterface, GroupedExportInterface { private bool $filterStatsByCenters; public function __construct( - private ExportAddressHelper $addressHelper, private ListPersonHelper $listPersonHelper, private EntityManagerInterface $entityManager, private RollingDateConverterInterface $rollingDateConverter, @@ -56,32 +50,6 @@ final readonly class ListPersonHavingAccompanyingPeriod implements ExportElement public function buildForm(FormBuilderInterface $builder) { - $choices = array_combine(ListPersonHelper::FIELDS, ListPersonHelper::FIELDS); - - $builder->add('fields', ChoiceType::class, [ - 'multiple' => true, - 'expanded' => true, - 'choices' => $choices, - 'label' => 'Fields to include in export', - 'choice_attr' => static function (string $val): array { - // add a 'data-display-target' for address fields - if (str_starts_with($val, 'address') || 'center' === $val || 'household' === $val) { - return ['data-display-target' => 'address_date']; - } - - return []; - }, - 'constraints' => [new Callback([ - 'callback' => static function ($selected, ExecutionContextInterface $context) { - if (0 === \count($selected)) { - $context->buildViolation('You must select at least one element') - ->atPath('fields') - ->addViolation(); - } - }, - ])], - ]); - $builder->add('address_date_rolling', PickRollingDateType::class, [ 'label' => 'Data valid at this date', 'help' => 'Data regarding center, addresses, and so on will be computed at this date', @@ -90,9 +58,7 @@ final readonly class ListPersonHavingAccompanyingPeriod implements ExportElement public function getFormDefaultData(): array { - $choices = array_combine(ListPersonHelper::FIELDS, ListPersonHelper::FIELDS); - - return ['fields' => array_values($choices), 'address_date_rolling' => new RollingDate(RollingDate::T_TODAY)]; + return ['address_date_rolling' => new RollingDate(RollingDate::T_TODAY)]; } public function getAllowedFormattersTypes() @@ -117,29 +83,7 @@ final readonly class ListPersonHavingAccompanyingPeriod implements ExportElement public function getQueryKeys($data) { - $fields = []; - - foreach (ListPersonHelper::FIELDS as $key) { - if (!\in_array($key, $data['fields'], true)) { - continue; - } - - if (str_starts_with($key, 'address_fields')) { - $fields = array_merge($fields, $this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields')); - - continue; - } - - if ('lifecycleUpdate' === $key) { - $fields = array_merge($fields, ['createdAt', 'createdBy', 'updatedAt', 'updatedBy']); - - continue; - } - - $fields[] = $key; - } - - return $fields; + return $this->listPersonHelper->getAllKeys(); } public function getResult($query, $data) @@ -164,11 +108,6 @@ final readonly class ListPersonHavingAccompanyingPeriod implements ExportElement { $centers = array_map(static fn ($el) => $el['center'], $acl); - // throw an error if any fields are present - if (!\array_key_exists('fields', $data)) { - throw new \Doctrine\DBAL\Exception\InvalidArgumentException('any fields have been checked'); - } - $qb = $this->entityManager->createQueryBuilder(); $qb->from(Person::class, 'person') @@ -185,9 +124,7 @@ final readonly class ListPersonHavingAccompanyingPeriod implements ExportElement )->setParameter('authorized_centers', $centers); } - $fields = $data['fields']; - - $this->listPersonHelper->addSelect($qb, $fields, $this->rollingDateConverter->convert($data['address_date_rolling'])); + $this->listPersonHelper->addSelect($qb, $this->rollingDateConverter->convert($data['address_date_rolling'])); AccompanyingCourseExportHelper::addClosingMotiveExclusionClause($qb); @@ -208,24 +145,4 @@ final readonly class ListPersonHavingAccompanyingPeriod implements ExportElement { return [Declarations::PERSON_TYPE, Declarations::ACP_TYPE]; } - - public function validateForm($data, ExecutionContextInterface $context) - { - // get the field starting with address_ - $addressFields = array_filter( - ListPersonHelper::FIELDS, - static fn (string $el): bool => str_starts_with($el, 'address_') - ); - - // check if there is one field starting with address in data - if (\count(array_intersect($data['fields'], $addressFields)) > 0) { - // if a field address is checked, the date must not be empty - if (!$data['address_date'] instanceof \DateTimeImmutable) { - $context - ->buildViolation('You must set this date if an address is checked') - ->atPath('address_date') - ->addViolation(); - } - } - } } diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriodDetails.php b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriodDetails.php index d283c8d8a..42a2205a1 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriodDetails.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriodDetails.php @@ -119,7 +119,7 @@ final readonly class ListPersonWithAccompanyingPeriodDetails implements ListInte $this->filterListAccompanyingPeriodHelper->addFilterAccompanyingPeriods($qb, $requiredModifiers, $acl, $data); - $this->listPersonHelper->addSelect($qb, ListPersonHelper::FIELDS, $this->rollingDateConverter->convert($data['address_date'])); + $this->listPersonHelper->addSelect($qb, $this->rollingDateConverter->convert($data['address_date'])); $this->listAccompanyingPeriodHelper->addSelectClauses($qb, $this->rollingDateConverter->convert($data['address_date'])); AccompanyingCourseExportHelper::addClosingMotiveExclusionClause($qb); diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/JobWorkingOnCourseFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/JobWorkingOnCourseFilter.php index 099320c95..622aa5801 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/JobWorkingOnCourseFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/JobWorkingOnCourseFilter.php @@ -83,13 +83,10 @@ readonly class JobWorkingOnCourseFilter implements FilterInterface public function buildForm(FormBuilderInterface $builder): void { - $jobs = $this->userJobRepository->findAllActive(); - usort($jobs, fn (UserJob $a, UserJob $b) => $this->translatableStringHelper->localize($a->getLabel()) <=> $this->translatableStringHelper->localize($b->getLabel())); - $builder ->add('jobs', EntityType::class, [ 'class' => UserJob::class, - 'choices' => $jobs, + 'choices' => $this->userJobRepository->findAllActive(), 'choice_label' => fn (UserJob $userJob) => $this->translatableStringHelper->localize($userJob->getLabel()), 'multiple' => true, 'expanded' => true, diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ScopeWorkingOnCourseFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ScopeWorkingOnCourseFilter.php index 403ddd0b5..63d93805f 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ScopeWorkingOnCourseFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ScopeWorkingOnCourseFilter.php @@ -83,13 +83,10 @@ readonly class ScopeWorkingOnCourseFilter implements FilterInterface public function buildForm(FormBuilderInterface $builder): void { - $scopes = $this->scopeRepository->findAllActive(); - usort($scopes, fn (Scope $a, Scope $b) => $this->translatableStringHelper->localize($a->getName()) <=> $this->translatableStringHelper->localize($b->getName())); - $builder ->add('scopes', EntityType::class, [ 'class' => Scope::class, - 'choices' => $scopes, + 'choices' => $this->scopeRepository->findAllActive(), 'choice_label' => fn (Scope $scope) => $this->translatableStringHelper->localize($scope->getName()), 'multiple' => true, 'expanded' => true, diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkWithEvaluationBetweenDatesFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkWithEvaluationBetweenDatesFilter.php new file mode 100644 index 000000000..439cb47ca --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkWithEvaluationBetweenDatesFilter.php @@ -0,0 +1,85 @@ +add('start_date', PickRollingDateType::class, [ + 'label' => 'export.filter.work.evaluation_between_dates.start_date', + ]) + ->add('end_date', PickRollingDateType::class, [ + 'label' => 'export.filter.work.evaluation_between_dates.end_date', + ]); + } + + public function getFormDefaultData(): array + { + return ['start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)]; + } + + public function getTitle(): string + { + return 'export.filter.work.evaluation_between_dates.title'; + } + + public function describeAction($data, $format = 'string'): array + { + return [ + 'export.filter.work.evaluation_between_dates.description', + [ + '%startDate%' => null !== $data['start_date'] ? $this->rollingDateConverter->convert($data['start_date'])->format('d-m-Y') : '', + '%endDate%' => null !== $data['end_date'] ? $this->rollingDateConverter->convert($data['end_date'])->format('d-m-Y') : '', + ], + ]; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data): void + { + $s = 'workeval_between_filter_start'; + $e = 'workeval_between_filter_end'; + + $qb->andWhere( + $qb->expr()->exists( + 'SELECT 1 FROM '.AccompanyingPeriodWorkEvaluation::class." workeval_between_filter_workeval WHERE workeval_between_filter_workeval.createdAt BETWEEN :{$s} AND :{$e} AND IDENTITY(workeval_between_filter_workeval.accompanyingPeriodWork) = acpw.id" + ) + ) + ->setParameter($s, $this->rollingDateConverter->convert($data['start_date'])) + ->setParameter($e, $this->rollingDateConverter->convert($data['end_date'])); + } + + public function applyOn(): string + { + return Declarations::SOCIAL_WORK_ACTION_TYPE; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/JobFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/JobFilter.php index d3e54f2a5..bb54f288b 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/JobFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/JobFilter.php @@ -13,6 +13,7 @@ namespace Chill\PersonBundle\Export\Filter\SocialWorkFilters; use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Repository\UserJobRepositoryInterface; use Chill\MainBundle\Service\RollingDate\RollingDate; use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkReferrerHistory; @@ -28,8 +29,10 @@ class JobFilter implements FilterInterface public function __construct( protected TranslatorInterface $translator, - private readonly TranslatableStringHelper $translatableStringHelper - ) {} + private readonly TranslatableStringHelper $translatableStringHelper, + private readonly UserJobRepositoryInterface $userJobRepository, + ) { + } public function addRole(): ?string { @@ -60,6 +63,7 @@ class JobFilter implements FilterInterface $builder ->add('job', EntityType::class, [ 'class' => UserJob::class, + 'choices' => $this->userJobRepository->findAllActive(), 'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize( $j->getLabel() ), diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ScopeFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ScopeFilter.php index d4867a884..9a0847cfa 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ScopeFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ScopeFilter.php @@ -13,6 +13,7 @@ namespace Chill\PersonBundle\Export\Filter\SocialWorkFilters; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Repository\ScopeRepositoryInterface; use Chill\MainBundle\Service\RollingDate\RollingDate; use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkReferrerHistory; @@ -28,8 +29,10 @@ class ScopeFilter implements FilterInterface public function __construct( protected TranslatorInterface $translator, - private readonly TranslatableStringHelper $translatableStringHelper - ) {} + private readonly TranslatableStringHelper $translatableStringHelper, + private readonly ScopeRepositoryInterface $scopeRepository + ) { + } public function addRole(): ?string { @@ -59,6 +62,7 @@ class ScopeFilter implements FilterInterface $builder ->add('scope', EntityType::class, [ 'class' => Scope::class, + 'choices' => $this->scopeRepository->findAllActive(), 'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize( $s->getName() ), diff --git a/src/Bundle/ChillPersonBundle/Export/Helper/CustomizeListPersonHelperInterface.php b/src/Bundle/ChillPersonBundle/Export/Helper/CustomizeListPersonHelperInterface.php new file mode 100644 index 000000000..049f79f58 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Helper/CustomizeListPersonHelperInterface.php @@ -0,0 +1,36 @@ + $this->translator->trans('regular'), default => $value, }, + 'centers' => function ($value) use ($key) { + if ('_header' === $value) { + return 'export.list.acp.'.$key; + } + + if (null === $value || '' === $value || !json_validate((string) $value)) { + return ''; + } + + return implode( + '|', + array_map( + fn ($cid) => $this->centerRepository->find($cid)->getName(), + array_unique( + json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR) + ) + ) + ); + }, default => static function ($value) use ($key) { if ('_header' === $value) { return 'export.list.acp.'.$key; @@ -314,6 +339,19 @@ final readonly class ListAccompanyingPeriodHelper // social issues ->addSelect('(SELECT AGGREGATE(socialIssue.id) FROM '.SocialIssue::class.' socialIssue WHERE socialIssue MEMBER OF acp.socialIssues) AS socialIssues'); + // duration + $qb->addSelect('DATE_DIFF(COALESCE(acp.closingDate, :calcDate), acp.openingDate) AS duration'); + + // centers + $qb->addSelect( + '(SELECT AGGREGATE(IDENTITY(cppch.center)) + FROM '.AccompanyingPeriodParticipation::class.' part + JOIN '.PersonCenterHistory::class." cppch + WITH IDENTITY(cppch.person) = IDENTITY(part.person) + AND OVERLAPSI (cppch.startDate, cppch.endDate), (part.startDate, part.endDate) = 'TRUE' + WHERE part.accompanyingPeriod = acp + ) AS centers" + ); // add parameter $qb->setParameter('calcDate', $calcDate); } diff --git a/src/Bundle/ChillPersonBundle/Export/Helper/ListPersonHelper.php b/src/Bundle/ChillPersonBundle/Export/Helper/ListPersonHelper.php index 576885f2d..2ffc3940f 100644 --- a/src/Bundle/ChillPersonBundle/Export/Helper/ListPersonHelper.php +++ b/src/Bundle/ChillPersonBundle/Export/Helper/ListPersonHelper.php @@ -35,9 +35,9 @@ use Symfony\Contracts\Translation\TranslatorInterface; * * for some fields */ -class ListPersonHelper +final readonly class ListPersonHelper { - final public const FIELDS = [ + private const FIELDS = [ 'personId', 'civility', 'firstName', @@ -60,16 +60,32 @@ class ListPersonHelper 'countryOfBirth', 'nationality', // add full addresses - 'address_fields', + // 'address_fields', // add a list of spoken languages 'spokenLanguages', // add household id 'household_id', // add created at, created by, updated at, and updated by - 'lifecycleUpdate', + // 'lifecycleUpdate', + 'createdAt', 'createdBy', 'updatedAt', 'updatedBy', ]; - public function __construct(private readonly ExportAddressHelper $addressHelper, private readonly CenterRepositoryInterface $centerRepository, private readonly CivilityRepositoryInterface $civilityRepository, private readonly CountryRepository $countryRepository, private readonly LanguageRepositoryInterface $languageRepository, private readonly MaritalStatusRepositoryInterface $maritalStatusRepository, private readonly TranslatableStringHelper $translatableStringHelper, private readonly TranslatorInterface $translator, private readonly UserRepositoryInterface $userRepository) {} + public function __construct( + private ExportAddressHelper $addressHelper, + private CenterRepositoryInterface $centerRepository, + private CivilityRepositoryInterface $civilityRepository, + private CountryRepository $countryRepository, + private LanguageRepositoryInterface $languageRepository, + private MaritalStatusRepositoryInterface $maritalStatusRepository, + private TranslatableStringHelper $translatableStringHelper, + private TranslatorInterface $translator, + private UserRepositoryInterface $userRepository, + /** + * @var iterable + */ + private iterable $customPersonHelpers, + ) { + } /** * Those keys are the "direct" keys, which are created when we decide to use to list all the keys. @@ -80,26 +96,34 @@ class ListPersonHelper */ public function getAllKeys(): array { - return [ - ...array_filter( - ListPersonHelper::FIELDS, - fn (string $key) => !\in_array($key, ['address_fields', 'lifecycleUpdate'], true) - ), + $keys = [ + ...ListPersonHelper::FIELDS, ...$this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields'), - ...['createdAt', 'createdBy', 'updatedAt', 'updatedBy'], ]; + + foreach ($this->customPersonHelpers as $customize) { + $keys = $customize->alterKeys($keys); + } + + return $keys; } - /** - * @param array> $fields - */ - public function addSelect(QueryBuilder $qb, array $fields, \DateTimeImmutable $computedDate): void + public function addSelect(QueryBuilder $qb, \DateTimeImmutable $computedDate): void { - foreach (ListPersonHelper::FIELDS as $f) { - if (!\in_array($f, $fields, true)) { - continue; - } + // we first add all the fields which are handled by the + $focusedFieldKeys = [ + 'personId', 'countryOfBirth', 'nationality', // 'address_fields', + 'spokenLanguages', 'household_id', 'center', // 'lifecycleUpdate', + 'genderComment', 'maritalStatus', 'maritalStatusComment', 'civility', + 'createdAt', 'createdBy', 'updatedAt', 'updatedBy', + ]; + $filteredFields = array_filter( + ListPersonHelper::FIELDS, + fn ($field) => !in_array($field, $focusedFieldKeys, true) + ); + + foreach ($this->getAllKeys() as $f) { switch ($f) { case 'personId': $qb->addSelect('person.id AS personId'); @@ -112,13 +136,6 @@ class ListPersonHelper break; - case 'address_fields': - $this->addCurrentAddressAt($qb, $computedDate); - $qb->leftJoin('personHouseholdAddress.address', 'personAddress'); - $this->addressHelper->addSelectClauses(ExportAddressHelper::F_ALL, $qb, 'personAddress', 'address_fields'); - - break; - case 'spokenLanguages': $qb->addSelect('(SELECT AGGREGATE(language.id) FROM '.Language::class.' language WHERE language MEMBER OF person.spokenLanguages) AS spokenLanguages'); @@ -152,15 +169,6 @@ class ListPersonHelper break; - case 'lifecycleUpdate': - $qb - ->addSelect('person.createdAt AS createdAt') - ->addSelect('IDENTITY(person.createdBy) AS createdBy') - ->addSelect('person.updatedAt AS updatedAt') - ->addSelect('IDENTITY(person.updatedBy) AS updatedBy'); - - break; - case 'genderComment': $qb->addSelect('person.genderComment.comment AS genderComment'); @@ -182,25 +190,47 @@ class ListPersonHelper break; default: - $qb->addSelect(sprintf('person.%s as %s', $f, $f)); + if (in_array($f, $filteredFields, true)) { + $qb->addSelect(sprintf('person.%s as %s', $f, $f)); + } } } + + // address + $this->addCurrentAddressAt($qb, $computedDate); + $qb->leftJoin('personHouseholdAddress.address', 'personAddress'); + $this->addressHelper->addSelectClauses(ExportAddressHelper::F_ALL, $qb, 'personAddress', 'address_fields'); + + // lifecycle update + $qb + ->addSelect('person.createdAt AS createdAt') + ->addSelect('IDENTITY(person.createdBy) AS createdBy') + ->addSelect('person.updatedAt AS updatedAt') + ->addSelect('IDENTITY(person.updatedBy) AS updatedBy'); + + foreach ($this->customPersonHelpers as $customPersonHelper) { + $customPersonHelper->alterSelect($qb, $computedDate); + } } /** * @return array|string[] + * + * @deprecated */ public function getAllPossibleFields(): array { - return array_merge( - self::FIELDS, - ['createdAt', 'createdBy', 'updatedAt', 'updatedBy'], - $this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields') - ); + return $this->getAllKeys(); } public function getLabels($key, array $values, $data): callable { + foreach ($this->customPersonHelpers as $customPersonHelper) { + if (null !== $callable = $customPersonHelper->getLabels($key, $values, $data)) { + return $callable; + } + } + if (str_starts_with((string) $key, 'address_fields')) { return $this->addressHelper->getLabel($key, $values, $data, 'address_fields'); } @@ -364,7 +394,7 @@ class ListPersonHelper }; default: - if (!\in_array($key, self::getAllPossibleFields(), true)) { + if (!\in_array($key, self::getAllKeys(), true)) { throw new \RuntimeException("this key is not supported by this helper: {$key}"); } diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/index.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/index.html.twig index f1dbf7e89..3ef4825ef 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/index.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/index.html.twig @@ -35,9 +35,11 @@

    {{ 'This course is closed'|trans }}

    -

    - {{ 'Closing motive'|trans }} : {{ accompanyingCourse.closingMotive.name|localize_translatable_string }} -

    + {% if accompanyingCourse.closingMotive is not same as null %} +

    + {{ 'Closing motive'|trans }} : {{ accompanyingCourse.closingMotive.name|localize_translatable_string }} +

    + {% endif %}
    diff --git a/src/Bundle/ChillPersonBundle/Service/DocGenerator/AccompanyingPeriodContext.php b/src/Bundle/ChillPersonBundle/Service/DocGenerator/AccompanyingPeriodContext.php index b01e7c5f3..61edc0205 100644 --- a/src/Bundle/ChillPersonBundle/Service/DocGenerator/AccompanyingPeriodContext.php +++ b/src/Bundle/ChillPersonBundle/Service/DocGenerator/AccompanyingPeriodContext.php @@ -226,7 +226,7 @@ class AccompanyingPeriodContext implements } } - if ($options['thirdParty']) { + if ($options['thirdParty'] ?? false) { $data['thirdParty'] = $this->normalizer->normalize($contextGenerationData['thirdParty'], 'docgen', [ 'docgen:expects' => ThirdParty::class, 'groups' => 'docgen:read', diff --git a/src/Bundle/ChillPersonBundle/Tests/Export/Export/ListPersonHavingAccompanyingPeriodTest.php b/src/Bundle/ChillPersonBundle/Tests/Export/Export/ListPersonHavingAccompanyingPeriodTest.php index e4edaaf9a..363439e38 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Export/Export/ListPersonHavingAccompanyingPeriodTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Export/Export/ListPersonHavingAccompanyingPeriodTest.php @@ -11,7 +11,6 @@ declare(strict_types=1); namespace Chill\PersonBundle\Tests\Export\Export; -use Chill\MainBundle\Export\Helper\ExportAddressHelper; use Chill\MainBundle\Service\RollingDate\RollingDate; use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface; use Chill\MainBundle\Test\Export\AbstractExportTest; @@ -35,13 +34,11 @@ class ListPersonHavingAccompanyingPeriodTest extends AbstractExportTest public function getExport() { - $addressHelper = self::getContainer()->get(ExportAddressHelper::class); $listPersonHelper = self::getContainer()->get(ListPersonHelper::class); $entityManager = self::getContainer()->get(EntityManagerInterface::class); $rollingDateconverter = self::getContainer()->get(RollingDateConverterInterface::class); yield new ListPersonHavingAccompanyingPeriod( - $addressHelper, $listPersonHelper, $entityManager, $rollingDateconverter, @@ -49,7 +46,6 @@ class ListPersonHavingAccompanyingPeriodTest extends AbstractExportTest ); yield new ListPersonHavingAccompanyingPeriod( - $addressHelper, $listPersonHelper, $entityManager, $rollingDateconverter, @@ -59,7 +55,7 @@ class ListPersonHavingAccompanyingPeriodTest extends AbstractExportTest public static function getFormData(): array { - return [['address_date_rolling' => new RollingDate(RollingDate::T_TODAY), 'fields' => ListPersonHelper::FIELDS]]; + return [['address_date_rolling' => new RollingDate(RollingDate::T_TODAY)]]; } public static function getModifiersCombination(): array diff --git a/src/Bundle/ChillPersonBundle/Tests/Export/Export/ListPersonTest.php b/src/Bundle/ChillPersonBundle/Tests/Export/Export/ListPersonTest.php index 88b192f51..624bbf3cc 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Export/Export/ListPersonTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Export/Export/ListPersonTest.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\PersonBundle\Tests\Export\Export; use Chill\CustomFieldsBundle\Service\CustomFieldProvider; -use Chill\MainBundle\Export\Helper\ExportAddressHelper; use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\MainBundle\Test\Export\AbstractExportTest; use Chill\PersonBundle\Export\Export\ListPerson; @@ -50,14 +49,12 @@ final class ListPersonTest extends AbstractExportTest public function getExport() { - $addressHelper = self::getContainer()->get(ExportAddressHelper::class); $customFieldProvider = self::getContainer()->get(CustomFieldProvider::class); $listPersonHelper = self::getContainer()->get(ListPersonHelper::class); $entityManager = self::getContainer()->get(EntityManagerInterface::class); $translatableStringHelper = self::getContainer()->get(TranslatableStringHelper::class); yield new ListPerson( - $addressHelper, $customFieldProvider, $listPersonHelper, $entityManager, @@ -66,7 +63,6 @@ final class ListPersonTest extends AbstractExportTest ); yield new ListPerson( - $addressHelper, $customFieldProvider, $listPersonHelper, $entityManager, @@ -77,18 +73,7 @@ final class ListPersonTest extends AbstractExportTest public static function getFormData(): array { - $data = []; - foreach ([ - ['fields' => ['id', 'firstName', 'lastName']], - ['fields' => ['id', 'birthdate', 'gender', 'memo', 'email', 'phonenumber']], - ['fields' => ['firstName', 'lastName', 'phonenumber']], - ['fields' => ['id', 'nationality']], - ['fields' => ['id', 'countryOfBirth']], - ] as $base) { - $data[] = [...$base, 'address_date' => new \DateTimeImmutable('today')]; - } - - return $data; + yield ['address_date' => new \DateTimeImmutable('today')]; } public static function getModifiersCombination(): array diff --git a/src/Bundle/ChillPersonBundle/Tests/Export/Filter/SocialWorkFilters/WithEvaluationBetweenDatesFilterTest.php b/src/Bundle/ChillPersonBundle/Tests/Export/Filter/SocialWorkFilters/WithEvaluationBetweenDatesFilterTest.php new file mode 100644 index 000000000..680eb4cc9 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Export/Filter/SocialWorkFilters/WithEvaluationBetweenDatesFilterTest.php @@ -0,0 +1,63 @@ +filter = self::$container->get(AccompanyingPeriodWorkWithEvaluationBetweenDatesFilter::class); + } + + public function getFilter() + { + return $this->filter; + } + + public function getFormData() + { + return [ + [ + 'start_date' => new RollingDate(RollingDate::T_MONTH_CURRENT_START), + 'end_date' => new RollingDate(RollingDate::T_TODAY), + ], + ]; + } + + public function getQueryBuilders() + { + self::bootKernel(); + + $em = self::$container->get(EntityManagerInterface::class); + + return [ + $em->createQueryBuilder() + ->select('acpw.id') + ->from(AccompanyingPeriodWork::class, 'acpw'), + ]; + } +} diff --git a/src/Bundle/ChillPersonBundle/Tests/Repository/AccompanyingPeriodACLAwareRepositoryTest.php b/src/Bundle/ChillPersonBundle/Tests/Repository/AccompanyingPeriodACLAwareRepositoryTest.php index 917a42ef2..9781a8e96 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Repository/AccompanyingPeriodACLAwareRepositoryTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Repository/AccompanyingPeriodACLAwareRepositoryTest.php @@ -529,7 +529,7 @@ class AccompanyingPeriodACLAwareRepositoryTest extends KernelTestCase /** * @param array $scopes */ - private static function buildPeriod(Person $person, array $scopes, User|null $creator, bool $confirm): AccompanyingPeriod + private static function buildPeriod(Person $person, array $scopes, ?User $creator, bool $confirm): AccompanyingPeriod { $entityManager = self::getContainer()->get(EntityManagerInterface::class); $registry = self::getContainer()->get(Registry::class); diff --git a/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml b/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml index c0ed03115..263e172d0 100644 --- a/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml @@ -16,6 +16,10 @@ services: tags: - { name: chill.export, alias: list_person } + Chill\PersonBundle\Export\Helper\ListPersonHelper: + arguments: + $customPersonHelpers: !tagged_iterator chill_person.list_person_customizer + Chill\PersonBundle\Export\Export\ListPersonHavingAccompanyingPeriod: tags: - { name: chill.export, alias: list_person_with_acp } diff --git a/src/Bundle/ChillPersonBundle/config/services/exports_social_actions.yaml b/src/Bundle/ChillPersonBundle/config/services/exports_social_actions.yaml index 1987568e3..9e199679f 100644 --- a/src/Bundle/ChillPersonBundle/config/services/exports_social_actions.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/exports_social_actions.yaml @@ -79,6 +79,10 @@ services: tags: - { name: chill.export_filter, alias: social_work_actions_creator_scope_filter } + Chill\PersonBundle\Export\Filter\SocialWorkFilters\AccompanyingPeriodWorkWithEvaluationBetweenDatesFilter: + tags: + - { name: chill.export_filter, alias: social_work_actions_evaluation_btw_dates_filter } + ## AGGREGATORS chill.person.export.aggregator_action_type: diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 50c6a87d5..013298ba5 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -57,7 +57,7 @@ Add new phone: Ajouter un numéro de téléphone Remove phone: Supprimer 'Notes on contact information': 'Remarques sur les informations de contact' 'Remarks': 'Remarques' -Spoken languages': 'Langues parlées' +Spoken languages: 'Langues parlées' 'Unknown spoken languages': 'Langues parlées inconnues' Male: Homme Female: Femme @@ -1291,6 +1291,11 @@ export: by_creator_scope: title: Filtrer les actions par service du créateur "Filtered by creator scope: only %scopes%": "Filtré par service du créateur: uniquement %scopes%" + evaluation_between_dates: + title: Filtrer les actions associées à une évaluation créée entre deux dates + description: Uniquement les actions associées à une évaluation créée entre le %startDate% et le %endDate% + start_date: Date de début + end_date: Date de fin list: person_with_acp: @@ -1341,6 +1346,8 @@ export: requestorThirdParty: Demandeur (tiers) acpParticipantPersons: Usagers concernés acpParticipantPersonsIds: Usagers concernés (identifiants) + duration: Durée du parcours (en jours) + centers: Centres des usagers eval: List of evaluations: Liste des évaluations diff --git a/src/Bundle/ChillTaskBundle/Entity/SingleTask.php b/src/Bundle/ChillTaskBundle/Entity/SingleTask.php index 25b5008b0..5e8f8eb0b 100644 --- a/src/Bundle/ChillTaskBundle/Entity/SingleTask.php +++ b/src/Bundle/ChillTaskBundle/Entity/SingleTask.php @@ -112,7 +112,7 @@ class SingleTask extends AbstractTask * message="An end date is required if a warning interval is set" * ) */ - private \DateInterval|null $warningInterval = null; + private ?\DateInterval $warningInterval = null; public function __construct() { @@ -122,7 +122,7 @@ class SingleTask extends AbstractTask /** * Get endDate. */ - public function getEndDate(): \DateTime|null + public function getEndDate(): ?\DateTime { return $this->endDate; } @@ -184,7 +184,7 @@ class SingleTask extends AbstractTask /** * Get warningInterval. */ - public function getWarningInterval(): \DateInterval|null + public function getWarningInterval(): ?\DateInterval { return $this->warningInterval; } @@ -234,7 +234,7 @@ class SingleTask extends AbstractTask * * @return SingleTask */ - public function setWarningInterval(\DateInterval|null $warningInterval) + public function setWarningInterval(?\DateInterval $warningInterval) { $this->warningInterval = $warningInterval; diff --git a/src/Bundle/ChillWopiBundle/src/Controller/Editor.php b/src/Bundle/ChillWopiBundle/src/Controller/Editor.php index 053b7abd0..c845174ca 100644 --- a/src/Bundle/ChillWopiBundle/src/Controller/Editor.php +++ b/src/Bundle/ChillWopiBundle/src/Controller/Editor.php @@ -15,7 +15,6 @@ use ChampsLibres\WopiLib\Contract\Service\Configuration\ConfigurationInterface; use ChampsLibres\WopiLib\Contract\Service\Discovery\DiscoveryInterface; use ChampsLibres\WopiLib\Contract\Service\DocumentManagerInterface; use Chill\DocStoreBundle\Entity\StoredObject; -use Chill\MainBundle\Entity\User; use Chill\WopiBundle\Service\Controller\ResponderInterface; use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; use loophp\psr17\Psr17Interface; @@ -41,13 +40,11 @@ final readonly class Editor public function __invoke(string $fileId, Request $request): Response { - if (null === $user = $this->security->getUser()) { + if (!($this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN'))) { throw new AccessDeniedHttpException('Please authenticate to access this feature'); } - if (!$user instanceof User) { - throw new AccessDeniedHttpException('Please authenticate as a user to access this feature'); - } + $user = $this->security->getUser(); $configuration = $this->wopiConfiguration->jsonSerialize(); /** @var StoredObject $storedObject */ @@ -75,7 +72,12 @@ final readonly class Editor } if ([] === $discoverExtension = $this->wopiDiscovery->discoverMimeType($storedObject->getType())) { - throw new \Exception(sprintf('Unable to find mime type %s', $storedObject->getType())); + return new Response( + $this->engine + ->render('@ChillWopi/Editor/unable_to_edit_such_document.html.twig', [ + 'document' => $storedObject, + ]) + ); } $configuration['favIconUrl'] = ''; diff --git a/src/Bundle/ChillWopiBundle/src/Resources/views/Editor/unable_to_edit_such_document.html.twig b/src/Bundle/ChillWopiBundle/src/Resources/views/Editor/unable_to_edit_such_document.html.twig new file mode 100644 index 000000000..36b8cd631 --- /dev/null +++ b/src/Bundle/ChillWopiBundle/src/Resources/views/Editor/unable_to_edit_such_document.html.twig @@ -0,0 +1,34 @@ +{% extends '@ChillMain/layout.html.twig' %} + +{% block js %} + {{ parent() }} + {{ encore_entry_script_tags('mod_document_action_buttons_group') }} +{% endblock %} + +{% block css %} + {{ parent() }} + {{ encore_entry_link_tags('mod_document_action_buttons_group') }} +{% endblock %} + +{% block content %} +
    +
    +

    + {{ 'wopi_editor.document unsupported for edition'|trans }} +

    +
    + +
    +

    {{ document|chill_document_button_group(document.title|default('Document'), false) }}

    +
    + +
    + + +{% endblock content %} diff --git a/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php b/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php index 1496ba479..c54f187ca 100644 --- a/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php +++ b/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php @@ -36,7 +36,7 @@ class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\Authori $user = $this->security->getUser(); - if (!$user instanceof User) { + if (!($user instanceof User || $this->security->isGranted('ROLE_ADMIN'))) { return false; } diff --git a/src/Bundle/ChillWopiBundle/src/Service/Wopi/UserManager.php b/src/Bundle/ChillWopiBundle/src/Service/Wopi/UserManager.php index f72ae3cde..8c85982d7 100644 --- a/src/Bundle/ChillWopiBundle/src/Service/Wopi/UserManager.php +++ b/src/Bundle/ChillWopiBundle/src/Service/Wopi/UserManager.php @@ -23,6 +23,10 @@ class UserManager implements \ChampsLibres\WopiBundle\Contracts\UserManagerInter { $user = $this->security->getUser(); + if (!$user instanceof User && $this->security->isGranted('ROLE_ADMIN')) { + return $user->getUsername(); + } + if (!$user instanceof User) { return null; } @@ -34,6 +38,10 @@ class UserManager implements \ChampsLibres\WopiBundle\Contracts\UserManagerInter { $user = $this->security->getUser(); + if (!$user instanceof User && $this->security->isGranted('ROLE_ADMIN')) { + return $user->getUsername(); + } + if (!$user instanceof User) { return null; } diff --git a/src/Bundle/ChillWopiBundle/src/translations/messages.fr.yml b/src/Bundle/ChillWopiBundle/src/translations/messages.fr.yml new file mode 100644 index 000000000..2875550a1 --- /dev/null +++ b/src/Bundle/ChillWopiBundle/src/translations/messages.fr.yml @@ -0,0 +1,2 @@ +wopi_editor: + document unsupported for edition: Ce format de document n'est pas éditable