diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/Providers/PersonDocumentGenericDocProvider.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/Providers/PersonDocumentGenericDocProvider.php index 08a0df960..613f8d758 100644 --- a/src/Bundle/ChillDocStoreBundle/GenericDoc/Providers/PersonDocumentGenericDocProvider.php +++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/Providers/PersonDocumentGenericDocProvider.php @@ -12,14 +12,16 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\GenericDoc\Providers; use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface; +use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface; use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface; use Chill\DocStoreBundle\Repository\PersonDocumentACLAwareRepositoryInterface; use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter; +use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; use DateTimeImmutable; use Symfony\Component\Security\Core\Security; -final readonly class PersonDocumentGenericDocProvider implements GenericDocForPersonProviderInterface +final readonly class PersonDocumentGenericDocProvider implements GenericDocForPersonProviderInterface, GenericDocForAccompanyingPeriodProviderInterface { public const KEY = 'person_document'; @@ -48,4 +50,16 @@ final readonly class PersonDocumentGenericDocProvider implements GenericDocForPe { return $this->security->isGranted(PersonDocumentVoter::SEE, $person); } + + public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface + { + return $this->personDocumentACLAwareRepository->buildFetchQueryForAccompanyingPeriod($accompanyingPeriod, $startDate, $endDate, $content); + } + + public function isAllowedForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): bool + { + // we assume that the user is allowed to see at least one person of the course + // this will be double checked when running the query + return true; + } } diff --git a/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepository.php index 5d85541aa..26a42b894 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepository.php @@ -22,6 +22,8 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface; use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher; use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface; use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; +use Chill\PersonBundle\Entity\AccompanyingPeriod; +use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; use Chill\PersonBundle\Entity\Person; use DateTimeImmutable; use Doctrine\DBAL\Types\Types; @@ -29,19 +31,14 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Security\Core\Security; -class PersonDocumentACLAwareRepository implements PersonDocumentACLAwareRepositoryInterface +final readonly class PersonDocumentACLAwareRepository implements PersonDocumentACLAwareRepositoryInterface { - private AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser; - - private CenterResolverManagerInterface $centerResolverManager; - - private EntityManagerInterface $em; - - public function __construct(EntityManagerInterface $em, CenterResolverManagerInterface $centerResolverManager, AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser) - { - $this->em = $em; - $this->centerResolverManager = $centerResolverManager; - $this->authorizationHelperForCurrentUser = $authorizationHelperForCurrentUser; + public function __construct( + private EntityManagerInterface $em, + private CenterResolverManagerInterface $centerResolverManager, + private AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser, + private Security $security, + ) { } public function buildQueryByPerson(Person $person): QueryBuilder @@ -63,6 +60,66 @@ class PersonDocumentACLAwareRepository implements PersonDocumentACLAwareReposito return $this->addFetchQueryByPersonACL($query, $person); } + public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $period, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQueryInterface + { + $personDocMetadata = $this->em->getClassMetadata(PersonDocument::class); + $participationMetadata = $this->em->getClassMetadata(AccompanyingPeriodParticipation::class); + + $query = new FetchQuery( + PersonDocumentGenericDocProvider::KEY, + sprintf('jsonb_build_object(\'id\', person_document.%s)', $personDocMetadata->getSingleIdentifierColumnName()), + sprintf('person_document.%s', $personDocMetadata->getColumnName('date')), + sprintf('%s AS person_document', $personDocMetadata->getSchemaName().'.'.$personDocMetadata->getTableName()) + ); + + $query->addJoinClause( + sprintf( + 'JOIN %s AS participation ON participation.%s = person_document.%s '. + 'AND person_document.%s BETWEEN participation.%s AND COALESCE(participation.%s, \'infinity\'::date)', + $participationMetadata->getTableName(), + $participationMetadata->getSingleAssociationJoinColumnName('person'), + $personDocMetadata->getSingleAssociationJoinColumnName('person'), + $personDocMetadata->getColumnName('date'), + $participationMetadata->getColumnName('startDate'), + $participationMetadata->getColumnName('endDate') + ) + ); + + $query->addWhereClause( + sprintf('participation.%s = ?', $participationMetadata->getSingleAssociationJoinColumnName('accompanyingPeriod')), + [$period->getId()], + [Types::INTEGER] + ); + + // can we see the document for this person ? + $orPersonId = []; + foreach ($period->getParticipations() as $participation) { + if (!$this->security->isGranted(PersonDocumentVoter::SEE, $participation->getPerson())) { + continue; + } + $orPersonId[] = $participation->getPerson()->getId(); + + } + + if ([] === $orPersonId) { + $query->addWhereClause('FALSE = TRUE'); + + return $query; + } + + $query->addWhereClause( + sprintf( + 'participation.%s IN (%s)', + $participationMetadata->getSingleAssociationJoinColumnName('person'), + implode(', ', array_fill(0, count($orPersonId), '?')) + ), + $orPersonId, + array_fill(0, count($orPersonId), Types::INTEGER) + ); + + return $this->addFilterClauses($query, $startDate, $endDate, $content); + } + public function buildBaseFetchQueryForPerson(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery { $personDocMetadata = $this->em->getClassMetadata(PersonDocument::class); @@ -80,6 +137,13 @@ class PersonDocumentACLAwareRepository implements PersonDocumentACLAwareReposito [Types::INTEGER] ); + return $this->addFilterClauses($query, $startDate, $endDate, $content); + } + + private function addFilterClauses(FetchQuery $query, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery + { + $personDocMetadata = $this->em->getClassMetadata(PersonDocument::class); + if (null !== $startDate) { $query->addWhereClause( sprintf('? <= %s', $personDocMetadata->getColumnName('date')), @@ -107,7 +171,6 @@ class PersonDocumentACLAwareRepository implements PersonDocumentACLAwareReposito [Types::STRING, Types::STRING] ); } - return $query; } diff --git a/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepositoryInterface.php b/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepositoryInterface.php index 0b5e26792..f1bc70812 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepositoryInterface.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepositoryInterface.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Repository; use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface; +use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; interface PersonDocumentACLAwareRepositoryInterface @@ -32,4 +33,11 @@ interface PersonDocumentACLAwareRepositoryInterface ?\DateTimeImmutable $endDate = null, ?string $content = null ): FetchQueryInterface; + + public function buildFetchQueryForAccompanyingPeriod( + AccompanyingPeriod $period, + ?\DateTimeImmutable $startDate = null, + ?\DateTimeImmutable $endDate = null, + ?string $content = null + ): FetchQueryInterface; } diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig index 9be38074d..58504b095 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig @@ -17,7 +17,14 @@ {{ accompanyingCourse.id }}   - {% endif %} + {% elseif context == 'accompanying-period' and person is defined %} +
+ + {{ 'Document from person %name%'|trans({ '%name%': document.person|chill_entity_render_string }) }} +   +
+ + {% endif %}
{{ document.title|chill_print_or_message("No title") }}
diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Repository/PersonDocumentACLAwareRepositoryTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Repository/PersonDocumentACLAwareRepositoryTest.php index 98fca5622..fd611042c 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Repository/PersonDocumentACLAwareRepositoryTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Repository/PersonDocumentACLAwareRepositoryTest.php @@ -21,12 +21,14 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface; use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher; use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface; use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; +use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; use DateTimeImmutable; use Doctrine\ORM\EntityManagerInterface; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\Security\Core\Security; /** * @internal @@ -66,7 +68,8 @@ class PersonDocumentACLAwareRepositoryTest extends KernelTestCase $repository = new PersonDocumentACLAwareRepository( $this->entityManager, $centerManager->reveal(), - $authorizationHelper->reveal() + $authorizationHelper->reveal(), + $this->prophesize(Security::class)->reveal() ); $person = $this->entityManager->createQuery("SELECT p FROM " . Person::class . " p") @@ -86,6 +89,62 @@ class PersonDocumentACLAwareRepositoryTest extends KernelTestCase self::assertIsInt($nb, "test that the query could be executed"); } + /** + * @dataProvider provideDateForFetchQueryForAccompanyingPeriod + */ + public function testBuildFetchQueryForAccompanyingPeriod( + AccompanyingPeriod $period, + ?\DateTimeImmutable $startDate = null, + ?\DateTimeImmutable $endDate = null, + ?string $content = null + ): void { + $centerManager = $this->prophesize(CenterResolverManagerInterface::class); + $centerManager->resolveCenters(Argument::type(Person::class)) + ->willReturn([new Center()]); + + $scopes = $this->scopeRepository->findAll(); + $authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class); + $authorizationHelper->getReachableScopes(PersonDocumentVoter::SEE, Argument::any())->willReturn($scopes); + + $security = $this->prophesize(Security::class); + $security->isGranted(PersonDocumentVoter::SEE, Argument::type(Person::class))->willReturn(true); + + $repository = new PersonDocumentACLAwareRepository( + $this->entityManager, + $centerManager->reveal(), + $authorizationHelper->reveal(), + $security->reveal() + ); + + $query = $repository->buildFetchQueryForAccompanyingPeriod($period, $startDate, $endDate, $content); + ['sql' => $sql, 'params' => $params, 'types' => $types] = (new FetchQueryToSqlBuilder())->toSql($query); + + $nb = $this->entityManager->getConnection() + ->fetchOne("SELECT COUNT(*) FROM ({$sql}) AS sq", $params, $types); + + self::assertIsInt($nb, "test that the query could be executed"); + } + + public function provideDateForFetchQueryForAccompanyingPeriod(): iterable + { + $this->setUp(); + + if (null === $period = $this->entityManager->createQuery( + "SELECT p FROM " . AccompanyingPeriod::class . " p WHERE SIZE(p.participations) > 0" + ) + ->setMaxResults(1)->getSingleResult()) { + throw new \RuntimeException("no course found"); + } + + yield [$period, null, null, null]; + yield [$period, new DateTimeImmutable('1 year ago'), null, null]; + yield [$period, null, new DateTimeImmutable('1 year ago'), null]; + yield [$period, new DateTimeImmutable('2 years ago'), new DateTimeImmutable('1 year ago'), null]; + yield [$period, null, null, 'test']; + yield [$period, new DateTimeImmutable('2 years ago'), new DateTimeImmutable('1 year ago'), 'test']; + + } + public function provideDataBuildFetchQueryForPerson(): iterable { yield [null, null, null]; diff --git a/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml b/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml index 1dde57eee..d4531fa2b 100644 --- a/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml @@ -18,6 +18,7 @@ No document found: Aucun document trouvé The document is successfully registered: Le document est enregistré The document is successfully updated: Le document est mis à jour Any description: Aucune description +Document from person %name%: Document de l'usager %name% document: Any title: Aucun titre @@ -26,6 +27,7 @@ generic_doc: filter: keys: accompanying_course_document: Document du parcours + person_document: Documents de l'usager date-range: Date du document # delete diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php index a8e191df3..18ad2da7d 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php @@ -269,6 +269,7 @@ class AccompanyingPeriod implements * cascade={"persist", "refresh", "remove", "merge", "detach"}) * @Groups({"read", "docgen:read"}) * @ParticipationOverlap(groups={AccompanyingPeriod::STEP_DRAFT, AccompanyingPeriod::STEP_CONFIRMED}) + * @var Collection */ private Collection $participations; @@ -870,6 +871,7 @@ class AccompanyingPeriod implements /** * Get Participations Collection. + * @return Collection */ public function getParticipations(): Collection { diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 6205fabae..aeaa2bb3f 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -1236,4 +1236,3 @@ generic_doc: filter: keys: accompanying_period_work_evaluation_document: Document des actions d'accompagnement - person_document: Documents de la personne