Compare commits

...

35 Commits

Author SHA1 Message Date
eb2dfc8591 Release v4.14.2 2026-03-18 09:18:33 +01:00
b5a22508ff Merge branch 'fix/fix-link-notification-email' into 'master'
Fix notification email links to handle user and non-user contexts

See merge request Chill-Projet/chill-bundles!973
2026-03-18 08:16:41 +00:00
f12bc2f35f Fix notification email links to handle user and non-user contexts 2026-03-18 08:16:40 +00:00
9ba8ec8f41 Release v4.14.1 2026-03-16 15:55:52 +01:00
6a66f05451 Merge branch '506-fix-permissions-list-activity-by-person' into 'master'
Replace `ActivityVoter::SEE` with `AccompanyingPeriodVoter::SEE` for correct authorization check

Closes #506

See merge request Chill-Projet/chill-bundles!972
2026-03-16 14:54:47 +00:00
1524ed8ce9 Replace ActivityVoter::SEE with AccompanyingPeriodVoter::SEE for correct authorization check 2026-03-16 14:54:47 +00:00
0aa0824831 Merge branch '505-fix-user-group-notification-email' into 'master'
Resolve "Notification aux groupes utilisateurs"

Closes #505

See merge request Chill-Projet/chill-bundles!971
2026-03-16 14:08:36 +00:00
dd429ca02a Resolve "Notification aux groupes utilisateurs" 2026-03-16 14:08:35 +00:00
81193376a4 Merge branch '504-fix-random-tests' into 'master'
Add seeds to data fixtures, to avoid random failures in tests

Closes #504

See merge request Chill-Projet/chill-bundles!970
2026-03-09 13:00:30 +00:00
a921009eff Add seeds to data fixtures, to avoid random failures in tests 2026-03-09 13:00:30 +00:00
e2dec28577 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.
2026-03-09 12:27:00 +01:00
30385da409 Merge branch '486-user-center-filter-aggregator' into 'master'
Resolve "Create a filter/aggregator by user center for the exports"

Closes #486

See merge request Chill-Projet/chill-bundles!946
2026-03-09 11:19:38 +00:00
562fecb4aa Resolve "Create a filter/aggregator by user center for the exports" 2026-03-09 11:19:38 +00:00
8e8f459f90 Merge branch '503-reassign-ui-message' into 'master'
Resolve "Lors de la ré-assignation des parcours, l'UI ne mentionne pas qu'une opération a été réalisée"

Closes #503

See merge request Chill-Projet/chill-bundles!969
2026-03-09 09:25:08 +00:00
5de3862ec2 Resolve "Lors de la ré-assignation des parcours, l'UI ne mentionne pas qu'une opération a été réalisée" 2026-03-09 09:25:08 +00:00
26838648c8 Merge branch '502-fix-import-postal-code-removed' into 'master'
Resolve "Lors de l'import de code postaux, les codes absents de l'import depuis la même source ne sont pas supprimés"

Closes #502

See merge request Chill-Projet/chill-bundles!968
2026-02-23 20:05:05 +00:00
030553a4de Resolve "Lors de l'import de code postaux, les codes absents de l'import depuis la même source ne sont pas supprimés" 2026-02-23 20:05:04 +00:00
966f9f7e33 Release v4.13.0 2026-02-23 17:13:24 +01:00
7a5300b713 Merge branch '495-fix-quote-notification-email' into 'master'
Remove unused method `sendNotificationEmailsToAddressesEmails` from `NotificationMailer`

Closes #495

See merge request Chill-Projet/chill-bundles!967
2026-02-23 15:49:39 +00:00
dc3a585e5b Remove unused method sendNotificationEmailsToAddressesEmails from NotificationMailer 2026-02-23 15:49:39 +00:00
7712d76889 Merge branch '494-titre-toute-la-journée-tronqué-sur-la-page-mes-rendez-vous' into 'master'
Resolve "Titre 'Toute la journée' tronqué sur la page Mes Rendez-vous"

Closes #494

See merge request Chill-Projet/chill-bundles!965
2026-02-23 15:08:48 +00:00
Boris Waaub
69bb7026c9 Resolve "Titre 'Toute la journée' tronqué sur la page Mes Rendez-vous" 2026-02-23 15:08:48 +00:00
acd7240903 Merge branch '501-fix-deprecation-markdown-parser' into 'master'
Resolve "Depréciation dans le paquet de transformation markdown"

Closes #501

See merge request Chill-Projet/chill-bundles!966
2026-02-23 14:50:15 +00:00
22049558da Resolve "Depréciation dans le paquet de transformation markdown" 2026-02-23 14:50:14 +00:00
c0f2f3f3e0 Merge branch '500-limit-public-download' into 'master'
Resolve "Téléchargement des documents d'un workflow: limiter à 30 téléchargements plutôt que 100"

Closes #500

See merge request Chill-Projet/chill-bundles!964
2026-02-23 14:24:53 +00:00
bf56b3cc65 Resolve "Téléchargement des documents d'un workflow: limiter à 30 téléchargements plutôt que 100" 2026-02-23 14:24:53 +00:00
f85973f7ae Merge branch '499-fix-loading-postal-code' into 'master'
Resolve "Des codes postaux marqués comme supprimés apparaissent toujours dans la recherche d'adresse"

Closes #499

See merge request Chill-Projet/chill-bundles!963
2026-02-23 14:16:46 +00:00
f1446d7abe Resolve "Des codes postaux marqués comme supprimés apparaissent toujours dans la recherche d'adresse" 2026-02-23 14:16:45 +00:00
76d675ac02 Fixed translations of address in exports (addresse -> adresse) 2026-02-17 14:11:15 +01:00
cf0a2b7393 Merge branch '438-parcours-designer-comme-adresse-du-parcours-to-be-green' into 'master'
Resolve "Parcours - "Désigner comme adresse du parcours" to be green"

Closes #438

See merge request Chill-Projet/chill-bundles!958
2026-02-12 08:50:00 +00:00
Boris Waaub
80b05a8133 Resolve "Parcours - "Désigner comme adresse du parcours" to be green" 2026-02-12 08:50:00 +00:00
69aba8d9c9 Merge branch 'changie/add-mr-to-question' into 'master'
Changie/add mr to question

See merge request Chill-Projet/chill-bundles!960
2026-02-11 13:27:48 +00:00
a87d936828 Changie/add mr to question 2026-02-11 13:27:48 +00:00
290fa7a77c Merge branch '498-fix-workflow-initiator' into 'master'
Take workflow creator into account when granting edit permissions on documents

Closes #498

See merge request Chill-Projet/chill-bundles!959
2026-02-10 15:05:50 +00:00
0e1d233d79 Take workflow creator into account when granting edit permissions on documents 2026-02-10 15:05:49 +00:00
63 changed files with 1555 additions and 205 deletions

15
.changes/v4.13.0.md Normal file
View File

@@ -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

6
.changes/v4.14.0.md Normal file
View File

