From fb1c34f9c141851cc503b3b295d2bb6d83ba4c88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 25 Apr 2025 13:07:52 +0200 Subject: [PATCH] Add UserGroup and UserJob synchronization feature Implement UserGroupRelatedToUserJobSync to manage associations between UserGroups and UserJobs, including creating, updating, and removing relationships. Introduce a cron job to automate the synchronization process and add tests to ensure functionality. Update translations and repository logic as part of the implementation. --- .../Repository/UserJobRepository.php | 21 +++- .../Repository/UserJobRepositoryInterface.php | 9 +- .../UserGroupRelatedToUserJobSync.php | 114 ++++++++++++++++++ .../UserGroupRelatedToUserJobSyncCronJob.php | 42 +++++++ ...UserGroupRelatedToUserJobSyncInterface.php | 20 +++ ...erGroupRelatedToUserJobSyncCronJobTest.php | 58 +++++++++ .../translations/messages+intl-icu.fr.yaml | 1 + 7 files changed, 256 insertions(+), 9 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Service/UserGroup/UserGroupRelatedToUserJobSync.php create mode 100644 src/Bundle/ChillMainBundle/Service/UserGroup/UserGroupRelatedToUserJobSyncCronJob.php create mode 100644 src/Bundle/ChillMainBundle/Service/UserGroup/UserGroupRelatedToUserJobSyncInterface.php create mode 100644 src/Bundle/ChillMainBundle/Tests/Services/UserGroup/UserGroupRelatedToUserJobSyncCronJobTest.php diff --git a/src/Bundle/ChillMainBundle/Repository/UserJobRepository.php b/src/Bundle/ChillMainBundle/Repository/UserJobRepository.php index 3acd0be67..ff8e5ffd2 100644 --- a/src/Bundle/ChillMainBundle/Repository/UserJobRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/UserJobRepository.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Repository; +use Chill\MainBundle\Entity\UserGroup; use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Doctrine\ORM\EntityManagerInterface; @@ -53,12 +54,20 @@ readonly class UserJobRepository implements UserJobRepositoryInterface return $jobs; } - /** - * @param mixed|null $limit - * @param mixed|null $offset - * - * @return array|object[]|UserJob[] - */ + public function findAllNotAssociatedWithUserGroup(): array + { + $qb = $this->repository->createQueryBuilder('u'); + $qb->select('u'); + + $qb->where( + $qb->expr()->not( + $qb->expr()->exists(sprintf('SELECT 1 FROM %s ug WHERE ug.userJob = u', UserGroup::class)) + ) + ); + + return $qb->getQuery()->getResult(); + } + public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null) { return $this->repository->findBy($criteria, $orderBy, $limit, $offset); diff --git a/src/Bundle/ChillMainBundle/Repository/UserJobRepositoryInterface.php b/src/Bundle/ChillMainBundle/Repository/UserJobRepositoryInterface.php index c082daf94..df042101c 100644 --- a/src/Bundle/ChillMainBundle/Repository/UserJobRepositoryInterface.php +++ b/src/Bundle/ChillMainBundle/Repository/UserJobRepositoryInterface.php @@ -33,11 +33,14 @@ interface UserJobRepositoryInterface extends ObjectRepository public function findAllOrderedByName(): array; /** - * @param mixed|null $limit - * @param mixed|null $offset + * Find all the user job which are not related to a UserGroup. * - * @return array|object[]|UserJob[] + * This is useful for synchronizing UserGroups with jobs. + * + * @return list */ + public function findAllNotAssociatedWithUserGroup(): array; + public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null); public function findOneBy(array $criteria): ?UserJob; diff --git a/src/Bundle/ChillMainBundle/Service/UserGroup/UserGroupRelatedToUserJobSync.php b/src/Bundle/ChillMainBundle/Service/UserGroup/UserGroupRelatedToUserJobSync.php new file mode 100644 index 000000000..d54f089d2 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Service/UserGroup/UserGroupRelatedToUserJobSync.php @@ -0,0 +1,114 @@ +createNotExistingUserGroups(); + + $connection = $this->entityManager->getConnection(); + $stats = $connection->transactional(function (Connection $connection) { + $removed = $this->removeUserNotRelatedToJob($connection); + $created = $this->createNewAssociations($connection); + + return ['association_removed' => $removed, 'association_created' => $created]; + }); + + $fullStats = ['userjob_created' => $created, ...$stats]; + + $this->logger->info(self::LOG_PREFIX.'Executed synchronisation', $fullStats); + + return $fullStats; + } + + private function createNotExistingUserGroups(): int + { + $jobs = $this->userJobRepository->findAllNotAssociatedWithUserGroup(); + $counter = 0; + + foreach ($jobs as $job) { + $userGroup = new UserGroup(); + $userGroup->setUserJob($job); + $userGroup->setLabel( + [ + $this->translator->getLocale() => $this->translator->trans( + 'user_group.label_related_to_user_job', + ['job' => $this->translatableStringHelper->localize($job->getLabel())] + )] + ); + $userGroup->setBackgroundColor('#e5a50a')->setForegroundColor('#f6f5f4'); + + $this->entityManager->persist($userGroup); + $this->logger->info(self::LOG_PREFIX.'Will create user group', ['job' => $this->translatableStringHelper->localize($userGroup->getLabel())]); + ++$counter; + } + + $this->entityManager->flush(); + + return $counter; + } + + private function removeUserNotRelatedToJob(Connection $connection): int + { + $sql = <<<'SQL' + DELETE FROM chill_main_user_group_user + USING users AS u, chill_main_user_group ug, chill_main_user_job_history jh + WHERE + chill_main_user_group_user.usergroup_id = ug.id + AND chill_main_user_group_user.user_id = u.id + AND jh.user_id = u.id AND tsrange(jh.startdate, jh.enddate) @> localtimestamp + -- only when the user's jobid is different than the user_group id + AND ug.userjob_id IS NOT NULL + AND jh.job_id <> ug.userjob_id + SQL; + + $result = $connection->executeQuery($sql); + + return $result->rowCount(); + } + + private function createNewAssociations(Connection $connection): int + { + $sql = <<<'SQL' + INSERT INTO chill_main_user_group_user (usergroup_id, user_id) + SELECT cmug.id, jh.user_id + FROM chill_main_user_group cmug + JOIN chill_main_user_job_history jh ON jh.job_id = cmug.userjob_id AND tsrange(jh.startdate, jh.enddate) @> localtimestamp + WHERE cmug.userjob_id IS NOT NULL + ON CONFLICT DO NOTHING + SQL; + + $result = $connection->executeQuery($sql); + + return $result->rowCount(); + } +} diff --git a/src/Bundle/ChillMainBundle/Service/UserGroup/UserGroupRelatedToUserJobSyncCronJob.php b/src/Bundle/ChillMainBundle/Service/UserGroup/UserGroupRelatedToUserJobSyncCronJob.php new file mode 100644 index 000000000..673578f80 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Service/UserGroup/UserGroupRelatedToUserJobSyncCronJob.php @@ -0,0 +1,42 @@ +getLastStart() < $this->clock->now()->sub(new \DateInterval('P1D')); + } + + public function getKey(): string + { + return self::KEY; + } + + public function run(array $lastExecutionData): ?array + { + return ($this->userJobSync)(); + } +} diff --git a/src/Bundle/ChillMainBundle/Service/UserGroup/UserGroupRelatedToUserJobSyncInterface.php b/src/Bundle/ChillMainBundle/Service/UserGroup/UserGroupRelatedToUserJobSyncInterface.php new file mode 100644 index 000000000..e596cafc3 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Service/UserGroup/UserGroupRelatedToUserJobSyncInterface.php @@ -0,0 +1,20 @@ +prophesize(UserGroupRelatedToUserJobSyncInterface::class); + + $cronJob = new UserGroupRelatedToUserJobSyncCronJob($clock, $job->reveal()); + + if (null !== $lastStartExecution) { + $lastExecution = new CronJobExecution('user-group-related-to-user-job-sync'); + $lastExecution->setLastStart($lastStartExecution); + } + + $actual = $cronJob->canRun($lastExecution ?? null); + + self::assertEquals($exected, $actual); + } + + public static function canRunDataProvider(): iterable + { + $now = new \DateTimeImmutable('2025-04-27T00:00:00Z'); + + yield 'never executed' => [$now, null, true]; + yield 'executed 12 hours ago' => [$now, new \DateTimeImmutable('2025-04-26T12:00:00Z'), false]; + yield 'executed more than 12 hours ago' => [$now, new \DateTimeImmutable('2025-04-25T12:00:00Z'), true]; + } +} diff --git a/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml b/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml index e86484114..2982d94db 100644 --- a/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml +++ b/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml @@ -15,6 +15,7 @@ user_group: } user_removed: L'utilisateur {user} est enlevé du groupe {user_group} avec succès user_added: L'utilisateur {user} est ajouté groupe {user_group} avec succès + label_related_to_user_job: Groupe {job} (Groupe métier) notification: My notifications with counter: >-