From 0e1d233d7973245209b4d737e000932077259716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 10 Feb 2026 15:05:49 +0000 Subject: [PATCH 01/22] Take workflow creator into account when granting edit permissions on documents --- .changes/unreleased/Fixed-20260210-155209.yaml | 6 ++++++ .../ChillMainBundle/Entity/Workflow/EntityWorkflow.php | 4 ++++ 2 files changed, 10 insertions(+) create mode 100644 .changes/unreleased/Fixed-20260210-155209.yaml diff --git a/.changes/unreleased/Fixed-20260210-155209.yaml b/.changes/unreleased/Fixed-20260210-155209.yaml new file mode 100644 index 000000000..9cd6c9cb1 --- /dev/null +++ b/.changes/unreleased/Fixed-20260210-155209.yaml @@ -0,0 +1,6 @@ +kind: Fixed +body: For giving edit permissions on documents, take into account the workflow creator +time: 2026-02-10T15:52:09.785649121+01:00 +custom: + Issue: "498" + SchemaChange: No schema change diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php index ad5fa9e95..914a71615 100644 --- a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php @@ -394,6 +394,10 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface public function isUserInvolved(User $user): bool { + if ($this->getCreatedBy() === $user) { + return true; + } + foreach ($this->getSteps() as $step) { if ($step->getAllDestUser()->contains($user)) { return true; From a87d93682828a3ccc76b3368c4adf22fc760867e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 11 Feb 2026 13:27:48 +0000 Subject: [PATCH 02/22] Changie/add mr to question --- .changes/unreleased/DX-20260210-161202.yaml | 7 +++++++ .changes/unreleased/Fixed-20260210-155209.yaml | 1 + .changie.yaml | 8 +++++++- 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 .changes/unreleased/DX-20260210-161202.yaml diff --git a/.changes/unreleased/DX-20260210-161202.yaml b/.changes/unreleased/DX-20260210-161202.yaml new file mode 100644 index 000000000..43843456f --- /dev/null +++ b/.changes/unreleased/DX-20260210-161202.yaml @@ -0,0 +1,7 @@ +kind: DX +body: Configure changie to ask for merge request number for a better tracking of changes +time: 2026-02-10T16:12:02.400722306+01:00 +custom: + Issue: "" + MR: "960" + SchemaChange: No schema change diff --git a/.changes/unreleased/Fixed-20260210-155209.yaml b/.changes/unreleased/Fixed-20260210-155209.yaml index 9cd6c9cb1..1047a9334 100644 --- a/.changes/unreleased/Fixed-20260210-155209.yaml +++ b/.changes/unreleased/Fixed-20260210-155209.yaml @@ -3,4 +3,5 @@ body: For giving edit permissions on documents, take into account the workflow c time: 2026-02-10T15:52:09.785649121+01:00 custom: Issue: "498" + MR: "" SchemaChange: No schema change diff --git a/.changie.yaml b/.changie.yaml index 49a885a2e..0bcab1f5b 100644 --- a/.changie.yaml +++ b/.changie.yaml @@ -7,7 +7,7 @@ versionFormat: '## {{.Version}} - {{.Time.Format "2006-01-02"}}' kindFormat: '### {{.Kind}}' # Note: it is possible to add a `.custom.Long` text manually into the yaml file produced by `changie new`. This will add a long description. changeFormat: >- - * {{ if not (eq .Custom.Issue "") }}([#{{ .Custom.Issue }}](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/{{ .Custom.Issue }})) {{ end }}{{ .Body }} {{ if and .Custom.SchemaChange (ne .Custom.SchemaChange "No schema change") }} + * {{ if not (eq .Custom.Issue "") }}([#{{ .Custom.Issue }}](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/{{ .Custom.Issue }})) {{ end }}{{ if not (eq .Custom.MR "") }}([!{{ .Custom.MR }}](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/{{ .Custom.MR }})) {{ end }}{{ .Body }} {{ if and .Custom.SchemaChange (ne .Custom.SchemaChange "No schema change") }} **Schema Change**: {{ .Custom.SchemaChange }} {{- end -}} @@ -30,6 +30,12 @@ custom: type: int minInt: 1 + - key: MR + label: Merge request number (on chill-bundles repository) (optional) + optional: true + type: int + minInt: 1 + body: # allow multiline messages block: true From 80b05a81335820d9d566cb603996217da3853992 Mon Sep 17 00:00:00 2001 From: Boris Waaub Date: Thu, 12 Feb 2026 08:50:00 +0000 Subject: [PATCH 03/22] =?UTF-8?q?Resolve=20"Parcours=20-=20"D=C3=A9signer?= =?UTF-8?q?=20comme=20adresse=20du=20parcours"=20to=20be=20green"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changes/unreleased/Fixed-20260210-141731.yaml | 6 ++++++ .../vuejs/AccompanyingCourse/components/ButtonLocation.vue | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changes/unreleased/Fixed-20260210-141731.yaml diff --git a/.changes/unreleased/Fixed-20260210-141731.yaml b/.changes/unreleased/Fixed-20260210-141731.yaml new file mode 100644 index 000000000..013080c9f --- /dev/null +++ b/.changes/unreleased/Fixed-20260210-141731.yaml @@ -0,0 +1,6 @@ +kind: Fixed +body: Change wrong color of submit button "Désigner comme adresse du parcours" +time: 2026-02-10T14:17:31.926008236+01:00 +custom: + Issue: "438" + SchemaChange: No schema change diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/ButtonLocation.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/ButtonLocation.vue index e84d5a9f2..39c90ddeb 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/ButtonLocation.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/ButtonLocation.vue @@ -27,7 +27,7 @@

{{ $t("courselocation.sure_description") }}

From 76d675ac02d9a0c2e8385945cf266bba1312d794 Mon Sep 17 00:00:00 2001 From: nobohan Date: Tue, 17 Feb 2026 14:11:15 +0100 Subject: [PATCH 04/22] Fixed translations of address in exports (addresse -> adresse) --- .changes/unreleased/Fixed-20260217-140838.yaml | 7 +++++++ src/Bundle/ChillPersonBundle/translations/messages.fr.yml | 4 ++-- .../ChillThirdPartyBundle/translations/messages.fr.yml | 6 +++--- 3 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 .changes/unreleased/Fixed-20260217-140838.yaml diff --git a/.changes/unreleased/Fixed-20260217-140838.yaml b/.changes/unreleased/Fixed-20260217-140838.yaml new file mode 100644 index 000000000..d6b3d20be --- /dev/null +++ b/.changes/unreleased/Fixed-20260217-140838.yaml @@ -0,0 +1,7 @@ +kind: Fixed +body: 'Fixed mispelling of address in translations: addresse -> adresse' +time: 2026-02-17T14:08:38.82625052+01:00 +custom: + Issue: "" + MR: "" + SchemaChange: No schema change diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index b62908dc0..ad97767d2 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -453,8 +453,8 @@ Filtered by entrusted child status: Uniquement les usagers qui sont "enfant conf Filter by nomadic status: Filtrer les usagers "gens du voyage" Filtered by nomadic status: Uniquement les usagers qui sont "gens du voyage" -"Filter by person's who have a residential address located at another user": Filtrer les usagers qui ont une addresse de résidence chez une autre usager -"Filtered by person's who have a residential address located at another user": Uniquement les usagers qui ont une addresse de résidence chez une autre usager +"Filter by person's who have a residential address located at another user": Filtrer les usagers qui ont une adresse de résidence chez une autre usager +"Filtered by person's who have a residential address located at another user": Uniquement les usagers qui ont une adresse de résidence chez une autre usager Filter by person's that are alive or have deceased at a certain date: Filtrer les usagers qui sont décédés ou vivantes à une certaine date Filtered by person's that are alive or have deceased at a certain date: Uniquement les usagers qui sont décédés ou vivantes à une certaine date diff --git a/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml b/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml index bdd9420f7..62df67616 100644 --- a/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml @@ -129,11 +129,11 @@ export: thirdParties: Tiers intervenant # exports filters/aggregators -Filtered by person\'s who have a residential address located at a thirdparty of type %thirparty_type%: Uniquement les usagers qui ont une addresse de résidence chez un tiers de catégorie %thirdparty_type% +Filtered by person\'s who have a residential address located at a thirdparty of type %thirparty_type%: Uniquement les usagers qui ont une adresse de résidence chez un tiers de catégorie %thirdparty_type% is thirdparty: Le demandeur est un tiers -Filter by person's who have a residential address located at a thirdparty of type: Filtrer les usagers qui ont une addresse de résidence chez un tiers -"Filtered by person's who have a residential address located at a thirdparty of type %thirdparty_type% and valid on %date_calc%": "Uniquement les usagers qui ont une addresse de résidence chez un tiers de catégorie %thirdparty_type% et valide sur la date %date_calc%" +Filter by person's who have a residential address located at a thirdparty of type: Filtrer les usagers qui ont une adresse de résidence chez un tiers +"Filtered by person's who have a residential address located at a thirdparty of type %thirdparty_type% and valid on %date_calc%": "Uniquement les usagers qui ont une adresse de résidence chez un tiers de catégorie %thirdparty_type% et valide sur la date %date_calc%" # admin admin: From f1446d7abee67734d4fee9d9a36982add5a7b1e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 23 Feb 2026 14:16:45 +0000 Subject: [PATCH 05/22] =?UTF-8?q?Resolve=20"Des=20codes=20postaux=20marqu?= =?UTF-8?q?=C3=A9s=20comme=20supprim=C3=A9s=20apparaissent=20toujours=20da?= =?UTF-8?q?ns=20la=20recherche=20d'adresse"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unreleased/Fixed-20260223-144735.yaml | 8 ++++ .../Controller/PostalCodeAPIController.php | 2 + .../Repository/PostalCodeRepository.php | 4 +- .../migrations/Version20260223134919.php | 41 +++++++++++++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 .changes/unreleased/Fixed-20260223-144735.yaml create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20260223134919.php diff --git a/.changes/unreleased/Fixed-20260223-144735.yaml b/.changes/unreleased/Fixed-20260223-144735.yaml new file mode 100644 index 000000000..d20a8b4de --- /dev/null +++ b/.changes/unreleased/Fixed-20260223-144735.yaml @@ -0,0 +1,8 @@ +kind: Fixed +body: | + Fix: some postal code appears in the UI, although they are marked as deleted +time: 2026-02-23T14:47:35.925109983+01:00 +custom: + Issue: "499" + MR: "963" + SchemaChange: No schema change diff --git a/src/Bundle/ChillMainBundle/Controller/PostalCodeAPIController.php b/src/Bundle/ChillMainBundle/Controller/PostalCodeAPIController.php index cd13ad455..4ca82053d 100644 --- a/src/Bundle/ChillMainBundle/Controller/PostalCodeAPIController.php +++ b/src/Bundle/ChillMainBundle/Controller/PostalCodeAPIController.php @@ -79,5 +79,7 @@ final class PostalCodeAPIController extends ApiController $qb->andWhere('e.origin = :zero') ->setParameter('zero', 0); + + $qb->andWhere('e.deletedAt IS NULL'); } } diff --git a/src/Bundle/ChillMainBundle/Repository/PostalCodeRepository.php b/src/Bundle/ChillMainBundle/Repository/PostalCodeRepository.php index 0779ea90a..b26b5bed2 100644 --- a/src/Bundle/ChillMainBundle/Repository/PostalCodeRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/PostalCodeRepository.php @@ -100,7 +100,9 @@ final readonly class PostalCodeRepository implements PostalCodeRepositoryInterfa $query ->setFromClause('chill_main_postal_code cmpc') - ->andWhereClause('cmpc.origin = 0'); + ->andWhereClause('cmpc.origin = 0') + ->andWhereClause('cmpc.deletedAt IS NULL') + ; if (null !== $country) { $query->andWhereClause('cmpc.country_id = ?', [$country->getId()]); diff --git a/src/Bundle/ChillMainBundle/migrations/Version20260223134919.php b/src/Bundle/ChillMainBundle/migrations/Version20260223134919.php new file mode 100644 index 000000000..170519940 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20260223134919.php @@ -0,0 +1,41 @@ +addSql('DROP INDEX public.search_name_code'); + $this->addSql('CREATE INDEX search_name_code ON public.chill_main_postal_code USING GIN (LOWER(code) gin_trgm_ops, LOWER(label) gin_trgm_ops) WHERE deletedAt IS NULL'); + $this->addSql('DROP INDEX public.chill_internal_postal_code_canonicalized'); + $this->addSql('CREATE INDEX chill_internal_postal_code_canonicalized ON chill_main_postal_code USING GIST (canonical gist_trgm_ops) WHERE origin = 0 AND deletedAt IS NULL'); + + } + + public function down(Schema $schema): void + { + $this->addSql('DROP INDEX public.search_name_code'); + $this->addSql('CREATE INDEX search_name_code ON chill_main_postal_code USING GIN (LOWER(code) gin_trgm_ops, LOWER(label) gin_trgm_ops)'); + + $this->addSql('DROP INDEX public.chill_internal_postal_code_canonicalized'); + $this->addSql('CREATE INDEX chill_internal_postal_code_canonicalized ON chill_main_postal_code USING GIST (canonical gist_trgm_ops) WHERE origin = 0'); + } +} From bf56b3cc65baa0c431fe24ca2005d32e51cf9fac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 23 Feb 2026 14:24:53 +0000 Subject: [PATCH 06/22] =?UTF-8?q?Resolve=20"T=C3=A9l=C3=A9chargement=20des?= =?UTF-8?q?=20documents=20d'un=20workflow:=20limiter=20=C3=A0=2030=20t?= =?UTF-8?q?=C3=A9l=C3=A9chargements=20plut=C3=B4t=20que=20100"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changes/unreleased/Feature-20260223-151808.yaml | 7 +++++++ .../Controller/WorkflowViewSendPublicController.php | 8 ++++---- 2 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 .changes/unreleased/Feature-20260223-151808.yaml diff --git a/.changes/unreleased/Feature-20260223-151808.yaml b/.changes/unreleased/Feature-20260223-151808.yaml new file mode 100644 index 000000000..2d134c8af --- /dev/null +++ b/.changes/unreleased/Feature-20260223-151808.yaml @@ -0,0 +1,7 @@ +kind: Feature +body: Limit the number of public download of stored object to 30 downloads +time: 2026-02-23T15:18:08.688011924+01:00 +custom: + Issue: "500" + MR: "964" + SchemaChange: No schema change diff --git a/src/Bundle/ChillMainBundle/Controller/WorkflowViewSendPublicController.php b/src/Bundle/ChillMainBundle/Controller/WorkflowViewSendPublicController.php index 9cd4fd6f7..1a005c4c7 100644 --- a/src/Bundle/ChillMainBundle/Controller/WorkflowViewSendPublicController.php +++ b/src/Bundle/ChillMainBundle/Controller/WorkflowViewSendPublicController.php @@ -62,15 +62,15 @@ final readonly class WorkflowViewSendPublicController ); } - if (100 < $workflowSend->getViews()->count()) { - $this->chillLogger->info(self::LOG_PREFIX.'100 view reached, not allowed to see it again'); - throw new AccessDeniedHttpException('100 views reached, not allowed to see it again'); + if (30 < $workflowSend->getViews()->count()) { + $this->chillLogger->info(self::LOG_PREFIX.'30 view reached, not allowed to see it again'); + throw new AccessDeniedHttpException('30 views reached, not allowed to see it again'); } try { $metadata = new EntityWorkflowViewMetadataDTO( $workflowSend->getViews()->count(), - 100 - $workflowSend->getViews()->count(), + 30 - $workflowSend->getViews()->count(), ); $response = new Response( $this->entityWorkflowManager->renderPublicView($workflowSend, $metadata), From 22049558da867336a26d881c8d2bbfb1538e4f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 23 Feb 2026 14:50:14 +0000 Subject: [PATCH 07/22] =?UTF-8?q?Resolve=20"Depr=C3=A9ciation=20dans=20le?= =?UTF-8?q?=20paquet=20de=20transformation=20markdown"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changes/unreleased/Fixed-20260223-154405.yaml | 7 +++++++ .../Templating/ChillMarkdownRenderExtension.php | 2 +- .../Tests/Templating/ChillMarkdownRenderExtensionTest.php | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 .changes/unreleased/Fixed-20260223-154405.yaml diff --git a/.changes/unreleased/Fixed-20260223-154405.yaml b/.changes/unreleased/Fixed-20260223-154405.yaml new file mode 100644 index 000000000..9e089c807 --- /dev/null +++ b/.changes/unreleased/Fixed-20260223-154405.yaml @@ -0,0 +1,7 @@ +kind: Fixed +body: Fix deprecation in the markdown rendering +time: 2026-02-23T15:44:05.800494138+01:00 +custom: + Issue: "501" + MR: "966" + SchemaChange: No schema change diff --git a/src/Bundle/ChillMainBundle/Templating/ChillMarkdownRenderExtension.php b/src/Bundle/ChillMainBundle/Templating/ChillMarkdownRenderExtension.php index 54c9c4f63..3f227b6d6 100644 --- a/src/Bundle/ChillMainBundle/Templating/ChillMarkdownRenderExtension.php +++ b/src/Bundle/ChillMainBundle/Templating/ChillMarkdownRenderExtension.php @@ -41,6 +41,6 @@ final class ChillMarkdownRenderExtension extends AbstractExtension public function renderMarkdownToHtml(?string $var): string { - return $this->parsedown->parse((string) $var); + return $this->parsedown->text((string) $var); } } diff --git a/src/Bundle/ChillMainBundle/Tests/Templating/ChillMarkdownRenderExtensionTest.php b/src/Bundle/ChillMainBundle/Tests/Templating/ChillMarkdownRenderExtensionTest.php index d4eb8afa8..3dbd7fd3b 100644 --- a/src/Bundle/ChillMainBundle/Tests/Templating/ChillMarkdownRenderExtensionTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Templating/ChillMarkdownRenderExtensionTest.php @@ -37,7 +37,7 @@ final class ChillMarkdownRenderExtensionTest extends TestCase MD; private const UNAUTHORIZED_HTML = <<<'HTML' -

<script>alert("ok");</script>

+

<script>alert("ok");</script>

HTML; private const UNAUTHORIZED_MARKDOWN = <<<'MD' From 69bb7026c9f55a757447c39235675d3d5a149e82 Mon Sep 17 00:00:00 2001 From: Boris Waaub Date: Mon, 23 Feb 2026 15:08:48 +0000 Subject: [PATCH 08/22] =?UTF-8?q?Resolve=20"Titre=20'Toute=20la=20journ?= =?UTF-8?q?=C3=A9e'=20tronqu=C3=A9=20sur=20la=20page=20Mes=20Rendez-vous"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changes/unreleased/Fixed-20260223-155645.yaml | 8 ++++++++ .../Resources/public/vuejs/MyCalendarRange/App2.vue | 1 + 2 files changed, 9 insertions(+) create mode 100644 .changes/unreleased/Fixed-20260223-155645.yaml diff --git a/.changes/unreleased/Fixed-20260223-155645.yaml b/.changes/unreleased/Fixed-20260223-155645.yaml new file mode 100644 index 000000000..7d9b8fccc --- /dev/null +++ b/.changes/unreleased/Fixed-20260223-155645.yaml @@ -0,0 +1,8 @@ +kind: Fixed +body: | + - Remove unused all-day slot display +time: 2026-02-23T15:56:45.517079+01:00 +custom: + Issue: "494" + MR: "965" + SchemaChange: No schema change diff --git a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue index 40c3200b9..952ab748f 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue +++ b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue @@ -346,6 +346,7 @@ const baseOptions = ref({ center: "title", right: "timeGridWeek,timeGridDay", }, + allDaySlot: false, }); const ranges = computed(() => { From dc3a585e5bd1b55f7d1c96e122928e41ec272a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 23 Feb 2026 15:49:39 +0000 Subject: [PATCH 09/22] Remove unused method `sendNotificationEmailsToAddressesEmails` from `NotificationMailer` --- .../unreleased/Feature-20260223-164835.yaml | 7 +++ .../Notification/Email/NotificationMailer.php | 44 +++---------------- ...il_non_system_notification_content.md.twig | 19 ++++---- ...l_non_system_notification_content.txt.twig | 14 ++++++ ...stem_notification_content_to_email.md.twig | 20 --------- ...email_notification_comment_persist.md.twig | 12 +++-- ...mail_notification_comment_persist.txt.twig | 14 ++++++ 7 files changed, 54 insertions(+), 76 deletions(-) create mode 100644 .changes/unreleased/Feature-20260223-164835.yaml create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.txt.twig delete mode 100644 src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content_to_email.md.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Notification/email_notification_comment_persist.txt.twig diff --git a/.changes/unreleased/Feature-20260223-164835.yaml b/.changes/unreleased/Feature-20260223-164835.yaml new file mode 100644 index 000000000..07ffcc676 --- /dev/null +++ b/.changes/unreleased/Feature-20260223-164835.yaml @@ -0,0 +1,7 @@ +kind: Feature +body: Send email related to notification in both html and txt format, and render quote correctly +time: 2026-02-23T16:48:35.48244838+01:00 +custom: + Issue: "495" + MR: "967" + SchemaChange: No schema change diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php index 1cb64ff4a..237cb178b 100644 --- a/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php @@ -59,7 +59,8 @@ readonly class NotificationMailer $email ->to($dest->getEmail()) ->subject('Re: '.$comment->getNotification()->getTitle()) - ->textTemplate('@ChillMain/Notification/email_notification_comment_persist.md.twig') + ->textTemplate('@ChillMain/Notification/email_notification_comment_persist.txt.twig') + ->htmlTemplate('@ChillMain/Notification/email_notification_comment_persist.md.twig') ->context([ 'comment' => $comment, 'dest' => $dest, @@ -83,7 +84,6 @@ readonly class NotificationMailer public function postPersistNotification(Notification $notification, PostPersistEventArgs $eventArgs): void { $this->sendNotificationEmailsToAddressees($notification); - $this->sendNotificationEmailsToAddressesEmails($notification); } private function sendNotificationEmailsToAddressees(Notification $notification): void @@ -149,7 +149,8 @@ readonly class NotificationMailer } else { $email = new TemplatedEmail(); $email - ->textTemplate('@ChillMain/Notification/email_non_system_notification_content.md.twig') + ->textTemplate('@ChillMain/Notification/email_non_system_notification_content.txt.twig') + ->htmlTemplate('@ChillMain/Notification/email_non_system_notification_content.md.twig') ->context([ 'notification' => $notification, 'dest' => $addressee, @@ -186,7 +187,8 @@ readonly class NotificationMailer } else { $email = new TemplatedEmail(); $email - ->textTemplate('@ChillMain/Notification/email_non_system_notification_content.md.twig') + ->textTemplate('@ChillMain/Notification/email_non_system_notification_content.txt.twig') + ->htmlTemplate('@ChillMain/Notification/email_non_system_notification_content.md.twig') ->context([ 'notification' => $notification, 'dest' => $addressee, @@ -286,38 +288,4 @@ readonly class NotificationMailer throw $e; } } - - private function sendNotificationEmailsToAddressesEmails(Notification $notification): void - { - foreach ($notification->getAddresseeUserGroups() as $userGroup) { - - if (!$userGroup->hasEmail()) { - continue; - } - - $emailAddress = $userGroup->getEmail(); - - $email = new TemplatedEmail(); - $email - ->textTemplate('@ChillMain/Notification/email_non_system_notification_content_to_email.md.twig') - ->context([ - 'notification' => $notification, - 'dest' => $emailAddress, - ]); - - $email - ->subject($notification->getTitle()) - ->to($emailAddress); - - try { - $this->mailer->send($email); - } catch (TransportExceptionInterface $e) { - $this->logger->warning('[NotificationMailer] could not send an email notification', [ - 'to' => $emailAddress, - 'error_message' => $e->getMessage(), - 'error_trace' => $e->getTraceAsString(), - ]); - } - } - } } diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.md.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.md.twig index 023f2901a..5c07e2ef7 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.md.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.md.twig @@ -1,20 +1,17 @@ +{% apply markdown_to_html %} {{ dest.label }}, {{ notification.sender.label }} a créé une notification pour vous: -> {{ notification.title }} -> -> -{%- for line in notification.message|split("\n") %} +**Titre de la notification**: {{ notification.title }} + +{% for line in notification.message|split("\n") %} > {{ line }} -{%- if not loop.last %} -> -{%- endif %} -{%- endfor %} +{% endfor %} -Vous pouvez visualiser la notification et y répondre ici: +[Vous pouvez visualiser la notification et y répondre ici.]({{ absolute_url(path('chill_main_notification_show', {'_locale': dest.locale, 'id': notification.id }, false)) }}) -{{ absolute_url(path('chill_main_notification_show', {'_locale': dest.locale, 'id': notification.id }, false)) }} +----- --- Le logiciel Chill +{% endapply %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.txt.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.txt.twig new file mode 100644 index 000000000..58f138322 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.txt.twig @@ -0,0 +1,14 @@ +{{ dest.label }}, + +{{ notification.sender.label }} a créé une notification pour vous: + +Titre de la notification: {{ notification.title }} + +{% for line in notification.message|split("\n") %} +> {{ line|raw }} +{% endfor %} + +Vous pouvez visualiser la notification et y répondre ici: {{ absolute_url(path('chill_main_notification_show', {'_locale': dest.locale, 'id': notification.id }, false)) }}. + +-- +Le logiciel Chill diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content_to_email.md.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content_to_email.md.twig deleted file mode 100644 index 9a32f0c15..000000000 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content_to_email.md.twig +++ /dev/null @@ -1,20 +0,0 @@ -{{ dest }}, - -{{ notification.sender.label }} a créé une notification pour vous: - -> {{ notification.title }} -> -> -{%- for line in notification.message|split("\n") %} -> {{ line }} -{%- if not loop.last %} -> -{%- endif %} -{%- endfor %} - -Vous pouvez cliquer sur ce lien pour obtenir un accès permanent à la notification: - -{{ absolute_url(path('chill_main_notification_grant_access_by_access_key', {'_locale': 'fr', 'id': notification.id, 'accessKey': notification.accessKey, 'email': dest})) }} - --- -Le logiciel Chill diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/email_notification_comment_persist.md.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_notification_comment_persist.md.twig index e7e212492..176484d9f 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/email_notification_comment_persist.md.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_notification_comment_persist.md.twig @@ -1,3 +1,4 @@ +{% apply markdown_to_html %} {{ dest.label }}, {{ comment.createdBy.label }} a créé un commentaire sur la notification "{{ comment.notification.title }}". @@ -6,14 +7,11 @@ Commentaire: {% for line in comment.content|split("\n") %} > {{ line }} -{%- if not loop.last %} -> -{%- endif %} -{%- endfor %} +{% endfor %} -Vous pouvez visualiser la notification et y répondre ici: +[Vous pouvez visualiser la notification et y répondre ici.]({{ absolute_url(path('chill_main_notification_show', {'_locale': dest.locale, 'id': comment.notification.id }, false)) }}) -{{ absolute_url(path('chill_main_notification_show', {'_locale': dest.locale, 'id': comment.notification.id }, false)) }} +---- --- Le logiciel Chill +{% endapply %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/email_notification_comment_persist.txt.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_notification_comment_persist.txt.twig new file mode 100644 index 000000000..952c38662 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_notification_comment_persist.txt.twig @@ -0,0 +1,14 @@ +{{ dest.label }}, + +{{ comment.createdBy.label }} a créé un commentaire sur la notification "{{ comment.notification.title }}". + +Commentaire: + +{% for line in comment.content|split("\n") %} +> {{ line }} +{%- endfor %} + +Vous pouvez visualiser la notification et y répondre ici: {{ absolute_url(path('chill_main_notification_show', {'_locale': dest.locale, 'id': comment.notification.id }, false)) }} + +-- +Le logiciel Chill From 966f9f7e33dd0f33902cdaaab8d047dfb6c3f033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 23 Feb 2026 17:13:24 +0100 Subject: [PATCH 10/22] Release v4.13.0 --- .changes/unreleased/DX-20260210-161202.yaml | 7 ------- .changes/unreleased/Feature-20260223-151808.yaml | 7 ------- .changes/unreleased/Feature-20260223-164835.yaml | 7 ------- .changes/unreleased/Fixed-20260210-141731.yaml | 6 ------ .changes/unreleased/Fixed-20260210-155209.yaml | 7 ------- .changes/unreleased/Fixed-20260217-140838.yaml | 7 ------- .changes/unreleased/Fixed-20260223-144735.yaml | 8 -------- .changes/unreleased/Fixed-20260223-154405.yaml | 7 ------- .changes/unreleased/Fixed-20260223-155645.yaml | 8 -------- .changes/v4.13.0.md | 15 +++++++++++++++ CHANGELOG.md | 16 ++++++++++++++++ 11 files changed, 31 insertions(+), 64 deletions(-) delete mode 100644 .changes/unreleased/DX-20260210-161202.yaml delete mode 100644 .changes/unreleased/Feature-20260223-151808.yaml delete mode 100644 .changes/unreleased/Feature-20260223-164835.yaml delete mode 100644 .changes/unreleased/Fixed-20260210-141731.yaml delete mode 100644 .changes/unreleased/Fixed-20260210-155209.yaml delete mode 100644 .changes/unreleased/Fixed-20260217-140838.yaml delete mode 100644 .changes/unreleased/Fixed-20260223-144735.yaml delete mode 100644 .changes/unreleased/Fixed-20260223-154405.yaml delete mode 100644 .changes/unreleased/Fixed-20260223-155645.yaml create mode 100644 .changes/v4.13.0.md diff --git a/.changes/unreleased/DX-20260210-161202.yaml b/.changes/unreleased/DX-20260210-161202.yaml deleted file mode 100644 index 43843456f..000000000 --- a/.changes/unreleased/DX-20260210-161202.yaml +++ /dev/null @@ -1,7 +0,0 @@ -kind: DX -body: Configure changie to ask for merge request number for a better tracking of changes -time: 2026-02-10T16:12:02.400722306+01:00 -custom: - Issue: "" - MR: "960" - SchemaChange: No schema change diff --git a/.changes/unreleased/Feature-20260223-151808.yaml b/.changes/unreleased/Feature-20260223-151808.yaml deleted file mode 100644 index 2d134c8af..000000000 --- a/.changes/unreleased/Feature-20260223-151808.yaml +++ /dev/null @@ -1,7 +0,0 @@ -kind: Feature -body: Limit the number of public download of stored object to 30 downloads -time: 2026-02-23T15:18:08.688011924+01:00 -custom: - Issue: "500" - MR: "964" - SchemaChange: No schema change diff --git a/.changes/unreleased/Feature-20260223-164835.yaml b/.changes/unreleased/Feature-20260223-164835.yaml deleted file mode 100644 index 07ffcc676..000000000 --- a/.changes/unreleased/Feature-20260223-164835.yaml +++ /dev/null @@ -1,7 +0,0 @@ -kind: Feature -body: Send email related to notification in both html and txt format, and render quote correctly -time: 2026-02-23T16:48:35.48244838+01:00 -custom: - Issue: "495" - MR: "967" - SchemaChange: No schema change diff --git a/.changes/unreleased/Fixed-20260210-141731.yaml b/.changes/unreleased/Fixed-20260210-141731.yaml deleted file mode 100644 index 013080c9f..000000000 --- a/.changes/unreleased/Fixed-20260210-141731.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: Fixed -body: Change wrong color of submit button "Désigner comme adresse du parcours" -time: 2026-02-10T14:17:31.926008236+01:00 -custom: - Issue: "438" - SchemaChange: No schema change diff --git a/.changes/unreleased/Fixed-20260210-155209.yaml b/.changes/unreleased/Fixed-20260210-155209.yaml deleted file mode 100644 index 1047a9334..000000000 --- a/.changes/unreleased/Fixed-20260210-155209.yaml +++ /dev/null @@ -1,7 +0,0 @@ -kind: Fixed -body: For giving edit permissions on documents, take into account the workflow creator -time: 2026-02-10T15:52:09.785649121+01:00 -custom: - Issue: "498" - MR: "" - SchemaChange: No schema change diff --git a/.changes/unreleased/Fixed-20260217-140838.yaml b/.changes/unreleased/Fixed-20260217-140838.yaml deleted file mode 100644 index d6b3d20be..000000000 --- a/.changes/unreleased/Fixed-20260217-140838.yaml +++ /dev/null @@ -1,7 +0,0 @@ -kind: Fixed -body: 'Fixed mispelling of address in translations: addresse -> adresse' -time: 2026-02-17T14:08:38.82625052+01:00 -custom: - Issue: "" - MR: "" - SchemaChange: No schema change diff --git a/.changes/unreleased/Fixed-20260223-144735.yaml b/.changes/unreleased/Fixed-20260223-144735.yaml deleted file mode 100644 index d20a8b4de..000000000 --- a/.changes/unreleased/Fixed-20260223-144735.yaml +++ /dev/null @@ -1,8 +0,0 @@ -kind: Fixed -body: | - Fix: some postal code appears in the UI, although they are marked as deleted -time: 2026-02-23T14:47:35.925109983+01:00 -custom: - Issue: "499" - MR: "963" - SchemaChange: No schema change diff --git a/.changes/unreleased/Fixed-20260223-154405.yaml b/.changes/unreleased/Fixed-20260223-154405.yaml deleted file mode 100644 index 9e089c807..000000000 --- a/.changes/unreleased/Fixed-20260223-154405.yaml +++ /dev/null @@ -1,7 +0,0 @@ -kind: Fixed -body: Fix deprecation in the markdown rendering -time: 2026-02-23T15:44:05.800494138+01:00 -custom: - Issue: "501" - MR: "966" - SchemaChange: No schema change diff --git a/.changes/unreleased/Fixed-20260223-155645.yaml b/.changes/unreleased/Fixed-20260223-155645.yaml deleted file mode 100644 index 7d9b8fccc..000000000 --- a/.changes/unreleased/Fixed-20260223-155645.yaml +++ /dev/null @@ -1,8 +0,0 @@ -kind: Fixed -body: | - - Remove unused all-day slot display -time: 2026-02-23T15:56:45.517079+01:00 -custom: - Issue: "494" - MR: "965" - SchemaChange: No schema change diff --git a/.changes/v4.13.0.md b/.changes/v4.13.0.md new file mode 100644 index 000000000..cb92860e2 --- /dev/null +++ b/.changes/v4.13.0.md @@ -0,0 +1,15 @@ +## v4.13.0 - 2026-02-23 +### Feature +* ([#500](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/500)) ([!964](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/964)) Limit the number of public download of stored object to 30 downloads +* ([#495](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/495)) ([!967](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/967)) Send email related to notification in both html and txt format, and render quote correctly + +### Fixed +* ([#438](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/438)) Change wrong color of submit button "Désigner comme adresse du parcours" +* ([#498](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/498)) For giving edit permissions on documents, take into account the workflow creator +* Fixed mispelling of address in translations: addresse -> adresse +* ([#499](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/499)) ([!963](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/963)) Fix: some postal code appears in the UI, although they are marked as deleted +* ([#501](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/501)) ([!966](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/966)) Fix deprecation in the markdown rendering +* ([#494](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/494)) ([!965](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/965)) Remove unused all-day slot display + +### DX +* ([!960](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/960)) Configure changie to ask for merge request number for a better tracking of changes diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f93dc783..6fd8931fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). +## v4.13.0 - 2026-02-23 +### Feature +* ([#500](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/500)) ([!964](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/964)) Limit the number of public download of stored object to 30 downloads +* ([#495](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/495)) ([!967](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/967)) Send email related to notification in both html and txt format, and render quote correctly + +### Fixed +* ([#438](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/438)) Change wrong color of submit button "Désigner comme adresse du parcours" +* ([#498](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/498)) For giving edit permissions on documents, take into account the workflow creator +* Fixed mispelling of address in translations: addresse -> adresse +* ([#499](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/499)) ([!963](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/963)) Fix: some postal code appears in the UI, although they are marked as deleted +* ([#501](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/501)) ([!966](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/966)) Fix deprecation in the markdown rendering +* ([#494](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/494)) ([!965](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/965)) Remove unused all-day slot display + +### DX +* ([!960](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/960)) Configure changie to ask for merge request number for a better tracking of changes + ## v4.12.1 - 2026-02-01 ### Fixed * ([#496](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/496)) Add the option to deal with duplicate address in BAN adress importer From 030553a4de2a32d0ef484748c37499735b3ce49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 23 Feb 2026 20:05:04 +0000 Subject: [PATCH 11/22] =?UTF-8?q?Resolve=20"Lors=20de=20l'import=20de=20co?= =?UTF-8?q?de=20postaux,=20les=20codes=20absents=20de=20l'import=20depuis?= =?UTF-8?q?=20la=20m=C3=AAme=20source=20ne=20sont=20pas=20supprim=C3=A9s"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unreleased/Fixed-20260223-182212.yaml | 7 ++ .../ChillMainBundle/Entity/PostalCode.php | 10 ++ .../Service/Import/PostalCodeBaseImporter.php | 104 ++++++++++++++++-- .../Import/PostalCodeBaseImporterTest.php | 76 +++++++++++++ 4 files changed, 185 insertions(+), 12 deletions(-) create mode 100644 .changes/unreleased/Fixed-20260223-182212.yaml diff --git a/.changes/unreleased/Fixed-20260223-182212.yaml b/.changes/unreleased/Fixed-20260223-182212.yaml new file mode 100644 index 000000000..ec6a4423c --- /dev/null +++ b/.changes/unreleased/Fixed-20260223-182212.yaml @@ -0,0 +1,7 @@ +kind: Fixed +body: 'Fix import of postal code: mark postal code as deleted if they are not present in the import any more' +time: 2026-02-23T18:22:12.92214987+01:00 +custom: + Issue: "502" + MR: "968" + SchemaChange: No schema change diff --git a/src/Bundle/ChillMainBundle/Entity/PostalCode.php b/src/Bundle/ChillMainBundle/Entity/PostalCode.php index ecab19a5f..d6ffe763f 100644 --- a/src/Bundle/ChillMainBundle/Entity/PostalCode.php +++ b/src/Bundle/ChillMainBundle/Entity/PostalCode.php @@ -215,4 +215,14 @@ class PostalCode implements TrackUpdateInterface, TrackCreationInterface return $this; } + + public function isDeleted(): bool + { + return null !== $this->deletedAt; + } + + public function getDeletedAt(): ?\DateTimeImmutable + { + return $this->deletedAt; + } } diff --git a/src/Bundle/ChillMainBundle/Service/Import/PostalCodeBaseImporter.php b/src/Bundle/ChillMainBundle/Service/Import/PostalCodeBaseImporter.php index b1ace3875..a10579b23 100644 --- a/src/Bundle/ChillMainBundle/Service/Import/PostalCodeBaseImporter.php +++ b/src/Bundle/ChillMainBundle/Service/Import/PostalCodeBaseImporter.php @@ -19,31 +19,66 @@ use Doctrine\DBAL\Statement; */ class PostalCodeBaseImporter { - private const QUERY = <<<'SQL' + private const CREATE_TEMP_TABLE = <<<'SQL' + CREATE TEMPORARY TABLE chill_main_postal_code_temp ( + countrycode VARCHAR(10), + label VARCHAR(255), + code VARCHAR(100), + refpostalcodeid VARCHAR(255), + postalcodeSource VARCHAR(255), + lon FLOAT, + lat FLOAT, + srid INT + ) + SQL; + + private const INSERT_TEMP = <<<'SQL' + INSERT INTO chill_main_postal_code_temp + (countrycode, label, code, refpostalcodeid, postalcodeSource, lon, lat, srid) + VALUES + {{ values }} + SQL; + + private const UPSERT = <<<'SQL' WITH g AS ( SELECT DISTINCT country.id AS country_id, - g.* - FROM (VALUES - {{ values }} - ) AS g (countrycode, label, code, refpostalcodeid, postalcodeSource, lon, lat, srid) - JOIN country ON country.countrycode = g.countrycode + temp.* + FROM chill_main_postal_code_temp temp + JOIN country ON country.countrycode = temp.countrycode ) - INSERT INTO chill_main_postal_code (id, country_id, label, code, origin, refpostalcodeid, postalcodeSource, center, createdAt, updatedAt) + INSERT INTO chill_main_postal_code (id, country_id, label, code, origin, refpostalcodeid, postalcodeSource, center, createdAt, updatedAt, deletedAt) SELECT nextval('chill_main_postal_code_id_seq'), g.country_id, - g.label AS glabel, + g.label, g.code, 0, g.refpostalcodeid, g.postalcodeSource, - CASE WHEN (g.lon::float != 0.0 AND g.lat::float != 0.0) THEN ST_Transform(ST_setSrid(ST_point(g.lon::float, g.lat::float), g.srid::int), 4326) ELSE NULL END, + CASE WHEN (g.lon != 0.0 AND g.lat != 0.0) THEN ST_Transform(ST_setSrid(ST_point(g.lon, g.lat), g.srid), 4326) ELSE NULL END, NOW(), - NOW() + NOW(), + NULL FROM g ON CONFLICT (code, refpostalcodeid, postalcodeSource) WHERE refpostalcodeid IS NOT NULL DO UPDATE - SET label = excluded.label, center = excluded.center, updatedAt = CASE WHEN NOT st_equals(excluded.center, chill_main_postal_code.center) OR excluded.label != chill_main_postal_code.label THEN NOW() ELSE chill_main_postal_code.updatedAt END + SET label = excluded.label, + center = excluded.center, + deletedAt = NULL, + updatedAt = CASE WHEN NOT st_equals(excluded.center, chill_main_postal_code.center) OR excluded.label != chill_main_postal_code.label OR chill_main_postal_code.deletedAt IS NOT NULL THEN NOW() ELSE chill_main_postal_code.updatedAt END + SQL; + + private const DELETE_MISSING = <<<'SQL' + UPDATE chill_main_postal_code + SET deletedAt = NOW(), updatedAt = NOW() + WHERE postalcodeSource = ? + AND deletedAt IS NULL + AND NOT EXISTS ( + SELECT 1 FROM chill_main_postal_code_temp temp + WHERE temp.code = chill_main_postal_code.code + AND temp.refpostalcodeid = chill_main_postal_code.refpostalcodeid + AND temp.postalcodeSource = chill_main_postal_code.postalcodeSource + ) SQL; private const VALUE = '(?, ?, ?, ?, ?, ?, ?, ?)'; @@ -55,11 +90,26 @@ class PostalCodeBaseImporter private array $waitingForInsert = []; + private bool $isInitialized = false; + + private ?string $currentSource = null; + public function __construct(private readonly Connection $defaultConnection) {} public function finalize(): void { $this->doInsertPending(); + + if ($this->isInitialized && null !== $this->currentSource) { + $this->defaultConnection->transactional(function (Connection $connection): void { + $connection->executeStatement(self::UPSERT); + $connection->executeStatement(self::DELETE_MISSING, [$this->currentSource]); + }); + $this->deleteTemporaryTable(); + } + + $this->isInitialized = false; + $this->currentSource = null; } public function importCode( @@ -72,6 +122,14 @@ class PostalCodeBaseImporter float $centerLon, int $centerSRID, ): void { + if (!$this->isInitialized) { + $this->initialize($refPostalCodeSource); + } + + if ($this->currentSource !== $refPostalCodeSource) { + throw new \LogicException('Cannot store postal codes from different sources during same import. Execute finalize to commit inserts before changing the source'); + } + $this->waitingForInsert[] = [ $countryCode, $label, @@ -88,10 +146,32 @@ class PostalCodeBaseImporter } } + private function initialize(string $source): void + { + $this->currentSource = $source; + $this->deleteTemporaryTable(); + $this->createTemporaryTable(); + $this->isInitialized = true; + } + + private function createTemporaryTable(): void + { + $this->defaultConnection->executeStatement(self::CREATE_TEMP_TABLE); + } + + private function deleteTemporaryTable(): void + { + $this->defaultConnection->executeStatement('DROP TABLE IF EXISTS chill_main_postal_code_temp'); + } + private function doInsertPending(): void { + if ([] == $this->waitingForInsert) { + return; + } + if (!\array_key_exists($forNumber = \count($this->waitingForInsert), $this->cachingStatements)) { - $sql = strtr(self::QUERY, [ + $sql = strtr(self::INSERT_TEMP, [ '{{ values }}' => implode( ', ', array_fill(0, $forNumber, self::VALUE) diff --git a/src/Bundle/ChillMainBundle/Tests/Services/Import/PostalCodeBaseImporterTest.php b/src/Bundle/ChillMainBundle/Tests/Services/Import/PostalCodeBaseImporterTest.php index d7447e7a0..eb313ba0d 100644 --- a/src/Bundle/ChillMainBundle/Tests/Services/Import/PostalCodeBaseImporterTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Services/Import/PostalCodeBaseImporterTest.php @@ -93,4 +93,80 @@ final class PostalCodeBaseImporterTest extends KernelTestCase $this->assertStringStartsWith('tested with adapted pattern', $postalCodes[0]->getName()); $this->assertEquals($previousId, $postalCodes[0]->getId()); } + + public function testPostalCodeRemoval(): void + { + $source = 'removal_test_'.uniqid(); + $refId1 = 'ref1_'.uniqid(); + $refId2 = 'ref2_'.uniqid(); + + // 1. Import two postal codes + $this->importer->importCode('BE', 'Label 1', '1000', $refId1, $source, 50.0, 5.0, 4326); + $this->importer->importCode('BE', 'Label 2', '2000', $refId2, $source, 50.0, 5.0, 4326); + $this->importer->finalize(); + + $pc1 = $this->postalCodeRepository->findOneBy(['refPostalCodeId' => $refId1, 'postalCodeSource' => $source]); + $pc2 = $this->postalCodeRepository->findOneBy(['refPostalCodeId' => $refId2, 'postalCodeSource' => $source]); + + $this->assertNotNull($pc1); + $this->assertNotNull($pc2); + + // 2. Import only the first one + $this->importer->importCode('BE', 'Label 1 updated', '1000', $refId1, $source, 50.0, 5.0, 4326); + $this->importer->finalize(); + + $this->entityManager->clear(); + + $pc1 = $this->postalCodeRepository->findOneBy(['refPostalCodeId' => $refId1, 'postalCodeSource' => $source]); + $pc2 = $this->postalCodeRepository->findOneBy(['refPostalCodeId' => $refId2, 'postalCodeSource' => $source]); + + $this->assertNotNull($pc1); + $this->assertEquals('Label 1 updated', $pc1->getName()); + + $this->assertFalse($pc1->isDeleted(), 'pc1 should NOT be marked as deleted'); + + // pc2 should be marked as deleted. Note: findOneBy might still find it if it doesn't filter by deletedAt + $this->assertNotNull($pc2); + + $this->assertTrue($pc2->isDeleted(), 'Postal code should be marked as deleted (deletedAt is not null)'); + + // 3. Reactivate pc2 by re-importing it + $this->importer->importCode('BE', 'Label 2 restored', '2000', $refId2, $source, 50.0, 5.0, 4326); + $this->importer->finalize(); + + $this->entityManager->clear(); + $pc2 = $this->postalCodeRepository->findOneBy(['refPostalCodeId' => $refId2, 'postalCodeSource' => $source]); + $this->assertFalse($pc2->isDeleted(), 'Postal code should NOT be marked as deleted after restoration'); + $this->assertEquals('Label 2 restored', $pc2->getName()); + } + + public function testNoInterferenceBetweenSources(): void + { + $source1 = 'source1_'.uniqid(); + $source2 = 'source2_'.uniqid(); + $refId1 = 'ref1_'.uniqid(); + $refId2 = 'ref2_'.uniqid(); + + // 1. Import from source1 + $this->importer->importCode('BE', 'Label 1', '1000', $refId1, $source1, 50.0, 5.0, 4326); + $this->importer->finalize(); + + $pc1 = $this->postalCodeRepository->findOneBy(['refPostalCodeId' => $refId1, 'postalCodeSource' => $source1]); + $this->assertNotNull($pc1); + $this->assertFalse($pc1->isDeleted()); + + // 2. Import from source2 + $this->importer->importCode('BE', 'Label 2', '2000', $refId2, $source2, 50.0, 5.0, 4326); + $this->importer->finalize(); + + $this->entityManager->clear(); + + $pc1 = $this->postalCodeRepository->findOneBy(['refPostalCodeId' => $refId1, 'postalCodeSource' => $source1]); + $pc2 = $this->postalCodeRepository->findOneBy(['refPostalCodeId' => $refId2, 'postalCodeSource' => $source2]); + + $this->assertNotNull($pc1); + $this->assertNotNull($pc2); + $this->assertFalse($pc1->isDeleted(), 'pc1 from source1 should NOT be deleted after import from source2'); + $this->assertFalse($pc2->isDeleted(), 'pc2 from source2 should NOT be deleted'); + } } From 5de3862ec262af10ce3098120adae0174827cb30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 9 Mar 2026 09:25:08 +0000 Subject: [PATCH 12/22] =?UTF-8?q?Resolve=20"Lors=20de=20la=20r=C3=A9-assig?= =?UTF-8?q?nation=20des=20parcours,=20l'UI=20ne=20mentionne=20pas=20qu'une?= =?UTF-8?q?=20op=C3=A9ration=20a=20=C3=A9t=C3=A9=20r=C3=A9alis=C3=A9e"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unreleased/Fixed-20260309-101721.yaml | 7 +++++++ .../ReassignAccompanyingPeriodController.php | 19 +++++++++++++++---- .../translations/messages+intl-icu.fr.yaml | 8 ++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 .changes/unreleased/Fixed-20260309-101721.yaml diff --git a/.changes/unreleased/Fixed-20260309-101721.yaml b/.changes/unreleased/Fixed-20260309-101721.yaml new file mode 100644 index 000000000..5e369b184 --- /dev/null +++ b/.changes/unreleased/Fixed-20260309-101721.yaml @@ -0,0 +1,7 @@ +kind: Fixed +body: Add a flash message when reassigning accompanying course (reassign list) +time: 2026-03-09T10:17:21.923487588+01:00 +custom: + Issue: "503" + MR: "969" + SchemaChange: No schema change diff --git a/src/Bundle/ChillPersonBundle/Controller/ReassignAccompanyingPeriodController.php b/src/Bundle/ChillPersonBundle/Controller/ReassignAccompanyingPeriodController.php index 6e3fa5117..f44470f45 100644 --- a/src/Bundle/ChillPersonBundle/Controller/ReassignAccompanyingPeriodController.php +++ b/src/Bundle/ChillPersonBundle/Controller/ReassignAccompanyingPeriodController.php @@ -17,7 +17,6 @@ use Chill\MainBundle\Form\Type\PickPostalCodeType; use Chill\MainBundle\Form\Type\PickUserDynamicType; use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Repository\UserRepository; -use Chill\MainBundle\Templating\Entity\UserRender; use Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepositoryInterface; use Chill\PersonBundle\Repository\AccompanyingPeriodRepository; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; @@ -31,18 +30,29 @@ use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Security; +use Symfony\Component\Translation\TranslatableMessage; use Symfony\Component\Validator\Constraints\NotIdenticalTo; use Symfony\Component\Validator\Constraints\NotNull; class ReassignAccompanyingPeriodController extends AbstractController { - public function __construct(private readonly AccompanyingPeriodACLAwareRepositoryInterface $accompanyingPeriodACLAwareRepository, private readonly UserRepository $userRepository, private readonly AccompanyingPeriodRepository $courseRepository, private readonly \Twig\Environment $engine, private readonly FormFactoryInterface $formFactory, private readonly PaginatorFactory $paginatorFactory, private readonly Security $security, private readonly UserRender $userRender, private readonly EntityManagerInterface $em) {} + public function __construct( + private readonly AccompanyingPeriodACLAwareRepositoryInterface $accompanyingPeriodACLAwareRepository, + private readonly UserRepository $userRepository, + private readonly AccompanyingPeriodRepository $courseRepository, + private readonly \Twig\Environment $engine, + private readonly FormFactoryInterface $formFactory, + private readonly PaginatorFactory $paginatorFactory, + private readonly Security $security, + private readonly EntityManagerInterface $entityManager, + ) {} #[Route(path: '/{_locale}/person/accompanying-periods/reassign', name: 'chill_course_list_reassign')] - public function listAction(Request $request): Response + public function listAction(Request $request, Session $session): Response { if (!$this->security->isGranted(AccompanyingPeriodVoter::REASSIGN_BULK)) { throw new AccessDeniedHttpException('no right to reassign bulk'); @@ -96,7 +106,8 @@ class ReassignAccompanyingPeriodController extends AbstractController } } - $this->em->flush(); + $this->entityManager->flush(); + $this->addFlash('success', new TranslatableMessage('period_by_user_list.successfully_re_assigned', ['count' => count($assignPeriodIds)])); // redirect to the first page return $this->redirectToRoute('chill_course_list_reassign', $request->query->all()); diff --git a/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml b/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml index 201157214..dc5f2e992 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml +++ b/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml @@ -24,6 +24,14 @@ accompanying_period: number: >- n° {id} +period_by_user_list: + successfully_re_assigned: >- + {count, plural, + =0 {Aucune assignation de référent effectuée} + =1 {Assignation d'un nouveau référent pour un parcours} + other {Assignation d'un nouveau référent pour # parcours} + } + person: from_the: depuis le And himself: >- From 562fecb4aa728b0317f902b4e895220ef37e1468 Mon Sep 17 00:00:00 2001 From: LenaertsJ Date: Mon, 9 Mar 2026 11:19:38 +0000 Subject: [PATCH 13/22] Resolve "Create a filter/aggregator by user center for the exports" --- .../unreleased/Feature-20251230-164303.yaml | 6 + .junie/guidelines.md | 6 +- .../Test/Export/AbstractFilterTest.php | 5 +- .../translations/messages.fr.yml | 4 + .../ReferrerMainCenterAggregator.php | 143 ++++++++++++++++ .../PersonAggregators/CenterAggregator.php | 2 +- .../ReferrerMainCenterFilter.php | 155 ++++++++++++++++++ .../ReferrerMainCenterAggregatorTest.php | 123 ++++++++++++++ .../ReferrerMainCenterFilterTest.php | 69 ++++++++ .../services/exports_accompanying_course.yaml | 8 + .../translations/messages+intl-icu.fr.yaml | 7 + .../translations/messages.fr.yml | 15 +- 12 files changed, 534 insertions(+), 9 deletions(-) create mode 100644 .changes/unreleased/Feature-20251230-164303.yaml create mode 100644 src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerMainCenterAggregator.php create mode 100644 src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ReferrerMainCenterFilter.php create mode 100644 src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/ReferrerMainCenterAggregatorTest.php create mode 100644 src/Bundle/ChillPersonBundle/Tests/Export/Filter/AccompanyingCourseFilters/ReferrerMainCenterFilterTest.php diff --git a/.changes/unreleased/Feature-20251230-164303.yaml b/.changes/unreleased/Feature-20251230-164303.yaml new file mode 100644 index 000000000..f402e60bf --- /dev/null +++ b/.changes/unreleased/Feature-20251230-164303.yaml @@ -0,0 +1,6 @@ +kind: Feature +body: Add filter and aggregator based on referrer's main center for exports of accompanying period +time: 2025-12-30T16:43:03.898677616+01:00 +custom: + Issue: "486" + SchemaChange: No schema change diff --git a/.junie/guidelines.md b/.junie/guidelines.md index ff84fdffa..df0efdfbb 100644 --- a/.junie/guidelines.md +++ b/.junie/guidelines.md @@ -238,13 +238,13 @@ The tests are run from the project's root (not from the bundle's root). ```bash # Run all tests -vendor/bin/phpunit +symfony composer exec phpunit # Run a specific test file -vendor/bin/phpunit path/to/TestFile.php +symfony composer exec phpunit -- path/to/TestFile.php # Run a specific test method -vendor/bin/phpunit --filter methodName path/to/TestFile.php +symfony composer exec phpunit --filter methodName path/to/TestFile.php ``` When writing tests, only test specific files. Do not run all tests or the full diff --git a/src/Bundle/ChillMainBundle/Test/Export/AbstractFilterTest.php b/src/Bundle/ChillMainBundle/Test/Export/AbstractFilterTest.php index e76d7d504..21a8e78c5 100644 --- a/src/Bundle/ChillMainBundle/Test/Export/AbstractFilterTest.php +++ b/src/Bundle/ChillMainBundle/Test/Export/AbstractFilterTest.php @@ -17,6 +17,7 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Contracts\Translation\TranslatableInterface; /** * Helper to test filters. @@ -255,8 +256,8 @@ abstract class AbstractFilterTest extends KernelTestCase $description = $this->getFilter()->describeAction($data, $context); $this->assertTrue( - \is_string($description) || \is_array($description), - 'test that the description is a string or an array' + \is_string($description) || \is_array($description) || $description instanceof TranslatableInterface, + 'test that the description is a string or an array, or a TranslatableInterface' ); if (\is_string($description)) { diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index 387c16662..9a3f56afd 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -1,3 +1,7 @@ +common: + after: Après + until: Jusqu'à + centers: Territoires "This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License": "Ce programme est un logiciel libre: vous pouvez le redistribuer et/ou le modifier selon les termes de la licence GNU Affero GPL" User manual: Manuel d'utilisation Search: Rechercher diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerMainCenterAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerMainCenterAggregator.php new file mode 100644 index 000000000..6079b0a1b --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerMainCenterAggregator.php @@ -0,0 +1,143 @@ +leftJoin('acp.userHistories', "{$p}_uh", Join::WITH, $qb->expr()->andX( + $qb->expr()->eq("{$p}_uh.accompanyingPeriod", 'acp.id'), + "OVERLAPSI (acp.openingDate, acp.closingDate), ({$p}_uh.startDate, {$p}_uh.endDate) = TRUE", + "OVERLAPSI (:{$p}_startDate, :{$p}_endDate), ({$p}_uh.startDate, {$p}_uh.endDate) = TRUE" + )) + ->leftJoin("{$p}_uh.user", "{$p}_user") + ->addSelect("IDENTITY({$p}_user.mainCenter) AS {$p}_select") + ->addGroupBy("{$p}_select") + ->setParameter("{$p}_startDate", $this->rollingDateConverter->convert($data['start_date'])) + ->setParameter("{$p}_endDate", $this->rollingDateConverter->convert($data['end_date'])); + } + + public function applyOn(): string + { + return Declarations::ACP_TYPE; + } + + public function buildForm(FormBuilderInterface $builder): void + { + $builder + ->add('start_date', PickRollingDateType::class, [ + 'label' => 'common.after', + 'required' => true, + ]) + ->add('end_date', PickRollingDateType::class, [ + 'label' => 'common.until', + 'required' => true, + ]); + } + + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return [ + 'start_date' => $formData['start_date']->normalize(), + 'end_date' => $formData['end_date']->normalize(), + ]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + $default = $this->getFormDefaultData(); + + return [ + 'start_date' => array_key_exists('start_date', $formData) ? RollingDate::fromNormalized($formData['start_date']) : $default['start_date'], + 'end_date' => array_key_exists('end_date', $formData) ? RollingDate::fromNormalized($formData['end_date']) : $default['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 transformData(?array $before): array + { + $default = $this->getFormDefaultData(); + + if (null === $before) { + return $default; + } + + return [ + 'start_date' => $before['start_date'] ?? $before['date_calc'] ?? $default['start_date'], + 'end_date' => $before['end_date'] ?? $before['date_calc'] ?? $default['end_date'], + ]; + } + + public function getLabels($key, array $values, $data): callable + { + return function ($value): string { + if ('_header' === $value) { + return 'person.export.period.aggregator.by_referrer_main_center.column_header'; + } + + if (null === $value || '' === $value) { + return ''; + } + + return (string) $this->centerRepository->find((int) $value)?->getName(); + }; + } + + public function getQueryKeys($data): array + { + return [self::P.'_select']; + } + + public function getTitle(): string + { + return 'person.export.period.aggregator.by_referrer_main_center.title'; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/CenterAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/CenterAggregator.php index bf9901207..a8c4d090e 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/CenterAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/CenterAggregator.php @@ -64,7 +64,7 @@ final readonly class CenterAggregator implements AggregatorInterface { return function (int|string|null $value) { if (null === $value || '' === $value) { - return $this->translator->trans('person.export.aggregator.by_center.no_center'); + return $this->translator->trans('person.export.period.aggregator.by_center.no_center'); } if ('_header' === $value) { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ReferrerMainCenterFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ReferrerMainCenterFilter.php new file mode 100644 index 000000000..d534cfa2e --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ReferrerMainCenterFilter.php @@ -0,0 +1,155 @@ += :'.self::DATE_PARAM_SINCE.' + ) + ) + AND '.self::UH.'_user.mainCenter IN (:'.self::CENTER_PARAM.')'; + + $qb->andWhere( + $qb->expr()->exists($dql) + ); + $qb + ->setParameter(self::DATE_PARAM_SINCE, $this->rollingDateConverter->convert($data['date_calc_since'])) + ->setParameter(self::DATE_PARAM_UNTIL, $this->rollingDateConverter->convert($data['date_calc_until'])) + ->setParameter(self::CENTER_PARAM, $data['centers']); + } + + public function applyOn(): string + { + return Declarations::ACP_TYPE; + } + + public function buildForm(FormBuilderInterface $builder): void + { + $builder + ->add('centers', EntityType::class, [ + 'class' => Center::class, + 'choices' => $this->centerRepository->findActive(), + 'multiple' => true, + 'expanded' => false, + 'choice_label' => static fn (Center $c) => $c->getName(), + 'required' => true, + 'label' => 'common.centers', + 'attr' => [ + 'class' => 'select2', + ], + ]) + ->add('date_calc_since', PickRollingDateType::class, [ + 'label' => 'person.export.period.filter.by_referrer_main_center.referrer_since', + 'required' => true, + ]) + ->add('date_calc_until', PickRollingDateType::class, [ + 'label' => 'person.export.period.filter.by_referrer_main_center.referrer_until', + 'required' => true, + ]); + } + + public function getNormalizationVersion(): int + { + return 1; + } + + public function normalizeFormData(array $formData): array + { + return [ + 'centers' => array_values(array_map(static fn (Center $c) => $c->getId(), $formData['centers'])), + 'date_calc_since' => $formData['date_calc_since']->normalize(), + 'date_calc_until' => $formData['date_calc_until']->normalize(), + ]; + } + + public function denormalizeFormData(array $formData, int $fromVersion): array + { + return [ + 'centers' => array_values(array_filter(array_map( + fn (int $id) => $this->centerRepository->find($id), + $formData['centers'] ?? [] + ))), + 'date_calc_since' => RollingDate::fromNormalized($formData['date_calc_since']), + 'date_calc_until' => RollingDate::fromNormalized($formData['date_calc_until']), + ]; + } + + public function getFormDefaultData(): array + { + return [ + 'centers' => [], + 'date_calc_since' => new RollingDate(RollingDate::T_TODAY), + 'date_calc_until' => new RollingDate(RollingDate::T_TODAY), + ]; + } + + public function describeAction($data, ExportGenerationContext $context): TranslatableInterface + { + $names = array_map(static fn (Center $c) => $c->getName(), $data['centers']); + + return new TranslatableMessage( + 'person.export.period.filter.by_referrer_main_center.description', + [ + 'centers' => implode(', ', $names), + 'date_since' => $this->rollingDateConverter->convert($data['date_calc_since']), + 'date_until' => $this->rollingDateConverter->convert($data['date_calc_until']), + ] + ); + } + + public function getTitle(): TranslatableInterface + { + return new TranslatableMessage('person.export.period.filter.by_referrer_main_center.title'); + } +} diff --git a/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/ReferrerMainCenterAggregatorTest.php b/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/ReferrerMainCenterAggregatorTest.php new file mode 100644 index 000000000..5141f1899 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/ReferrerMainCenterAggregatorTest.php @@ -0,0 +1,123 @@ +aggregator = self::getContainer()->get('Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\ReferrerMainCenterAggregator'); + } + + /** + * @dataProvider provideBeforeData + */ + public function testDataTransformer(?array $before, array $expected): void + { + $actual = $this->getAggregator()->transformData($before); + + self::assertEqualsCanonicalizing(array_keys($expected), array_keys($actual)); + foreach (['start_date', 'end_date'] as $key) { + self::assertInstanceOf(RollingDate::class, $actual[$key]); + self::assertEquals($expected[$key]->getRoll(), $actual[$key]->getRoll(), "Check that the roll is the same for {$key}"); + } + } + + public static function provideBeforeData(): iterable + { + yield [ + ['date_calc' => new RollingDate(RollingDate::T_TODAY)], + ['start_date' => new RollingDate(RollingDate::T_TODAY), 'end_date' => new RollingDate(RollingDate::T_TODAY)], + ]; + + yield [ + ['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)], + ['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)], + ]; + + yield [ + null, + // this is the default configuration + ['start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)], + ]; + } + + public function getAggregator(): ReferrerMainCenterAggregator + { + return $this->aggregator; + } + + public static function getFormData(): array + { + self::bootKernel(); + $em = self::getContainer()->get(EntityManagerInterface::class); + $centers = $em->getRepository(Center::class)->findBy([], null, 1); + + return [ + [ + 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), + 'end_date' => new RollingDate(RollingDate::T_TODAY), + ], + ]; + } + + public static function provideGetResultsAndLabels(): iterable + { + self::bootKernel(); + $em = self::getContainer()->get(EntityManagerInterface::class); + $centers = $em->getRepository(Center::class)->findAll(); + + $qb = $em->createQueryBuilder() + ->select('count(acp.id)') + ->from(AccompanyingPeriod::class, 'acp'); + + $data = [ + 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), + 'end_date' => new RollingDate(RollingDate::T_TODAY), + ]; + + // Yield result with each center ID and null + foreach ($centers as $center) { + yield [$qb, $data, [(string) $center->getId() => 0]]; + } + + yield [$qb, $data, ['' => 0]]; + } + + public static function getQueryBuilders(): iterable + { + self::bootKernel(); + $em = self::getContainer()->get(EntityManagerInterface::class); + + return [ + $em->createQueryBuilder() + ->select('count(acp.id)') + ->from(AccompanyingPeriod::class, 'acp'), + ]; + } +} diff --git a/src/Bundle/ChillPersonBundle/Tests/Export/Filter/AccompanyingCourseFilters/ReferrerMainCenterFilterTest.php b/src/Bundle/ChillPersonBundle/Tests/Export/Filter/AccompanyingCourseFilters/ReferrerMainCenterFilterTest.php new file mode 100644 index 000000000..b1bc27888 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Export/Filter/AccompanyingCourseFilters/ReferrerMainCenterFilterTest.php @@ -0,0 +1,69 @@ +filter = self::getContainer()->get(ReferrerMainCenterFilter::class); + } + + public function getFilter(): ReferrerMainCenterFilter + { + return $this->filter; + } + + public static function getFormData(): array + { + self::bootKernel(); + $em = self::getContainer()->get(EntityManagerInterface::class); + $centers = $em->getRepository(Center::class)->findAll(); + + if ([] === $centers) { + throw new \RuntimeException('No centers found in database'); + } + + return [ + [ + 'centers' => [$centers[0]], + 'date_calc_since' => new RollingDate(RollingDate::T_TODAY), + 'date_calc_until' => new RollingDate(RollingDate::T_TODAY), + ], + ]; + } + + public static function getQueryBuilders(): iterable + { + self::bootKernel(); + $em = self::getContainer()->get(EntityManagerInterface::class); + yield $em->createQueryBuilder() + ->from(AccompanyingPeriod::class, 'acp') + ->select('acp.id'); + } +} diff --git a/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml b/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml index d488df8d6..b7ab71d40 100644 --- a/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml @@ -104,6 +104,10 @@ services: tags: - { name: chill.export_filter, alias: accompanyingcourse_referrer_filter_between_dates } + Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\ReferrerMainCenterFilter: + tags: + - { name: chill.export_filter, alias: accompanyingcourse_referrer_main_center_filter } + chill.person.export.filter_openbetweendates: class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\OpenBetweenDatesFilter tags: @@ -270,3 +274,7 @@ services: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\PersonParticipatingAggregator: tags: - { name: chill.export_aggregator, alias: accompanyingcourse_person_part_aggregator } + + Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\ReferrerMainCenterAggregator: + tags: + - { name: chill.export_aggregator, alias: accompanyingcourse_referrer_main_center_aggregator } diff --git a/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml b/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml index dc5f2e992..5a3d3d5a3 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml +++ b/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml @@ -41,6 +41,13 @@ person: neutral {et lui·elle-même} other {et lui·elle-même} } + export: + period: + filter: + by_referrer_main_center: + description: >- + Filtre les parcours par territoire du référent, entre le {date_since, date, medium} et le {date_until, date, medium}, uniquement {centers} + household: Household: Ménage diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index ad97767d2..a2a2fb048 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -105,9 +105,18 @@ Administrative status: Situation administrative person: # trans key according to new conventions export: - aggregator: - by_center: - no_center: Sans territoire + period: + aggregator: + by_center: + no_center: Sans territoire + by_referrer_main_center: + title: Grouper les parcours par territoire du référent + column_header: Territoire du référent + filter: + by_referrer_main_center: + title: Filtrer les parcours par territoire du référent + referrer_since: Référent depuis le + referrer_until: Référent avant le Identifiers: Identifiants From e2dec285770532b1ae504cdb9513b75e41042d7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 9 Mar 2026 12:27:00 +0100 Subject: [PATCH 14/22] Release v4.14.0 - Implemented `ReferrerMainCenterAggregatorTest` to validate data transformation and query logic. - Added data providers and query builders to ensure comprehensive test coverage. - Verified correct handling of rolling dates and aggregator logic. --- .changes/unreleased/Feature-20251230-164303.yaml | 6 ------ .changes/unreleased/Fixed-20260223-182212.yaml | 7 ------- .changes/unreleased/Fixed-20260309-101721.yaml | 7 ------- .changes/v4.14.0.md | 6 ++++++ CHANGELOG.md | 7 +++++++ 5 files changed, 13 insertions(+), 20 deletions(-) delete mode 100644 .changes/unreleased/Feature-20251230-164303.yaml delete mode 100644 .changes/unreleased/Fixed-20260223-182212.yaml delete mode 100644 .changes/unreleased/Fixed-20260309-101721.yaml create mode 100644 .changes/v4.14.0.md diff --git a/.changes/unreleased/Feature-20251230-164303.yaml b/.changes/unreleased/Feature-20251230-164303.yaml deleted file mode 100644 index f402e60bf..000000000 --- a/.changes/unreleased/Feature-20251230-164303.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: Feature -body: Add filter and aggregator based on referrer's main center for exports of accompanying period -time: 2025-12-30T16:43:03.898677616+01:00 -custom: - Issue: "486" - SchemaChange: No schema change diff --git a/.changes/unreleased/Fixed-20260223-182212.yaml b/.changes/unreleased/Fixed-20260223-182212.yaml deleted file mode 100644 index ec6a4423c..000000000 --- a/.changes/unreleased/Fixed-20260223-182212.yaml +++ /dev/null @@ -1,7 +0,0 @@ -kind: Fixed -body: 'Fix import of postal code: mark postal code as deleted if they are not present in the import any more' -time: 2026-02-23T18:22:12.92214987+01:00 -custom: - Issue: "502" - MR: "968" - SchemaChange: No schema change diff --git a/.changes/unreleased/Fixed-20260309-101721.yaml b/.changes/unreleased/Fixed-20260309-101721.yaml deleted file mode 100644 index 5e369b184..000000000 --- a/.changes/unreleased/Fixed-20260309-101721.yaml +++ /dev/null @@ -1,7 +0,0 @@ -kind: Fixed -body: Add a flash message when reassigning accompanying course (reassign list) -time: 2026-03-09T10:17:21.923487588+01:00 -custom: - Issue: "503" - MR: "969" - SchemaChange: No schema change diff --git a/.changes/v4.14.0.md b/.changes/v4.14.0.md new file mode 100644 index 000000000..9ad3b65c9 --- /dev/null +++ b/.changes/v4.14.0.md @@ -0,0 +1,6 @@ +## v4.14.0 - 2026-03-09 +### Feature +* ([#486](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/486)) ([!](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/)) Add filter and aggregator based on referrer's main center for exports of accompanying period +### Fixed +* ([#502](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/502)) ([!968](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/968)) Fix import of postal code: mark postal code as deleted if they are not present in the import any more +* ([#503](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/503)) ([!969](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/969)) Add a flash message when reassigning accompanying course (reassign list) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fd8931fc..842ddce69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). +## v4.14.0 - 2026-03-09 +### Feature +* ([#486](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/486)) ([!](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/)) Add filter and aggregator based on referrer's main center for exports of accompanying period +### Fixed +* ([#502](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/502)) ([!968](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/968)) Fix import of postal code: mark postal code as deleted if they are not present in the import any more +* ([#503](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/503)) ([!969](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/969)) Add a flash message when reassigning accompanying course (reassign list) + ## v4.13.0 - 2026-02-23 ### Feature * ([#500](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/500)) ([!964](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/964)) Limit the number of public download of stored object to 30 downloads From a921009eff79109b7aa6268a5f0761fe9024f4ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 9 Mar 2026 13:00:30 +0000 Subject: [PATCH 15/22] Add seeds to data fixtures, to avoid random failures in tests --- .changes/unreleased/DX-20260309-131710.yaml | 7 +++++++ config/packages/nelmio_alice.yaml | 1 + .../DataFixtures/ORM/LoadActivity.php | 5 +++-- .../DataFixtures/ORM/LoadAsideActivity.php | 7 +++++-- .../DataFixtures/ORM/LoadOption.php | 5 +++-- .../DataFixtures/ORM/LoadParticipation.php | 5 +++-- .../ORM/LoadAddressReferences.php | 7 ++++--- .../Helper/RandomPersonHelperTrait.php | 2 +- .../DataFixtures/ORM/LoadCustomFields.php | 8 +++++--- .../DataFixtures/ORM/LoadHousehold.php | 19 ++++++++++--------- .../DataFixtures/ORM/LoadPeople.php | 11 ++++++----- .../DataFixtures/ORM/LoadRelationships.php | 9 ++++++--- .../RelationshipApiControllerTest.php | 10 +++++++++- .../DataFixtures/ORM/LoadCustomField.php | 11 ++++++++--- .../DataFixtures/ORM/LoadReports.php | 9 +++++---- .../DataFixtures/ORM/LoadThirdParty.php | 3 ++- 16 files changed, 78 insertions(+), 41 deletions(-) create mode 100644 .changes/unreleased/DX-20260309-131710.yaml diff --git a/.changes/unreleased/DX-20260309-131710.yaml b/.changes/unreleased/DX-20260309-131710.yaml new file mode 100644 index 000000000..865c1fe6f --- /dev/null +++ b/.changes/unreleased/DX-20260309-131710.yaml @@ -0,0 +1,7 @@ +kind: DX +body: Add seeds in DataFixtures and in some tests to avoid random test failures +time: 2026-03-09T13:17:10.915852317+01:00 +custom: + Issue: "504" + MR: "970" + SchemaChange: No schema change diff --git a/config/packages/nelmio_alice.yaml b/config/packages/nelmio_alice.yaml index e82c32982..e6e912228 100644 --- a/config/packages/nelmio_alice.yaml +++ b/config/packages/nelmio_alice.yaml @@ -8,5 +8,6 @@ when@dev: &dev - 'file' - 'md5' - 'sha1' + seed: 1234567890 when@test: *dev diff --git a/src/Bundle/ChillActivityBundle/DataFixtures/ORM/LoadActivity.php b/src/Bundle/ChillActivityBundle/DataFixtures/ORM/LoadActivity.php index 80c1e4254..8736e78f5 100644 --- a/src/Bundle/ChillActivityBundle/DataFixtures/ORM/LoadActivity.php +++ b/src/Bundle/ChillActivityBundle/DataFixtures/ORM/LoadActivity.php @@ -33,6 +33,7 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface public function __construct(private readonly EntityManagerInterface $em) { + mt_srand(123456789); $this->faker = FakerFactory::create('fr_FR'); } @@ -48,7 +49,7 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface ->findAll(); foreach ($persons as $person) { - $activityNbr = random_int(0, 3); + $activityNbr = mt_rand(0, 3); for ($i = 0; $i < $activityNbr; ++$i) { $activity = $this->newRandomActivity($person); @@ -73,7 +74,7 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface // ->setAttendee($this->faker->boolean()) - for ($i = 0; random_int(0, 4) > $i; ++$i) { + for ($i = 0; mt_rand(0, 4) > $i; ++$i) { $reason = $this->getRandomActivityReason(); if (null !== $reason) { diff --git a/src/Bundle/ChillAsideActivityBundle/src/DataFixtures/ORM/LoadAsideActivity.php b/src/Bundle/ChillAsideActivityBundle/src/DataFixtures/ORM/LoadAsideActivity.php index a60398507..cb778ab1e 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/DataFixtures/ORM/LoadAsideActivity.php +++ b/src/Bundle/ChillAsideActivityBundle/src/DataFixtures/ORM/LoadAsideActivity.php @@ -21,7 +21,10 @@ use Doctrine\Persistence\ObjectManager; class LoadAsideActivity extends Fixture implements DependentFixtureInterface { - public function __construct(private readonly UserRepository $userRepository) {} + public function __construct(private readonly UserRepository $userRepository) + { + mt_srand(123456789); + } public function getDependencies(): array { @@ -47,7 +50,7 @@ class LoadAsideActivity extends Fixture implements DependentFixtureInterface $this->getReference('aside_activity_category_0', AsideActivityCategory::class) ) ->setDate((new \DateTimeImmutable('today')) - ->sub(new \DateInterval('P'.\random_int(1, 100).'D'))); + ->sub(new \DateInterval('P'.\mt_rand(1, 100).'D'))); $manager->persist($activity); } diff --git a/src/Bundle/ChillCustomFieldsBundle/DataFixtures/ORM/LoadOption.php b/src/Bundle/ChillCustomFieldsBundle/DataFixtures/ORM/LoadOption.php index 4da28baac..87cf46024 100644 --- a/src/Bundle/ChillCustomFieldsBundle/DataFixtures/ORM/LoadOption.php +++ b/src/Bundle/ChillCustomFieldsBundle/DataFixtures/ORM/LoadOption.php @@ -41,6 +41,7 @@ class LoadOption extends AbstractFixture implements OrderedFixtureInterface public function __construct() { + mt_srand(123456789); $this->fakerFr = \Faker\Factory::create('fr_FR'); $this->fakerEn = \Faker\Factory::create('en_EN'); $this->fakerNl = \Faker\Factory::create('nl_NL'); @@ -104,7 +105,7 @@ class LoadOption extends AbstractFixture implements OrderedFixtureInterface $manager->persist($parent); // Load children - $expected_nb_children = random_int(10, 50); + $expected_nb_children = mt_rand(10, 50); for ($i = 0; $i < $expected_nb_children; ++$i) { $companyName = $this->fakerFr->company; @@ -144,7 +145,7 @@ class LoadOption extends AbstractFixture implements OrderedFixtureInterface $manager->persist($parent); // Load children - $expected_nb_children = random_int(10, 50); + $expected_nb_children = mt_rand(10, 50); for ($i = 0; $i < $expected_nb_children; ++$i) { $manager->persist($this->createChildOption($parent, [ diff --git a/src/Bundle/ChillEventBundle/DataFixtures/ORM/LoadParticipation.php b/src/Bundle/ChillEventBundle/DataFixtures/ORM/LoadParticipation.php index 94167f6b8..c5d7e37fb 100644 --- a/src/Bundle/ChillEventBundle/DataFixtures/ORM/LoadParticipation.php +++ b/src/Bundle/ChillEventBundle/DataFixtures/ORM/LoadParticipation.php @@ -34,6 +34,7 @@ class LoadParticipation extends AbstractFixture implements OrderedFixtureInterfa public function __construct() { + mt_srand(123456789); $this->faker = \Faker\Factory::create('fr_FR'); } @@ -45,7 +46,7 @@ class LoadParticipation extends AbstractFixture implements OrderedFixtureInterfa for ($i = 0; $i < $expectedNumber; ++$i) { $event = (new Event()) ->setDate($this->faker->dateTimeBetween('-2 years', '+6 months')) - ->setName($this->faker->words(random_int(2, 4), true)) + ->setName($this->faker->words(mt_rand(2, 4), true)) ->setType($this->getReference(LoadEventTypes::$refs[array_rand(LoadEventTypes::$refs)], EventType::class)) ->setCenter($center) ->setCircle( @@ -78,7 +79,7 @@ class LoadParticipation extends AbstractFixture implements OrderedFixtureInterfa /** @var Person $person */ foreach ($people as $person) { - $nb = random_int(0, 3); + $nb = mt_rand(0, 3); for ($i = 0; $i < $nb; ++$i) { $event = $events[array_rand($events)]; diff --git a/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadAddressReferences.php b/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadAddressReferences.php index 1c71e17cb..55e52b65e 100644 --- a/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadAddressReferences.php +++ b/src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadAddressReferences.php @@ -31,6 +31,7 @@ class LoadAddressReferences extends AbstractFixture implements ContainerAwareInt public function __construct() { + mt_srand(123456789); $this->faker = \Faker\Factory::create('fr_FR'); } @@ -67,7 +68,7 @@ class LoadAddressReferences extends AbstractFixture implements ContainerAwareInt $ar->setRefId($this->faker->numerify('ref-id-######')); $ar->setStreet($this->faker->streetName); - $ar->setStreetNumber((string) random_int(0, 199)); + $ar->setStreetNumber((string) mt_rand(0, 199)); $ar->setPoint($this->getRandomPoint()); $ar->setPostcode($this->getReference( LoadPostalCodes::$refs[array_rand(LoadPostalCodes::$refs)], @@ -88,8 +89,8 @@ class LoadAddressReferences extends AbstractFixture implements ContainerAwareInt { $lonBrussels = 4.35243; $latBrussels = 50.84676; - $lon = $lonBrussels + 0.01 * random_int(-5, 5); - $lat = $latBrussels + 0.01 * random_int(-5, 5); + $lon = $lonBrussels + 0.01 * mt_rand(-5, 5); + $lat = $latBrussels + 0.01 * mt_rand(-5, 5); return Point::fromLonLat($lon, $lat); } diff --git a/src/Bundle/ChillPersonBundle/DataFixtures/Helper/RandomPersonHelperTrait.php b/src/Bundle/ChillPersonBundle/DataFixtures/Helper/RandomPersonHelperTrait.php index 942f6cc5a..dfe6b37ad 100644 --- a/src/Bundle/ChillPersonBundle/DataFixtures/Helper/RandomPersonHelperTrait.php +++ b/src/Bundle/ChillPersonBundle/DataFixtures/Helper/RandomPersonHelperTrait.php @@ -34,7 +34,7 @@ trait RandomPersonHelperTrait return $qb ->select('p') ->setMaxResults(1) - ->setFirstResult(\random_int(0, $this->nbOfPersons)) + ->setFirstResult(\mt_rand(0, $this->nbOfPersons)) ->getQuery() ->getSingleResult(); } diff --git a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadCustomFields.php b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadCustomFields.php index 5bff01f20..058c80c39 100644 --- a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadCustomFields.php +++ b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadCustomFields.php @@ -37,7 +37,9 @@ class LoadCustomFields extends AbstractFixture implements OrderedFixtureInterfac private readonly EntityManagerInterface $entityManager, private readonly CustomFieldChoice $customFieldChoice, private readonly CustomFieldText $customFieldText, - ) {} + ) { + mt_srand(123456789); + } // put your code here public function getOrder(): int @@ -78,12 +80,12 @@ class LoadCustomFields extends AbstractFixture implements OrderedFixtureInterfac // select a set of people and add data foreach ($personIds as $id) { // add info on 1 person on 2 - if (1 === random_int(0, 1)) { + if (1 === mt_rand(0, 1)) { /** @var Person $person */ $person = $manager->getRepository(Person::class)->find($id); $person->setCFData([ 'remarques' => $this->createCustomFieldText() - ->serialize($faker->text(random_int(150, 250)), $this->cfText), + ->serialize($faker->text(mt_rand(150, 250)), $this->cfText), 'document-d-identite' => $this->createCustomFieldChoice() ->serialize([$choices[array_rand($choices)]], $this->cfChoice), ]); diff --git a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadHousehold.php b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadHousehold.php index fdefa5dc8..bedf29002 100644 --- a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadHousehold.php +++ b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadHousehold.php @@ -36,6 +36,7 @@ class LoadHousehold extends Fixture implements DependentFixtureInterface public function __construct(private readonly MembersEditorFactory $editorFactory, private readonly EntityManagerInterface $em) { + mt_srand(123456789); $this->loader = new NativeLoader(); } @@ -72,12 +73,12 @@ class LoadHousehold extends Fixture implements DependentFixtureInterface private function addAddressToHousehold(Household $household, \DateTimeImmutable $date, ObjectManager $manager) { - if (\random_int(0, 10) > 8) { + if (\mt_rand(0, 10) > 8) { // 20% of household without address return; } - $nb = \random_int(1, 6); + $nb = \mt_rand(1, 6); $i = 0; @@ -85,15 +86,15 @@ class LoadHousehold extends Fixture implements DependentFixtureInterface $address = $this->createAddress(); $address->setValidFrom(\DateTime::createFromImmutable($date)); - if (\random_int(0, 20) < 1) { - $date = $date->add(new \DateInterval('P'.\random_int(8, 52).'W')); + if (\mt_rand(0, 20) < 1) { + $date = $date->add(new \DateInterval('P'.\mt_rand(8, 52).'W')); $address->setValidTo(\DateTime::createFromImmutable($date)); } $household->addAddress($address); $manager->persist($address); - $date = $date->add(new \DateInterval('P'.\random_int(8, 52).'W')); + $date = $date->add(new \DateInterval('P'.\mt_rand(8, 52).'W')); ++$i; } } @@ -127,7 +128,7 @@ class LoadHousehold extends Fixture implements DependentFixtureInterface $k = 0; foreach ($this->getRandomPersons(1, 3) as $person) { - $date = $startDate->add(new \DateInterval('P'.\random_int(1, 200).'W')); + $date = $startDate->add(new \DateInterval('P'.\mt_rand(1, 200).'W')); $position = $this->getReference(LoadHouseholdPosition::ADULT, Position::class); $movement->addMovement($date, $person, $position, 0 === $k, 'self generated'); @@ -136,7 +137,7 @@ class LoadHousehold extends Fixture implements DependentFixtureInterface // load children foreach ($this->getRandomPersons(0, 3) as $person) { - $date = $startDate->add(new \DateInterval('P'.\random_int(1, 200).'W')); + $date = $startDate->add(new \DateInterval('P'.\mt_rand(1, 200).'W')); $position = $this->getReference(LoadHouseholdPosition::CHILD, Position::class); $movement->addMovement($date, $person, $position, 0 === $k, 'self generated'); @@ -145,7 +146,7 @@ class LoadHousehold extends Fixture implements DependentFixtureInterface // load children out foreach ($this->getRandomPersons(0, 2) as $person) { - $date = $startDate->add(new \DateInterval('P'.\random_int(1, 200).'W')); + $date = $startDate->add(new \DateInterval('P'.\mt_rand(1, 200).'W')); $position = $this->getReference(LoadHouseholdPosition::CHILD_OUT, Position::class); $movement->addMovement($date, $person, $position, 0 === $k, 'self generated'); @@ -169,7 +170,7 @@ class LoadHousehold extends Fixture implements DependentFixtureInterface { $persons = []; - $nb = \random_int($min, $max); + $nb = \mt_rand($min, $max); for ($i = 0; $i < $nb; ++$i) { $personId = \array_pop($this->personIds)['id']; diff --git a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadPeople.php b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadPeople.php index 753094ec1..b048ba3d4 100644 --- a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadPeople.php +++ b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadPeople.php @@ -240,6 +240,7 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord protected UserRepository $userRepository, protected GenderRepository $genderRepository, ) { + mt_srand(123456789); $this->faker = Factory::create('fr_FR'); $this->faker->addProvider($this); $this->loader = new NativeLoader($this->faker); @@ -273,7 +274,7 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord $this->cacheCountries = $this->countryRepository->findAll(); } - if (\random_int(0, 100) > $nullPercentage) { + if (\mt_rand(0, 100) > $nullPercentage) { return null; } @@ -289,7 +290,7 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord $this->cacheGenders = $this->genderRepository->findByActiveOrdered(); } - if (\random_int(0, 100) > $nullPercentage) { + if (\mt_rand(0, 100) > $nullPercentage) { return null; } @@ -307,7 +308,7 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord $this->cacheMaritalStatuses = $this->maritalStatusRepository->findAll(); } - if (\random_int(0, 100) > $nullPercentage) { + if (\mt_rand(0, 100) > $nullPercentage) { return null; } @@ -352,7 +353,7 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord $accompanyingPeriod = new AccompanyingPeriod( (new \DateTime()) ->sub( - new \DateInterval('P'.\random_int(0, 180).'D') + new \DateInterval('P'.\mt_rand(0, 180).'D') ) ); $accompanyingPeriod->setCreatedBy($this->getRandomUser()) @@ -360,7 +361,7 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord $person->addAccompanyingPeriod($accompanyingPeriod); $accompanyingPeriod->addSocialIssue($this->getRandomSocialIssue()); - if (\random_int(0, 10) > 3) { + if (\mt_rand(0, 10) > 3) { // always add social scope: $accompanyingPeriod->addScope($this->getReference('scope_social', Scope::class)); $origin = $this->getReference(LoadAccompanyingPeriodOrigin::ACCOMPANYING_PERIOD_ORIGIN, AccompanyingPeriod\Origin::class); diff --git a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadRelationships.php b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadRelationships.php index dcbb3a4c3..957074014 100644 --- a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadRelationships.php +++ b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadRelationships.php @@ -25,7 +25,10 @@ class LoadRelationships extends Fixture implements DependentFixtureInterface { use PersonRandomHelper; - public function __construct(private readonly EntityManagerInterface $em) {} + public function __construct(private readonly EntityManagerInterface $em) + { + mt_srand(123456789); + } public function getDependencies(): array { @@ -47,8 +50,8 @@ class LoadRelationships extends Fixture implements DependentFixtureInterface ->setFromPerson($this->getRandomPerson($this->em)) ->setToPerson($this->getRandomPerson($this->em)) ->setRelation($this->getReference(LoadRelations::RELATION_KEY. - random_int(0, \count(LoadRelations::RELATIONS) - 1), Relation::class)) - ->setReverse((bool) random_int(0, 1)) + mt_rand(0, \count(LoadRelations::RELATIONS) - 1), Relation::class)) + ->setReverse((bool) mt_rand(0, 1)) ->setCreatedBy($user) ->setUpdatedBy($user) ->setCreatedAt($date) diff --git a/src/Bundle/ChillPersonBundle/Tests/Controller/RelationshipApiControllerTest.php b/src/Bundle/ChillPersonBundle/Tests/Controller/RelationshipApiControllerTest.php index 65f2e60a6..ded2e4b1b 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Controller/RelationshipApiControllerTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Controller/RelationshipApiControllerTest.php @@ -55,6 +55,9 @@ final class RelationshipApiControllerTest extends WebTestCase public static function personProvider(): array { + // fix a seed to avoid random errors + mt_srand(1234588755); + self::bootKernel(); $em = self::getContainer()->get(EntityManagerInterface::class); $personIdHavingRelation = $em->createQueryBuilder() @@ -116,6 +119,9 @@ final class RelationshipApiControllerTest extends WebTestCase public static function relationProvider(): array { + // fix a seed to avoid random errors + mt_srand(1234588755); + self::bootKernel(); $em = self::getContainer()->get(EntityManagerInterface::class); $personIdWithoutRelations = $em->createQueryBuilder() @@ -144,6 +150,8 @@ final class RelationshipApiControllerTest extends WebTestCase ->findAll(); } - return self::$relations[\array_rand(self::$relations)]; + $keys = array_keys(self::$relations); + + return self::$relations[mt_rand(0, \count($keys) - 1)]; } } diff --git a/src/Bundle/ChillReportBundle/DataFixtures/ORM/LoadCustomField.php b/src/Bundle/ChillReportBundle/DataFixtures/ORM/LoadCustomField.php index 707133890..c346f2bff 100644 --- a/src/Bundle/ChillReportBundle/DataFixtures/ORM/LoadCustomField.php +++ b/src/Bundle/ChillReportBundle/DataFixtures/ORM/LoadCustomField.php @@ -22,6 +22,11 @@ use Doctrine\Persistence\ObjectManager; */ class LoadCustomField extends AbstractFixture implements OrderedFixtureInterface { + public function __construct() + { + mt_srand(123456789); + } + public function getOrder(): int { return 15001; @@ -67,15 +72,15 @@ class LoadCustomField extends AbstractFixture implements OrderedFixtureInterface ]; for ($i = 0; 25 >= $i; ++$i) { - $cFType = $cFTypes[random_int(0, \count($cFTypes) - 1)]; + $cFType = $cFTypes[mt_rand(0, \count($cFTypes) - 1)]; $customField = (new CustomField()) ->setSlug("cf_report_{$i}") ->setType($cFType['type']) ->setOptions($cFType['options']) ->setName(['fr' => "CustomField {$i}"]) - ->setOrdering(random_int(0, 1000) / 1000) - ->setCustomFieldsGroup($this->getReference('cf_group_report_'.random_int(0, 3), CustomFieldsGroup::class)); + ->setOrdering(mt_rand(0, 1000) / 1000) + ->setCustomFieldsGroup($this->getReference('cf_group_report_'.mt_rand(0, 3), CustomFieldsGroup::class)); $manager->persist($customField); } diff --git a/src/Bundle/ChillReportBundle/DataFixtures/ORM/LoadReports.php b/src/Bundle/ChillReportBundle/DataFixtures/ORM/LoadReports.php index 3519ef4cd..ef966805e 100644 --- a/src/Bundle/ChillReportBundle/DataFixtures/ORM/LoadReports.php +++ b/src/Bundle/ChillReportBundle/DataFixtures/ORM/LoadReports.php @@ -35,6 +35,7 @@ final class LoadReports extends AbstractFixture implements OrderedFixtureInterfa public function __construct( private readonly EntityManagerInterface $entityManager, ) { + mt_srand(123456789); $this->faker = FakerFactory::create('fr_FR'); } @@ -83,7 +84,7 @@ final class LoadReports extends AbstractFixture implements OrderedFixtureInterfa $report = (new Report()) ->setPerson($person) ->setCFGroup( - random_int(0, 10) > 5 ? + mt_rand(0, 10) > 5 ? $this->getReference('cf_group_report_logement', CustomFieldsGroup::class) : $this->getReference('cf_group_report_education', CustomFieldsGroup::class) ) @@ -106,7 +107,7 @@ final class LoadReports extends AbstractFixture implements OrderedFixtureInterfa // set date. 30% of the dates are 2015-05-01 $expectedDate = new \DateTime('2015-01-05'); - if (random_int(0, 100) < 30) { + if (mt_rand(0, 100) < 30) { $report->setDate($expectedDate); } else { $report->setDate($this->faker->dateTimeBetween('-1 year', 'now') @@ -150,7 +151,7 @@ final class LoadReports extends AbstractFixture implements OrderedFixtureInterfa $selectedPeople = []; foreach ($people as $person) { - if (random_int(0, 100) < $percentage) { + if (mt_rand(0, 100) < $percentage) { $selectedPeople[] = $person; } } @@ -178,7 +179,7 @@ final class LoadReports extends AbstractFixture implements OrderedFixtureInterfa $picked = []; if ($multiple) { - $numberSelected = random_int(1, \count($choices) - 1); + $numberSelected = mt_rand(1, \count($choices) - 1); for ($i = 0; $i < $numberSelected; ++$i) { $picked[] = $this->pickChoice($choices); diff --git a/src/Bundle/ChillThirdPartyBundle/DataFixtures/ORM/LoadThirdParty.php b/src/Bundle/ChillThirdPartyBundle/DataFixtures/ORM/LoadThirdParty.php index dc4b5472b..8ed008ddf 100644 --- a/src/Bundle/ChillThirdPartyBundle/DataFixtures/ORM/LoadThirdParty.php +++ b/src/Bundle/ChillThirdPartyBundle/DataFixtures/ORM/LoadThirdParty.php @@ -30,6 +30,7 @@ class LoadThirdParty extends Fixture implements DependentFixtureInterface public function __construct() { + mt_srand(123456789); $this->phoneNumberUtil = PhoneNumberUtil::getInstance(); } @@ -68,7 +69,7 @@ class LoadThirdParty extends Fixture implements DependentFixtureInterface static fn ($a) => $a['ref'], LoadCenters::$centers ); - $number = random_int(1, \count($references)); + $number = mt_rand(1, \count($references)); if (1 === $number) { yield $this->getReference($references[array_rand($references)], Center::class); From dd429ca02ac2cb2b31d7c4778af766081d3d3378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 16 Mar 2026 14:08:35 +0000 Subject: [PATCH 16/22] Resolve "Notification aux groupes utilisateurs" --- .../ChillMainBundle/Entity/Notification.php | 8 +- src/Bundle/ChillMainBundle/Entity/User.php | 5 + .../ChillMainBundle/Entity/UserGroup.php | 15 ++ .../SendImmediateNotificationEmailHandler.php | 22 +- .../SendImmediateNotificationEmailMessage.php | 35 ++- .../Notification/Email/NotificationMailer.php | 32 +-- .../Repository/NotificationRepository.php | 2 +- .../Repository/UserGroupRepository.php | 2 +- ...il_non_system_notification_content.md.twig | 4 + ...l_non_system_notification_content.txt.twig | 4 + ...dImmediateNotificationEmailHandlerTest.php | 187 +++++++++++++++ .../Email/NotificationMailTwigContentTest.php | 71 ++++++ .../Email/NotificationMailerTest.php | 223 ++++++++++++++++-- 13 files changed, 562 insertions(+), 48 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Tests/Notification/Email/NotificationEmailHandler/SendImmediateNotificationEmailHandlerTest.php create mode 100644 src/Bundle/ChillMainBundle/Tests/Notification/Email/NotificationMailTwigContentTest.php diff --git a/src/Bundle/ChillMainBundle/Entity/Notification.php b/src/Bundle/ChillMainBundle/Entity/Notification.php index 20773b884..8075ac638 100644 --- a/src/Bundle/ChillMainBundle/Entity/Notification.php +++ b/src/Bundle/ChillMainBundle/Entity/Notification.php @@ -215,17 +215,21 @@ class Notification implements TrackUpdateInterface return $this->addressees; } + /** + * @return list + */ public function getAllAddressees(): array { $allUsers = []; foreach ($this->getAddressees() as $user) { - $allUsers[$user->getId()] = $user; + $allUsers['u_'.$user->getId()] = $user; } foreach ($this->getAddresseeUserGroups() as $userGroup) { + $allUsers['ug_'.$userGroup->getId()] = $userGroup; foreach ($userGroup->getUsers() as $user) { - $allUsers[$user->getId()] = $user; + $allUsers['u_'.$user->getId()] = $user; } } diff --git a/src/Bundle/ChillMainBundle/Entity/User.php b/src/Bundle/ChillMainBundle/Entity/User.php index b5539aa83..a273fefd7 100644 --- a/src/Bundle/ChillMainBundle/Entity/User.php +++ b/src/Bundle/ChillMainBundle/Entity/User.php @@ -658,6 +658,11 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter return true; } + public function isUserGroup(): bool + { + return false; + } + private function getNotificationFlagData(string $flag): array { return $this->notificationFlags[$flag] ?? [self::NOTIF_FLAG_IMMEDIATE_EMAIL]; diff --git a/src/Bundle/ChillMainBundle/Entity/UserGroup.php b/src/Bundle/ChillMainBundle/Entity/UserGroup.php index 39df04b31..f6586d4c9 100644 --- a/src/Bundle/ChillMainBundle/Entity/UserGroup.php +++ b/src/Bundle/ChillMainBundle/Entity/UserGroup.php @@ -256,6 +256,21 @@ class UserGroup return true; } + public function isUser(): bool + { + return false; + } + + /** + * Return a locale for the userGroup. + * + * Currently hardcoded, should be replaced by a property. + */ + public function getLocale(): string + { + return 'fr'; + } + public function contains(User $user): bool { return $this->users->contains($user); diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/SendImmediateNotificationEmailHandler.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/SendImmediateNotificationEmailHandler.php index b27f16423..26648b5a5 100644 --- a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/SendImmediateNotificationEmailHandler.php +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailHandlers/SendImmediateNotificationEmailHandler.php @@ -14,7 +14,8 @@ namespace Chill\MainBundle\Notification\Email\NotificationEmailHandlers; use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage; use Chill\MainBundle\Notification\Email\NotificationMailer; use Chill\MainBundle\Repository\NotificationRepository; -use Chill\MainBundle\Repository\UserRepository; +use Chill\MainBundle\Repository\UserGroupRepository; +use Chill\MainBundle\Repository\UserRepositoryInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; @@ -24,7 +25,8 @@ readonly class SendImmediateNotificationEmailHandler { public function __construct( private NotificationRepository $notificationRepository, - private UserRepository $userRepository, + private UserRepositoryInterface $userRepository, + private UserGroupRepository $userGroupRepository, private NotificationMailer $notificationMailer, private LoggerInterface $logger, ) {} @@ -36,7 +38,13 @@ readonly class SendImmediateNotificationEmailHandler public function __invoke(SendImmediateNotificationEmailMessage $message): void { $notification = $this->notificationRepository->find($message->getNotificationId()); - $addressee = $this->userRepository->find($message->getAddresseeId()); + if (null !== $message->getUserId()) { + $addressee = $this->userRepository->find($message->getUserId()); + } elseif (null !== $message->getUserGroupId()) { + $addressee = $this->userGroupRepository->find($message->getUserGroupId()); + } else { + throw new \InvalidArgumentException('Addressee not found: nor an user nor a user group'); + } if (null === $notification) { $this->logger->error('[SendImmediateNotificationEmailHandler] Notification not found', [ @@ -48,10 +56,11 @@ readonly class SendImmediateNotificationEmailHandler if (null === $addressee) { $this->logger->error('[SendImmediateNotificationEmailHandler] Addressee not found', [ - 'addressee_id' => $message->getAddresseeId(), + 'user_id' => $message->getUserId(), + 'user_group_id' => $message->getUserGroupId(), ]); - throw new \InvalidArgumentException(sprintf('User with ID %s not found', $message->getAddresseeId())); + throw new \InvalidArgumentException(sprintf('User with ID %s or user group with id %s not found', $message->getUserId(), $message->getUserGroupId())); } try { @@ -59,7 +68,8 @@ readonly class SendImmediateNotificationEmailHandler } catch (\Exception $e) { $this->logger->error('[SendImmediateNotificationEmailHandler] Failed to send email', [ 'notification_id' => $message->getNotificationId(), - 'addressee_id' => $message->getAddresseeId(), + 'user_id' => $message->getUserId(), + 'user_group_id' => $message->getUserGroupId(), 'stacktrace' => $e->getTraceAsString(), ]); throw $e; diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/SendImmediateNotificationEmailMessage.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/SendImmediateNotificationEmailMessage.php index fb9908b21..82bc84ec1 100644 --- a/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/SendImmediateNotificationEmailMessage.php +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationEmailMessages/SendImmediateNotificationEmailMessage.php @@ -11,20 +11,45 @@ declare(strict_types=1); namespace Chill\MainBundle\Notification\Email\NotificationEmailMessages; +use Chill\MainBundle\Entity\Notification; +use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\UserGroup; + readonly class SendImmediateNotificationEmailMessage { + private int $notificationId; + + private ?int $userId; + + private ?int $userGroupId; + public function __construct( - private int $notificationId, - private int $addresseeId, - ) {} + Notification $notification, + UserGroup|User $addressee, + ) { + $this->notificationId = $notification->getId(); + + if ($addressee instanceof User) { + $this->userId = $addressee->getId(); + $this->userGroupId = null; + } else { + $this->userGroupId = $addressee->getId(); + $this->userId = null; + } + } public function getNotificationId(): int { return $this->notificationId; } - public function getAddresseeId(): int + public function getUserId(): ?int { - return $this->addresseeId; + return $this->userId; + } + + public function getUserGroupId(): ?int + { + return $this->userGroupId; } } diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php index 237cb178b..c1eb03c49 100644 --- a/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php @@ -14,6 +14,7 @@ namespace Chill\MainBundle\Notification\Email; use Chill\MainBundle\Entity\Notification; use Chill\MainBundle\Entity\NotificationComment; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\UserGroup; use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage; use Doctrine\ORM\Event\PostPersistEventArgs; use Psr\Log\LoggerInterface; @@ -26,13 +27,13 @@ use Symfony\Contracts\Translation\TranslatorInterface; // use Symfony\Component\Translation\LocaleSwitcher; -readonly class NotificationMailer +class NotificationMailer { public function __construct( - private MailerInterface $mailer, - private LoggerInterface $logger, - private MessageBusInterface $messageBus, - private TranslatorInterface $translator, + private readonly MailerInterface $mailer, + private readonly LoggerInterface $logger, + private readonly MessageBusInterface $messageBus, + private readonly TranslatorInterface $translator, // private LocaleSwitcher $localeSwitcher, ) {} @@ -100,25 +101,24 @@ readonly class NotificationMailer if (null === $addressee->getEmail()) { continue; } - $this->processNotificationForAddressee($notification, $addressee); } } - private function processNotificationForAddressee(Notification $notification, User $addressee): void + private function processNotificationForAddressee(Notification $notification, User|UserGroup $addressee): void { $notificationType = $notification->getType(); - if ($addressee->isNotificationSendImmediately($notificationType)) { + if ($addressee instanceof UserGroup || $addressee->isNotificationSendImmediately($notificationType)) { $this->scheduleImmediateEmail($notification, $addressee); } } - private function scheduleImmediateEmail(Notification $notification, User $addressee): void + private function scheduleImmediateEmail(Notification $notification, User|UserGroup $addressee): void { $message = new SendImmediateNotificationEmailMessage( - $notification->getId(), - $addressee->getId() + $notification, + $addressee, ); $this->messageBus->dispatch($message); @@ -130,13 +130,17 @@ readonly class NotificationMailer } /** - * This method sends the email but is now called by the immediate notification email message handler. + * Send an email about a Notification. + * + * It is called by immediate notification email message handler: + * + * @see{\Chill\MainBundle\Notification\Email\NotificationEmailHandlers\SendImmediateNotificationEmailHandler} * * @throws TransportExceptionInterface */ - public function sendEmailToAddressee(Notification $notification, User $addressee): void + public function sendEmailToAddressee(Notification $notification, User|UserGroup $addressee): void { - if (null === $addressee->getEmail()) { + if (null === $addressee->getEmail() || '' === $addressee->getEmail()) { return; } diff --git a/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php b/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php index 99fb57094..3e98c7496 100644 --- a/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php @@ -23,7 +23,7 @@ use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; -final class NotificationRepository implements ObjectRepository +class NotificationRepository implements ObjectRepository { private ?Statement $notificationByRelatedEntityAndUserAssociatedStatement = null; diff --git a/src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php b/src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php index 71266f8e5..c7f9c43cc 100644 --- a/src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php @@ -18,7 +18,7 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Symfony\Contracts\Translation\LocaleAwareInterface; -final class UserGroupRepository implements UserGroupRepositoryInterface, LocaleAwareInterface +class UserGroupRepository implements UserGroupRepositoryInterface, LocaleAwareInterface { private readonly EntityRepository $repository; diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.md.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.md.twig index 5c07e2ef7..854547c92 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.md.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.md.twig @@ -1,5 +1,9 @@ {% apply markdown_to_html %} +{% if dest.isUser %} {{ dest.label }}, +{% else %} +{{ dest.label|localize_translatable_string }}, +{% endif %} {{ notification.sender.label }} a créé une notification pour vous: diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.txt.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.txt.twig index 58f138322..b14f77ef7 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.txt.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.txt.twig @@ -1,4 +1,8 @@ +{% if dest.isUser %} {{ dest.label }}, +{% else %} +{{ dest.label|localize_translatable_string }}, +{% endif %} {{ notification.sender.label }} a créé une notification pour vous: diff --git a/src/Bundle/ChillMainBundle/Tests/Notification/Email/NotificationEmailHandler/SendImmediateNotificationEmailHandlerTest.php b/src/Bundle/ChillMainBundle/Tests/Notification/Email/NotificationEmailHandler/SendImmediateNotificationEmailHandlerTest.php new file mode 100644 index 000000000..da1621390 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Notification/Email/NotificationEmailHandler/SendImmediateNotificationEmailHandlerTest.php @@ -0,0 +1,187 @@ +notificationRepository = $this->prophesize(NotificationRepository::class); + $this->userRepository = $this->prophesize(UserRepositoryInterface::class); + $this->userGroupRepository = $this->prophesize(UserGroupRepository::class); + $this->notificationMailer = $this->prophesize(NotificationMailer::class); + + $this->handler = new SendImmediateNotificationEmailHandler( + $this->notificationRepository->reveal(), + $this->userRepository->reveal(), + $this->userGroupRepository->reveal(), + $this->notificationMailer->reveal(), + new NullLogger() + ); + } + + public function testInvokeWithUserAddressee(): void + { + $notificationId = 123; + $userId = 456; + + $notification = $this->prophesize(Notification::class); + $notification->getId()->willReturn($notificationId); + $user = $this->prophesize(User::class); + $user->getId()->willReturn($userId); + + $message = new SendImmediateNotificationEmailMessage($notification->reveal(), $user->reveal()); + + $this->notificationRepository->find($notificationId)->willReturn($notification->reveal()); + $this->userRepository->find($userId)->willReturn($user->reveal()); + + $this->notificationMailer->sendEmailToAddressee($notification->reveal(), $user->reveal()) + ->shouldBeCalledOnce(); + + ($this->handler)($message); + } + + public function testInvokeWithUserGroupAddressee(): void + { + $notificationId = 123; + $userGroupId = 789; + + $notification = $this->prophesize(Notification::class); + $notification->getId()->willReturn($notificationId); + $userGroup = $this->prophesize(UserGroup::class); + $userGroup->getId()->willReturn($userGroupId); + + $message = new SendImmediateNotificationEmailMessage($notification->reveal(), $userGroup->reveal()); + + $this->notificationRepository->find($notificationId)->willReturn($notification->reveal()); + $this->userGroupRepository->find($userGroupId)->willReturn($userGroup->reveal()); + + $this->notificationMailer->sendEmailToAddressee($notification->reveal(), $userGroup->reveal()) + ->shouldBeCalledOnce(); + + ($this->handler)($message); + } + + public function testInvokeThrowsExceptionWhenNotificationNotFound(): void + { + $notificationId = 123; + $userId = 456; + + $notification = $this->prophesize(Notification::class); + $notification->getId()->willReturn($notificationId); + $user = $this->prophesize(User::class); + $user->getId()->willReturn($userId); + + $message = new SendImmediateNotificationEmailMessage($notification->reveal(), $user->reveal()); + + $this->notificationRepository->find($notificationId)->willReturn(null); + $this->userRepository->find($userId)->willReturn($user->reveal()); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf('Notification with ID %s not found', $notificationId)); + + ($this->handler)($message); + } + + public function testInvokeThrowsExceptionWhenUserNotFound(): void + { + $notificationId = 123; + $userId = 456; + + $notification = $this->prophesize(Notification::class); + $notification->getId()->willReturn($notificationId); + $user = $this->prophesize(User::class); + $user->getId()->willReturn($userId); + + $message = new SendImmediateNotificationEmailMessage($notification->reveal(), $user->reveal()); + + $this->notificationRepository->find($notificationId)->willReturn($notification->reveal()); + $this->userRepository->find($userId)->willReturn(null); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf('User with ID %s or user group with id %s not found', $userId, '')); + + ($this->handler)($message); + } + + public function testInvokeThrowsExceptionWhenUserGroupNotFound(): void + { + $notificationId = 123; + $userGroupId = 789; + + $notification = $this->prophesize(Notification::class); + $notification->getId()->willReturn($notificationId); + $userGroup = $this->prophesize(UserGroup::class); + $userGroup->getId()->willReturn($userGroupId); + + $message = new SendImmediateNotificationEmailMessage($notification->reveal(), $userGroup->reveal()); + + $this->notificationRepository->find($notificationId)->willReturn($notification->reveal()); + $this->userGroupRepository->find($userGroupId)->willReturn(null); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf('User with ID %s or user group with id %s not found', '', $userGroupId)); + + ($this->handler)($message); + } + + public function testInvokeRethrowsExceptionWhenMailerFails(): void + { + $notificationId = 123; + $userId = 456; + + $notification = $this->prophesize(Notification::class); + $notification->getId()->willReturn($notificationId); + $user = $this->prophesize(User::class); + $user->getId()->willReturn($userId); + + $message = new SendImmediateNotificationEmailMessage($notification->reveal(), $user->reveal()); + + $this->notificationRepository->find($notificationId)->willReturn($notification->reveal()); + $this->userRepository->find($userId)->willReturn($user->reveal()); + + $exception = new \Exception('Mailer error'); + $this->notificationMailer->sendEmailToAddressee($notification->reveal(), $user->reveal()) + ->willThrow($exception); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Mailer error'); + + ($this->handler)($message); + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Notification/Email/NotificationMailTwigContentTest.php b/src/Bundle/ChillMainBundle/Tests/Notification/Email/NotificationMailTwigContentTest.php new file mode 100644 index 000000000..23ae05261 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Notification/Email/NotificationMailTwigContentTest.php @@ -0,0 +1,71 @@ +twig = $this->getContainer()->get('twig'); + } + + /** + * @dataProvider provideContent + */ + public function testContent(string $template, array $args): void + { + $actual = $this->twig->render($template, $args); + + self::assertIsString($actual); + } + + public static function provideContent(): iterable + { + $notification = new Notification(); + $notification->setMessage('test message'); + $notification->setSender(new User()); + + $class = new \ReflectionClass($notification); + $method = $class->getProperty('id'); + $method->setValue($notification, 1); + + $txt = '@ChillMain/Notification/email_non_system_notification_content.txt.twig'; + $md = '@ChillMain/Notification/email_non_system_notification_content.md.twig'; + + $user = new User(); + $user->setLocale('fr'); + $user->setLabel('test'); + + $userGroup = new UserGroup(); + $userGroup->setLabel(['fr' => 'test user group']); + + foreach ([$md, $txt] as $template) { + yield 'test with a user for '.$template => [$template, ['notification' => $notification, 'dest' => $user]]; + yield 'test with a group for '.$template => [$template, ['notification' => $notification, 'dest' => $userGroup]]; + } + + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Notification/Email/NotificationMailerTest.php b/src/Bundle/ChillMainBundle/Tests/Notification/Email/NotificationMailerTest.php index 173cb8a0e..a13018a7f 100644 --- a/src/Bundle/ChillMainBundle/Tests/Notification/Email/NotificationMailerTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Notification/Email/NotificationMailerTest.php @@ -9,11 +9,12 @@ declare(strict_types=1); * the LICENSE file that was distributed with this source code. */ -namespace Notification\Email; +namespace Chill\MainBundle\Tests\Notification\Email; use Chill\MainBundle\Entity\Notification; use Chill\MainBundle\Entity\NotificationComment; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\UserGroup; use Chill\MainBundle\Notification\Email\NotificationMailer; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\PostPersistEventArgs; @@ -64,13 +65,22 @@ class NotificationMailerTest extends TestCase // a mail only to user1 and user3 should have been sent $mailer->send(Argument::that(function (Email $email) { foreach ($email->getTo() as $address) { - if ('user1@foo.com' === $address->getAddress() || 'user3@foo.com' === $address->getAddress()) { + if ('user1@foo.com' === $address->getAddress()) { return true; } } return false; - }))->shouldBeCalledTimes(2); + }))->shouldBeCalledTimes(1); + $mailer->send(Argument::that(function (Email $email) { + foreach ($email->getTo() as $address) { + if ('user3@foo.com' === $address->getAddress()) { + return true; + } + } + + return false; + }))->shouldBeCalledTimes(1); $objectManager = $this->prophesize(EntityManagerInterface::class); @@ -121,7 +131,83 @@ class NotificationMailerTest extends TestCase * @throws \ReflectionException * @throws Exception */ - public function testProcessNotificationForAddresseeWithImmediateEmailPreference(): void + public function testPostPersistNotificationToGroup(): void + { + // Create a real notification entity + $notification = new Notification(); + $notification->setType('test_notification_type'); + + // Use reflection to set the ID since it's normally generated by the database + $reflectionNotification = new \ReflectionClass(Notification::class); + $idProperty = $reflectionNotification->getProperty('id'); + $idProperty->setValue($notification, 123); + + // Create a real user entity + $user = new User(); + $user->setEmail('user@example.com'); + $userGroup = new UserGroup(); + $userGroup->addUser($user); + $notification->addAddressee($userGroup); + + // Use reflection to set the ID since it's normally generated by the database + $reflectionUser = new \ReflectionClass($user); + $idProperty = $reflectionUser->getProperty('id'); + $idProperty->setValue($user, 456); + + $reflectionUser = new \ReflectionClass($userGroup); + $idProperty = $reflectionUser->getProperty('id'); + $idProperty->setValue($userGroup, 789); + + // Set notification flags for the user + $user->setNotificationImmediately('test_notification_type', true); + + $messageBus = $this->prophesize(MessageBusInterface::class); + $messageBus->dispatch(Argument::that(fn (SendImmediateNotificationEmailMessage $message) => 123 === $message->getNotificationId() && 456 === $message->getUserId() && null === $message->getUserGroupId()))->willReturn(new Envelope(new \stdClass()))->shouldBeCalled(); + + $messageBus->dispatch(Argument::that(fn (SendImmediateNotificationEmailMessage $message) => 123 === $message->getNotificationId() && null === $message->getUserId() && 789 === $message->getUserGroupId()))->willReturn(new Envelope(new \stdClass()))->shouldBeCalled(); + + $notificationMailer = $this->buildNotificationMailer(null, $messageBus->reveal()); + + $notificationMailer->postPersistNotification($notification, new PostPersistEventArgs($notification, $this->prophesize(EntityManagerInterface::class)->reveal())); + } + + /** + * @throws \ReflectionException + * @throws Exception + */ + public function testPostPersistNotificationWithImmediateEmailPreference(): void + { + // Create a real notification entity + $notification = new Notification(); + $notification->setType('test_notification_type'); + + // Use reflection to set the ID since it's normally generated by the database + $reflectionNotification = new \ReflectionClass(Notification::class); + $idProperty = $reflectionNotification->getProperty('id'); + $idProperty->setValue($notification, 123); + + // Create a real user entity + $user = new User(); + $user->setEmail('user@example.com'); + $notification->addAddressee($user); + + // Use reflection to set the ID since it's normally generated by the database + $reflectionUser = new \ReflectionClass(User::class); + $idProperty = $reflectionUser->getProperty('id'); + $idProperty->setValue($user, 456); + + // Set notification flags for the user + $user->setNotificationImmediately('test_notification_type', true); + + $messageBus = $this->prophesize(MessageBusInterface::class); + $messageBus->dispatch(Argument::that(fn (SendImmediateNotificationEmailMessage $message) => 123 === $message->getNotificationId() && 456 === $message->getUserId() && null === $message->getUserGroupId()))->willReturn(new Envelope(new \stdClass()))->shouldBeCalled(); + + $notificationMailer = $this->buildNotificationMailer(null, $messageBus->reveal()); + + $notificationMailer->postPersistNotification($notification, new PostPersistEventArgs($notification, $this->prophesize(EntityManagerInterface::class)->reveal())); + } + + public function testPostPersistNotificationWithDailyDigestPreference(): void { // Create a real notification entity $notification = new Notification(); @@ -136,6 +222,11 @@ class NotificationMailerTest extends TestCase // Create a real user entity $user = new User(); $user->setEmail('user@example.com'); + // Set notification flags for the user + $user->setNotificationImmediately('test_notification_type', false); + $user->setNotificationDailyDigest('test_notification_type', true); + + $notification->addAddressee($user); // Use reflection to set the ID since it's normally generated by the database $reflectionUser = new \ReflectionClass(User::class); @@ -143,23 +234,15 @@ class NotificationMailerTest extends TestCase $idProperty->setAccessible(true); $idProperty->setValue($user, 456); - // Set notification flags for the user - $user->setNotificationImmediately('test_notification_type', true); + $messageBus = $this->prophesize(MessageBusInterface::class); + $messageBus->dispatch(Argument::that(fn (SendImmediateNotificationEmailMessage $message) => 123 === $message->getNotificationId() && 456 === $message->getUserId() && null === $message->getUserGroupId()))->willReturn(new Envelope(new \stdClass()))->shouldNotBeCalled(); - $messageBus = $this->createMock(MessageBusInterface::class); - $messageBus->expects($this->once()) - ->method('dispatch') - ->with($this->callback(fn (SendImmediateNotificationEmailMessage $message) => 123 === $message->getNotificationId() - && 456 === $message->getAddresseeId())) - ->willReturn(new Envelope(new \stdClass())); + $notificationMailer = $this->buildNotificationMailer( + null, + $messageBus->reveal() + ); - $mailer = $this->buildNotificationMailer(null, $messageBus); - - // Call the method that processes notifications - $reflection = new \ReflectionClass(NotificationMailer::class); - $method = $reflection->getMethod('processNotificationForAddressee'); - $method->setAccessible(true); - $method->invoke($mailer, $notification, $user); + $notificationMailer->postPersistNotification($notification, new PostPersistEventArgs($notification, $this->prophesize(EntityManagerInterface::class)->reveal())); } public function testSendDailyDigest(): void @@ -250,6 +333,108 @@ class NotificationMailerTest extends TestCase $notificationMailer->sendDailyDigest($user, $notifications); } + public function testSendEmailToAddresseeUser(): void + { + $user = new User(); + $user->setEmail('user@example.com'); + $notification = new Notification(); + $notification->setSender(new User()); + $notification->setTitle('Notification 1'); + $notification->setType('test_notification_type'); + $notification->addAddressee($user); + + $mailer = $this->prophesize(MailerInterface::class); + $mailer->send(Argument::that(function ($arg) { + if (!$arg instanceof Email) { + return false; + } + + if ('Notification 1' !== $arg->getSubject()) { + return false; + } + + foreach ($arg->getTo() as $address) { + if ('user@example.com' === $address->getAddress()) { + return true; + } + } + + return false; + }))->shouldBeCalledOnce(); + + $notificationMailer = $this->buildNotificationMailer($mailer->reveal()); + + $notificationMailer->sendEmailToAddressee($notification, $user); + } + + public function testSendEmailToAddresseeGroup(): void + { + $userGroup = new UserGroup(); + $userGroup->setEmail('user@example.com'); + $notification = new Notification(); + $notification->setSender(new User()); + $notification->setTitle('Notification 1'); + $notification->setType('test_notification_type'); + $notification->addAddressee($userGroup); + + $mailer = $this->prophesize(MailerInterface::class); + $mailer->send(Argument::that(function ($arg) { + if (!$arg instanceof Email) { + return false; + } + + if ('Notification 1' !== $arg->getSubject()) { + return false; + } + + foreach ($arg->getTo() as $address) { + if ('user@example.com' === $address->getAddress()) { + return true; + } + } + + return false; + }))->shouldBeCalledOnce(); + + $notificationMailer = $this->buildNotificationMailer($mailer->reveal()); + + $notificationMailer->sendEmailToAddressee($notification, $userGroup); + } + + public function testSendEmailToAddresseeGroupWithNoAddress(): void + { + $userGroup = new UserGroup(); + $notification = new Notification(); + $notification->setSender(new User()); + $notification->setTitle('Notification 1'); + $notification->setType('test_notification_type'); + $notification->addAddressee($userGroup); + + $mailer = $this->prophesize(MailerInterface::class); + $mailer->send(Argument::any())->shouldNotBeCalled(); + + $notificationMailer = $this->buildNotificationMailer($mailer->reveal()); + + $notificationMailer->sendEmailToAddressee($notification, $userGroup); + } + + public function testSendEmailToAddresseeUserWithNoAddress(): void + { + $user = new User(); + $notification = new Notification(); + $notification->setSender(new User()); + $notification->setTitle('Notification 1'); + $notification->setType('test_notification_type'); + $notification->addAddressee($user); + + $mailer = $this->prophesize(MailerInterface::class); + $mailer->send(Argument::any())->shouldNotBeCalled(); + + $notificationMailer = $this->buildNotificationMailer($mailer->reveal()); + + $notificationMailer->sendEmailToAddressee($notification, $user); + } + private function buildNotificationMailer( ?MailerInterface $mailer = null, ?MessageBusInterface $messageBus = null, From 1524ed8ce99f1d42cdb496e1bf8fc6ffebeb853c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 16 Mar 2026 14:54:47 +0000 Subject: [PATCH 17/22] Replace `ActivityVoter::SEE` with `AccompanyingPeriodVoter::SEE` for correct authorization check --- .changes/unreleased/Security-20260316-153605.yaml | 7 +++++++ .../Repository/ActivityACLAwareRepository.php | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 .changes/unreleased/Security-20260316-153605.yaml diff --git a/.changes/unreleased/Security-20260316-153605.yaml b/.changes/unreleased/Security-20260316-153605.yaml new file mode 100644 index 000000000..1c383d2d6 --- /dev/null +++ b/.changes/unreleased/Security-20260316-153605.yaml @@ -0,0 +1,7 @@ +kind: Security +body: Fix permission in list of activities in person context +time: 2026-03-16T15:36:05.243511868+01:00 +custom: + Issue: "506" + MR: "972" + SchemaChange: No schema change diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php index 34ddfe432..57935a2d9 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php @@ -24,6 +24,7 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInt use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; +use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\EntityManagerInterface; @@ -340,7 +341,7 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos } foreach ($person->getAccompanyingPeriodParticipations() as $participation) { - if (!$this->security->isGranted(ActivityVoter::SEE, $participation->getAccompanyingPeriod())) { + if (!$this->security->isGranted(AccompanyingPeriodVoter::SEE, $participation->getAccompanyingPeriod())) { continue; } From 9ba8ec8f41747ed2d7e996fbe34544746082a56f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 16 Mar 2026 15:55:52 +0100 Subject: [PATCH 18/22] Release v4.14.1 --- .changes/unreleased/DX-20260309-131710.yaml | 7 ------- .changes/unreleased/Security-20260316-153605.yaml | 7 ------- .changes/v4.14.1.md | 5 +++++ CHANGELOG.md | 6 ++++++ 4 files changed, 11 insertions(+), 14 deletions(-) delete mode 100644 .changes/unreleased/DX-20260309-131710.yaml delete mode 100644 .changes/unreleased/Security-20260316-153605.yaml create mode 100644 .changes/v4.14.1.md diff --git a/.changes/unreleased/DX-20260309-131710.yaml b/.changes/unreleased/DX-20260309-131710.yaml deleted file mode 100644 index 865c1fe6f..000000000 --- a/.changes/unreleased/DX-20260309-131710.yaml +++ /dev/null @@ -1,7 +0,0 @@ -kind: DX -body: Add seeds in DataFixtures and in some tests to avoid random test failures -time: 2026-03-09T13:17:10.915852317+01:00 -custom: - Issue: "504" - MR: "970" - SchemaChange: No schema change diff --git a/.changes/unreleased/Security-20260316-153605.yaml b/.changes/unreleased/Security-20260316-153605.yaml deleted file mode 100644 index 1c383d2d6..000000000 --- a/.changes/unreleased/Security-20260316-153605.yaml +++ /dev/null @@ -1,7 +0,0 @@ -kind: Security -body: Fix permission in list of activities in person context -time: 2026-03-16T15:36:05.243511868+01:00 -custom: - Issue: "506" - MR: "972" - SchemaChange: No schema change diff --git a/.changes/v4.14.1.md b/.changes/v4.14.1.md new file mode 100644 index 000000000..e1d512776 --- /dev/null +++ b/.changes/v4.14.1.md @@ -0,0 +1,5 @@ +## v4.14.1 - 2026-03-16 +### Security +* ([#506](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/506)) ([!972](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/972)) Fix permission in list of activities in person context +### DX +* ([#504](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/504)) ([!970](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/970)) Add seeds in DataFixtures and in some tests to avoid random test failures diff --git a/CHANGELOG.md b/CHANGELOG.md index 842ddce69..32fc721ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). +## v4.14.1 - 2026-03-16 +### Security +* ([#506](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/506)) ([!972](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/972)) Fix permission in list of activities in person context +### DX +* ([#504](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/504)) ([!970](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/970)) Add seeds in DataFixtures and in some tests to avoid random test failures + ## v4.14.0 - 2026-03-09 ### Feature * ([#486](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/486)) ([!](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/)) Add filter and aggregator based on referrer's main center for exports of accompanying period From f12bc2f35f7c464ac28409125d8916d7e55ac270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 18 Mar 2026 08:16:40 +0000 Subject: [PATCH 19/22] Fix notification email links to handle user and non-user contexts --- .changes/unreleased/Fixed-20260318-090323.yaml | 7 +++++++ .../Notification/Email/NotificationMailer.php | 3 +++ .../email_non_system_notification_content.md.twig | 5 +++++ .../email_non_system_notification_content.txt.twig | 6 +++++- 4 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 .changes/unreleased/Fixed-20260318-090323.yaml diff --git a/.changes/unreleased/Fixed-20260318-090323.yaml b/.changes/unreleased/Fixed-20260318-090323.yaml new file mode 100644 index 000000000..f50b1a752 --- /dev/null +++ b/.changes/unreleased/Fixed-20260318-090323.yaml @@ -0,0 +1,7 @@ +kind: Fixed +body: Fix link inside notification email +time: 2026-03-18T09:03:23.896160821+01:00 +custom: + Issue: "" + MR: "" + SchemaChange: No schema change diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php index c1eb03c49..3a02cc38a 100644 --- a/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php @@ -101,6 +101,9 @@ class NotificationMailer if (null === $addressee->getEmail()) { continue; } + if ($notification->getSender() === $addressee) { + continue; + } $this->processNotificationForAddressee($notification, $addressee); } } diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.md.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.md.twig index 854547c92..177bb1b0e 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.md.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.md.twig @@ -13,7 +13,12 @@ > {{ line }} {% endfor %} +{% if dest.isUser %} [Vous pouvez visualiser la notification et y répondre ici.]({{ absolute_url(path('chill_main_notification_show', {'_locale': dest.locale, 'id': notification.id }, false)) }}) +{% else %} +[Vous pouvez visualiser la notification et y répondre ici.]({{ absolute_url(path('chill_main_notification_grant_access_by_access_key', {'_locale': dest.locale, 'id': notification.id, 'accessKey': notification.accessKey }, false)) }}) +{% endif %} + ----- diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.txt.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.txt.twig index b14f77ef7..48afa1be1 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.txt.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.txt.twig @@ -12,7 +12,11 @@ Titre de la notification: {{ notification.title }} > {{ line|raw }} {% endfor %} -Vous pouvez visualiser la notification et y répondre ici: {{ absolute_url(path('chill_main_notification_show', {'_locale': dest.locale, 'id': notification.id }, false)) }}. +{% if dest.isUser %} +[Vous pouvez visualiser la notification et y répondre ici.]({{ absolute_url(path('chill_main_notification_show', {'_locale': dest.locale, 'id': notification.id }, false)) }}) +{% else %} +[Vous pouvez visualiser la notification et y répondre ici.]({{ absolute_url(path('chill_main_notification_grant_access_by_access_key', {'_locale': dest.locale, 'id': notification.id, 'accessKey': notification.accessKey }, false)) }}) +{% endif %} -- Le logiciel Chill From eb2dfc85912793e68197f03bb3c4776e56ff766b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 18 Mar 2026 09:18:33 +0100 Subject: [PATCH 20/22] Release v4.14.2 --- .changes/unreleased/Fixed-20260318-090323.yaml | 7 ------- .changes/v4.14.2.md | 3 +++ CHANGELOG.md | 4 ++++ 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 .changes/unreleased/Fixed-20260318-090323.yaml create mode 100644 .changes/v4.14.2.md diff --git a/.changes/unreleased/Fixed-20260318-090323.yaml b/.changes/unreleased/Fixed-20260318-090323.yaml deleted file mode 100644 index f50b1a752..000000000 --- a/.changes/unreleased/Fixed-20260318-090323.yaml +++ /dev/null @@ -1,7 +0,0 @@ -kind: Fixed -body: Fix link inside notification email -time: 2026-03-18T09:03:23.896160821+01:00 -custom: - Issue: "" - MR: "" - SchemaChange: No schema change diff --git a/.changes/v4.14.2.md b/.changes/v4.14.2.md new file mode 100644 index 000000000..5de87f68a --- /dev/null +++ b/.changes/v4.14.2.md @@ -0,0 +1,3 @@ +## v4.14.2 - 2026-03-18 +### Fixed +* Fix link inside notification email diff --git a/CHANGELOG.md b/CHANGELOG.md index 32fc721ce..192a21e2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). +## v4.14.2 - 2026-03-18 +### Fixed +* Fix link inside notification email + ## v4.14.1 - 2026-03-16 ### Security * ([#506](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/506)) ([!972](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/972)) Fix permission in list of activities in person context From 6654bea48f6c53236deb88cccf34bc2e54517f72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 24 Mar 2026 14:45:00 +0000 Subject: [PATCH 21/22] Add IRN field to Changie configuration for release note tagging --- .changes/unreleased/DX-20260324-153805.yaml | 8 ++++++++ .changie.yaml | 10 +++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 .changes/unreleased/DX-20260324-153805.yaml diff --git a/.changes/unreleased/DX-20260324-153805.yaml b/.changes/unreleased/DX-20260324-153805.yaml new file mode 100644 index 000000000..a1b22a054 --- /dev/null +++ b/.changes/unreleased/DX-20260324-153805.yaml @@ -0,0 +1,8 @@ +kind: DX +body: 'Changie: add a field for adding a release note tag when creating an entry in changie.' +time: 2026-03-24T15:38:05.320350835+01:00 +custom: + IRN: "No" + Issue: "" + MR: "" + SchemaChange: No schema change diff --git a/.changie.yaml b/.changie.yaml index 0bcab1f5b..0b441f118 100644 --- a/.changie.yaml +++ b/.changie.yaml @@ -7,7 +7,7 @@ versionFormat: '## {{.Version}} - {{.Time.Format "2006-01-02"}}' kindFormat: '### {{.Kind}}' # Note: it is possible to add a `.custom.Long` text manually into the yaml file produced by `changie new`. This will add a long description. changeFormat: >- - * {{ if not (eq .Custom.Issue "") }}([#{{ .Custom.Issue }}](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/{{ .Custom.Issue }})) {{ end }}{{ if not (eq .Custom.MR "") }}([!{{ .Custom.MR }}](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/{{ .Custom.MR }})) {{ end }}{{ .Body }} {{ if and .Custom.SchemaChange (ne .Custom.SchemaChange "No schema change") }} + * {{ if not (eq .Custom.Issue "") }}([#{{ .Custom.Issue }}](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/{{ .Custom.Issue }})) {{ end }}{{ if not (eq .Custom.MR "") }}([!{{ .Custom.MR }}](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/{{ .Custom.MR }})) {{ end }}{{ .Body }} {{ if (eq .Custom.IRN "Yes") }}(RN){{ end }} {{ if and .Custom.SchemaChange (ne .Custom.SchemaChange "No schema change") }} **Schema Change**: {{ .Custom.SchemaChange }} {{- end -}} @@ -36,6 +36,14 @@ custom: type: int minInt: 1 + - key: IRN + label: Is this interesting for release notes ? + optional: false + type: enum + enumOptions: + - "No" + - "Yes" + body: # allow multiline messages block: true From dcccbb36f4f304e3479fba009a48ba40cf69d580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 24 Mar 2026 15:41:31 +0000 Subject: [PATCH 22/22] Resolve "Ajout d'un champ "externalId" pour les centres" --- .../unreleased/Feature-20260324-164018.yaml | 7 ++++ src/Bundle/ChillMainBundle/Entity/Center.php | 18 ++++++++++ .../Repository/CenterRepository.php | 5 +++ .../Repository/CenterRepositoryInterface.php | 2 ++ .../migrations/Version20260324144420.php | 35 +++++++++++++++++++ 5 files changed, 67 insertions(+) create mode 100644 .changes/unreleased/Feature-20260324-164018.yaml create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20260324144420.php diff --git a/.changes/unreleased/Feature-20260324-164018.yaml b/.changes/unreleased/Feature-20260324-164018.yaml new file mode 100644 index 000000000..6a2b6822c --- /dev/null +++ b/.changes/unreleased/Feature-20260324-164018.yaml @@ -0,0 +1,7 @@ +kind: Feature +body: Add a field "externalId" on center, to ease the synchronisation of centers with external tools +time: 2026-03-24T16:40:18.159561269+01:00 +custom: + Issue: "507" + MR: "977" + SchemaChange: Add columns or tables diff --git a/src/Bundle/ChillMainBundle/Entity/Center.php b/src/Bundle/ChillMainBundle/Entity/Center.php index 1ac7f5977..e7211f88a 100644 --- a/src/Bundle/ChillMainBundle/Entity/Center.php +++ b/src/Bundle/ChillMainBundle/Entity/Center.php @@ -45,6 +45,9 @@ class Center implements HasCenterInterface, \Stringable #[ORM\ManyToMany(targetEntity: Regroupment::class, mappedBy: 'centers')] private Collection $regroupments; + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, options: ['default' => ''])] + private string $externalId = ''; + /** * Center constructor. */ @@ -124,4 +127,19 @@ class Center implements HasCenterInterface, \Stringable return $this; } + + public function getExternalId(): string + { + return $this->externalId; + } + + public function setExternalId(string $externalId): void + { + $this->externalId = $externalId; + } + + public function hasExternalId(): bool + { + return '' !== $this->externalId; + } } diff --git a/src/Bundle/ChillMainBundle/Repository/CenterRepository.php b/src/Bundle/ChillMainBundle/Repository/CenterRepository.php index 1bc5a7405..686476b8c 100644 --- a/src/Bundle/ChillMainBundle/Repository/CenterRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/CenterRepository.php @@ -29,6 +29,11 @@ final readonly class CenterRepository implements CenterRepositoryInterface return $this->repository->find($id, $lockMode, $lockVersion); } + public function findOneByExternalId(string $externalId): ?Center + { + return $this->repository->findOneBy(['externalId' => $externalId]); + } + /** * @return Center[] */ diff --git a/src/Bundle/ChillMainBundle/Repository/CenterRepositoryInterface.php b/src/Bundle/ChillMainBundle/Repository/CenterRepositoryInterface.php index b933a34b7..1819628f0 100644 --- a/src/Bundle/ChillMainBundle/Repository/CenterRepositoryInterface.php +++ b/src/Bundle/ChillMainBundle/Repository/CenterRepositoryInterface.php @@ -24,4 +24,6 @@ interface CenterRepositoryInterface extends ObjectRepository * @return Center[] */ public function findActive(): array; + + public function findOneByExternalId(string $externalId): ?Center; } diff --git a/src/Bundle/ChillMainBundle/migrations/Version20260324144420.php b/src/Bundle/ChillMainBundle/migrations/Version20260324144420.php new file mode 100644 index 000000000..7988c32d2 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20260324144420.php @@ -0,0 +1,35 @@ +addSql('ALTER TABLE public.centers ADD externalId TEXT DEFAULT \'\' NOT NULL'); + $this->addSql('CREATE UNIQUE INDEX centers_external_id_unique ON public.centers (externalId) WHERE externalId <> \'\''); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP INDEX public.centers_external_id_unique'); + $this->addSql('ALTER TABLE public.centers DROP externalId'); + } +}