@@ -0,0 +1,6 @@
## v4.14.0 - 2026-03-09
### Feature
* ([#486](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/486)) ([!<no value>](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/<no value>)) 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)

5
.changes/v4.14.1.md Normal file
View File

@@ -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

3
.changes/v4.14.2.md Normal file
View File

@@ -0,0 +1,3 @@
## v4.14.2 - 2026-03-18
### Fixed
* Fix link inside notification email

View File

@@ -7,7 +7,7 @@ versionFormat: '## {{.Version}} - {{.Time.Format "2006-01-02"}}'
kindFormat: '### {{.Kind}}' 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. # 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: >- 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 }} **Schema Change**: {{ .Custom.SchemaChange }}
{{- end -}} {{- end -}}
@@ -30,6 +30,12 @@ custom:
type: int type: int
minInt: 1 minInt: 1
- key: MR
label: Merge request number (on chill-bundles repository) (optional)
optional: true
type: int
minInt: 1
body: body:
# allow multiline messages # allow multiline messages
block: true block: true

View File

@@ -238,13 +238,13 @@ The tests are run from the project's root (not from the bundle's root).
```bash ```bash
# Run all tests # Run all tests
vendor/bin/phpunit symfony composer exec phpunit
# Run a specific test file # 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 # 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 When writing tests, only test specific files. Do not run all tests or the full

View File

@@ -6,6 +6,39 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie). 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
### 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)) ([!<no value>](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/<no value>)) 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
* ([#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 ## v4.12.1 - 2026-02-01
### Fixed ### Fixed
* ([#496](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/496)) Add the option to deal with duplicate address in BAN adress importer * ([#496](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/496)) Add the option to deal with duplicate address in BAN adress importer

View File

@@ -8,5 +8,6 @@ when@dev: &dev
- 'file' - 'file'
- 'md5' - 'md5'
- 'sha1' - 'sha1'
seed: 1234567890
when@test: *dev when@test: *dev

View File

@@ -33,6 +33,7 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface
public function __construct(private readonly EntityManagerInterface $em) public function __construct(private readonly EntityManagerInterface $em)
{ {
mt_srand(123456789);
$this->faker = FakerFactory::create('fr_FR'); $this->faker = FakerFactory::create('fr_FR');
} }
@@ -48,7 +49,7 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface
->findAll(); ->findAll();
foreach ($persons as $person) { foreach ($persons as $person) {
$activityNbr = random_int(0, 3); $activityNbr = mt_rand(0, 3);
for ($i = 0; $i < $activityNbr; ++$i) { for ($i = 0; $i < $activityNbr; ++$i) {
$activity = $this->newRandomActivity($person); $activity = $this->newRandomActivity($person);
@@ -73,7 +74,7 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface
// ->setAttendee($this->faker->boolean()) // ->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(); $reason = $this->getRandomActivityReason();
if (null !== $reason) { if (null !== $reason) {

View File

@@ -24,6 +24,7 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInt
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@@ -340,7 +341,7 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos
} }
foreach ($person->getAccompanyingPeriodParticipations() as $participation) { foreach ($person->getAccompanyingPeriodParticipations() as $participation) {
if (!$this->security->isGranted(ActivityVoter::SEE, $participation->getAccompanyingPeriod())) { if (!$this->security->isGranted(AccompanyingPeriodVoter::SEE, $participation->getAccompanyingPeriod())) {
continue; continue;
} }

View File

@@ -21,7 +21,10 @@ use Doctrine\Persistence\ObjectManager;
class LoadAsideActivity extends Fixture implements DependentFixtureInterface 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 public function getDependencies(): array
{ {
@@ -47,7 +50,7 @@ class LoadAsideActivity extends Fixture implements DependentFixtureInterface
$this->getReference('aside_activity_category_0', AsideActivityCategory::class) $this->getReference('aside_activity_category_0', AsideActivityCategory::class)
) )
->setDate((new \DateTimeImmutable('today')) ->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); $manager->persist($activity);
} }

View File

@@ -346,6 +346,7 @@ const baseOptions = ref<CalendarOptions>({
center: "title", center: "title",
right: "timeGridWeek,timeGridDay", right: "timeGridWeek,timeGridDay",
}, },
allDaySlot: false,
}); });
const ranges = computed<EventInput[]>(() => { const ranges = computed<EventInput[]>(() => {

View File

@@ -41,6 +41,7 @@ class LoadOption extends AbstractFixture implements OrderedFixtureInterface
public function __construct() public function __construct()
{ {
mt_srand(123456789);
$this->fakerFr = \Faker\Factory::create('fr_FR'); $this->fakerFr = \Faker\Factory::create('fr_FR');
$this->fakerEn = \Faker\Factory::create('en_EN'); $this->fakerEn = \Faker\Factory::create('en_EN');
$this->fakerNl = \Faker\Factory::create('nl_NL'); $this->fakerNl = \Faker\Factory::create('nl_NL');
@@ -104,7 +105,7 @@ class LoadOption extends AbstractFixture implements OrderedFixtureInterface
$manager->persist($parent); $manager->persist($parent);
// Load children // Load children
$expected_nb_children = random_int(10, 50); $expected_nb_children = mt_rand(10, 50);
for ($i = 0; $i < $expected_nb_children; ++$i) { for ($i = 0; $i < $expected_nb_children; ++$i) {
$companyName = $this->fakerFr->company; $companyName = $this->fakerFr->company;
@@ -144,7 +145,7 @@ class LoadOption extends AbstractFixture implements OrderedFixtureInterface
$manager->persist($parent); $manager->persist($parent);
// Load children // Load children
$expected_nb_children = random_int(10, 50); $expected_nb_children = mt_rand(10, 50);
for ($i = 0; $i < $expected_nb_children; ++$i) { for ($i = 0; $i < $expected_nb_children; ++$i) {
$manager->persist($this->createChildOption($parent, [ $manager->persist($this->createChildOption($parent, [

View File

@@ -34,6 +34,7 @@ class LoadParticipation extends AbstractFixture implements OrderedFixtureInterfa
public function __construct() public function __construct()
{ {
mt_srand(123456789);
$this->faker = \Faker\Factory::create('fr_FR'); $this->faker = \Faker\Factory::create('fr_FR');
} }
@@ -45,7 +46,7 @@ class LoadParticipation extends AbstractFixture implements OrderedFixtureInterfa
for ($i = 0; $i < $expectedNumber; ++$i) { for ($i = 0; $i < $expectedNumber; ++$i) {
$event = (new Event()) $event = (new Event())
->setDate($this->faker->dateTimeBetween('-2 years', '+6 months')) ->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)) ->setType($this->getReference(LoadEventTypes::$refs[array_rand(LoadEventTypes::$refs)], EventType::class))
->setCenter($center) ->setCenter($center)
->setCircle( ->setCircle(
@@ -78,7 +79,7 @@ class LoadParticipation extends AbstractFixture implements OrderedFixtureInterfa
/** @var Person $person */ /** @var Person $person */
foreach ($people as $person) { foreach ($people as $person) {
$nb = random_int(0, 3); $nb = mt_rand(0, 3);
for ($i = 0; $i < $nb; ++$i) { for ($i = 0; $i < $nb; ++$i) {
$event = $events[array_rand($events)]; $event = $events[array_rand($events)];

View File

@@ -79,5 +79,7 @@ final class PostalCodeAPIController extends ApiController
$qb->andWhere('e.origin = :zero') $qb->andWhere('e.origin = :zero')
->setParameter('zero', 0); ->setParameter('zero', 0);
$qb->andWhere('e.deletedAt IS NULL');
} }
} }

View File

@@ -62,15 +62,15 @@ final readonly class WorkflowViewSendPublicController
); );
} }
if (100 < $workflowSend->getViews()->count()) { if (30 < $workflowSend->getViews()->count()) {
$this->chillLogger->info(self::LOG_PREFIX.'100 view reached, not allowed to see it again'); $this->chillLogger->info(self::LOG_PREFIX.'30 view reached, not allowed to see it again');
throw new AccessDeniedHttpException('100 views reached, not allowed to see it again'); throw new AccessDeniedHttpException('30 views reached, not allowed to see it again');
} }
try { try {
$metadata = new EntityWorkflowViewMetadataDTO( $metadata = new EntityWorkflowViewMetadataDTO(
$workflowSend->getViews()->count(), $workflowSend->getViews()->count(),
100 - $workflowSend->getViews()->count(), 30 - $workflowSend->getViews()->count(),
); );
$response = new Response( $response = new Response(
$this->entityWorkflowManager->renderPublicView($workflowSend, $metadata), $this->entityWorkflowManager->renderPublicView($workflowSend, $metadata),

View File

@@ -31,6 +31,7 @@ class LoadAddressReferences extends AbstractFixture implements ContainerAwareInt
public function __construct() public function __construct()
{ {
mt_srand(123456789);
$this->faker = \Faker\Factory::create('fr_FR'); $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->setRefId($this->faker->numerify('ref-id-######'));
$ar->setStreet($this->faker->streetName); $ar->setStreet($this->faker->streetName);
$ar->setStreetNumber((string) random_int(0, 199)); $ar->setStreetNumber((string) mt_rand(0, 199));
$ar->setPoint($this->getRandomPoint()); $ar->setPoint($this->getRandomPoint());
$ar->setPostcode($this->getReference( $ar->setPostcode($this->getReference(
LoadPostalCodes::$refs[array_rand(LoadPostalCodes::$refs)], LoadPostalCodes::$refs[array_rand(LoadPostalCodes::$refs)],
@@ -88,8 +89,8 @@ class LoadAddressReferences extends AbstractFixture implements ContainerAwareInt
{ {
$lonBrussels = 4.35243; $lonBrussels = 4.35243;
$latBrussels = 50.84676; $latBrussels = 50.84676;
$lon = $lonBrussels + 0.01 * random_int(-5, 5); $lon = $lonBrussels + 0.01 * mt_rand(-5, 5);
$lat = $latBrussels + 0.01 * random_int(-5, 5); $lat = $latBrussels + 0.01 * mt_rand(-5, 5);
return Point::fromLonLat($lon, $lat); return Point::fromLonLat($lon, $lat);
} }

View File

@@ -215,17 +215,21 @@ class Notification implements TrackUpdateInterface
return $this->addressees; return $this->addressees;
} }
/**
* @return list<User|UserGroup>
*/
public function getAllAddressees(): array public function getAllAddressees(): array
{ {
$allUsers = []; $allUsers = [];
foreach ($this->getAddressees() as $user) { foreach ($this->getAddressees() as $user) {
$allUsers[$user->getId()] = $user; $allUsers['u_'.$user->getId()] = $user;
} }
foreach ($this->getAddresseeUserGroups() as $userGroup) { foreach ($this->getAddresseeUserGroups() as $userGroup) {
$allUsers['ug_'.$userGroup->getId()] = $userGroup;
foreach ($userGroup->getUsers() as $user) { foreach ($userGroup->getUsers() as $user) {
$allUsers[$user->getId()] = $user; $allUsers['u_'.$user->getId()] = $user;
} }
} }

View File

@@ -215,4 +215,14 @@ class PostalCode implements TrackUpdateInterface, TrackCreationInterface
return $this; return $this;
} }
public function isDeleted(): bool
{
return null !== $this->deletedAt;
}
public function getDeletedAt(): ?\DateTimeImmutable
{
return $this->deletedAt;
}
} }

View File

@@ -658,6 +658,11 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
return true; return true;
} }
public function isUserGroup(): bool
{
return false;
}
private function getNotificationFlagData(string $flag): array private function getNotificationFlagData(string $flag): array
{ {
return $this->notificationFlags[$flag] ?? [self::NOTIF_FLAG_IMMEDIATE_EMAIL]; return $this->notificationFlags[$flag] ?? [self::NOTIF_FLAG_IMMEDIATE_EMAIL];

View File

@@ -256,6 +256,21 @@ class UserGroup
return true; 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 public function contains(User $user): bool
{ {
return $this->users->contains($user); return $this->users->contains($user);

View File

@@ -394,6 +394,10 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
public function isUserInvolved(User $user): bool public function isUserInvolved(User $user): bool
{ {
if ($this->getCreatedBy() === $user) {
return true;
}
foreach ($this->getSteps() as $step) { foreach ($this->getSteps() as $step) {
if ($step->getAllDestUser()->contains($user)) { if ($step->getAllDestUser()->contains($user)) {
return true; return true;

View File

@@ -14,7 +14,8 @@ namespace Chill\MainBundle\Notification\Email\NotificationEmailHandlers;
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage; use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage;
use Chill\MainBundle\Notification\Email\NotificationMailer; use Chill\MainBundle\Notification\Email\NotificationMailer;
use Chill\MainBundle\Repository\NotificationRepository; 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 Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Messenger\Attribute\AsMessageHandler;
@@ -24,7 +25,8 @@ readonly class SendImmediateNotificationEmailHandler
{ {
public function __construct( public function __construct(
private NotificationRepository $notificationRepository, private NotificationRepository $notificationRepository,
private UserRepository $userRepository, private UserRepositoryInterface $userRepository,
private UserGroupRepository $userGroupRepository,
private NotificationMailer $notificationMailer, private NotificationMailer $notificationMailer,
private LoggerInterface $logger, private LoggerInterface $logger,
) {} ) {}
@@ -36,7 +38,13 @@ readonly class SendImmediateNotificationEmailHandler
public function __invoke(SendImmediateNotificationEmailMessage $message): void public function __invoke(SendImmediateNotificationEmailMessage $message): void
{ {
$notification = $this->notificationRepository->find($message->getNotificationId()); $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) { if (null === $notification) {
$this->logger->error('[SendImmediateNotificationEmailHandler] Notification not found', [ $this->logger->error('[SendImmediateNotificationEmailHandler] Notification not found', [
@@ -48,10 +56,11 @@ readonly class SendImmediateNotificationEmailHandler
if (null === $addressee) { if (null === $addressee) {
$this->logger->error('[SendImmediateNotificationEmailHandler] Addressee not found', [ $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 { try {
@@ -59,7 +68,8 @@ readonly class SendImmediateNotificationEmailHandler
} catch (\Exception $e) { } catch (\Exception $e) {
$this->logger->error('[SendImmediateNotificationEmailHandler] Failed to send email', [ $this->logger->error('[SendImmediateNotificationEmailHandler] Failed to send email', [
'notification_id' => $message->getNotificationId(), 'notification_id' => $message->getNotificationId(),
'addressee_id' => $message->getAddresseeId(), 'user_id' => $message->getUserId(),
'user_group_id' => $message->getUserGroupId(),
'stacktrace' => $e->getTraceAsString(), 'stacktrace' => $e->getTraceAsString(),
]); ]);
throw $e; throw $e;

View File

@@ -11,20 +11,45 @@ declare(strict_types=1);
namespace Chill\MainBundle\Notification\Email\NotificationEmailMessages; namespace Chill\MainBundle\Notification\Email\NotificationEmailMessages;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
readonly class SendImmediateNotificationEmailMessage readonly class SendImmediateNotificationEmailMessage
{ {
private int $notificationId;
private ?int $userId;
private ?int $userGroupId;
public function __construct( public function __construct(
private int $notificationId, Notification $notification,
private int $addresseeId, 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 public function getNotificationId(): int
{ {
return $this->notificationId; return $this->notificationId;
} }
public function getAddresseeId(): int public function getUserId(): ?int
{ {
return $this->addresseeId; return $this->userId;
}
public function getUserGroupId(): ?int
{
return $this->userGroupId;
} }
} }

View File

@@ -14,6 +14,7 @@ namespace Chill\MainBundle\Notification\Email;
use Chill\MainBundle\Entity\Notification; use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\NotificationComment; use Chill\MainBundle\Entity\NotificationComment;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage; use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage;
use Doctrine\ORM\Event\PostPersistEventArgs; use Doctrine\ORM\Event\PostPersistEventArgs;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@@ -26,13 +27,13 @@ use Symfony\Contracts\Translation\TranslatorInterface;
// use Symfony\Component\Translation\LocaleSwitcher; // use Symfony\Component\Translation\LocaleSwitcher;
readonly class NotificationMailer class NotificationMailer
{ {
public function __construct( public function __construct(
private MailerInterface $mailer, private readonly MailerInterface $mailer,
private LoggerInterface $logger, private readonly LoggerInterface $logger,
private MessageBusInterface $messageBus, private readonly MessageBusInterface $messageBus,
private TranslatorInterface $translator, private readonly TranslatorInterface $translator,
// private LocaleSwitcher $localeSwitcher, // private LocaleSwitcher $localeSwitcher,
) {} ) {}
@@ -59,7 +60,8 @@ readonly class NotificationMailer
$email $email
->to($dest->getEmail()) ->to($dest->getEmail())
->subject('Re: '.$comment->getNotification()->getTitle()) ->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([ ->context([
'comment' => $comment, 'comment' => $comment,
'dest' => $dest, 'dest' => $dest,
@@ -83,7 +85,6 @@ readonly class NotificationMailer
public function postPersistNotification(Notification $notification, PostPersistEventArgs $eventArgs): void public function postPersistNotification(Notification $notification, PostPersistEventArgs $eventArgs): void
{ {
$this->sendNotificationEmailsToAddressees($notification); $this->sendNotificationEmailsToAddressees($notification);
$this->sendNotificationEmailsToAddressesEmails($notification);
} }
private function sendNotificationEmailsToAddressees(Notification $notification): void private function sendNotificationEmailsToAddressees(Notification $notification): void
@@ -100,25 +101,27 @@ readonly class NotificationMailer
if (null === $addressee->getEmail()) { if (null === $addressee->getEmail()) {
continue; continue;
} }
if ($notification->getSender() === $addressee) {
continue;
}
$this->processNotificationForAddressee($notification, $addressee); $this->processNotificationForAddressee($notification, $addressee);
} }
} }
private function processNotificationForAddressee(Notification $notification, User $addressee): void private function processNotificationForAddressee(Notification $notification, User|UserGroup $addressee): void
{ {
$notificationType = $notification->getType(); $notificationType = $notification->getType();
if ($addressee->isNotificationSendImmediately($notificationType)) { if ($addressee instanceof UserGroup || $addressee->isNotificationSendImmediately($notificationType)) {
$this->scheduleImmediateEmail($notification, $addressee); $this->scheduleImmediateEmail($notification, $addressee);
} }
} }
private function scheduleImmediateEmail(Notification $notification, User $addressee): void private function scheduleImmediateEmail(Notification $notification, User|UserGroup $addressee): void
{ {
$message = new SendImmediateNotificationEmailMessage( $message = new SendImmediateNotificationEmailMessage(
$notification->getId(), $notification,
$addressee->getId() $addressee,
); );
$this->messageBus->dispatch($message); $this->messageBus->dispatch($message);
@@ -130,13 +133,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 * @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; return;
} }
@@ -149,7 +156,8 @@ readonly class NotificationMailer
} else { } else {
$email = new TemplatedEmail(); $email = new TemplatedEmail();
$email $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([ ->context([
'notification' => $notification, 'notification' => $notification,
'dest' => $addressee, 'dest' => $addressee,
@@ -186,7 +194,8 @@ readonly class NotificationMailer
} else { } else {
$email = new TemplatedEmail(); $email = new TemplatedEmail();
$email $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([ ->context([
'notification' => $notification, 'notification' => $notification,
'dest' => $addressee, 'dest' => $addressee,
@@ -286,38 +295,4 @@ readonly class NotificationMailer
throw $e; 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(),
]);
}
}
}
} }

View File

@@ -23,7 +23,7 @@ use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository; use Doctrine\Persistence\ObjectRepository;
final class NotificationRepository implements ObjectRepository class NotificationRepository implements ObjectRepository
{ {
private ?Statement $notificationByRelatedEntityAndUserAssociatedStatement = null; private ?Statement $notificationByRelatedEntityAndUserAssociatedStatement = null;

View File

@@ -100,7 +100,9 @@ final readonly class PostalCodeRepository implements PostalCodeRepositoryInterfa
$query $query
->setFromClause('chill_main_postal_code cmpc') ->setFromClause('chill_main_postal_code cmpc')
->andWhereClause('cmpc.origin = 0'); ->andWhereClause('cmpc.origin = 0')
->andWhereClause('cmpc.deletedAt IS NULL')
;
if (null !== $country) { if (null !== $country) {
$query->andWhereClause('cmpc.country_id = ?', [$country->getId()]); $query->andWhereClause('cmpc.country_id = ?', [$country->getId()]);

View File

@@ -18,7 +18,7 @@ use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\LocaleAwareInterface;
final class UserGroupRepository implements UserGroupRepositoryInterface, LocaleAwareInterface class UserGroupRepository implements UserGroupRepositoryInterface, LocaleAwareInterface
{ {
private readonly EntityRepository $repository; private readonly EntityRepository $repository;

View File

@@ -1,20 +1,26 @@
{% apply markdown_to_html %}
{% if dest.isUser %}
{{ dest.label }}, {{ dest.label }},
{% else %}
{{ dest.label|localize_translatable_string }},
{% endif %}
{{ notification.sender.label }} a créé une notification pour vous: {{ notification.sender.label }} a créé une notification pour vous:
> {{ notification.title }} **Titre de la notification**: {{ notification.title }}
>
> {% for line in notification.message|split("\n") %}
{%- for line in notification.message|split("\n") %}
> {{ line }} > {{ line }}
{%- if not loop.last %} {% endfor %}
>
{%- endif %}
{%- endfor %}
Vous pouvez visualiser la notification et y répondre ici: {% 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 %}
{{ absolute_url(path('chill_main_notification_show', {'_locale': dest.locale, 'id': notification.id }, false)) }}
-- -----
Le logiciel Chill Le logiciel Chill
{% endapply %}

View File

@@ -0,0 +1,22 @@
{% if dest.isUser %}
{{ dest.label }},
{% else %}
{{ dest.label|localize_translatable_string }},
{% endif %}
{{ 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 %}
{% 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

View File

@@ -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

View File

@@ -1,3 +1,4 @@
{% apply markdown_to_html %}
{{ dest.label }}, {{ dest.label }},
{{ comment.createdBy.label }} a créé un commentaire sur la notification "{{ comment.notification.title }}". {{ 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") %} {% for line in comment.content|split("\n") %}
> {{ line }} > {{ line }}
{%- if not loop.last %} {% endfor %}
>
{%- endif %}
{%- 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 Le logiciel Chill
{% endapply %}

View File

@@ -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

View File

@@ -19,31 +19,66 @@ use Doctrine\DBAL\Statement;
*/ */
class PostalCodeBaseImporter 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 ( WITH g AS (
SELECT DISTINCT SELECT DISTINCT
country.id AS country_id, country.id AS country_id,
g.* temp.*
FROM (VALUES FROM chill_main_postal_code_temp temp
{{ values }} JOIN country ON country.countrycode = temp.countrycode
) AS g (countrycode, label, code, refpostalcodeid, postalcodeSource, lon, lat, srid)
JOIN country ON country.countrycode = g.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 SELECT
nextval('chill_main_postal_code_id_seq'), nextval('chill_main_postal_code_id_seq'),
g.country_id, g.country_id,
g.label AS glabel, g.label,
g.code, g.code,
0, 0,
g.refpostalcodeid, g.refpostalcodeid,
g.postalcodeSource, 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() NOW(),
NULL
FROM g FROM g
ON CONFLICT (code, refpostalcodeid, postalcodeSource) WHERE refpostalcodeid IS NOT NULL DO UPDATE 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; SQL;
private const VALUE = '(?, ?, ?, ?, ?, ?, ?, ?)'; private const VALUE = '(?, ?, ?, ?, ?, ?, ?, ?)';
@@ -55,11 +90,26 @@ class PostalCodeBaseImporter
private array $waitingForInsert = []; private array $waitingForInsert = [];
private bool $isInitialized = false;
private ?string $currentSource = null;
public function __construct(private readonly Connection $defaultConnection) {} public function __construct(private readonly Connection $defaultConnection) {}
public function finalize(): void public function finalize(): void
{ {
$this->doInsertPending(); $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( public function importCode(
@@ -72,6 +122,14 @@ class PostalCodeBaseImporter
float $centerLon, float $centerLon,
int $centerSRID, int $centerSRID,
): void { ): 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[] = [ $this->waitingForInsert[] = [
$countryCode, $countryCode,
$label, $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 private function doInsertPending(): void
{ {
if ([] == $this->waitingForInsert) {
return;
}
if (!\array_key_exists($forNumber = \count($this->waitingForInsert), $this->cachingStatements)) { if (!\array_key_exists($forNumber = \count($this->waitingForInsert), $this->cachingStatements)) {
$sql = strtr(self::QUERY, [ $sql = strtr(self::INSERT_TEMP, [
'{{ values }}' => implode( '{{ values }}' => implode(
', ', ', ',
array_fill(0, $forNumber, self::VALUE) array_fill(0, $forNumber, self::VALUE)

View File

@@ -41,6 +41,6 @@ final class ChillMarkdownRenderExtension extends AbstractExtension
public function renderMarkdownToHtml(?string $var): string public function renderMarkdownToHtml(?string $var): string
{ {
return $this->parsedown->parse((string) $var); return $this->parsedown->text((string) $var);
} }
} }

View File

@@ -17,6 +17,7 @@ use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Contracts\Translation\TranslatableInterface;
/** /**
* Helper to test filters. * Helper to test filters.
@@ -255,8 +256,8 @@ abstract class AbstractFilterTest extends KernelTestCase
$description = $this->getFilter()->describeAction($data, $context); $description = $this->getFilter()->describeAction($data, $context);
$this->assertTrue( $this->assertTrue(
\is_string($description) || \is_array($description), \is_string($description) || \is_array($description) || $description instanceof TranslatableInterface,
'test that the description is a string or an array' 'test that the description is a string or an array, or a TranslatableInterface'
); );
if (\is_string($description)) { if (\is_string($description)) {

View File

@@ -0,0 +1,187 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Tests\Notification\Email\NotificationEmailHandler;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
use Chill\MainBundle\Notification\Email\NotificationEmailHandlers\SendImmediateNotificationEmailHandler;
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage;
use Chill\MainBundle\Notification\Email\NotificationMailer;
use Chill\MainBundle\Repository\NotificationRepository;
use Chill\MainBundle\Repository\UserGroupRepository;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\NullLogger;
/**
* @internal
*
* @coversNothing
*/
class SendImmediateNotificationEmailHandlerTest extends TestCase
{
use ProphecyTrait;
private $notificationRepository;
private $userRepository;
private $userGroupRepository;
private $notificationMailer;
private SendImmediateNotificationEmailHandler $handler;
protected function setUp(): void
{
$this->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);
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Tests\Notification\Email;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Twig\Environment;
/**
* @internal
*
* @coversNothing
*/
class NotificationMailTwigContentTest extends KernelTestCase
{
private Environment $twig;
protected function setUp(): void
{
self::bootKernel();
$this->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]];
}
}
}

View File

@@ -9,11 +9,12 @@ declare(strict_types=1);
* the LICENSE file that was distributed with this source code. * 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\Notification;
use Chill\MainBundle\Entity\NotificationComment; use Chill\MainBundle\Entity\NotificationComment;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
use Chill\MainBundle\Notification\Email\NotificationMailer; use Chill\MainBundle\Notification\Email\NotificationMailer;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\PostPersistEventArgs; use Doctrine\ORM\Event\PostPersistEventArgs;
@@ -64,13 +65,22 @@ class NotificationMailerTest extends TestCase
// a mail only to user1 and user3 should have been sent // a mail only to user1 and user3 should have been sent
$mailer->send(Argument::that(function (Email $email) { $mailer->send(Argument::that(function (Email $email) {
foreach ($email->getTo() as $address) { 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 true;
} }
} }
return false; 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); $objectManager = $this->prophesize(EntityManagerInterface::class);
@@ -121,7 +131,83 @@ class NotificationMailerTest extends TestCase
* @throws \ReflectionException * @throws \ReflectionException
* @throws Exception * @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 // Create a real notification entity
$notification = new Notification(); $notification = new Notification();
@@ -136,6 +222,11 @@ class NotificationMailerTest extends TestCase
// Create a real user entity // Create a real user entity
$user = new User(); $user = new User();
$user->setEmail('user@example.com'); $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 // Use reflection to set the ID since it's normally generated by the database
$reflectionUser = new \ReflectionClass(User::class); $reflectionUser = new \ReflectionClass(User::class);
@@ -143,23 +234,15 @@ class NotificationMailerTest extends TestCase
$idProperty->setAccessible(true); $idProperty->setAccessible(true);
$idProperty->setValue($user, 456); $idProperty->setValue($user, 456);
// Set notification flags for the user $messageBus = $this->prophesize(MessageBusInterface::class);
$user->setNotificationImmediately('test_notification_type', true); $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); $notificationMailer = $this->buildNotificationMailer(
$messageBus->expects($this->once()) null,
->method('dispatch') $messageBus->reveal()
->with($this->callback(fn (SendImmediateNotificationEmailMessage $message) => 123 === $message->getNotificationId() );
&& 456 === $message->getAddresseeId()))
->willReturn(new Envelope(new \stdClass()));
$mailer = $this->buildNotificationMailer(null, $messageBus); $notificationMailer->postPersistNotification($notification, new PostPersistEventArgs($notification, $this->prophesize(EntityManagerInterface::class)->reveal()));
// Call the method that processes notifications
$reflection = new \ReflectionClass(NotificationMailer::class);
$method = $reflection->getMethod('processNotificationForAddressee');
$method->setAccessible(true);
$method->invoke($mailer, $notification, $user);
} }
public function testSendDailyDigest(): void public function testSendDailyDigest(): void
@@ -250,6 +333,108 @@ class NotificationMailerTest extends TestCase
$notificationMailer->sendDailyDigest($user, $notifications); $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( private function buildNotificationMailer(
?MailerInterface $mailer = null, ?MailerInterface $mailer = null,
?MessageBusInterface $messageBus = null, ?MessageBusInterface $messageBus = null,

View File

@@ -93,4 +93,80 @@ final class PostalCodeBaseImporterTest extends KernelTestCase
$this->assertStringStartsWith('tested with adapted pattern', $postalCodes[0]->getName()); $this->assertStringStartsWith('tested with adapted pattern', $postalCodes[0]->getName());
$this->assertEquals($previousId, $postalCodes[0]->getId()); $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');
}
} }

View File

@@ -37,7 +37,7 @@ final class ChillMarkdownRenderExtensionTest extends TestCase
MD; MD;
private const UNAUTHORIZED_HTML = <<<'HTML' private const UNAUTHORIZED_HTML = <<<'HTML'
<p>&lt;script&gt;alert(&quot;ok&quot;);&lt;/script&gt;</p> <p>&lt;script&gt;alert("ok");&lt;/script&gt;</p>
HTML; HTML;
private const UNAUTHORIZED_MARKDOWN = <<<'MD' private const UNAUTHORIZED_MARKDOWN = <<<'MD'

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260223134919 extends AbstractMigration
{
public function getDescription(): string
{
return 'Create a partial index for postal_code search_name_code, to avoid deleted records';
}
public function up(Schema $schema): void
{
$this->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');
}
}

View File

@@ -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 <strong>GNU Affero General Public License</strong>": "Ce programme est un logiciel libre: vous pouvez le redistribuer et/ou le modifier selon les termes de la licence <strong>GNU Affero GPL</strong>" "This program is free software: you can redistribute it and/or modify it under the terms of the <strong>GNU Affero General Public License</strong>": "Ce programme est un logiciel libre: vous pouvez le redistribuer et/ou le modifier selon les termes de la licence <strong>GNU Affero GPL</strong>"
User manual: Manuel d'utilisation User manual: Manuel d'utilisation
Search: Rechercher Search: Rechercher

View File

@@ -17,7 +17,6 @@ use Chill\MainBundle\Form\Type\PickPostalCodeType;
use Chill\MainBundle\Form\Type\PickUserDynamicType; use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\UserRepository; use Chill\MainBundle\Repository\UserRepository;
use Chill\MainBundle\Templating\Entity\UserRender;
use Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepositoryInterface; use Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepositoryInterface;
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository; use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
@@ -31,18 +30,29 @@ use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Component\Validator\Constraints\NotIdenticalTo; use Symfony\Component\Validator\Constraints\NotIdenticalTo;
use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\NotNull;
class ReassignAccompanyingPeriodController extends AbstractController 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')] #[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)) { if (!$this->security->isGranted(AccompanyingPeriodVoter::REASSIGN_BULK)) {
throw new AccessDeniedHttpException('no right to 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 // redirect to the first page
return $this->redirectToRoute('chill_course_list_reassign', $request->query->all()); return $this->redirectToRoute('chill_course_list_reassign', $request->query->all());

View File

@@ -34,7 +34,7 @@ trait RandomPersonHelperTrait
return $qb return $qb
->select('p') ->select('p')
->setMaxResults(1) ->setMaxResults(1)
->setFirstResult(\random_int(0, $this->nbOfPersons)) ->setFirstResult(\mt_rand(0, $this->nbOfPersons))
->getQuery() ->getQuery()
->getSingleResult(); ->getSingleResult();
} }

View File

@@ -37,7 +37,9 @@ class LoadCustomFields extends AbstractFixture implements OrderedFixtureInterfac
private readonly EntityManagerInterface $entityManager, private readonly EntityManagerInterface $entityManager,
private readonly CustomFieldChoice $customFieldChoice, private readonly CustomFieldChoice $customFieldChoice,
private readonly CustomFieldText $customFieldText, private readonly CustomFieldText $customFieldText,
) {} ) {
mt_srand(123456789);
}
// put your code here // put your code here
public function getOrder(): int public function getOrder(): int
@@ -78,12 +80,12 @@ class LoadCustomFields extends AbstractFixture implements OrderedFixtureInterfac
// select a set of people and add data // select a set of people and add data
foreach ($personIds as $id) { foreach ($personIds as $id) {
// add info on 1 person on 2 // add info on 1 person on 2
if (1 === random_int(0, 1)) { if (1 === mt_rand(0, 1)) {
/** @var Person $person */ /** @var Person $person */
$person = $manager->getRepository(Person::class)->find($id); $person = $manager->getRepository(Person::class)->find($id);
$person->setCFData([ $person->setCFData([
'remarques' => $this->createCustomFieldText() '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() 'document-d-identite' => $this->createCustomFieldChoice()
->serialize([$choices[array_rand($choices)]], $this->cfChoice), ->serialize([$choices[array_rand($choices)]], $this->cfChoice),
]); ]);

View File

@@ -36,6 +36,7 @@ class LoadHousehold extends Fixture implements DependentFixtureInterface
public function __construct(private readonly MembersEditorFactory $editorFactory, private readonly EntityManagerInterface $em) public function __construct(private readonly MembersEditorFactory $editorFactory, private readonly EntityManagerInterface $em)
{ {
mt_srand(123456789);
$this->loader = new NativeLoader(); $this->loader = new NativeLoader();
} }
@@ -72,12 +73,12 @@ class LoadHousehold extends Fixture implements DependentFixtureInterface
private function addAddressToHousehold(Household $household, \DateTimeImmutable $date, ObjectManager $manager) 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 // 20% of household without address
return; return;
} }
$nb = \random_int(1, 6); $nb = \mt_rand(1, 6);
$i = 0; $i = 0;
@@ -85,15 +86,15 @@ class LoadHousehold extends Fixture implements DependentFixtureInterface
$address = $this->createAddress(); $address = $this->createAddress();
$address->setValidFrom(\DateTime::createFromImmutable($date)); $address->setValidFrom(\DateTime::createFromImmutable($date));
if (\random_int(0, 20) < 1) { if (\mt_rand(0, 20) < 1) {
$date = $date->add(new \DateInterval('P'.\random_int(8, 52).'W')); $date = $date->add(new \DateInterval('P'.\mt_rand(8, 52).'W'));
$address->setValidTo(\DateTime::createFromImmutable($date)); $address->setValidTo(\DateTime::createFromImmutable($date));
} }
$household->addAddress($address); $household->addAddress($address);
$manager->persist($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; ++$i;
} }
} }
@@ -127,7 +128,7 @@ class LoadHousehold extends Fixture implements DependentFixtureInterface
$k = 0; $k = 0;
foreach ($this->getRandomPersons(1, 3) as $person) { 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); $position = $this->getReference(LoadHouseholdPosition::ADULT, Position::class);
$movement->addMovement($date, $person, $position, 0 === $k, 'self generated'); $movement->addMovement($date, $person, $position, 0 === $k, 'self generated');
@@ -136,7 +137,7 @@ class LoadHousehold extends Fixture implements DependentFixtureInterface
// load children // load children
foreach ($this->getRandomPersons(0, 3) as $person) { 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); $position = $this->getReference(LoadHouseholdPosition::CHILD, Position::class);
$movement->addMovement($date, $person, $position, 0 === $k, 'self generated'); $movement->addMovement($date, $person, $position, 0 === $k, 'self generated');
@@ -145,7 +146,7 @@ class LoadHousehold extends Fixture implements DependentFixtureInterface
// load children out // load children out
foreach ($this->getRandomPersons(0, 2) as $person) { 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); $position = $this->getReference(LoadHouseholdPosition::CHILD_OUT, Position::class);
$movement->addMovement($date, $person, $position, 0 === $k, 'self generated'); $movement->addMovement($date, $person, $position, 0 === $k, 'self generated');
@@ -169,7 +170,7 @@ class LoadHousehold extends Fixture implements DependentFixtureInterface
{ {
$persons = []; $persons = [];
$nb = \random_int($min, $max); $nb = \mt_rand($min, $max);
for ($i = 0; $i < $nb; ++$i) { for ($i = 0; $i < $nb; ++$i) {
$personId = \array_pop($this->personIds)['id']; $personId = \array_pop($this->personIds)['id'];

View File

@@ -240,6 +240,7 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord
protected UserRepository $userRepository, protected UserRepository $userRepository,
protected GenderRepository $genderRepository, protected GenderRepository $genderRepository,
) { ) {
mt_srand(123456789);
$this->faker = Factory::create('fr_FR'); $this->faker = Factory::create('fr_FR');
$this->faker->addProvider($this); $this->faker->addProvider($this);
$this->loader = new NativeLoader($this->faker); $this->loader = new NativeLoader($this->faker);
@@ -273,7 +274,7 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord
$this->cacheCountries = $this->countryRepository->findAll(); $this->cacheCountries = $this->countryRepository->findAll();
} }
if (\random_int(0, 100) > $nullPercentage) { if (\mt_rand(0, 100) > $nullPercentage) {
return null; return null;
} }
@@ -289,7 +290,7 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord
$this->cacheGenders = $this->genderRepository->findByActiveOrdered(); $this->cacheGenders = $this->genderRepository->findByActiveOrdered();
} }
if (\random_int(0, 100) > $nullPercentage) { if (\mt_rand(0, 100) > $nullPercentage) {
return null; return null;
} }
@@ -307,7 +308,7 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord
$this->cacheMaritalStatuses = $this->maritalStatusRepository->findAll(); $this->cacheMaritalStatuses = $this->maritalStatusRepository->findAll();
} }
if (\random_int(0, 100) > $nullPercentage) { if (\mt_rand(0, 100) > $nullPercentage) {
return null; return null;
} }
@@ -352,7 +353,7 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord
$accompanyingPeriod = new AccompanyingPeriod( $accompanyingPeriod = new AccompanyingPeriod(
(new \DateTime()) (new \DateTime())
->sub( ->sub(
new \DateInterval('P'.\random_int(0, 180).'D') new \DateInterval('P'.\mt_rand(0, 180).'D')
) )
); );
$accompanyingPeriod->setCreatedBy($this->getRandomUser()) $accompanyingPeriod->setCreatedBy($this->getRandomUser())
@@ -360,7 +361,7 @@ class LoadPeople extends AbstractFixture implements ContainerAwareInterface, Ord
$person->addAccompanyingPeriod($accompanyingPeriod); $person->addAccompanyingPeriod($accompanyingPeriod);
$accompanyingPeriod->addSocialIssue($this->getRandomSocialIssue()); $accompanyingPeriod->addSocialIssue($this->getRandomSocialIssue());
if (\random_int(0, 10) > 3) { if (\mt_rand(0, 10) > 3) {
// always add social scope: // always add social scope:
$accompanyingPeriod->addScope($this->getReference('scope_social', Scope::class)); $accompanyingPeriod->addScope($this->getReference('scope_social', Scope::class));
$origin = $this->getReference(LoadAccompanyingPeriodOrigin::ACCOMPANYING_PERIOD_ORIGIN, AccompanyingPeriod\Origin::class); $origin = $this->getReference(LoadAccompanyingPeriodOrigin::ACCOMPANYING_PERIOD_ORIGIN, AccompanyingPeriod\Origin::class);

View File

@@ -25,7 +25,10 @@ class LoadRelationships extends Fixture implements DependentFixtureInterface
{ {
use PersonRandomHelper; use PersonRandomHelper;
public function __construct(private readonly EntityManagerInterface $em) {} public function __construct(private readonly EntityManagerInterface $em)
{
mt_srand(123456789);
}
public function getDependencies(): array public function getDependencies(): array
{ {
@@ -47,8 +50,8 @@ class LoadRelationships extends Fixture implements DependentFixtureInterface
->setFromPerson($this->getRandomPerson($this->em)) ->setFromPerson($this->getRandomPerson($this->em))
->setToPerson($this->getRandomPerson($this->em)) ->setToPerson($this->getRandomPerson($this->em))
->setRelation($this->getReference(LoadRelations::RELATION_KEY. ->setRelation($this->getReference(LoadRelations::RELATION_KEY.
random_int(0, \count(LoadRelations::RELATIONS) - 1), Relation::class)) mt_rand(0, \count(LoadRelations::RELATIONS) - 1), Relation::class))
->setReverse((bool) random_int(0, 1)) ->setReverse((bool) mt_rand(0, 1))
->setCreatedBy($user) ->setCreatedBy($user)
->setUpdatedBy($user) ->setUpdatedBy($user)
->setCreatedAt($date) ->setCreatedAt($date)

View File

@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Export\DataTransformerInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Repository\CenterRepositoryInterface;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
final readonly class ReferrerMainCenterAggregator implements AggregatorInterface, DataTransformerInterface
{
private const P = 'acp_agg_referrer_main_center';
public function __construct(
private CenterRepositoryInterface $centerRepository,
private RollingDateConverterInterface $rollingDateConverter,
) {}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void
{
$p = self::P;
$qb
->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';
}
}

View File

@@ -64,7 +64,7 @@ final readonly class CenterAggregator implements AggregatorInterface
{ {
return function (int|string|null $value) { return function (int|string|null $value) {
if (null === $value || '' === $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) { if ('_header' === $value) {

View File

@@ -0,0 +1,155 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Export\ExportGenerationContext;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Repository\CenterRepositoryInterface;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod\UserHistory;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Contracts\Translation\TranslatableInterface;
/**
* Filter accompanying periods by the main center of their referrer (at a given date).
*/
final readonly class ReferrerMainCenterFilter implements FilterInterface
{
private const UH = 'acp_referrer_main_center_filter_uh';
private const DATE_PARAM_SINCE = 'acp_referrer_main_center_filter_date_since';
private const DATE_PARAM_UNTIL = 'acp_referrer_main_center_filter_date_until';
private const CENTER_PARAM = 'acp_referrer_main_center_filter_center';
public function __construct(
private RollingDateConverterInterface $rollingDateConverter,
private CenterRepositoryInterface $centerRepository,
) {}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void
{
$dql = 'SELECT 1 FROM '.UserHistory::class.' '.self::UH.'
JOIN '.self::UH.'.user '.self::UH.'_user
WHERE acp = '.self::UH.'.accompanyingPeriod
AND (
'.self::UH.'.startDate < :'.self::DATE_PARAM_UNTIL.'
AND (
'.self::UH.'.endDate IS NULL OR '.self::UH.'.endDate >= :'.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');
}
}

View File

@@ -27,7 +27,7 @@
<p>{{ $t("courselocation.sure_description") }}</p> <p>{{ $t("courselocation.sure_description") }}</p>
</template> </template>
<template #footer> <template #footer>
<button class="btn btn-danger" @click="assignAddress"> <button class="btn btn-submit" @click="assignAddress">
{{ $t("courselocation.ok") }} {{ $t("courselocation.ok") }}
</button> </button>
</template> </template>

View File

@@ -55,6 +55,9 @@ final class RelationshipApiControllerTest extends WebTestCase
public static function personProvider(): array public static function personProvider(): array
{ {
// fix a seed to avoid random errors
mt_srand(1234588755);
self::bootKernel(); self::bootKernel();
$em = self::getContainer()->get(EntityManagerInterface::class); $em = self::getContainer()->get(EntityManagerInterface::class);
$personIdHavingRelation = $em->createQueryBuilder() $personIdHavingRelation = $em->createQueryBuilder()
@@ -116,6 +119,9 @@ final class RelationshipApiControllerTest extends WebTestCase
public static function relationProvider(): array public static function relationProvider(): array
{ {
// fix a seed to avoid random errors
mt_srand(1234588755);
self::bootKernel(); self::bootKernel();
$em = self::getContainer()->get(EntityManagerInterface::class); $em = self::getContainer()->get(EntityManagerInterface::class);
$personIdWithoutRelations = $em->createQueryBuilder() $personIdWithoutRelations = $em->createQueryBuilder()
@@ -144,6 +150,8 @@ final class RelationshipApiControllerTest extends WebTestCase
->findAll(); ->findAll();
} }
return self::$relations[\array_rand(self::$relations)]; $keys = array_keys(self::$relations);
return self::$relations[mt_rand(0, \count($keys) - 1)];
} }
} }

View File

@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Tests\Export\Aggregator\AccompanyingCourseAggregators;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\ReferrerMainCenterAggregator;
use Doctrine\ORM\EntityManagerInterface;
/**
* @internal
*
* @covers \Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\ReferrerMainCenterAggregator
*/
final class ReferrerMainCenterAggregatorTest extends AbstractAggregatorTest
{
private ReferrerMainCenterAggregator $aggregator;
protected function setUp(): void
{
self::bootKernel();
$this->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'),
];
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Tests\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Test\Export\AbstractFilterTest;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\ReferrerMainCenterFilter;
use Doctrine\ORM\EntityManagerInterface;
/**
* @internal
*
* @coversNothing
*/
final class ReferrerMainCenterFilterTest extends AbstractFilterTest
{
private ReferrerMainCenterFilter $filter;
protected function setUp(): void
{
self::bootKernel();
$this->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');
}
}

View File

@@ -104,6 +104,10 @@ services:
tags: tags:
- { name: chill.export_filter, alias: accompanyingcourse_referrer_filter_between_dates } - { 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: chill.person.export.filter_openbetweendates:
class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\OpenBetweenDatesFilter class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\OpenBetweenDatesFilter
tags: tags:
@@ -270,3 +274,7 @@ services:
Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\PersonParticipatingAggregator: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\PersonParticipatingAggregator:
tags: tags:
- { name: chill.export_aggregator, alias: accompanyingcourse_person_part_aggregator } - { 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 }

View File

@@ -24,6 +24,14 @@ accompanying_period:
number: >- number: >-
n° {id} 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: person:
from_the: depuis le from_the: depuis le
And himself: >- And himself: >-
@@ -33,6 +41,13 @@ person:
neutral {et lui·elle-même} neutral {et lui·elle-même}
other {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:
Household: Ménage Household: Ménage

View File

@@ -105,9 +105,18 @@ Administrative status: Situation administrative
person: person:
# trans key according to new conventions # trans key according to new conventions
export: export:
aggregator: period:
by_center: aggregator:
no_center: Sans territoire 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 Identifiers: Identifiants
@@ -453,8 +462,8 @@ Filtered by entrusted child status: Uniquement les usagers qui sont "enfant conf
Filter by nomadic status: Filtrer les usagers "gens du voyage" Filter by nomadic status: Filtrer les usagers "gens du voyage"
Filtered by nomadic status: Uniquement les usagers qui sont "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 "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 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 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 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 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

View File

@@ -22,6 +22,11 @@ use Doctrine\Persistence\ObjectManager;
*/ */
class LoadCustomField extends AbstractFixture implements OrderedFixtureInterface class LoadCustomField extends AbstractFixture implements OrderedFixtureInterface
{ {
public function __construct()
{
mt_srand(123456789);
}
public function getOrder(): int public function getOrder(): int
{ {
return 15001; return 15001;
@@ -67,15 +72,15 @@ class LoadCustomField extends AbstractFixture implements OrderedFixtureInterface
]; ];
for ($i = 0; 25 >= $i; ++$i) { 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()) $customField = (new CustomField())
->setSlug("cf_report_{$i}") ->setSlug("cf_report_{$i}")
->setType($cFType['type']) ->setType($cFType['type'])
->setOptions($cFType['options']) ->setOptions($cFType['options'])
->setName(['fr' => "CustomField {$i}"]) ->setName(['fr' => "CustomField {$i}"])
->setOrdering(random_int(0, 1000) / 1000) ->setOrdering(mt_rand(0, 1000) / 1000)
->setCustomFieldsGroup($this->getReference('cf_group_report_'.random_int(0, 3), CustomFieldsGroup::class)); ->setCustomFieldsGroup($this->getReference('cf_group_report_'.mt_rand(0, 3), CustomFieldsGroup::class));
$manager->persist($customField); $manager->persist($customField);
} }

View File

@@ -35,6 +35,7 @@ final class LoadReports extends AbstractFixture implements OrderedFixtureInterfa
public function __construct( public function __construct(
private readonly EntityManagerInterface $entityManager, private readonly EntityManagerInterface $entityManager,
) { ) {
mt_srand(123456789);
$this->faker = FakerFactory::create('fr_FR'); $this->faker = FakerFactory::create('fr_FR');
} }
@@ -83,7 +84,7 @@ final class LoadReports extends AbstractFixture implements OrderedFixtureInterfa
$report = (new Report()) $report = (new Report())
->setPerson($person) ->setPerson($person)
->setCFGroup( ->setCFGroup(
random_int(0, 10) > 5 ? mt_rand(0, 10) > 5 ?
$this->getReference('cf_group_report_logement', CustomFieldsGroup::class) : $this->getReference('cf_group_report_logement', CustomFieldsGroup::class) :
$this->getReference('cf_group_report_education', 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 // set date. 30% of the dates are 2015-05-01
$expectedDate = new \DateTime('2015-01-05'); $expectedDate = new \DateTime('2015-01-05');
if (random_int(0, 100) < 30) { if (mt_rand(0, 100) < 30) {
$report->setDate($expectedDate); $report->setDate($expectedDate);
} else { } else {
$report->setDate($this->faker->dateTimeBetween('-1 year', 'now') $report->setDate($this->faker->dateTimeBetween('-1 year', 'now')
@@ -150,7 +151,7 @@ final class LoadReports extends AbstractFixture implements OrderedFixtureInterfa
$selectedPeople = []; $selectedPeople = [];
foreach ($people as $person) { foreach ($people as $person) {
if (random_int(0, 100) < $percentage) { if (mt_rand(0, 100) < $percentage) {
$selectedPeople[] = $person; $selectedPeople[] = $person;
} }
} }
@@ -178,7 +179,7 @@ final class LoadReports extends AbstractFixture implements OrderedFixtureInterfa
$picked = []; $picked = [];
if ($multiple) { if ($multiple) {
$numberSelected = random_int(1, \count($choices) - 1); $numberSelected = mt_rand(1, \count($choices) - 1);
for ($i = 0; $i < $numberSelected; ++$i) { for ($i = 0; $i < $numberSelected; ++$i) {
$picked[] = $this->pickChoice($choices); $picked[] = $this->pickChoice($choices);

View File

@@ -30,6 +30,7 @@ class LoadThirdParty extends Fixture implements DependentFixtureInterface
public function __construct() public function __construct()
{ {
mt_srand(123456789);
$this->phoneNumberUtil = PhoneNumberUtil::getInstance(); $this->phoneNumberUtil = PhoneNumberUtil::getInstance();
} }
@@ -68,7 +69,7 @@ class LoadThirdParty extends Fixture implements DependentFixtureInterface
static fn ($a) => $a['ref'], static fn ($a) => $a['ref'],
LoadCenters::$centers LoadCenters::$centers
); );
$number = random_int(1, \count($references)); $number = mt_rand(1, \count($references));
if (1 === $number) { if (1 === $number) {
yield $this->getReference($references[array_rand($references)], Center::class); yield $this->getReference($references[array_rand($references)], Center::class);

View File

@@ -129,11 +129,11 @@ export:
thirdParties: Tiers intervenant thirdParties: Tiers intervenant
# exports filters/aggregators # 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 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 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 addresse de résidence chez un tiers de catégorie %thirdparty_type% et valide sur la date %date_calc%" "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
admin: admin: