From 5a94ce49f1d25bc6f8f2fccf4e8b76d40b2a7109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 13 Oct 2022 15:22:48 +0200 Subject: [PATCH 01/13] [Feature] add active property to evaluation --- .../SocialWorkEvaluationApiController.php | 7 +- .../Entity/SocialWork/Evaluation.php | 17 ++++ .../Form/SocialWork/EvaluationType.php | 9 ++ .../SocialWork/EvaluationRepository.php | 8 +- .../EvaluationRepositoryInterface.php | 45 ++++++++++ .../SocialWorkEvaluationApiControllerTest.php | 86 +++++++++++++++++++ .../migrations/Version20221013131221.php | 33 +++++++ 7 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 src/Bundle/ChillPersonBundle/Repository/SocialWork/EvaluationRepositoryInterface.php create mode 100644 src/Bundle/ChillPersonBundle/Tests/Controller/SocialWorkEvaluationApiControllerTest.php create mode 100644 src/Bundle/ChillPersonBundle/migrations/Version20221013131221.php diff --git a/src/Bundle/ChillPersonBundle/Controller/SocialWorkEvaluationApiController.php b/src/Bundle/ChillPersonBundle/Controller/SocialWorkEvaluationApiController.php index 41ca55d0e..bd12ec8c2 100644 --- a/src/Bundle/ChillPersonBundle/Controller/SocialWorkEvaluationApiController.php +++ b/src/Bundle/ChillPersonBundle/Controller/SocialWorkEvaluationApiController.php @@ -13,6 +13,7 @@ namespace Chill\PersonBundle\Controller; use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Serializer\Model\Collection; +use Chill\PersonBundle\Entity\SocialWork\Evaluation; use Chill\PersonBundle\Entity\SocialWork\SocialAction; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -39,9 +40,11 @@ class SocialWorkEvaluationApiController extends AbstractController */ public function listEvaluationBySocialAction(SocialAction $action): Response { - $pagination = $this->paginatorFactory->create($action->getEvaluations()->count()); + $evaluations = $action->getEvaluations()->filter(static fn (Evaluation $eval) => $eval->isActive()); - $evaluations = $action->getEvaluations()->slice( + $pagination = $this->paginatorFactory->create($evaluations->count()); + + $evaluations = $evaluations->slice( $pagination->getCurrentPageFirstItemNumber(), $pagination->getItemsPerPage() ); diff --git a/src/Bundle/ChillPersonBundle/Entity/SocialWork/Evaluation.php b/src/Bundle/ChillPersonBundle/Entity/SocialWork/Evaluation.php index 3324a236f..08dc0f425 100644 --- a/src/Bundle/ChillPersonBundle/Entity/SocialWork/Evaluation.php +++ b/src/Bundle/ChillPersonBundle/Entity/SocialWork/Evaluation.php @@ -26,6 +26,11 @@ use Symfony\Component\Serializer\Annotation as Serializer; */ class Evaluation { + /** + * @ORM\Column(type="boolean", nullable=false, options={"default": true}) + */ + private bool $active = true; + /** * @ORM\Column(type="dateinterval", nullable=true, options={"default": null}) * @Serializer\Groups({"read"}) @@ -114,6 +119,11 @@ class Evaluation return $this->url; } + public function isActive(): bool + { + return $this->active; + } + /** * @return $this * @@ -128,6 +138,13 @@ class Evaluation return $this; } + public function setActive(bool $active): Evaluation + { + $this->active = $active; + + return $this; + } + public function setDelay(?DateInterval $delay): self { $this->delay = $delay; diff --git a/src/Bundle/ChillPersonBundle/Form/SocialWork/EvaluationType.php b/src/Bundle/ChillPersonBundle/Form/SocialWork/EvaluationType.php index 685915203..668a00276 100644 --- a/src/Bundle/ChillPersonBundle/Form/SocialWork/EvaluationType.php +++ b/src/Bundle/ChillPersonBundle/Form/SocialWork/EvaluationType.php @@ -16,6 +16,7 @@ use Chill\MainBundle\Form\Type\TranslatableStringFormType; use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\PersonBundle\Entity\SocialWork\Evaluation; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\UrlType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -52,6 +53,14 @@ class EvaluationType extends AbstractType ->add('notificationDelay', DateIntervalType::class, [ 'label' => 'evaluation.notificationDelay', 'required' => false, + ]) + ->add('active', ChoiceType::class, [ + 'label' => 'active', + 'choices' => [ + 'active' => true, + 'inactive' => false, + ], + 'required' => true, ]); } diff --git a/src/Bundle/ChillPersonBundle/Repository/SocialWork/EvaluationRepository.php b/src/Bundle/ChillPersonBundle/Repository/SocialWork/EvaluationRepository.php index b02fef8ba..925d4598b 100644 --- a/src/Bundle/ChillPersonBundle/Repository/SocialWork/EvaluationRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/SocialWork/EvaluationRepository.php @@ -14,9 +14,8 @@ namespace Chill\PersonBundle\Repository\SocialWork; use Chill\PersonBundle\Entity\SocialWork\Evaluation; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; -use Doctrine\Persistence\ObjectRepository; -final class EvaluationRepository implements ObjectRepository +final class EvaluationRepository implements EvaluationRepositoryInterface { private EntityRepository $repository; @@ -38,6 +37,11 @@ final class EvaluationRepository implements ObjectRepository return $this->repository->findAll(); } + public function findAllActive(): array + { + return $this->findBy(['active' => true]); + } + /** * @param mixed|null $limit * @param mixed|null $offset diff --git a/src/Bundle/ChillPersonBundle/Repository/SocialWork/EvaluationRepositoryInterface.php b/src/Bundle/ChillPersonBundle/Repository/SocialWork/EvaluationRepositoryInterface.php new file mode 100644 index 000000000..9ca390ff9 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Repository/SocialWork/EvaluationRepositoryInterface.php @@ -0,0 +1,45 @@ + + */ + public function findAll(): array; + + /** + * @return array + */ + public function findAllActive(): array; + + /** + * @param mixed|null $limit + * @param mixed|null $offset + * + * @return array + */ + public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array; + + public function findOneBy(array $criteria, ?array $orderBy = null): ?Evaluation; + + /** + * @return class-string + */ + public function getClassName(): string; +} diff --git a/src/Bundle/ChillPersonBundle/Tests/Controller/SocialWorkEvaluationApiControllerTest.php b/src/Bundle/ChillPersonBundle/Tests/Controller/SocialWorkEvaluationApiControllerTest.php new file mode 100644 index 000000000..aa98fdcd3 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Controller/SocialWorkEvaluationApiControllerTest.php @@ -0,0 +1,86 @@ +evaluationToReset) { + return; + } + + self::bootKernel(); + + $em = self::$container->get(EntityManagerInterface::class); + $evaluation = $em->find(Evaluation::class, $this->evaluationToReset->getId()); + + $evaluation->setActive(true); + $em->flush(); + } + + public function dataGenerateSocialActionWithEvaluations(): iterable + { + self::bootKernel(); + + $this->em = self::$container->get(EntityManagerInterface::class); + + /** @var SocialAction $socialAction */ + $socialAction = $this->em->createQuery( + 'SELECT s FROM ' . SocialAction::class . ' s WHERE SIZE(s.evaluations) >= 2' + ) + ->setMaxResults(1) + ->getSingleResult(); + + // set the first evaluation as inactive and save + $this->evaluationToReset = $socialAction->getEvaluations()->first(); + $this->evaluationToReset->setActive(false); + + $this->em->flush(); + + yield [$socialAction, $this->evaluationToReset]; + } + + /** + * @dataProvider dataGenerateSocialActionWithEvaluations + */ + public function testListEvaluationBySocialAction(SocialAction $action, Evaluation $inactiveEvaluation): void + { + $client = $this->getClientAuthenticated(); + + $client->request('GET', sprintf('/api/1.0/person/social-work/evaluation/by-social-action/%d.json', $action->getId())); + + $this->assertResponseIsSuccessful(); + + $content = json_decode($client->getResponse()->getContent(), true); + + $ids = array_map(static fn (array $item) => $item['id'], $content['results']); + + $this->assertNotContains($inactiveEvaluation->getId(), $ids); + } +} diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20221013131221.php b/src/Bundle/ChillPersonBundle/migrations/Version20221013131221.php new file mode 100644 index 000000000..e8cb0d392 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/migrations/Version20221013131221.php @@ -0,0 +1,33 @@ +addSql('ALTER TABLE chill_person_social_work_evaluation DROP active'); + } + + public function getDescription(): string + { + return 'Add an active column on evaluation'; + } + + public function up(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_person_social_work_evaluation ADD active BOOLEAN DEFAULT true NOT NULL'); + } +} From 75f6c8754edb87aa8829897e40e6f0da7c5d9ee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 13 Oct 2022 15:32:09 +0200 Subject: [PATCH 02/13] Feature: [export][acp] filter by acp which have an evaluation: use select2 --- .../EvaluationFilter.php | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/EvaluationFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/EvaluationFilter.php index ec4a5abe7..996ff36e3 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/EvaluationFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/EvaluationFilter.php @@ -15,7 +15,7 @@ use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\PersonBundle\Entity\SocialWork\Evaluation; use Chill\PersonBundle\Export\Declarations; -use Doctrine\ORM\Query\Expr\Andx; +use Chill\PersonBundle\Repository\SocialWork\EvaluationRepositoryInterface; use Doctrine\ORM\QueryBuilder; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\FormBuilderInterface; @@ -23,11 +23,15 @@ use function in_array; class EvaluationFilter implements FilterInterface { + private EvaluationRepositoryInterface $evaluationRepository; + private TranslatableStringHelper $translatableStringHelper; public function __construct( + EvaluationRepositoryInterface $evaluationRepository, TranslatableStringHelper $translatableStringHelper ) { + $this->evaluationRepository = $evaluationRepository; $this->translatableStringHelper = $translatableStringHelper; } @@ -50,16 +54,8 @@ class EvaluationFilter implements FilterInterface $qb->join('workeval.evaluation', 'eval'); } - $where = $qb->getDQLPart('where'); $clause = $qb->expr()->in('eval.id', ':evaluations'); - - if ($where instanceof Andx) { - $where->add($clause); - } else { - $where = $qb->expr()->andX($clause); - } - - $qb->add('where', $where); + $qb->andWhere($clause); $qb->setParameter('evaluations', $data['accepted_evaluations']); } @@ -72,11 +68,13 @@ class EvaluationFilter implements FilterInterface { $builder->add('accepted_evaluations', EntityType::class, [ 'class' => Evaluation::class, + 'choices' => $this->evaluationRepository->findAllActive(), 'choice_label' => function (Evaluation $ev) { return $this->translatableStringHelper->localize($ev->getTitle()); }, 'multiple' => true, - 'expanded' => true, + 'expanded' => false, + 'attr' => ['class' => 'select2'], ]); } From 66f282e221a6e4fc4393b41771e5f08734fc31c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 13 Oct 2022 16:50:21 +0200 Subject: [PATCH 03/13] DX: fix an accompanying period might be null in an AccompanyingPeriodComment --- .../ChillPersonBundle/Entity/AccompanyingPeriod/Comment.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Comment.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Comment.php index 9054aba5f..592c833d3 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Comment.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/Comment.php @@ -36,7 +36,7 @@ class Comment implements TrackCreationInterface, TrackUpdateInterface * inversedBy="comments") * @ORM\JoinColumn(nullable=false, onDelete="CASCADE") */ - private ?AccompanyingPeriod $accompanyingPeriod; + private ?AccompanyingPeriod $accompanyingPeriod = null; /** * @ORM\Column(type="text") From 59e21b68195540eefe5f789f373cfc9105db7fd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 14 Oct 2022 14:36:40 +0200 Subject: [PATCH 04/13] Feature: [acp] record the step history of each accompanying period Each time a step is changed on an history, a record is stored in a dedicated table. When the acp's opening date is moved, the first row is adapted to match the new opening's date. This mechanisme does not work if the opening date is move beyon the first end date (if any), nor on the closing date. --- .../Entity/AccompanyingPeriod.php | 77 +++++++++++- .../AccompanyingPeriodStepHistory.php | 115 ++++++++++++++++++ .../PersonFilters/MaritalStatusFilter.php | 27 +--- .../Tests/Entity/AccompanyingPeriodTest.php | 32 +++++ .../migrations/Version20221014115500.php | 69 +++++++++++ 5 files changed, 293 insertions(+), 27 deletions(-) create mode 100644 src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodStepHistory.php create mode 100644 src/Bundle/ChillPersonBundle/migrations/Version20221014115500.php diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php index e2114f05d..d3310fe15 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php @@ -22,6 +22,7 @@ use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\UserJob; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodLocationHistory; +use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodStepHistory; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; use Chill\PersonBundle\Entity\AccompanyingPeriod\ClosingMotive; use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment; @@ -336,6 +337,12 @@ class AccompanyingPeriod implements */ private string $step = self::STEP_DRAFT; + /** + * @ORM\OneToMany(targetEntity=AccompanyingPeriodStepHistory::class, + * mappedBy="period", cascade={"persist", "remove"}, orphanRemoval=true) + */ + private Collection $stepHistories; + /** * @ORM\Column(type="datetime", nullable=true, options={"default": NULL}) */ @@ -390,7 +397,6 @@ class AccompanyingPeriod implements */ public function __construct(?DateTime $dateOpening = null) { - $this->setOpeningDate($dateOpening ?? new DateTime('now')); $this->participations = new ArrayCollection(); $this->scopes = new ArrayCollection(); $this->socialIssues = new ArrayCollection(); @@ -399,6 +405,8 @@ class AccompanyingPeriod implements $this->resources = new ArrayCollection(); $this->userHistories = new ArrayCollection(); $this->locationHistories = new ArrayCollection(); + $this->stepHistories = new ArrayCollection(); + $this->setOpeningDate($dateOpening ?? new DateTime('now')); } /** @@ -966,6 +974,11 @@ class AccompanyingPeriod implements return $this->step; } + public function getStepHistories(): Collection + { + return $this->stepHistories; + } + public function getUser(): ?User { return $this->user; @@ -1234,7 +1247,11 @@ class AccompanyingPeriod implements */ public function setOpeningDate($openingDate) { - $this->openingDate = $openingDate; + if ($this->openingDate !== $openingDate) { + $this->openingDate = $openingDate; + + $this->ensureStepContinuity(); + } return $this; } @@ -1333,6 +1350,14 @@ class AccompanyingPeriod implements $this->bootPeriod(); } + if (self::STEP_DRAFT !== $this->step && $previous !== $step) { + // we create a new history + $history = new AccompanyingPeriodStepHistory(); + $history->setStep($this->step)->setStartDate(new DateTimeImmutable('now')); + + $this->addStepHistory($history); + } + return $this; } @@ -1373,6 +1398,17 @@ class AccompanyingPeriod implements return $this; } + private function addStepHistory(AccompanyingPeriodStepHistory $stepHistory): self + { + if (!$this->stepHistories->contains($stepHistory)) { + $this->stepHistories[] = $stepHistory; + $stepHistory->setPeriod($this); + $this->ensureStepContinuity(); + } + + return $this; + } + private function bootPeriod(): void { // first location history @@ -1384,6 +1420,43 @@ class AccompanyingPeriod implements $this->addLocationHistory($locationHistory); } + private function ensureStepContinuity(): void + { + // ensure continuity of histories + $criteria = new Criteria(); + $criteria->orderBy(['startDate' => Criteria::ASC, 'id' => Criteria::ASC]); + + /** @var Iterator $steps */ + $steps = $this->getStepHistories()->matching($criteria)->getIterator(); + $steps->rewind(); + + // we set the start date of the first step as the opening date, only if it is + // not greater than the end date + /** @var AccompanyingPeriodStepHistory $current */ + $current = $steps->current(); + + if (null === $current) { + return; + } + + if ($this->getOpeningDate()->format('Y-m-d') !== $current->getStartDate()->format('Y-m-d') + && ($this->getOpeningDate() <= $current->getEndDate() || null === $current->getEndDate())) { + $current->setStartDate(DateTimeImmutable::createFromMutable($this->getOpeningDate())); + } + + // then we set all the end date to the start date of the next one + do { + /** @var AccompanyingPeriodStepHistory $current */ + $current = $steps->current(); + $steps->next(); + + if ($steps->valid()) { + $next = $steps->current(); + $current->setEndDate($next->getStartDate()); + } + } while ($steps->valid()); + } + private function setRequestorPerson(?Person $requestorPerson = null): self { $this->requestorPerson = $requestorPerson; diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodStepHistory.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodStepHistory.php new file mode 100644 index 000000000..f9baffa35 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodStepHistory.php @@ -0,0 +1,115 @@ +endDate; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getPeriod(): AccompanyingPeriod + { + return $this->period; + } + + public function getStartDate(): ?DateTimeImmutable + { + return $this->startDate; + } + + public function getStep(): string + { + return $this->step; + } + + public function setEndDate(?DateTimeImmutable $endDate): self + { + $this->endDate = $endDate; + + return $this; + } + + /** + * @internal use AccompanyingPeriod::addLocationHistory + */ + public function setPeriod(AccompanyingPeriod $period): self + { + $this->period = $period; + + return $this; + } + + public function setStartDate(?DateTimeImmutable $startDate): self + { + $this->startDate = $startDate; + + return $this; + } + + public function setStep(string $step): AccompanyingPeriodStepHistory + { + $this->step = $step; + + return $this; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/MaritalStatusFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/MaritalStatusFilter.php index a3d2260e3..aad98a394 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/MaritalStatusFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/MaritalStatusFilter.php @@ -12,12 +12,9 @@ declare(strict_types=1); namespace Chill\PersonBundle\Export\Filter\PersonFilters; use Chill\MainBundle\Export\FilterInterface; -use Chill\MainBundle\Form\Type\ChillDateType; use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\PersonBundle\Entity\MaritalStatus; use Chill\PersonBundle\Export\Declarations; -use DateTime; -use Doctrine\ORM\Query\Expr\Andx; use Symfony\Bridge\Doctrine\Form\Type\EntityType; class MaritalStatusFilter implements FilterInterface @@ -37,25 +34,10 @@ class MaritalStatusFilter implements FilterInterface public function alterQuery(\Doctrine\ORM\QueryBuilder $qb, $data) { - $where = $qb->getDQLPart('where'); - - $clause = $qb->expr()->andX( - $qb->expr()->in('person.maritalStatus', ':maritalStatus'), - $qb->expr()->orX( - $qb->expr()->eq('person.maritalStatusDate', ':calc_date'), - $qb->expr()->isNull('person.maritalStatusDate') - ) + $qb->andWhere( + $qb->expr()->in('person.maritalStatus', ':maritalStatus') ); - - if ($where instanceof Andx) { - $where->add($clause); - } else { - $where = $qb->expr()->andX($clause); - } - - $qb->add('where', $where); $qb->setParameter('maritalStatus', $data['maritalStatus']); - $qb->setParameter('calc_date', $data['calc_date']); } public function applyOn() @@ -75,11 +57,6 @@ class MaritalStatusFilter implements FilterInterface 'multiple' => true, 'expanded' => true, ]); - - $builder->add('calc_date', ChillDateType::class, [ - 'label' => 'Marital status at this time', - 'data' => new DateTime(), - ]); } public function describeAction($data, $format = 'string') diff --git a/src/Bundle/ChillPersonBundle/Tests/Entity/AccompanyingPeriodTest.php b/src/Bundle/ChillPersonBundle/Tests/Entity/AccompanyingPeriodTest.php index 4c6033ca9..2f38da244 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Entity/AccompanyingPeriodTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Entity/AccompanyingPeriodTest.php @@ -29,6 +29,38 @@ use function count; */ final class AccompanyingPeriodTest extends \PHPUnit\Framework\TestCase { + public function testChangeStepKeepHistory() + { + $period = new AccompanyingPeriod(); + + $this->assertCount(0, $period->getStepHistories(), 'at initialization, period should not have any step history'); + + $period->setStep(AccompanyingPeriod::STEP_DRAFT); + + $this->assertCount(0, $period->getStepHistories(), 're applying a draft should not create a history'); + + $period->setStep(AccompanyingPeriod::STEP_CONFIRMED); + + $this->assertCount(1, $period->getStepHistories()); + $this->assertEquals(AccompanyingPeriod::STEP_CONFIRMED, $period->getStepHistories()->first()->getStep()); + + $period->setOpeningDate($aMonthAgo = new DateTime('1 month ago')); + + $this->assertCount(1, $period->getStepHistories()); + $this->assertEquals($aMonthAgo, $period->getStepHistories()->first()->getStartDate(), 'when changing the opening date, the start date of the first history should change'); + + $period->setOpeningDate($tenDaysAgo = new DateTime('10 days ago')); + + $this->assertCount(1, $period->getStepHistories()); + $this->assertEquals($tenDaysAgo, $period->getStepHistories()->first()->getStartDate(), 'when changing the opening date, the start date of the first history should change'); + + $period->setStep(AccompanyingPeriod::STEP_CLOSED); + $this->assertCount(2, $period->getStepHistories()); + + $period->setOpeningDate($tomorrow = new DateTime('tomorrow')); + $this->assertEquals($tenDaysAgo, $period->getStepHistories()->first()->getStartDate(), 'when changing the opening date to a later one and no history after, start date should change'); + } + public function testClosingEqualOpening() { $datetime = new DateTime('now'); diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20221014115500.php b/src/Bundle/ChillPersonBundle/migrations/Version20221014115500.php new file mode 100644 index 000000000..fe7b920d9 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/migrations/Version20221014115500.php @@ -0,0 +1,69 @@ +addSql('DROP SEQUENCE chill_person_accompanying_period_step_history_id_seq CASCADE'); + $this->addSql('DROP TABLE chill_person_accompanying_period_step_history'); + } + + public function getDescription(): string + { + return 'Add step history on accompanying periods'; + } + + public function up(Schema $schema): void + { + $this->addSql('CREATE SEQUENCE chill_person_accompanying_period_step_history_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE chill_person_accompanying_period_step_history (id INT NOT NULL, period_id INT DEFAULT NULL, + endDate DATE DEFAULT NULL, startDate DATE NOT NULL, step TEXT NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, + updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, createdBy_id INT DEFAULT NULL, + updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id)) + '); + $this->addSql('ALTER TABLE chill_person_accompanying_period_step_history ADD CHECK (startDate <= endDate)'); + + $this->addSql('ALTER TABLE chill_person_accompanying_period_step_history ADD CONSTRAINT ' . + 'chill_internal_acp_steps_not_overlaps EXCLUDE USING GIST( + -- extension btree_gist required to include comparaison with integer + period_id WITH =, + daterange(startDate, endDate, \'[)\') WITH && + ) + INITIALLY DEFERRED'); + + $this->addSql('CREATE INDEX IDX_84D514ACEC8B7ADE ON chill_person_accompanying_period_step_history (period_id)'); + $this->addSql('CREATE INDEX IDX_84D514AC3174800F ON chill_person_accompanying_period_step_history (createdBy_id)'); + $this->addSql('COMMENT ON COLUMN chill_person_accompanying_period_step_history.endDate IS \'(DC2Type:date_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_person_accompanying_period_step_history.startDate IS \'(DC2Type:date_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_person_accompanying_period_step_history.createdAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_person_accompanying_period_step_history.updatedAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE chill_person_accompanying_period_step_history ADD CONSTRAINT FK_84D514ACEC8B7ADE FOREIGN KEY (period_id) REFERENCES chill_person_accompanying_period (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_person_accompanying_period_step_history ADD CONSTRAINT FK_84D514AC3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX IDX_84D514AC65FF1AEC ON chill_person_accompanying_period_step_history (updatedBy_id)'); + + // fill the tables with current state + $this->addSql( + 'INSERT INTO chill_person_accompanying_period_step_history (id, period_id, startDate, endDate, step, createdAt, updatedAt) + SELECT nextval(\'chill_person_accompanying_period_step_history_id_seq\'), id, openingDate, null, step, NOW(), NOW() FROM chill_person_accompanying_period WHERE step = \'CONFIRMED\' + UNION + SELECT nextval(\'chill_person_accompanying_period_step_history_id_seq\'), id, openingDate, closingDate, \'CONFIRMED\', NOW(), NOW() FROM chill_person_accompanying_period WHERE step = \'CLOSED\' + UNION + SELECT nextval(\'chill_person_accompanying_period_step_history_id_seq\'), id, closingDate, null, \'CLOSED\', NOW(), NOW() FROM chill_person_accompanying_period WHERE step = \'CLOSED\' + ' + ); + } +} From 2ee4897db90a11982682e6dca91b4fe57e0058dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 14 Oct 2022 21:04:58 +0200 Subject: [PATCH 05/13] Feature: [export][acp] use the acp's step history to aggregate steps in StepAggregator --- exports_alias_conventions.md | 5 +- .../StepAggregator.php | 56 +++++++++---------- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/exports_alias_conventions.md b/exports_alias_conventions.md index 579d004a5..5ee875eac 100644 --- a/exports_alias_conventions.md +++ b/exports_alias_conventions.md @@ -17,12 +17,13 @@ These are alias conventions : | | Scope::class | acp.scopes | acpscope | | | SocialIssue::class | acp.socialIssues | acpsocialissue | | | User::class | acp.user | acpuser | +| | AccompanyingPeriopStepHistory::class | acp.stepHistories | acpstephistories | | AccompanyingPeriodWork::class | | | acpw | | | AccompanyingPeriodWorkEvaluation::class | acpw.accompanyingPeriodWorkEvaluations | workeval | | | User::class | acpw.referrers | acpwuser | | | SocialAction::class | acpw.socialAction | acpwsocialaction | | | Goal::class | acpw.goals | goal | -| | Result::class | acpw.results | result | +| | Result::class | acpw.results | result | | AccompanyingPeriodParticipation::class | | | acppart | | | Person::class | acppart.person | partperson | | AccompanyingPeriodWorkEvaluation::class | | | workeval | @@ -47,7 +48,7 @@ These are alias conventions : | | HouseholdComposition::class | household.compositions | composition | | Activity::class | | | activity | | | Person::class | activity.person | actperson | -| | AccompanyingPeriod::class | activity.accompanyingPeriod | acp | +| | AccompanyingPeriod::class | activity.accompanyingPeriod | acp | | | Person::class | activity\_person\_having\_activity.person | person\_person\_having\_activity | | | ActivityReason::class | activity\_person\_having\_activity.reasons | reasons\_person\_having\_activity | | | ActivityType::class | activity.activityType | acttype | diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/StepAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/StepAggregator.php index 79fed3160..fc2fcc7b2 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/StepAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/StepAggregator.php @@ -12,20 +12,21 @@ declare(strict_types=1); namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators; use Chill\MainBundle\Export\AggregatorInterface; -//use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Form\Type\ChillDateType; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Export\Declarations; use DateTime; -use Doctrine\DBAL\Types\Types; -use Doctrine\ORM\Query\Expr\Andx; use Doctrine\ORM\QueryBuilder; -use LogicException; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Contracts\Translation\TranslatorInterface; +use function in_array; -final class StepAggregator implements AggregatorInterface //, FilterInterface +final class StepAggregator implements AggregatorInterface { + private const A = 'acpstephistories'; + + private const P = 'acp_step_agg_date'; + private TranslatorInterface $translator; public function __construct( @@ -41,30 +42,26 @@ final class StepAggregator implements AggregatorInterface //, FilterInterface public function alterQuery(QueryBuilder $qb, $data) { - $qb->addSelect('acp.step AS step_aggregator'); - $qb->addGroupBy('step_aggregator'); - - /* - // add date in where clause - $where = $qb->getDQLPart('where'); - - $clause = $qb->expr()->andX( - $qb->expr()->lte('acp.openingDate', ':ondate'), - $qb->expr()->orX( - $qb->expr()->gt('acp.closingDate', ':ondate'), - $qb->expr()->isNull('acp.closingDate') - ) - ); - - if ($where instanceof Andx) { - $where->add($clause); - } else { - $where = $qb->expr()->andX($clause); + if (!in_array(self::A, $qb->getAllAliases(), true)) { + $qb->leftJoin('acp.stepHistories', self::A); } - $qb->add('where', $where); - $qb->setParameter('ondate', $data['on_date'], Types::DATE_MUTABLE); - */ + $qb + ->addSelect(self::A . '.step AS step_aggregator') + ->andWhere( + $qb->expr()->orX( + $qb->expr()->isNull(self::A . '.step'), + $qb->expr()->andX( + $qb->expr()->lte(self::A . '.startDate', ':' . self::P), + $qb->expr()->orX( + $qb->expr()->isNull(self::A . '.endDate'), + $qb->expr()->lt(self::A . '.endDate', ':' . self::P) + ) + ) + ) + ) + ->setParameter(self::P, $data['on_date']) + ->addGroupBy('step_aggregator'); } public function applyOn(): string @@ -95,8 +92,11 @@ final class StepAggregator implements AggregatorInterface //, FilterInterface case '_header': return 'Step'; + case null: + return ''; + default: - throw new LogicException(sprintf('The value %s is not valid', $value)); + return $value; } }; } From 8b71e141cfa2f6a9d6336a28cf809a9a05879b03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 14 Oct 2022 21:06:21 +0200 Subject: [PATCH 06/13] Feature: [activity] track creation and update on activity --- .../ChillActivityBundle/Entity/Activity.php | 10 +++- .../migrations/Version20221014130554.php | 59 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 src/Bundle/ChillActivityBundle/migrations/Version20221014130554.php diff --git a/src/Bundle/ChillActivityBundle/Entity/Activity.php b/src/Bundle/ChillActivityBundle/Entity/Activity.php index 98142149f..8fcae3e0b 100644 --- a/src/Bundle/ChillActivityBundle/Entity/Activity.php +++ b/src/Bundle/ChillActivityBundle/Entity/Activity.php @@ -13,6 +13,10 @@ namespace Chill\ActivityBundle\Entity; use Chill\ActivityBundle\Validator\Constraints as ActivityValidator; use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\MainBundle\Doctrine\Model\TrackCreationInterface; +use Chill\MainBundle\Doctrine\Model\TrackCreationTrait; +use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface; +use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait; use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable; use Chill\MainBundle\Entity\Embeddable\PrivateCommentEmbeddable; @@ -55,8 +59,12 @@ use Symfony\Component\Validator\Constraints as Assert; * getUserFunction="getUser", * path="scope") */ -class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterface, HasCentersInterface, HasScopesInterface +class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterface, HasCentersInterface, HasScopesInterface, TrackCreationInterface, TrackUpdateInterface { + use TrackCreationTrait; + + use TrackUpdateTrait; + public const SENTRECEIVED_RECEIVED = 'received'; public const SENTRECEIVED_SENT = 'sent'; diff --git a/src/Bundle/ChillActivityBundle/migrations/Version20221014130554.php b/src/Bundle/ChillActivityBundle/migrations/Version20221014130554.php new file mode 100644 index 000000000..4cbe47d26 --- /dev/null +++ b/src/Bundle/ChillActivityBundle/migrations/Version20221014130554.php @@ -0,0 +1,59 @@ +addSql('ALTER TABLE activity DROP updatedAt'); + $this->addSql('ALTER TABLE activity DROP createdAt'); + $this->addSql('ALTER TABLE activity DROP updatedBy_id'); + $this->addSql('ALTER TABLE activity DROP createdBy_id'); + + // rename some indexes on activity + $this->addSql('ALTER INDEX idx_ac74095a217bbb47 RENAME TO idx_55026b0c217bbb47'); + $this->addSql('ALTER INDEX idx_ac74095a682b5931 RENAME TO idx_55026b0c682b5931'); + $this->addSql('ALTER INDEX idx_ac74095aa76ed395 RENAME TO idx_55026b0ca76ed395'); + $this->addSql('ALTER INDEX idx_ac74095ac54c8c93 RENAME TO idx_55026b0cc54c8c93'); + } + + public function getDescription(): string + { + return 'Track update and create on activity'; + } + + public function up(Schema $schema): void + { + $this->addSql('ALTER TABLE activity ADD updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL'); + $this->addSql('ALTER TABLE activity ADD createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL'); + $this->addSql('ALTER TABLE activity ADD updatedBy_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE activity ADD createdBy_id INT DEFAULT NULL'); + $this->addSql('COMMENT ON COLUMN activity.updatedAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN activity.createdAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE activity ADD CONSTRAINT FK_AC74095A65FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE activity ADD CONSTRAINT FK_AC74095A3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX IDX_AC74095A65FF1AEC ON activity (updatedBy_id)'); + $this->addSql('CREATE INDEX IDX_AC74095A3174800F ON activity (createdBy_id)'); + + // rename some indexes on activity + $this->addSql('ALTER INDEX idx_55026b0cc54c8c93 RENAME TO IDX_AC74095AC54C8C93'); + $this->addSql('ALTER INDEX idx_55026b0c217bbb47 RENAME TO IDX_AC74095A217BBB47'); + $this->addSql('ALTER INDEX idx_55026b0c682b5931 RENAME TO IDX_AC74095A682B5931'); + $this->addSql('ALTER INDEX idx_55026b0ca76ed395 RENAME TO IDX_AC74095AA76ED395'); + + $this->addSql('UPDATE activity SET updatedBy_id=user_id, createdBy_id=user_id, createdAt="date", updatedAt="date"'); + } +} From cd3fc99b9a5e7ee6ebd009a6d0cbf4299bb4f14d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 14 Oct 2022 21:42:55 +0200 Subject: [PATCH 07/13] DX: extract interface for UserRepository --- .../Repository/UserRepository.php | 5 +- .../Repository/UserRepositoryInterface.php | 64 +++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Repository/UserRepositoryInterface.php diff --git a/src/Bundle/ChillMainBundle/Repository/UserRepository.php b/src/Bundle/ChillMainBundle/Repository/UserRepository.php index 89ed84308..fe8dd4368 100644 --- a/src/Bundle/ChillMainBundle/Repository/UserRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/UserRepository.php @@ -16,11 +16,10 @@ use Chill\MainBundle\Entity\User; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\QueryBuilder; -use Doctrine\Persistence\ObjectRepository; use function count; -final class UserRepository implements ObjectRepository +final class UserRepository implements UserRepositoryInterface { private EntityManagerInterface $entityManager; @@ -171,7 +170,7 @@ final class UserRepository implements ObjectRepository return $qb->getQuery()->getResult(); } - public function getClassName() + public function getClassName(): string { return User::class; } diff --git a/src/Bundle/ChillMainBundle/Repository/UserRepositoryInterface.php b/src/Bundle/ChillMainBundle/Repository/UserRepositoryInterface.php new file mode 100644 index 000000000..0201607fe --- /dev/null +++ b/src/Bundle/ChillMainBundle/Repository/UserRepositoryInterface.php @@ -0,0 +1,64 @@ + Date: Fri, 14 Oct 2022 21:43:30 +0200 Subject: [PATCH 08/13] Fixed: [export][activity] use createdBy property instead of user for grouping or filtering by creator --- exports_alias_conventions.md | 1 + ...Aggregator.php => ByCreatorAggregator.php} | 23 ++++++--------- ...regator.php => CreatorScopeAggregator.php} | 14 ++++----- .../{ByUserFilter.php => ByCreatorFilter.php} | 29 +++++-------------- .../ACPAggregators/ByUserAggregatorTest.php | 4 +-- .../UserScopeAggregatorTest.php | 4 +-- .../Filter/ACPFilters/ByUserFilterTest.php | 4 +-- .../config/services/export.yaml | 15 ++++------ .../translations/messages.fr.yml | 7 +++-- 9 files changed, 41 insertions(+), 60 deletions(-) rename src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/{ByUserAggregator.php => ByCreatorAggregator.php} (72%) rename src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/{UserScopeAggregator.php => CreatorScopeAggregator.php} (81%) rename src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/{ByUserFilter.php => ByCreatorFilter.php} (67%) diff --git a/exports_alias_conventions.md b/exports_alias_conventions.md index 5ee875eac..62fc745d8 100644 --- a/exports_alias_conventions.md +++ b/exports_alias_conventions.md @@ -60,6 +60,7 @@ These are alias conventions : | | User::class | activity.users | actusers | | | ActivityReason::class | activity.reasons | actreasons | | | Center::class | actperson.center | actcenter | +| | Person::class | activity.createdBy | actcreator | | ActivityReason::class | | | actreasons | | | ActivityReasonCategory::class | actreason.category | actreasoncat | | Calendar::class | | | cal | diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByUserAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByCreatorAggregator.php similarity index 72% rename from src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByUserAggregator.php rename to src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByCreatorAggregator.php index 337e38705..c09685e4e 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByUserAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByCreatorAggregator.php @@ -13,20 +13,19 @@ namespace Chill\ActivityBundle\Export\Aggregator\ACPAggregators; use Chill\ActivityBundle\Export\Declarations; use Chill\MainBundle\Export\AggregatorInterface; -use Chill\MainBundle\Repository\UserRepository; +use Chill\MainBundle\Repository\UserRepositoryInterface; use Chill\MainBundle\Templating\Entity\UserRender; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; -use function in_array; -class ByUserAggregator implements AggregatorInterface +class ByCreatorAggregator implements AggregatorInterface { private UserRender $userRender; - private UserRepository $userRepository; + private UserRepositoryInterface $userRepository; public function __construct( - UserRepository $userRepository, + UserRepositoryInterface $userRepository, UserRender $userRender ) { $this->userRepository = $userRepository; @@ -40,12 +39,8 @@ class ByUserAggregator implements AggregatorInterface public function alterQuery(QueryBuilder $qb, $data) { - if (!in_array('actusers', $qb->getAllAliases(), true)) { - $qb->leftJoin('activity.users', 'actusers'); - } - - $qb->addSelect('actusers.id AS users_aggregator'); - $qb->addGroupBy('users_aggregator'); + $qb->addSelect('IDENTITY(activity.createdBy) AS creator_aggregator'); + $qb->addGroupBy('creator_aggregator'); } public function applyOn(): string @@ -62,7 +57,7 @@ class ByUserAggregator implements AggregatorInterface { return function ($value): string { if ('_header' === $value) { - return 'Accepted users'; + return 'Created by'; } if (null === $value) { @@ -77,11 +72,11 @@ class ByUserAggregator implements AggregatorInterface public function getQueryKeys($data): array { - return ['users_aggregator']; + return ['creator_aggregator']; } public function getTitle(): string { - return 'Group activity by linked users'; + return 'Group activity by creator'; } } diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/UserScopeAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/CreatorScopeAggregator.php similarity index 81% rename from src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/UserScopeAggregator.php rename to src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/CreatorScopeAggregator.php index 2c2cb50d2..2041fcbb4 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/UserScopeAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/CreatorScopeAggregator.php @@ -19,7 +19,7 @@ use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; use function in_array; -class UserScopeAggregator implements AggregatorInterface +class CreatorScopeAggregator implements AggregatorInterface { private ScopeRepository $scopeRepository; @@ -40,12 +40,12 @@ class UserScopeAggregator implements AggregatorInterface public function alterQuery(QueryBuilder $qb, $data) { - if (!in_array('actuser', $qb->getAllAliases(), true)) { - $qb->leftJoin('activity.user', 'actuser'); + if (!in_array('actcreator', $qb->getAllAliases(), true)) { + $qb->leftJoin('activity.createdBy', 'actcreator'); } - $qb->addSelect('IDENTITY(actuser.mainScope) AS userscope_aggregator'); - $qb->addGroupBy('userscope_aggregator'); + $qb->addSelect('IDENTITY(actcreator.mainScope) AS creatorscope_aggregator'); + $qb->addGroupBy('creatorscope_aggregator'); } public function applyOn(): string @@ -79,11 +79,11 @@ class UserScopeAggregator implements AggregatorInterface public function getQueryKeys($data): array { - return ['userscope_aggregator']; + return ['creatorscope_aggregator']; } public function getTitle(): string { - return 'Group activity by userscope'; + return 'Group activity by creator scope'; } } diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/ByUserFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/ByCreatorFilter.php similarity index 67% rename from src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/ByUserFilter.php rename to src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/ByCreatorFilter.php index d276cdce8..ffabc5934 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/ByUserFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/ByCreatorFilter.php @@ -15,12 +15,10 @@ use Chill\ActivityBundle\Export\Declarations; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Form\Type\PickUserDynamicType; use Chill\MainBundle\Templating\Entity\UserRender; -use Doctrine\ORM\Query\Expr\Andx; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; -use function in_array; -class ByUserFilter implements FilterInterface +class ByCreatorFilter implements FilterInterface { private UserRender $userRender; @@ -36,22 +34,11 @@ class ByUserFilter implements FilterInterface public function alterQuery(QueryBuilder $qb, $data) { - $where = $qb->getDQLPart('where'); - - if (!in_array('actusers', $qb->getAllAliases(), true)) { - $qb->join('activity.users', 'actusers'); - } - - $clause = $qb->expr()->in('actusers.id', ':users'); - - if ($where instanceof Andx) { - $where->add($clause); - } else { - $where = $qb->expr()->andX($clause); - } - - $qb->add('where', $where); - $qb->setParameter('users', $data['accepted_users']); + $qb + ->andWhere( + $qb->expr()->in('activity.createdBy', ':users') + ) + ->setParameter('users', $data['accepted_users']); } public function applyOn(): string @@ -74,13 +61,13 @@ class ByUserFilter implements FilterInterface $users[] = $this->userRender->renderString($u, []); } - return ['Filtered activity by linked users: only %users%', [ + return ['Filtered activity by creator: only %users%', [ '%users%' => implode(', ou ', $users), ]]; } public function getTitle(): string { - return 'Filter activity by linked users'; + return 'Filter activity by creator'; } } diff --git a/src/Bundle/ChillActivityBundle/Tests/Export/Aggregator/ACPAggregators/ByUserAggregatorTest.php b/src/Bundle/ChillActivityBundle/Tests/Export/Aggregator/ACPAggregators/ByUserAggregatorTest.php index 78f3ee79d..ff4f42ec4 100644 --- a/src/Bundle/ChillActivityBundle/Tests/Export/Aggregator/ACPAggregators/ByUserAggregatorTest.php +++ b/src/Bundle/ChillActivityBundle/Tests/Export/Aggregator/ACPAggregators/ByUserAggregatorTest.php @@ -12,7 +12,7 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Tests\Export\Aggregator\ACPAggregators; use Chill\ActivityBundle\Entity\Activity; -use Chill\ActivityBundle\Export\Aggregator\ACPAggregators\ByUserAggregator; +use Chill\ActivityBundle\Export\Aggregator\ACPAggregators\ByCreatorAggregator; use Chill\MainBundle\Test\Export\AbstractAggregatorTest; use Doctrine\ORM\EntityManagerInterface; @@ -22,7 +22,7 @@ use Doctrine\ORM\EntityManagerInterface; */ final class ByUserAggregatorTest extends AbstractAggregatorTest { - private ByUserAggregator $aggregator; + private ByCreatorAggregator $aggregator; protected function setUp(): void { diff --git a/src/Bundle/ChillActivityBundle/Tests/Export/Aggregator/ACPAggregators/UserScopeAggregatorTest.php b/src/Bundle/ChillActivityBundle/Tests/Export/Aggregator/ACPAggregators/UserScopeAggregatorTest.php index 1d33fedbe..1265804f9 100644 --- a/src/Bundle/ChillActivityBundle/Tests/Export/Aggregator/ACPAggregators/UserScopeAggregatorTest.php +++ b/src/Bundle/ChillActivityBundle/Tests/Export/Aggregator/ACPAggregators/UserScopeAggregatorTest.php @@ -12,7 +12,7 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Tests\Export\Aggregator\ACPAggregators; use Chill\ActivityBundle\Entity\Activity; -use Chill\ActivityBundle\Export\Aggregator\ACPAggregators\UserScopeAggregator; +use Chill\ActivityBundle\Export\Aggregator\ACPAggregators\CreatorScopeAggregator; use Chill\MainBundle\Test\Export\AbstractAggregatorTest; use Doctrine\ORM\EntityManagerInterface; @@ -22,7 +22,7 @@ use Doctrine\ORM\EntityManagerInterface; */ final class UserScopeAggregatorTest extends AbstractAggregatorTest { - private UserScopeAggregator $aggregator; + private CreatorScopeAggregator $aggregator; protected function setUp(): void { diff --git a/src/Bundle/ChillActivityBundle/Tests/Export/Filter/ACPFilters/ByUserFilterTest.php b/src/Bundle/ChillActivityBundle/Tests/Export/Filter/ACPFilters/ByUserFilterTest.php index d6e3a465e..47e76e25c 100644 --- a/src/Bundle/ChillActivityBundle/Tests/Export/Filter/ACPFilters/ByUserFilterTest.php +++ b/src/Bundle/ChillActivityBundle/Tests/Export/Filter/ACPFilters/ByUserFilterTest.php @@ -12,7 +12,7 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Tests\Export\Filter\ACPFilters; use Chill\ActivityBundle\Entity\Activity; -use Chill\ActivityBundle\Export\Filter\ACPFilters\ByUserFilter; +use Chill\ActivityBundle\Export\Filter\ACPFilters\ByCreatorFilter; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Test\Export\AbstractFilterTest; use Doctrine\ORM\EntityManagerInterface; @@ -23,7 +23,7 @@ use Doctrine\ORM\EntityManagerInterface; */ final class ByUserFilterTest extends AbstractFilterTest { - private ByUserFilter $filter; + private ByCreatorFilter $filter; protected function setUp(): void { diff --git a/src/Bundle/ChillActivityBundle/config/services/export.yaml b/src/Bundle/ChillActivityBundle/config/services/export.yaml index bdaae8c8a..4b8dc28ac 100644 --- a/src/Bundle/ChillActivityBundle/config/services/export.yaml +++ b/src/Bundle/ChillActivityBundle/config/services/export.yaml @@ -77,10 +77,9 @@ services: tags: - { name: chill.export_filter, alias: 'activity_locationtype_filter' } - chill.activity.export.byuser_filter: # TMS (M2M) - class: Chill\ActivityBundle\Export\Filter\ACPFilters\ByUserFilter + Chill\ActivityBundle\Export\Filter\ACPFilters\ByCreatorFilter: tags: - - { name: chill.export_filter, alias: 'activity_byuser_filter' } + - { name: chill.export_filter, alias: 'activity_bycreator_filter' } chill.activity.export.emergency_filter: class: Chill\ActivityBundle\Export\Filter\ACPFilters\EmergencyFilter @@ -138,10 +137,9 @@ services: tags: - { name: chill.export_aggregator, alias: activity_date_aggregator } - chill.activity.export.byuser_aggregator: - class: Chill\ActivityBundle\Export\Aggregator\ACPAggregators\ByUserAggregator + Chill\ActivityBundle\Export\Aggregator\ACPAggregators\ByCreatorAggregator: tags: - - { name: chill.export_aggregator, alias: activity_byuser_aggregator } + - { name: chill.export_aggregator, alias: activity_by_creator_aggregator } chill.activity.export.bythirdparty_aggregator: class: Chill\ActivityBundle\Export\Aggregator\ACPAggregators\ByThirdpartyAggregator @@ -158,7 +156,6 @@ services: tags: - { name: chill.export_aggregator, alias: activity_bysocialissue_aggregator } - chill.activity.export.userscope_aggregator: - class: Chill\ActivityBundle\Export\Aggregator\ACPAggregators\UserScopeAggregator + Chill\ActivityBundle\Export\Aggregator\ACPAggregators\CreatorScopeAggregator: tags: - - { name: chill.export_aggregator, alias: activity_userscope_aggregator } + - { name: chill.export_aggregator, alias: activity_creator_scope_aggregator } diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml index 9f8ed3d7c..4b230bbcf 100644 --- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml @@ -252,8 +252,8 @@ Filter by activity type: Filtrer les activités par type Filter activity by locationtype: Filtrer les activités par type de localisation 'Filtered activity by locationtype: only %types%': "Filtré par type de localisation: uniquement %types%" Accepted locationtype: Types de localisation -Filter activity by linked users: Filtrer les activités par TMS -'Filtered activity by linked users: only %users%': "Filtré par TMS: uniquement %users%" +Filter activity by creator: Filtrer les activités par créateur de l'échange +'Filtered activity by creator: only %users%': "Filtré par créateur de l'échange: uniquement %users%" Accepted users: TMS(s) Filter activity by emergency: Filtrer les activités par urgence 'Filtered activity by emergency: only %emergency%': "Filtré par urgence: uniquement si %emergency%" @@ -294,7 +294,8 @@ by week: Par semaine for week: Semaine by year: Par année in year: En -Group activity by linked users: Grouper les activités par TMS impliqué +Group activity by creator: Grouper les activités par créateur de l'échange +Group activity by creator scope: Grouper les activités par service du créateur de l'échange Group activity by linked thirdparties: Grouper les activités par tiers impliqué Accepted thirdparty: Tiers impliqué Group activity by linked socialaction: Grouper les activités par action liée From f1e92887fd49368d92f4a67e87fd3a5b5dfb7e8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 14 Oct 2022 22:33:58 +0200 Subject: [PATCH 09/13] Fixed: [export][person][by age] use lower maximum age if not set, to avoid postgresql error --- .../ChillPersonBundle/Export/Export/CountAccompanyingCourse.php | 2 ++ .../ChillPersonBundle/Export/Filter/PersonFilters/AgeFilter.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Bundle/ChillPersonBundle/Export/Export/CountAccompanyingCourse.php b/src/Bundle/ChillPersonBundle/Export/Export/CountAccompanyingCourse.php index 7e5a01f3d..8c8744dfd 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/CountAccompanyingCourse.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/CountAccompanyingCourse.php @@ -99,6 +99,7 @@ class CountAccompanyingCourse implements ExportInterface, GroupedExportInterface $qb = $this->repository->createQueryBuilder('acp'); $qb + ->andWhere('acp.step != :count_acp_step') ->andWhere( $qb->expr()->exists( 'SELECT 1 FROM ' . AccompanyingPeriodParticipation::class . ' acl_count_part @@ -107,6 +108,7 @@ class CountAccompanyingCourse implements ExportInterface, GroupedExportInterface ' ) ) + ->setParameter('count_acp_step', AccompanyingPeriod::STEP_DRAFT) ->setParameter('authorized_centers', $centers); $qb->select('COUNT(DISTINCT acp.id) AS export_result'); diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/AgeFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/AgeFilter.php index 1c6b362d4..218ae483e 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/AgeFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/AgeFilter.php @@ -35,7 +35,7 @@ class AgeFilter implements ExportElementValidatedInterface, FilterInterface $where = $qb->getDQLPart('where'); $min = null !== $data['min_age'] ? $data['min_age'] : 0; - $max = null !== $data['max_age'] ? $data['max_age'] : 3000; + $max = null !== $data['max_age'] ? $data['max_age'] : 150; $calc = $data['date_calc']; $minDate = $calc->sub(new DateInterval('P' . $max . 'Y')); From dd0e2a169cacca9d8c0b507bbf0a95fabeda0b37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Sat, 15 Oct 2022 00:17:04 +0200 Subject: [PATCH 10/13] Feature: [export][acp] use a date of computation for each filter and aggregator linked with referrer --- .../ReferrerAggregator.php | 38 ++++++++++--- .../ReferrerFilter.php | 57 ++++++++++++------- .../UserJobFilter.php | 49 ++++++++++++---- .../UserScopeFilter.php | 54 +++++++++++++----- .../translations/messages.fr.yml | 9 +++ 5 files changed, 153 insertions(+), 54 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerAggregator.php index 1498fc3a1..327b76a65 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerAggregator.php @@ -12,15 +12,20 @@ declare(strict_types=1); namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators; use Chill\MainBundle\Export\AggregatorInterface; +use Chill\MainBundle\Form\Type\ChillDateType; use Chill\MainBundle\Repository\UserRepository; use Chill\MainBundle\Templating\Entity\UserRender; use Chill\PersonBundle\Export\Declarations; +use DateTimeImmutable; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; -use function in_array; final class ReferrerAggregator implements AggregatorInterface { + private const A = 'acp_ref_agg_uhistory'; + + private const P = 'acp_ref_agg_date'; + private UserRender $userRender; private UserRepository $userRepository; @@ -40,12 +45,23 @@ final class ReferrerAggregator implements AggregatorInterface public function alterQuery(QueryBuilder $qb, $data) { - if (!in_array('acpuser', $qb->getAllAliases(), true)) { - $qb->leftJoin('acp.user', 'acpuser'); - } - - $qb->addSelect('acpuser.id AS referrer_aggregator'); - $qb->addGroupBy('referrer_aggregator'); + $qb + ->addSelect('IDENTITY(' . self::A . '.user) AS referrer_aggregator') + ->addGroupBy('referrer_aggregator') + ->leftJoin('acp.userHistories', self::A) + ->andWhere( + $qb->expr()->orX( + $qb->expr()->isNull(self::A), + $qb->expr()->andX( + $qb->expr()->lte(self::A . '.startDate', ':' . self::P), + $qb->expr()->orX( + $qb->expr()->isNull(self::A . '.endDate'), + $qb->expr()->gt(self::A . '.endDate', ':' . self::P) + ) + ) + ) + ) + ->setParameter(self::P, $data['date_calc']); } public function applyOn(): string @@ -55,7 +71,13 @@ final class ReferrerAggregator implements AggregatorInterface public function buildForm(FormBuilderInterface $builder) { - // no form + $builder + ->add('date_calc', ChillDateType::class, [ + 'input' => 'datetime_immutable', + 'data' => new DateTimeImmutable('now'), + 'label' => 'export.aggregator.course.by_referrer.Computation date for referrer', + 'required' => true, + ]); } public function getLabels($key, array $values, $data) diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ReferrerFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ReferrerFilter.php index 53ab10564..8781f6cad 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ReferrerFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ReferrerFilter.php @@ -11,17 +11,23 @@ declare(strict_types=1); namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters; -use Chill\MainBundle\Entity\User; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Form\Type\ChillDateType; +use Chill\MainBundle\Form\Type\PickUserDynamicType; use Chill\MainBundle\Templating\Entity\UserRender; use Chill\PersonBundle\Export\Declarations; -use Doctrine\ORM\Query\Expr\Andx; +use DateTimeImmutable; use Doctrine\ORM\QueryBuilder; -use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\FormBuilderInterface; class ReferrerFilter implements FilterInterface { + private const A = 'acp_referrer_filter_uhistory'; + + private const P = 'acp_referrer_filter_date'; + + private const PU = 'acp_referrer_filter_users'; + private UserRender $userRender; public function __construct(UserRender $userRender) @@ -36,17 +42,22 @@ class ReferrerFilter implements FilterInterface public function alterQuery(QueryBuilder $qb, $data) { - $where = $qb->getDQLPart('where'); - $clause = $qb->expr()->in('acp.user', ':referrers'); - - if ($where instanceof Andx) { - $where->add($clause); - } else { - $where = $qb->expr()->andX($clause); - } - - $qb->add('where', $where); - $qb->setParameter('referrers', $data['accepted_referrers']); + $qb + ->join('acp.userHistories', self::A) + ->andWhere( + $qb->expr()->andX( + $qb->expr()->lte(self::A . '.startDate', ':' . self::P), + $qb->expr()->orX( + $qb->expr()->isNull(self::A . '.endDate'), + $qb->expr()->gt(self::A . '.endDate', ':' . self::P) + ) + ) + ) + ->andWhere( + $qb->expr()->in(self::A . '.user', ':' . self::PU) + ) + ->setParameter(self::PU, $data['accepted_referrers']) + ->setParameter(self::P, $data['date_calc']); } public function applyOn(): string @@ -56,14 +67,16 @@ class ReferrerFilter implements FilterInterface public function buildForm(FormBuilderInterface $builder) { - $builder->add('accepted_referrers', EntityType::class, [ - 'class' => User::class, - 'choice_label' => function (User $u) { - return $this->userRender->renderString($u, []); - }, - 'multiple' => true, - 'expanded' => true, - ]); + $builder + ->add('accepted_referrers', PickUserDynamicType::class, [ + 'multiple' => true, + ]) + ->add('date_calc', ChillDateType::class, [ + 'input' => 'datetime_immutable', + 'data' => new DateTimeImmutable('now'), + 'label' => 'export.filter.course.by_referrer.Computation date for referrer', + 'required' => true, + ]); } public function describeAction($data, $format = 'string'): array diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserJobFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserJobFilter.php index e55c391ef..a18442c2a 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserJobFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserJobFilter.php @@ -14,9 +14,11 @@ namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Form\Type\ChillDateType; use Chill\MainBundle\Repository\UserJobRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\PersonBundle\Export\Declarations; +use DateTimeImmutable; use Doctrine\ORM\QueryBuilder; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\FormBuilderInterface; @@ -24,6 +26,14 @@ use Symfony\Component\Security\Core\Security; class UserJobFilter implements FilterInterface { + private const A = 'acp_ujob_filter_uhistory'; + + private const AU = 'acp_ujob_filter_uhistory_user'; + + private const P = 'acp_ujob_filter_date'; + + private const PJ = 'acp_ujob_filter_job'; + private Security $security; private TranslatableStringHelper $translatableStringHelper; @@ -48,10 +58,22 @@ class UserJobFilter implements FilterInterface public function alterQuery(QueryBuilder $qb, $data) { $qb + ->join('acp.userHistories', self::A) ->andWhere( - $qb->expr()->in('acp.job', ':acp_user_job_filter_j') + $qb->expr()->andX( + $qb->expr()->lte(self::A . '.startDate', ':' . self::P), + $qb->expr()->orX( + $qb->expr()->isNull(self::A . '.endDate'), + $qb->expr()->gt(self::A . '.endDate', ':' . self::P) + ) + ) ) - ->setParameter('acp_user_job_filter_j', $data['jobs']); + ->setParameter(self::P, $data['date_calc']) + ->join(self::A . '.user', self::AU) + ->andWhere( + $qb->expr()->in(self::AU . '.userJob', ':' . self::PJ) + ) + ->setParameter(self::PJ, $data['jobs']); } public function applyOn() @@ -61,14 +83,21 @@ class UserJobFilter implements FilterInterface public function buildForm(FormBuilderInterface $builder) { - $builder->add('jobs', EntityType::class, [ - 'class' => UserJob::class, - 'choices' => $this->userJobRepository->findAllActive(), - 'multiple' => true, - 'expanded' => true, - 'choice_label' => fn (UserJob $job) => $this->translatableStringHelper->localize($job->getLabel()), - 'label' => 'Job', - ]); + $builder + ->add('jobs', EntityType::class, [ + 'class' => UserJob::class, + 'choices' => $this->userJobRepository->findAllActive(), + 'multiple' => true, + 'expanded' => true, + 'choice_label' => fn (UserJob $job) => $this->translatableStringHelper->localize($job->getLabel()), + 'label' => 'Job', + ]) + ->add('date_calc', ChillDateType::class, [ + 'input' => 'datetime_immutable', + 'data' => new DateTimeImmutable('now'), + 'label' => 'export.filter.course.by_user_scope.Computation date for referrer', + 'required' => true, + ]); } public function describeAction($data, $format = 'string') diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserScopeFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserScopeFilter.php index c079b2bec..48a97f9f7 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserScopeFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserScopeFilter.php @@ -14,9 +14,11 @@ namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Form\Type\ChillDateType; use Chill\MainBundle\Repository\ScopeRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\PersonBundle\Export\Declarations; +use DateTimeImmutable; use Doctrine\ORM\QueryBuilder; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\FormBuilderInterface; @@ -24,6 +26,14 @@ use Symfony\Component\Security\Core\Security; class UserScopeFilter implements FilterInterface { + private const A = 'acp_uscope_filter_uhistory'; + + private const AU = 'acp_uscope_filter_uhistory_user'; + + private const P = 'acp_uscope_filter_date'; + + private const PS = 'acp_uscope_filter_scopes'; + private ScopeRepositoryInterface $scopeRepository; private Security $security; @@ -47,14 +57,23 @@ class UserScopeFilter implements FilterInterface public function alterQuery(QueryBuilder $qb, $data) { - foreach ($data['scopes'] as $key => $scope) { - /** @var Scope $scope */ - $qb - ->andWhere( - $qb->expr()->isMemberOf(':acp_scope_filter_s_' . $key, 'acp.scopes') + $qb + ->join('acp.userHistories', self::A) + ->andWhere( + $qb->expr()->andX( + $qb->expr()->lte(self::A . '.startDate', ':' . self::P), + $qb->expr()->orX( + $qb->expr()->isNull(self::A . '.endDate'), + $qb->expr()->gt(self::A . '.endDate', ':' . self::P) + ) ) - ->setParameter('acp_scope_filter_s_' . $key, $scope); - } + ) + ->setParameter(self::P, $data['date_calc']) + ->join(self::A . '.user', self::AU) + ->andWhere( + $qb->expr()->in(self::AU . '.mainScope', ':' . self::PS) + ) + ->setParameter(self::PS, $data['scopes']); } public function applyOn() @@ -64,13 +83,20 @@ class UserScopeFilter implements FilterInterface public function buildForm(FormBuilderInterface $builder) { - $builder->add('scopes', EntityType::class, [ - 'class' => Scope::class, - 'choices' => $this->scopeRepository->findAllActive(), - 'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()), - 'multiple' => true, - 'expanded' => true, - ]); + $builder + ->add('scopes', EntityType::class, [ + 'class' => Scope::class, + 'choices' => $this->scopeRepository->findAllActive(), + 'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()), + 'multiple' => true, + 'expanded' => true, + ]) + ->add('date_calc', ChillDateType::class, [ + 'input' => 'datetime_immutable', + 'data' => new DateTimeImmutable('now'), + 'label' => 'export.filter.course.by_user_scope.Computation date for referrer', + 'required' => true, + ]); } public function describeAction($data, $format = 'string') diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 7e9fbfeaa..76fb12d18 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -947,6 +947,8 @@ notification: export: aggregator: course: + by_referrer: + Computation date for referrer: Date à laquelle le référent était actif by_user_scope: Group course by referrer's scope: Grouper les parcours par service du référent Computation date for referrer: Date à laquelle le référent était actif @@ -956,3 +958,10 @@ export: week: Durée du parcours en semaines month: Durée du parcours en mois Precision: Unité de la durée + filter: + course: + by_user_scope: + Computation date for referrer: Date à laquelle le référent était actif + by_referrer: + Computation date for referrer: Date à laquelle le référent était actif + From 3e5be50a8769a9362b62a84de9d5adcf32b733cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Sat, 15 Oct 2022 00:35:13 +0200 Subject: [PATCH 11/13] fix some tests --- .../PersonAggregators/CountryOfBirthAggregator.php | 5 ++++- .../Repository/SocialWork/GoalRepository.php | 6 +----- .../Repository/SocialWork/ResultRepository.php | 6 +----- .../DurationAggregatorTest.php | 4 +++- .../GeographicalUnitStatAggregatorTest.php | 4 +++- .../ReferrerAggregatorTest.php | 3 ++- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/CountryOfBirthAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/CountryOfBirthAggregator.php index 1ba9d9f06..395070a8a 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/CountryOfBirthAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/CountryOfBirthAggregator.php @@ -23,6 +23,7 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Contracts\Translation\TranslatorInterface; +use function in_array; final class CountryOfBirthAggregator implements AggregatorInterface, ExportElementValidatedInterface { @@ -83,7 +84,9 @@ final class CountryOfBirthAggregator implements AggregatorInterface, ExportEleme . ' is not known.'); } - $qb->leftJoin('person.countryOfBirth', 'countryOfBirth'); + if (!in_array('countryOfBirth', $qb->getAllAliases(), true)) { + $qb->leftJoin('person.countryOfBirth', 'countryOfBirth'); + } // add group by $qb->addGroupBy('country_of_birth_aggregator'); diff --git a/src/Bundle/ChillPersonBundle/Repository/SocialWork/GoalRepository.php b/src/Bundle/ChillPersonBundle/Repository/SocialWork/GoalRepository.php index f6212e608..fd24b7b9d 100644 --- a/src/Bundle/ChillPersonBundle/Repository/SocialWork/GoalRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/SocialWork/GoalRepository.php @@ -63,13 +63,9 @@ final class GoalRepository implements ObjectRepository } /** - * @param mixed|null $orderBy - * @param mixed|null $limit - * @param mixed|null $offset - * * @return Goal[] */ - public function findBySocialActionWithDescendants(SocialAction $action, $orderBy = null, $limit = null, $offset = null): array + public function findBySocialActionWithDescendants(SocialAction $action, array $orderBy = [], ?int $limit = null, ?int $offset = null): array { $qb = $this->buildQueryBySocialActionWithDescendants($action); $qb->select('g'); diff --git a/src/Bundle/ChillPersonBundle/Repository/SocialWork/ResultRepository.php b/src/Bundle/ChillPersonBundle/Repository/SocialWork/ResultRepository.php index df73bde21..e751f88cb 100644 --- a/src/Bundle/ChillPersonBundle/Repository/SocialWork/ResultRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/SocialWork/ResultRepository.php @@ -96,13 +96,9 @@ final class ResultRepository implements ObjectRepository } /** - * @param mixed|null $orderBy - * @param mixed|null $limit - * @param mixed|null $offset - * * @return Result[] */ - public function findBySocialActionWithDescendants(SocialAction $action, $orderBy = null, $limit = null, $offset = null): array + public function findBySocialActionWithDescendants(SocialAction $action, array $orderBy = [], ?int $limit = null, ?int $offset = null): array { $qb = $this->buildQueryBySocialActionWithDescendants($action); $qb->select('r'); diff --git a/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/DurationAggregatorTest.php b/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/DurationAggregatorTest.php index 013dddd4f..15f4796d2 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/DurationAggregatorTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/DurationAggregatorTest.php @@ -39,7 +39,9 @@ final class DurationAggregatorTest extends AbstractAggregatorTest public function getFormData(): array { return [ - [], + ['precision' => 'day'], + ['precision' => 'week'], + ['precision' => 'month'], ]; } diff --git a/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/GeographicalUnitStatAggregatorTest.php b/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/GeographicalUnitStatAggregatorTest.php index 49ca0a6c1..dc7ccaad7 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/GeographicalUnitStatAggregatorTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/GeographicalUnitStatAggregatorTest.php @@ -14,6 +14,7 @@ namespace Chill\PersonBundle\Tests\Export\Aggregator\AccompanyingCourseAggregato use Chill\MainBundle\Test\Export\AbstractAggregatorTest; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\GeographicalUnitStatAggregator; +use DateTimeImmutable; use Doctrine\ORM\EntityManagerInterface; /** @@ -38,8 +39,9 @@ final class GeographicalUnitStatAggregatorTest extends AbstractAggregatorTest public function getFormData(): array { + // TODO: add geographical unit stat into fixtures and provide a level return [ - [], + ['date_calc' => new DateTimeImmutable('today'), 'level' => null], ]; } diff --git a/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/ReferrerAggregatorTest.php b/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/ReferrerAggregatorTest.php index baee88871..e60130a6f 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/ReferrerAggregatorTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/ReferrerAggregatorTest.php @@ -14,6 +14,7 @@ namespace Chill\PersonBundle\Tests\Export\Aggregator\AccompanyingCourseAggregato use Chill\MainBundle\Test\Export\AbstractAggregatorTest; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\ReferrerAggregator; +use DateTimeImmutable; use Doctrine\ORM\EntityManagerInterface; /** @@ -39,7 +40,7 @@ final class ReferrerAggregatorTest extends AbstractAggregatorTest public function getFormData(): array { return [ - [], + ['date_calc' => new DateTimeImmutable('now')], ]; } From 4ed760949a1ccf045d1b5d039b5b3a3511f1eb5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Sun, 16 Oct 2022 23:56:25 +0200 Subject: [PATCH 12/13] Feature: [export][activity] Add filter and aggregator by users (participating to the activity) --- .../Aggregator/ActivityUsersAggregator.php | 86 +++++++++++++++++++ .../Export/Filter/ActivityUsersFilter.php | 77 +++++++++++++++++ .../config/services/export.yaml | 8 ++ .../translations/messages.fr.yml | 11 ++- 4 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersAggregator.php create mode 100644 src/Bundle/ChillActivityBundle/Export/Filter/ActivityUsersFilter.php diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersAggregator.php new file mode 100644 index 000000000..ccccc48a0 --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersAggregator.php @@ -0,0 +1,86 @@ +userRepository = $userRepository; + $this->userRender = $userRender; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + if (!in_array('actusers', $qb->getAllAliases(), true)) { + $qb->leftJoin('activity.users', 'actusers'); + } + + $qb + ->addSelect('actusers.id AS activity_users_aggregator') + ->addGroupBy('activity_users_aggregator'); + } + + public function applyOn(): string + { + return Declarations::ACTIVITY; + } + + public function buildForm(FormBuilderInterface $builder) + { + // nothing to add on the form + } + + public function getLabels($key, array $values, $data) + { + return function ($value) { + if ('_header' === $value) { + return 'Activity users'; + } + + if (null === $value) { + return ''; + } + + $u = $this->userRepository->find($value); + + return $this->userRender->renderString($u, []); + }; + } + + public function getQueryKeys($data) + { + return ['activity_users_aggregator']; + } + + public function getTitle() + { + return 'Aggregate by activity users'; + } +} diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ActivityUsersFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ActivityUsersFilter.php new file mode 100644 index 000000000..2f6cd8462 --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ActivityUsersFilter.php @@ -0,0 +1,77 @@ +userRender = $userRender; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $orX = $qb->expr()->orX(); + + foreach ($data['accepted_users'] as $key => $user) { + $orX->add($qb->expr()->isMemberOf(':activity_users_filter_u' . $key, 'activity.users')); + $qb->setParameter('activity_users_filter_u' . $key, $user); + } + + $qb->andWhere($orX); + } + + public function applyOn() + { + return Declarations::ACTIVITY; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('accepted_users', PickUserDynamicType::class, [ + 'multiple' => true, + 'label' => 'Users', + ]); + } + + public function describeAction($data, $format = 'string') + { + $users = []; + + foreach ($data['accepted_users'] as $u) { + $users[] = $this->userRender->renderString($u, []); + } + + return ['Filtered activity by users: only %users%', [ + '%users%' => implode(', ', $users), + ]]; + } + + public function getTitle(): string + { + return 'Filter activity by users'; + } +} diff --git a/src/Bundle/ChillActivityBundle/config/services/export.yaml b/src/Bundle/ChillActivityBundle/config/services/export.yaml index 4b8dc28ac..9af8c257f 100644 --- a/src/Bundle/ChillActivityBundle/config/services/export.yaml +++ b/src/Bundle/ChillActivityBundle/config/services/export.yaml @@ -55,6 +55,10 @@ services: tags: - { name: chill.export_filter, alias: 'activity_date_filter' } + Chill\ActivityBundle\Export\Filter\ActivityUsersFilter: + tags: + - { name: chill.export_filter, alias: 'activity_users_filter' } + chill.activity.export.reason_filter: class: Chill\ActivityBundle\Export\Filter\PersonFilters\ActivityReasonFilter tags: @@ -159,3 +163,7 @@ services: Chill\ActivityBundle\Export\Aggregator\ACPAggregators\CreatorScopeAggregator: tags: - { name: chill.export_aggregator, alias: activity_creator_scope_aggregator } + + Chill\ActivityBundle\Export\Aggregator\ActivityUsersAggregator: + tags: + - { name: chill.export_aggregator, alias: activity_users_aggregator } diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml index 4b230bbcf..55749a9ca 100644 --- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml @@ -252,8 +252,6 @@ Filter by activity type: Filtrer les activités par type Filter activity by locationtype: Filtrer les activités par type de localisation 'Filtered activity by locationtype: only %types%': "Filtré par type de localisation: uniquement %types%" Accepted locationtype: Types de localisation -Filter activity by creator: Filtrer les activités par créateur de l'échange -'Filtered activity by creator: only %users%': "Filtré par créateur de l'échange: uniquement %users%" Accepted users: TMS(s) Filter activity by emergency: Filtrer les activités par urgence 'Filtered activity by emergency: only %emergency%': "Filtré par urgence: uniquement si %emergency%" @@ -269,7 +267,11 @@ Filter activity by linked socialaction: Filtrer les activités par action liée Filter activity by linked socialissue: Filtrer les activités par problématique liée 'Filtered activity by linked socialissue: only %issues%': "Filtré par problématique liée: uniquement %issues%" Filter activity by user: Filtrer les activités par créateur -'Filtered activity by user: only %users%': "Filtré par créateur: uniquement %users%" +Filter activity by users: Filtrer les activités par utilisateur participant +Filter activity by creator: Filtrer les activités par créateur de l'échange +'Filtered activity by user: only %users%': "Filtré par référent: uniquement %users%" +'Filtered activity by users: only %users%': "Filtré par utilisateurs participants: uniquement %users%" +'Filtered activity by creator: only %users%': "Filtré par créateur: uniquement %users%" Creators: Créateurs Filter activity by userscope: Filtrer les activités par service du créateur 'Filtered activity by userscope: only %scopes%': "Filtré par service du créateur: uniquement %scopes%" @@ -282,7 +284,8 @@ By reason: Par sujet By category of reason: Par catégorie de sujet Reason's level: Niveau du sujet Group by reasons: Sujet d'activité -Aggregate by activity user: Grouper les activités par utilisateur +Aggregate by activity user: Grouper les activités par référent +Aggregate by activity users: Grouper les activités par utilisateurs participants Aggregate by activity type: Grouper les activités par type Aggregate by activity reason: Grouper les activités par sujet From 50e12375f882923a4991542c963bafdbcee9f6b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 17 Oct 2022 00:25:16 +0200 Subject: [PATCH 13/13] Feature: [export][activity] Group by users (participating) job and scope on activity exports --- .../Aggregator/ActivityUsersJobAggregator.php | 87 +++++++++++++++++++ .../ActivityUsersScopeAggregator.php | 87 +++++++++++++++++++ .../config/services/export.yaml | 8 ++ .../translations/messages.fr.yml | 4 + 4 files changed, 186 insertions(+) create mode 100644 src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersJobAggregator.php create mode 100644 src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersScopeAggregator.php diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersJobAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersJobAggregator.php new file mode 100644 index 000000000..a0a6a439b --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersJobAggregator.php @@ -0,0 +1,87 @@ +userJobRepository = $userJobRepository; + $this->translatableStringHelper = $translatableStringHelper; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + if (!in_array('actusers', $qb->getAllAliases(), true)) { + $qb->leftJoin('activity.users', 'actusers'); + } + + $qb + ->addSelect('IDENTITY(actusers.userJob) AS activity_users_job_aggregator') + ->addGroupBy('activity_users_job_aggregator'); + } + + public function applyOn() + { + return Declarations::ACTIVITY; + } + + public function buildForm(FormBuilderInterface $builder) + { + // nothing to add in the form + } + + public function getLabels($key, array $values, $data) + { + return function ($value): string { + if ('_header' === $value) { + return 'Users \'s job'; + } + + if (null === $value) { + return ''; + } + + $j = $this->userJobRepository->find($value); + + return $this->translatableStringHelper->localize( + $j->getLabel() + ); + }; + } + + public function getQueryKeys($data): array + { + return ['activity_users_job_aggregator']; + } + + public function getTitle() + { + return 'Aggregate by users job'; + } +} diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersScopeAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersScopeAggregator.php new file mode 100644 index 000000000..975c5df27 --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersScopeAggregator.php @@ -0,0 +1,87 @@ +scopeRepository = $scopeRepository; + $this->translatableStringHelper = $translatableStringHelper; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + if (!in_array('actusers', $qb->getAllAliases(), true)) { + $qb->leftJoin('activity.users', 'actusers'); + } + + $qb + ->addSelect('IDENTITY(actusers.mainScope) AS activity_users_main_scope_aggregator') + ->addGroupBy('activity_users_main_scope_aggregator'); + } + + public function applyOn() + { + return Declarations::ACTIVITY; + } + + public function buildForm(FormBuilderInterface $builder) + { + // nothing to add in the form + } + + public function getLabels($key, array $values, $data) + { + return function ($value): string { + if ('_header' === $value) { + return 'Users \'s scope'; + } + + if (null === $value) { + return ''; + } + + $s = $this->scopeRepository->find($value); + + return $this->translatableStringHelper->localize( + $s->getName() + ); + }; + } + + public function getQueryKeys($data): array + { + return ['activity_users_main_scope_aggregator']; + } + + public function getTitle() + { + return 'Aggregate by users scope'; + } +} diff --git a/src/Bundle/ChillActivityBundle/config/services/export.yaml b/src/Bundle/ChillActivityBundle/config/services/export.yaml index 9af8c257f..69565e29a 100644 --- a/src/Bundle/ChillActivityBundle/config/services/export.yaml +++ b/src/Bundle/ChillActivityBundle/config/services/export.yaml @@ -167,3 +167,11 @@ services: Chill\ActivityBundle\Export\Aggregator\ActivityUsersAggregator: tags: - { name: chill.export_aggregator, alias: activity_users_aggregator } + + Chill\ActivityBundle\Export\Aggregator\ActivityUsersScopeAggregator: + tags: + - { name: chill.export_aggregator, alias: activity_users_scope_aggregator } + + Chill\ActivityBundle\Export\Aggregator\ActivityUsersJobAggregator: + tags: + - { name: chill.export_aggregator, alias: activity_users_job_aggregator } diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml index 55749a9ca..3a6ff1f05 100644 --- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml @@ -288,6 +288,10 @@ Aggregate by activity user: Grouper les activités par référent Aggregate by activity users: Grouper les activités par utilisateurs participants Aggregate by activity type: Grouper les activités par type Aggregate by activity reason: Grouper les activités par sujet +Aggregate by users scope: Grouper les activités par service principal de l'utilisateur +Users 's scope: Service principal des utilisateurs participants à l'activité +Aggregate by users job: Grouper les activités par métier des utilisateurs participants +Users 's job: Métier des utilisateurs participants à l'activité Group activity by locationtype: Grouper les activités par type de localisation Group activity by date: Grouper les activités par date