Compare commits

...

17 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
48 changed files with 1408 additions and 116 deletions

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

@@ -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,23 @@ 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 ## v4.13.0 - 2026-02-23
### Feature ### 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 * ([#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

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

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

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

@@ -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,
) {} ) {}
@@ -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;
} }

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

@@ -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,5 +1,9 @@
{% apply markdown_to_html %} {% 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:
@@ -9,7 +13,12 @@
> {{ line }} > {{ line }}
{% endfor %} {% 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)) }}) [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 %}
----- -----

View File

@@ -1,4 +1,8 @@
{% 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:
@@ -8,7 +12,11 @@ Titre de la notification: {{ notification.title }}
> {{ line|raw }} > {{ line|raw }}
{% endfor %} {% endfor %}
Vous pouvez visualiser la notification et y répondre ici: {{ absolute_url(path('chill_main_notification_show', {'_locale': dest.locale, 'id': notification.id }, false)) }}. {% if dest.isUser %}
[Vous pouvez visualiser la notification et y répondre ici.]({{ absolute_url(path('chill_main_notification_show', {'_locale': dest.locale, 'id': notification.id }, false)) }})
{% else %}
[Vous pouvez visualiser la notification et y répondre ici.]({{ absolute_url(path('chill_main_notification_grant_access_by_access_key', {'_locale': dest.locale, 'id': notification.id, 'accessKey': notification.accessKey }, false)) }})
{% endif %}
-- --
Le logiciel Chill 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

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

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

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

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);