diff --git a/.changes/unreleased/Feature-20230613-151546.yaml b/.changes/unreleased/Feature-20230613-151546.yaml
new file mode 100644
index 000000000..e66076aa5
--- /dev/null
+++ b/.changes/unreleased/Feature-20230613-151546.yaml
@@ -0,0 +1,5 @@
+kind: Feature
+body: Get an unified list of document in person and accompanying period context
+time: 2023-06-13T15:15:46.146899906+02:00
+custom:
+ Issue: "103"
diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityDocumentACLAwareRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityDocumentACLAwareRepository.php
new file mode 100644
index 000000000..ce70409ba
--- /dev/null
+++ b/src/Bundle/ChillActivityBundle/Repository/ActivityDocumentACLAwareRepository.php
@@ -0,0 +1,198 @@
+buildBaseFetchQueryActivityDocumentLinkedToPersonFromPersonContext($person, $startDate, $endDate, $content);
+
+ return $this->addFetchQueryByPersonACL($query, $person);
+ }
+
+ public function buildBaseFetchQueryActivityDocumentLinkedToPersonFromPersonContext(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery
+ {
+ $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class);
+ $activityMetadata = $this->em->getClassMetadata(Activity::class);
+
+ $query = new FetchQuery(
+ PersonActivityGenericDocProvider::KEY,
+ sprintf('jsonb_build_object(\'id\', stored_obj.%s, \'activity_id\', activity.%s)', $storedObjectMetadata->getSingleIdentifierColumnName(), $activityMetadata->getSingleIdentifierColumnName()),
+ sprintf('stored_obj.%s', $storedObjectMetadata->getColumnName('createdAt')),
+ sprintf('%s AS stored_obj', $storedObjectMetadata->getSchemaName().'.'.$storedObjectMetadata->getTableName())
+ );
+
+ $query->addJoinClause(
+ 'JOIN public.activity_storedobject activity_doc ON activity_doc.storedobject_id = stored_obj.id'
+ );
+
+ $query->addJoinClause(
+ 'JOIN public.activity activity ON activity.id = activity_doc.activity_id'
+ );
+
+ $query->addWhereClause(
+ sprintf('activity.%s = ?', $activityMetadata->getSingleAssociationJoinColumnName('person')),
+ [$person->getId()],
+ [Types::INTEGER]
+ );
+
+ return $this->addWhereClauses($query, $startDate, $endDate, $content);
+ }
+
+ public function buildFetchQueryActivityDocumentLinkedToAccompanyingPeriodFromPersonContext(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery
+ {
+ $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class);
+ $activityMetadata = $this->em->getClassMetadata(Activity::class);
+
+ $query = new FetchQuery(
+ AccompanyingPeriodActivityGenericDocProvider::KEY,
+ sprintf('jsonb_build_object(\'id\', stored_obj.%s, \'activity_id\', activity.%s)', $storedObjectMetadata->getSingleIdentifierColumnName(), $activityMetadata->getSingleIdentifierColumnName()),
+ sprintf('stored_obj.%s', $storedObjectMetadata->getColumnName('createdAt')),
+ sprintf('%s AS stored_obj', $storedObjectMetadata->getSchemaName().'.'.$storedObjectMetadata->getTableName())
+ );
+
+ $query->addJoinClause(
+ 'JOIN public.activity_storedobject activity_doc ON activity_doc.storedobject_id = stored_obj.id'
+ );
+
+ $query->addJoinClause(
+ 'JOIN public.activity activity ON activity.id = activity_doc.activity_id'
+ );
+
+ // add documents of activities from parcours context
+ $or = [];
+ $orParams = [];
+ $orTypes = [];
+ foreach ($person->getAccompanyingPeriodParticipations() as $participation) {
+ if (!$this->security->isGranted(ActivityVoter::SEE, $participation->getAccompanyingPeriod())) {
+ continue;
+ }
+
+ $or[] = sprintf(
+ '(activity.%s = ? AND stored_obj.%s BETWEEN ?::date AND COALESCE(?::date, \'infinity\'::date))',
+ $activityMetadata->getSingleAssociationJoinColumnName('accompanyingPeriod'),
+ $storedObjectMetadata->getColumnName('createdAt')
+ );
+ $orParams = [...$orParams, $participation->getAccompanyingPeriod()->getId(),
+ DateTimeImmutable::createFromInterface($participation->getStartDate()),
+ null === $participation->getEndDate() ? null : DateTimeImmutable::createFromInterface($participation->getEndDate())];
+ $orTypes = [...$orTypes, Types::INTEGER, Types::DATE_IMMUTABLE, Types::DATE_IMMUTABLE];
+ }
+
+ if ([] === $or) {
+ $query->addWhereClause('TRUE = FALSE');
+
+ return $query;
+ }
+
+ $query->addWhereClause(sprintf('(%s)', implode(' OR ', $or)), $orParams, $orTypes);
+
+ return $this->addWhereClauses($query, $startDate, $endDate, $content);
+ }
+
+ private function addWhereClauses(FetchQuery $query, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery
+ {
+ $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class);
+
+ if (null !== $startDate) {
+ $query->addWhereClause(
+ sprintf('stored_obj.%s >= ?', $storedObjectMetadata->getColumnName('createdAt')),
+ [$startDate],
+ [Types::DATE_IMMUTABLE]
+ );
+ }
+
+ if (null !== $endDate) {
+ $query->addWhereClause(
+ sprintf('stored_obj.%s < ?', $storedObjectMetadata->getColumnName('createdAt')),
+ [$endDate],
+ [Types::DATE_IMMUTABLE]
+ );
+ }
+
+ if (null !== $content and '' !== $content) {
+ $query->addWhereClause(
+ 'stored_obj.title ilike ?',
+ ['%' . $content . '%'],
+ [Types::STRING]
+ );
+ }
+
+ return $query;
+ }
+
+ private function addFetchQueryByPersonACL(FetchQuery $fetchQuery, Person $person): FetchQuery
+ {
+ $activityMetadata = $this->em->getClassMetadata(Activity::class);
+
+ $reachableScopes = [];
+
+ foreach ($this->centerResolverManager->resolveCenters($person) as $center) {
+ $reachableScopes = [
+ ...$reachableScopes,
+ ...$this->authorizationHelperForCurrentUser->getReachableScopes(ActivityVoter::SEE, $center)
+ ];
+ }
+
+ if ([] === $reachableScopes) {
+ $fetchQuery->addWhereClause('FALSE = TRUE');
+
+ return $fetchQuery;
+ }
+
+ $fetchQuery->addWhereClause(
+ sprintf(
+ 'activity.%s IN (%s)',
+ $activityMetadata->getSingleAssociationJoinColumnName('scope'),
+ implode(', ', array_fill(0, count($reachableScopes), '?'))
+ ),
+ array_map(static fn (Scope $s) => $s->getId(), $reachableScopes),
+ array_fill(0, count($reachableScopes), Types::INTEGER)
+ );
+
+ return $fetchQuery;
+ }
+}
diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityDocumentACLAwareRepositoryInterface.php b/src/Bundle/ChillActivityBundle/Repository/ActivityDocumentACLAwareRepositoryInterface.php
new file mode 100644
index 000000000..9f4a9c0f8
--- /dev/null
+++ b/src/Bundle/ChillActivityBundle/Repository/ActivityDocumentACLAwareRepositoryInterface.php
@@ -0,0 +1,37 @@
+
+
+
+ {% if document.isPending %}
+
{{ 'docgen.Doc generation is pending'|trans }}
+ {% elseif document.isFailure %}
+
{{ 'docgen.Doc generation failed'|trans }}
+ {% endif %}
+
+
+ {% if activity.accompanyingPeriod is not null and context == 'person' %}
+
+ {{ activity.accompanyingPeriod.id }}
+
+ {% endif %}
+
+
+
+ {{ activity.type.name | localize_translatable_string }}
+ {% if activity.emergency %}
+ {{ 'Emergency'|trans|upper }}
+ {% endif %}
+
+
+
+
+ {{ document.title|chill_print_or_message("No title") }}
+
+ {% if document.hasTemplate %}
+
+
{{ document.template.name|localize_translatable_string }}
+
+ {% endif %}
+
+
+
+
+
+ {{ document.createdAt|format_date('short') }}
+
+
+
+
+
+
+
+
+ {{ mmm.createdBy(document) }}
+
+
+ {% if is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) %}
+ -
+ {{ document|chill_document_button_group(document.title, is_granted('CHILL_ACTIVITY_UPDATE', activity), {small: false}) }}
+
+ {% endif %}
+ {% if is_granted('CHILL_ACTIVITY_SEE', activity)%}
+ -
+
+
+ {% endif %}
+ {% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %}
+ -
+
+
+ {% endif %}
+
+
+
+
diff --git a/src/Bundle/ChillActivityBundle/Service/GenericDoc/Providers/AccompanyingPeriodActivityGenericDocProvider.php b/src/Bundle/ChillActivityBundle/Service/GenericDoc/Providers/AccompanyingPeriodActivityGenericDocProvider.php
new file mode 100644
index 000000000..334b5d2df
--- /dev/null
+++ b/src/Bundle/ChillActivityBundle/Service/GenericDoc/Providers/AccompanyingPeriodActivityGenericDocProvider.php
@@ -0,0 +1,114 @@
+em->getClassMetadata(StoredObject::class);
+ $activityMetadata = $this->em->getClassMetadata(Activity::class);
+
+ $query = new FetchQuery(
+ self::KEY,
+ sprintf("jsonb_build_object('id', doc_obj.%s, 'activity_id', activity.%s)", $storedObjectMetadata->getSingleIdentifierColumnName(), $activityMetadata->getSingleIdentifierColumnName()),
+ 'doc_obj.'.$storedObjectMetadata->getColumnName('createdAt'),
+ $storedObjectMetadata->getSchemaName().'.'.$storedObjectMetadata->getTableName().' AS doc_obj'
+ );
+
+ $query->addJoinClause(
+ 'JOIN public.activity_storedobject activity_doc ON activity_doc.storedobject_id = doc_obj.id'
+ );
+
+ $query->addJoinClause(
+ 'JOIN public.activity activity ON activity.id = activity_doc.activity_id'
+ );
+
+ $query->addWhereClause(
+ 'activity.accompanyingperiod_id = ?',
+ [$accompanyingPeriod->getId()],
+ [Types::INTEGER]
+ );
+
+ if (null !== $startDate) {
+ $query->addWhereClause(
+ sprintf('doc_obj.%s >= ?', $storedObjectMetadata->getColumnName('createdAt')),
+ [$startDate],
+ [Types::DATE_IMMUTABLE]
+ );
+ }
+
+ if (null !== $endDate) {
+ $query->addWhereClause(
+ sprintf('doc_obj.%s < ?', $storedObjectMetadata->getColumnName('createdAt')),
+ [$endDate],
+ [Types::DATE_IMMUTABLE]
+ );
+ }
+
+ if (null !== $content) {
+ $query->addWhereClause(
+ 'doc_obj.title ilike ?',
+ ['%' . $content . '%'],
+ [Types::STRING]
+ );
+ }
+
+ return $query;
+ }
+
+ /**
+ * @param AccompanyingPeriod $accompanyingPeriod
+ * @return bool
+ */
+ public function isAllowedForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): bool
+ {
+ return $this->security->isGranted(ActivityVoter::SEE, $accompanyingPeriod);
+ }
+
+ public function isAllowedForPerson(Person $person): bool
+ {
+ return $this->security->isGranted(AccompanyingPeriodVoter::SEE, $person);
+ }
+
+ public function buildFetchQueryForPerson(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
+ {
+ return $this->activityDocumentACLAwareRepository
+ ->buildFetchQueryActivityDocumentLinkedToAccompanyingPeriodFromPersonContext($person, $startDate, $endDate, $content);
+ }
+}
diff --git a/src/Bundle/ChillActivityBundle/Service/GenericDoc/Providers/PersonActivityGenericDocProvider.php b/src/Bundle/ChillActivityBundle/Service/GenericDoc/Providers/PersonActivityGenericDocProvider.php
new file mode 100644
index 000000000..cf96449ab
--- /dev/null
+++ b/src/Bundle/ChillActivityBundle/Service/GenericDoc/Providers/PersonActivityGenericDocProvider.php
@@ -0,0 +1,53 @@
+personActivityDocumentACLAwareRepository->buildFetchQueryActivityDocumentLinkedToPersonFromPersonContext(
+ $person,
+ $startDate,
+ $endDate,
+ $content
+ );
+ }
+
+ /**
+ * @param Person $person
+ * @return bool
+ */
+ public function isAllowedForPerson(Person $person): bool
+ {
+ return $this->security->isGranted(ActivityVoter::SEE, $person);
+ }
+}
diff --git a/src/Bundle/ChillActivityBundle/Service/GenericDoc/Renderers/AccompanyingPeriodActivityGenericDocRenderer.php b/src/Bundle/ChillActivityBundle/Service/GenericDoc/Renderers/AccompanyingPeriodActivityGenericDocRenderer.php
new file mode 100644
index 000000000..c465dbca1
--- /dev/null
+++ b/src/Bundle/ChillActivityBundle/Service/GenericDoc/Renderers/AccompanyingPeriodActivityGenericDocRenderer.php
@@ -0,0 +1,52 @@
+objectRepository = $storedObjectRepository;
+ $this->activityRepository = $activityRepository;
+ }
+
+ public function supports(GenericDocDTO $genericDocDTO, $options = []): bool
+ {
+ return $genericDocDTO->key === AccompanyingPeriodActivityGenericDocProvider::KEY || $genericDocDTO->key === PersonActivityGenericDocProvider::KEY;
+ }
+
+ public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string
+ {
+ return '@ChillActivity/GenericDoc/activity_document.html.twig';
+ }
+
+ public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array
+ {
+ return [
+ 'activity' => $this->activityRepository->find($genericDocDTO->identifiers['activity_id']),
+ 'document' => $this->objectRepository->find($genericDocDTO->identifiers['id']),
+ 'context' => $genericDocDTO->getContext(),
+ ];
+ }
+}
diff --git a/src/Bundle/ChillActivityBundle/Tests/Repository/ActivityDocumentACLAwareRepositoryTest.php b/src/Bundle/ChillActivityBundle/Tests/Repository/ActivityDocumentACLAwareRepositoryTest.php
new file mode 100644
index 000000000..ce4f318e3
--- /dev/null
+++ b/src/Bundle/ChillActivityBundle/Tests/Repository/ActivityDocumentACLAwareRepositoryTest.php
@@ -0,0 +1,126 @@
+entityManager = self::$container->get(EntityManagerInterface::class);
+ $this->centerResolverManager = self::$container->get(CenterResolverManagerInterface::class);
+ $this->authorizationHelperForCurrentUser = self::$container->get(AuthorizationHelperForCurrentUserInterface::class);
+ $this->security = self::$container->get(Security::class);
+ }
+
+ /**
+ * @dataProvider provideDataForPerson
+ */
+ public function testBuildFetchQueryActivityDocumentLinkedToPersonFromPersonContext(Person $person, array $reachableScopes, bool $_unused, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?string $content): void
+ {
+ $authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class);
+ $authorizationHelper->getReachableScopes(ActivityVoter::SEE, Argument::any())
+ ->willReturn($reachableScopes);
+
+ $repository = new ActivityDocumentACLAwareRepository(
+ $this->entityManager,
+ $this->centerResolverManager,
+ $authorizationHelper->reveal(),
+ $this->security
+ );
+
+ $query = $repository->buildFetchQueryActivityDocumentLinkedToPersonFromPersonContext($person, $startDate, $endDate, $content);
+ ['sql' => $sql, 'params' => $params, 'types' => $types] = (new FetchQueryToSqlBuilder())->toSql($query);
+
+ $nb = $this->entityManager->getConnection()->fetchOne("SELECT COUNT(*) FROM ({$sql}) sq", $params, $types);
+
+ self::assertIsInt($nb);
+ }
+
+ /**
+ * @dataProvider provideDataForPerson
+ */
+ public function testBuildFetchQueryActivityDocumentLinkedToAccompanyingPeriodFromPersonContext(Person $person, array $_unused, bool $canSeePeriod, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?string $content): void
+ {
+ $security = $this->prophesize(Security::class);
+ $security->isGranted(ActivityVoter::SEE, Argument::type(AccompanyingPeriod::class))
+ ->willReturn($canSeePeriod);
+
+ $repository = new ActivityDocumentACLAwareRepository(
+ $this->entityManager,
+ $this->centerResolverManager,
+ $this->authorizationHelperForCurrentUser,
+ $security->reveal()
+ );
+
+ $query = $repository->buildFetchQueryActivityDocumentLinkedToAccompanyingPeriodFromPersonContext($person, $startDate, $endDate, $content);
+
+ ['sql' => $sql, 'params' => $params, 'types' => $types] = (new FetchQueryToSqlBuilder())->toSql($query);
+
+ $nb = $this->entityManager->getConnection()->fetchOne("SELECT COUNT(*) FROM ({$sql}) sq", $params, $types);
+
+ self::assertIsInt($nb);
+ }
+
+ public function provideDataForPerson(): iterable
+ {
+ $this->setUp();
+
+ if (null === $person = $this->entityManager->createQuery("SELECT p FROM " . Person::class . " p WHERE SIZE(p.accompanyingPeriodParticipations) > 0 ")
+ ->setMaxResults(1)
+ ->getSingleResult()) {
+ throw new \RuntimeException("no person in dtabase");
+ }
+
+ if ([] === $scopes = $this->entityManager->createQuery("SELECT s FROM " . Scope::class . " s ")->setMaxResults(5)->getResult()) {
+ throw new \RuntimeException("no scopes in database");
+ }
+
+ yield [$person, [], true, null, null, null];
+ yield [$person, $scopes, true, null, null, null];
+ yield [$person, $scopes, true, new \DateTimeImmutable("1 month ago"), null, null];
+ yield [$person, $scopes, true, new \DateTimeImmutable("1 month ago"), new \DateTimeImmutable("1 week ago"), null];
+ yield [$person, $scopes, true, new \DateTimeImmutable("1 month ago"), new \DateTimeImmutable("1 week ago"), "content"];
+ yield [$person, $scopes, true, null, new \DateTimeImmutable("1 week ago"), "content"];
+ yield [$person, [], true, new \DateTimeImmutable("1 month ago"), new \DateTimeImmutable("1 week ago"), "content"];
+ }
+
+}
diff --git a/src/Bundle/ChillActivityBundle/config/services.yaml b/src/Bundle/ChillActivityBundle/config/services.yaml
index d55f86d4f..18be76ec9 100644
--- a/src/Bundle/ChillActivityBundle/config/services.yaml
+++ b/src/Bundle/ChillActivityBundle/config/services.yaml
@@ -38,3 +38,6 @@ services:
Chill\ActivityBundle\Service\EntityInfo\:
resource: '../Service/EntityInfo/'
+
+ Chill\ActivityBundle\Service\GenericDoc\:
+ resource: '../Service/GenericDoc/'
diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml
index 042fccc69..abef160d3 100644
--- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml
+++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml
@@ -372,3 +372,8 @@ export:
is sent: envoyé
is received: reçu
Group activity by sentreceived: Grouper les échanges par envoyé / reçu
+
+generic_doc:
+ filter:
+ keys:
+ accompanying_period_activity_document: Document des échanges des parcours
diff --git a/src/Bundle/ChillCalendarBundle/Resources/public/chill/chill.js b/src/Bundle/ChillCalendarBundle/Resources/public/chill/chill.js
new file mode 100644
index 000000000..56a8ce563
--- /dev/null
+++ b/src/Bundle/ChillCalendarBundle/Resources/public/chill/chill.js
@@ -0,0 +1 @@
+import './scss/badge.scss';
diff --git a/src/Bundle/ChillCalendarBundle/Resources/public/chill/scss/badge.scss b/src/Bundle/ChillCalendarBundle/Resources/public/chill/scss/badge.scss
new file mode 100644
index 000000000..ffcda8f0f
--- /dev/null
+++ b/src/Bundle/ChillCalendarBundle/Resources/public/chill/scss/badge.scss
@@ -0,0 +1,25 @@
+@import '~ChillPersonAssets/chill/scss/mixins.scss';
+@import '~ChillMainAssets/module/bootstrap/shared';
+
+.badge-calendar {
+ display: inline-block;
+ background-color: #f3f3f3;
+
+ .title_label {
+ @include chill_badge($chill-l-gray);
+ }
+
+ .title_action {
+ padding: var(--bs-badge-padding-y) var(--bs-badge-padding-x);
+ margin-right: 1rem;
+
+ font-size: var(--bs-badge-font-size);
+ font-weight: var(--bs-badge-font-weight);
+ line-height: 1;
+ color: var(--bs-badge-color);
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: baseline;
+ }
+}
+
diff --git a/src/Bundle/ChillCalendarBundle/Resources/public/chill/scss/calendar.scss b/src/Bundle/ChillCalendarBundle/Resources/public/chill/scss/calendar.scss
index a2c0c4b89..ce54b0fa8 100644
--- a/src/Bundle/ChillCalendarBundle/Resources/public/chill/scss/calendar.scss
+++ b/src/Bundle/ChillCalendarBundle/Resources/public/chill/scss/calendar.scss
@@ -17,4 +17,4 @@ span.calendarRangeItems {
text-decoration: none;
padding: 3px;
}
-}
\ No newline at end of file
+}
diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/GenericDoc/calendar_document.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/GenericDoc/calendar_document.html.twig
new file mode 100644
index 000000000..facf5be50
--- /dev/null
+++ b/src/Bundle/ChillCalendarBundle/Resources/views/GenericDoc/calendar_document.html.twig
@@ -0,0 +1,75 @@
+{% import "@ChillDocStore/Macro/macro.html.twig" as m %}
+{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %}
+{% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %}
+
+{% set c = document.calendar %}
+
+
+
+
+ {% if document.storedObject.isPending %}
+
{{ 'docgen.Doc generation is pending'|trans }}
+ {% elseif document.storedObject.isFailure %}
+
{{ 'docgen.Doc generation failed'|trans }}
+ {% endif %}
+
+
+ {% if c.accompanyingPeriod is not null and context == 'person' %}
+
+ {{ c.accompanyingPeriod.id }}
+
+ {% endif %}
+
+
+
+
+ {{ 'Calendar'|trans }}
+ {% if c.endDate.diff(c.startDate).days >= 1 %}
+ {{ c.startDate|format_datetime('short', 'short') }}
+ - {{ c.endDate|format_datetime('short', 'short') }}
+ {% else %}
+ {{ c.startDate|format_datetime('short', 'short') }}
+ - {{ c.endDate|format_datetime('none', 'short') }}
+ {% endif %}
+
+
+
+
+
+ {{ document.storedObject.title|chill_print_or_message("No title") }}
+
+ {% if document.storedObject.hasTemplate %}
+
+
{{ document.storedObject.template.name|localize_translatable_string }}
+
+ {% endif %}
+
+
+
+
+
+ {{ document.storedObject.createdAt|format_date('short') }}
+
+
+
+
+
+
+
+ {{ mmm.createdBy(document) }}
+
+
+ {% if is_granted('CHILL_CALENDAR_DOC_SEE', document) %}
+ -
+ {{ document.storedObject|chill_document_button_group(document.storedObject.title, is_granted('CHILL_CALENDAR_CALENDAR_EDIT', c)) }}
+
+ {% endif %}
+ {% if is_granted('CHILL_CALENDAR_CALENDAR_EDIT', c) %}
+ -
+
+
+ {% endif %}
+
+
+
+
diff --git a/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Providers/AccompanyingPeriodCalendarGenericDocProvider.php b/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Providers/AccompanyingPeriodCalendarGenericDocProvider.php
new file mode 100644
index 000000000..c33ccd853
--- /dev/null
+++ b/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Providers/AccompanyingPeriodCalendarGenericDocProvider.php
@@ -0,0 +1,192 @@
+em->getClassMetadata(CalendarDoc::class);
+ $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class);
+ $calendarMetadata = $this->em->getClassMetadata(Calendar::class);
+
+ $query = new FetchQuery(
+ self::KEY,
+ sprintf("jsonb_build_object('id', cd.%s)", $classMetadata->getColumnName('id')),
+ 'cd.'.$storedObjectMetadata->getColumnName('createdAt'),
+ $classMetadata->getSchemaName().'.'.$classMetadata->getTableName().' AS cd'
+ );
+ $query->addJoinClause(
+ sprintf(
+ 'JOIN %s doc_store ON doc_store.%s = cd.%s',
+ $storedObjectMetadata->getSchemaName().'.'.$storedObjectMetadata->getTableName(),
+ $storedObjectMetadata->getColumnName('id'),
+ $classMetadata->getSingleAssociationJoinColumnName('storedObject')
+ )
+ );
+
+ $query->addJoinClause(
+ sprintf(
+ 'JOIN %s calendar ON calendar.%s = cd.%s',
+ $calendarMetadata->getSchemaName().'.'.$calendarMetadata->getTableName(),
+ $calendarMetadata->getColumnName('id'),
+ $classMetadata->getSingleAssociationJoinColumnName('calendar')
+ )
+ );
+
+ $query->addWhereClause(
+ sprintf(
+ 'calendar.%s = ?',
+ $calendarMetadata->getAssociationMapping('accompanyingPeriod')['joinColumns'][0]['name']
+ ),
+ [$accompanyingPeriod->getId()],
+ [Types::INTEGER]
+ );
+
+ return $query;
+ }
+
+ public function isAllowedForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): bool
+ {
+ return $this->security->isGranted(CalendarVoter::SEE, $accompanyingPeriod);
+ }
+
+ public function buildFetchQueryForPerson(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
+ {
+ $classMetadata = $this->em->getClassMetadata(CalendarDoc::class);
+ $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class);
+ $calendarMetadata = $this->em->getClassMetadata(Calendar::class);
+
+ $query = new FetchQuery(
+ self::KEY,
+ sprintf("jsonb_build_object('id', cd.%s)", $classMetadata->getColumnName('id')),
+ 'cd.'.$storedObjectMetadata->getColumnName('createdAt'),
+ $classMetadata->getSchemaName().'.'.$classMetadata->getTableName().' AS cd'
+ );
+ $query->addJoinClause(
+ sprintf(
+ 'JOIN %s doc_store ON doc_store.%s = cd.%s',
+ $storedObjectMetadata->getSchemaName().'.'.$storedObjectMetadata->getTableName(),
+ $storedObjectMetadata->getColumnName('id'),
+ $classMetadata->getSingleAssociationJoinColumnName('storedObject')
+ )
+ );
+
+ $query->addJoinClause(
+ sprintf(
+ 'JOIN %s calendar ON calendar.%s = cd.%s',
+ $calendarMetadata->getSchemaName().'.'.$calendarMetadata->getTableName(),
+ $calendarMetadata->getColumnName('id'),
+ $classMetadata->getSingleAssociationJoinColumnName('calendar')
+ )
+ );
+
+ // get the documents associated with accompanying periods in which person participates
+ $or = [];
+ $orParams = [];
+ $orTypes = [];
+ foreach ($person->getAccompanyingPeriodParticipations() as $participation) {
+ if (!$this->security->isGranted(CalendarVoter::SEE, $participation->getAccompanyingPeriod())) {
+ continue;
+ }
+
+ $or[] = sprintf(
+ '(calendar.%s = ? AND cd.%s BETWEEN ?::date AND COALESCE(?::date, \'infinity\'::date))',
+ $calendarMetadata->getSingleAssociationJoinColumnName('accompanyingPeriod'),
+ $storedObjectMetadata->getColumnName('createdAt')
+ );
+ $orParams = [...$orParams, $participation->getAccompanyingPeriod()->getId(),
+ DateTimeImmutable::createFromInterface($participation->getStartDate()),
+ null === $participation->getEndDate() ? null : DateTimeImmutable::createFromInterface($participation->getEndDate())];
+ $orTypes = [...$orTypes, Types::INTEGER, Types::DATE_IMMUTABLE, Types::DATE_IMMUTABLE];
+ }
+
+ if ([] === $or) {
+ $query->addWhereClause('TRUE = FALSE');
+
+ return $query;
+ }
+ return $this->addWhereClausesToQuery($query, $startDate, $endDate, $content);
+ }
+
+ public function isAllowedForPerson(Person $person): bool
+ {
+ // check that the person is allowed to see an accompanying period. If yes, the
+ // ACL on each accompanying period will be checked when the query is build
+ return $this->security->isGranted(AccompanyingPeriodVoter::SEE, $person);
+ }
+
+ private function addWhereClausesToQuery(FetchQuery $query, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate, ?string $content): FetchQuery
+ {
+ $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class);
+
+ if (null !== $startDate) {
+ $query->addWhereClause(
+ sprintf('doc_store.%s >= ?', $storedObjectMetadata->getColumnName('createdAt')),
+ [$startDate],
+ [Types::DATE_IMMUTABLE]
+ );
+ }
+
+ if (null !== $endDate) {
+ $query->addWhereClause(
+ sprintf('doc_store.%s < ?', $storedObjectMetadata->getColumnName('createdAt')),
+ [$endDate],
+ [Types::DATE_IMMUTABLE]
+ );
+ }
+
+ if (null !== $content) {
+ $query->addWhereClause(
+ sprintf('doc_store.%s ilike ?', $storedObjectMetadata->getColumnName('title')),
+ ['%' . $content . '%'],
+ [Types::STRING]
+ );
+ }
+
+ return $query;
+ }
+
+
+}
diff --git a/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Providers/PersonCalendarGenericDocProvider.php b/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Providers/PersonCalendarGenericDocProvider.php
new file mode 100644
index 000000000..f5d4b3cbb
--- /dev/null
+++ b/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Providers/PersonCalendarGenericDocProvider.php
@@ -0,0 +1,126 @@
+em->getClassMetadata(StoredObject::class);
+
+ if (null !== $startDate) {
+ $query->addWhereClause(
+ sprintf('doc_store.%s >= ?', $storedObjectMetadata->getColumnName('createdAt')),
+ [$startDate],
+ [Types::DATE_IMMUTABLE]
+ );
+ }
+
+ if (null !== $endDate) {
+ $query->addWhereClause(
+ sprintf('doc_store.%s < ?', $storedObjectMetadata->getColumnName('createdAt')),
+ [$endDate],
+ [Types::DATE_IMMUTABLE]
+ );
+ }
+
+ if (null !== $content) {
+ $query->addWhereClause(
+ sprintf('doc_store.%s ilike ?', $storedObjectMetadata->getColumnName('title')),
+ ['%' . $content . '%'],
+ [Types::STRING]
+ );
+ }
+
+ return $query;
+ }
+
+ /**
+ * @throws MappingException
+ */
+ public function buildFetchQueryForPerson(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
+ {
+ $classMetadata = $this->em->getClassMetadata(CalendarDoc::class);
+ $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class);
+ $calendarMetadata = $this->em->getClassMetadata(Calendar::class);
+
+ $query = new FetchQuery(
+ self::KEY,
+ sprintf("jsonb_build_object('id', cd.%s)", $classMetadata->getColumnName('id')),
+ 'cd.'.$storedObjectMetadata->getColumnName('createdAt'),
+ $classMetadata->getSchemaName().'.'.$classMetadata->getTableName().' AS cd'
+ );
+ $query->addJoinClause(
+ sprintf(
+ 'JOIN %s doc_store ON doc_store.%s = cd.%s',
+ $storedObjectMetadata->getSchemaName().'.'.$storedObjectMetadata->getTableName(),
+ $storedObjectMetadata->getColumnName('id'),
+ $classMetadata->getSingleAssociationJoinColumnName('storedObject')
+ )
+ );
+
+ $query->addJoinClause(
+ sprintf(
+ 'JOIN %s calendar ON calendar.%s = cd.%s',
+ $calendarMetadata->getSchemaName().'.'.$calendarMetadata->getTableName(),
+ $calendarMetadata->getColumnName('id'),
+ $classMetadata->getSingleAssociationJoinColumnName('calendar')
+ )
+ );
+
+ $query->addWhereClause(
+ sprintf('calendar.%s = ?', $calendarMetadata->getSingleAssociationJoinColumnName('person')),
+ [$person->getId()],
+ [Types::INTEGER]
+ );
+
+ return $this->addWhereClausesToQuery($query, $startDate, $endDate, $content);
+ }
+
+ /**
+ * @param Person $person
+ * @return bool
+ */
+ public function isAllowedForPerson(Person $person): bool
+ {
+ return $this->security->isGranted(CalendarVoter::SEE, $person);
+ }
+}
diff --git a/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Renderers/AccompanyingPeriodCalendarGenericDocRenderer.php b/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Renderers/AccompanyingPeriodCalendarGenericDocRenderer.php
new file mode 100644
index 000000000..b15c091c6
--- /dev/null
+++ b/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Renderers/AccompanyingPeriodCalendarGenericDocRenderer.php
@@ -0,0 +1,46 @@
+repository = $calendarDocRepository;
+ }
+
+ public function supports(GenericDocDTO $genericDocDTO, $options = []): bool
+ {
+ return $genericDocDTO->key === AccompanyingPeriodCalendarGenericDocProvider::KEY || $genericDocDTO->key === PersonCalendarGenericDocProvider::KEY;
+ }
+
+ public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string
+ {
+ return '@ChillCalendar/GenericDoc/calendar_document.html.twig';
+ }
+
+ public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array
+ {
+ return [
+ 'document' => $this->repository->find($genericDocDTO->identifiers['id']),
+ 'context' => $genericDocDTO->getContext(),
+ ];
+ }
+}
diff --git a/src/Bundle/ChillCalendarBundle/chill.webpack.config.js b/src/Bundle/ChillCalendarBundle/chill.webpack.config.js
index e82210087..9d45a3142 100644
--- a/src/Bundle/ChillCalendarBundle/chill.webpack.config.js
+++ b/src/Bundle/ChillCalendarBundle/chill.webpack.config.js
@@ -1,6 +1,8 @@
// this file loads all assets from the Chill calendar bundle
module.exports = function(encore, entries) {
+ entries.push(__dirname + '/Resources/public/chill/chill.js');
+
encore.addAliases({
ChillCalendarAssets: __dirname + '/Resources/public'
});
diff --git a/src/Bundle/ChillCalendarBundle/translations/messages.fr.yml b/src/Bundle/ChillCalendarBundle/translations/messages.fr.yml
index eb02be280..c56d7835f 100644
--- a/src/Bundle/ChillCalendarBundle/translations/messages.fr.yml
+++ b/src/Bundle/ChillCalendarBundle/translations/messages.fr.yml
@@ -43,6 +43,7 @@ crud:
title_edit: Modifier le motif d'annulation
chill_calendar:
+ Document: Document d'un rendez-vous
form:
The main user is mandatory. He will organize the appointment.: L'utilisateur principal est obligatoire. Il est l'organisateur de l'événement.
Create for referrer: Créer pour le référent
@@ -65,6 +66,7 @@ chill_calendar:
Document outdated: La date et l'heure du rendez-vous ont été modifiés après la création du document
+
remote_ms_graph:
freebusy_statuses:
busy: Occupé
@@ -145,3 +147,9 @@ CHILL_CALENDAR_CALENDAR_EDIT: Modifier les rendez-vous
CHILL_CALENDAR_CALENDAR_DELETE: Supprimer les rendez-vous
CHILL_CALENDAR_CALENDAR_SEE: Voir les rendez-vous
+
+generic_doc:
+ filter:
+ keys:
+ accompanying_period_calendar_document: Document des rendez-vous des parcours
+ person_calendar_document: Document des rendez-vous de l'usager
diff --git a/src/Bundle/ChillDocStoreBundle/ChillDocStoreBundle.php b/src/Bundle/ChillDocStoreBundle/ChillDocStoreBundle.php
index 81c71f45f..8dcbe72c3 100644
--- a/src/Bundle/ChillDocStoreBundle/ChillDocStoreBundle.php
+++ b/src/Bundle/ChillDocStoreBundle/ChillDocStoreBundle.php
@@ -11,8 +11,21 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle;
+use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
+use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
+use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class ChillDocStoreBundle extends Bundle
{
+ public function build(ContainerBuilder $container)
+ {
+ $container->registerForAutoconfiguration(GenericDocForAccompanyingPeriodProviderInterface::class)
+ ->addTag('chill_doc_store.generic_doc_accompanying_period_provider');
+ $container->registerForAutoconfiguration(GenericDocForPersonProviderInterface::class)
+ ->addTag('chill_doc_store.generic_doc_person_provider');
+ $container->registerForAutoconfiguration(GenericDocRendererInterface::class)
+ ->addTag('chill_doc_store.generic_doc_renderer');
+ }
}
diff --git a/src/Bundle/ChillDocStoreBundle/Controller/DocumentAccompanyingCourseController.php b/src/Bundle/ChillDocStoreBundle/Controller/DocumentAccompanyingCourseController.php
index 1bc3db221..384eeb510 100644
--- a/src/Bundle/ChillDocStoreBundle/Controller/DocumentAccompanyingCourseController.php
+++ b/src/Bundle/ChillDocStoreBundle/Controller/DocumentAccompanyingCourseController.php
@@ -39,10 +39,6 @@ class DocumentAccompanyingCourseController extends AbstractController
protected TranslatorInterface $translator;
- private AccompanyingCourseDocumentRepository $courseRepository;
-
- private PaginatorFactory $paginatorFactory;
-
/**
* DocumentAccompanyingCourseController constructor.
*/
@@ -50,14 +46,10 @@ class DocumentAccompanyingCourseController extends AbstractController
TranslatorInterface $translator,
EventDispatcherInterface $eventDispatcher,
AuthorizationHelper $authorizationHelper,
- PaginatorFactory $paginatorFactory,
- AccompanyingCourseDocumentRepository $courseRepository
) {
$this->translator = $translator;
$this->eventDispatcher = $eventDispatcher;
$this->authorizationHelper = $authorizationHelper;
- $this->paginatorFactory = $paginatorFactory;
- $this->courseRepository = $courseRepository;
}
/**
@@ -82,7 +74,7 @@ class DocumentAccompanyingCourseController extends AbstractController
return $this->redirect($request->query->get('returnPath'));
}
- return $this->redirectToRoute('accompanying_course_document_index', ['course' => $course->getId()]);
+ return $this->redirectToRoute('chill_docstore_generic-doc_by-period_index', ['id' => $course->getId()]);
}
return $this->render(
@@ -136,40 +128,6 @@ class DocumentAccompanyingCourseController extends AbstractController
);
}
- /**
- * @Route("/", name="accompanying_course_document_index", methods="GET")
- */
- public function index(AccompanyingPeriod $course): Response
- {
- $em = $this->getDoctrine()->getManager();
-
- if (null === $course) {
- throw $this->createNotFoundException('Accompanying period not found');
- }
-
- $this->denyAccessUnlessGranted(AccompanyingCourseDocumentVoter::SEE, $course);
-
- $total = $this->courseRepository->countByCourse($course);
- $pagination = $this->paginatorFactory->create($total);
-
- $documents = $this->courseRepository
- ->findBy(
- ['course' => $course],
- ['date' => 'DESC', 'id' => 'DESC'],
- $pagination->getItemsPerPage(),
- $pagination->getCurrentPageFirstItemNumber()
- );
-
- return $this->render(
- 'ChillDocStoreBundle:AccompanyingCourseDocument:index.html.twig',
- [
- 'documents' => $documents,
- 'accompanyingCourse' => $course,
- 'pagination' => $pagination,
- ]
- );
- }
-
/**
* @Route("/new", name="accompanying_course_document_new", methods="GET|POST")
*/
@@ -202,7 +160,7 @@ class DocumentAccompanyingCourseController extends AbstractController
$this->addFlash('success', $this->translator->trans('The document is successfully registered'));
- return $this->redirectToRoute('accompanying_course_document_index', ['course' => $course->getId()]);
+ return $this->redirectToRoute('chill_docstore_generic-doc_by-period_index', ['id' => $course->getId()]);
}
if ($form->isSubmitted() && !$form->isValid()) {
diff --git a/src/Bundle/ChillDocStoreBundle/Controller/DocumentPersonController.php b/src/Bundle/ChillDocStoreBundle/Controller/DocumentPersonController.php
index 6fcb6a8e5..20e8e9b03 100644
--- a/src/Bundle/ChillDocStoreBundle/Controller/DocumentPersonController.php
+++ b/src/Bundle/ChillDocStoreBundle/Controller/DocumentPersonController.php
@@ -45,10 +45,6 @@ class DocumentPersonController extends AbstractController
protected TranslatorInterface $translator;
- private PaginatorFactory $paginatorFactory;
-
- private PersonDocumentACLAwareRepositoryInterface $personDocumentACLAwareRepository;
-
/**
* DocumentPersonController constructor.
*/
@@ -56,14 +52,10 @@ class DocumentPersonController extends AbstractController
TranslatorInterface $translator,
EventDispatcherInterface $eventDispatcher,
AuthorizationHelper $authorizationHelper,
- PaginatorFactory $paginatorFactory,
- PersonDocumentACLAwareRepositoryInterface $personDocumentACLAwareRepository
) {
$this->translator = $translator;
$this->eventDispatcher = $eventDispatcher;
$this->authorizationHelper = $authorizationHelper;
- $this->paginatorFactory = $paginatorFactory;
- $this->personDocumentACLAwareRepository = $personDocumentACLAwareRepository;
}
/**
@@ -88,7 +80,7 @@ class DocumentPersonController extends AbstractController
return $this->redirect($request->query->get('returnPath'));
}
- return $this->redirectToRoute('person_document_index', ['person' => $person->getId()]);
+ return $this->redirectToRoute('chill_docstore_generic-doc_by-person_index', ['id' => $person->getId()]);
}
return $this->render(
@@ -160,45 +152,6 @@ class DocumentPersonController extends AbstractController
);
}
- /**
- * @Route("/", name="person_document_index", methods="GET")
- */
- public function index(Person $person): Response
- {
- $em = $this->getDoctrine()->getManager();
-
- if (null === $person) {
- throw $this->createNotFoundException('Person not found');
- }
-
- $this->denyAccessUnlessGranted(PersonVoter::SEE, $person);
-
- $total = $this->personDocumentACLAwareRepository->countByPerson($person);
- $pagination = $this->paginatorFactory->create($total);
-
- $documents = $this->personDocumentACLAwareRepository->findByPerson(
- $person,
- ['date' => 'DESC', 'id' => 'DESC'],
- $pagination->getItemsPerPage(),
- $pagination->getCurrentPageFirstItemNumber()
- );
-
- $event = new PrivacyEvent($person, [
- 'element_class' => PersonDocument::class,
- 'action' => 'index',
- ]);
- $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event);
-
- return $this->render(
- 'ChillDocStoreBundle:PersonDocument:index.html.twig',
- [
- 'documents' => $documents,
- 'person' => $person,
- 'pagination' => $pagination,
- ]
- );
- }
-
/**
* @Route("/new", name="person_document_new", methods="GET|POST")
*/
@@ -233,7 +186,7 @@ class DocumentPersonController extends AbstractController
$this->addFlash('success', $this->translator->trans('The document is successfully registered'));
- return $this->redirectToRoute('person_document_index', ['person' => $person->getId()]);
+ return $this->redirectToRoute('chill_docstore_generic-doc_by-person_index', ['id' => $person->getId()]);
}
if ($form->isSubmitted() && !$form->isValid()) {
diff --git a/src/Bundle/ChillDocStoreBundle/Controller/GenericDocForAccompanyingPeriodController.php b/src/Bundle/ChillDocStoreBundle/Controller/GenericDocForAccompanyingPeriodController.php
new file mode 100644
index 000000000..70c41db50
--- /dev/null
+++ b/src/Bundle/ChillDocStoreBundle/Controller/GenericDocForAccompanyingPeriodController.php
@@ -0,0 +1,98 @@
+security->isGranted(AccompanyingCourseDocumentVoter::SEE, $accompanyingPeriod)) {
+ throw new AccessDeniedHttpException("not allowed to see the documents for accompanying period");
+ }
+
+ $filterBuilder = $this->filterOrderHelperFactory
+ ->create(self::class)
+ ->addSearchBox()
+ ->addDateRange('dateRange', 'generic_doc.filter.date-range');
+
+ if ([] !== $places = $this->manager->placesForAccompanyingPeriod($accompanyingPeriod)) {
+ $filterBuilder->addCheckbox('places', $places, [], array_map(
+ static fn (string $k) => 'generic_doc.filter.keys.' . $k,
+ $places
+ ));
+ }
+
+ $filter = $filterBuilder
+ ->build();
+
+ ['to' => $endDate, 'from' => $startDate ] = $filter->getDateRangeData('dateRange');
+ $content = $filter->getQueryString();
+
+ $nb = $this->manager->countDocForAccompanyingPeriod(
+ $accompanyingPeriod,
+ $startDate,
+ $endDate,
+ $content,
+ $filter->hasCheckBox('places') ? array_values($filter->getCheckboxData('places')) : []
+ );
+ $paginator = $this->paginator->create($nb);
+
+ $documents = $this->manager->findDocForAccompanyingPeriod(
+ $accompanyingPeriod,
+ $paginator->getCurrentPageFirstItemNumber(),
+ $paginator->getItemsPerPage(),
+ $startDate,
+ $endDate,
+ $content,
+ $filter->hasCheckBox('places') ? array_values($filter->getCheckboxData('places')) : []
+ );
+
+ return new Response($this->twig->render(
+ '@ChillDocStore/GenericDoc/accompanying_period_list.html.twig',
+ [
+ 'accompanyingCourse' => $accompanyingPeriod,
+ 'pagination' => $paginator,
+ 'documents' => iterator_to_array($documents),
+ 'filter' => $filter,
+ ]
+ ));
+ }
+
+}
diff --git a/src/Bundle/ChillDocStoreBundle/Controller/GenericDocForPerson.php b/src/Bundle/ChillDocStoreBundle/Controller/GenericDocForPerson.php
new file mode 100644
index 000000000..3484e0904
--- /dev/null
+++ b/src/Bundle/ChillDocStoreBundle/Controller/GenericDocForPerson.php
@@ -0,0 +1,95 @@
+security->isGranted(PersonDocumentVoter::SEE, $person)) {
+ throw new AccessDeniedHttpException("not allowed to see the documents for person");
+ }
+
+ $filterBuilder = $this->filterOrderHelperFactory
+ ->create(self::class)
+ ->addSearchBox()
+ ->addDateRange('dateRange', 'generic_doc.filter.date-range');
+
+ if ([] !== $places = $this->manager->placesForPerson($person)) {
+ $filterBuilder->addCheckbox('places', $places, [], array_map(
+ static fn (string $k) => 'generic_doc.filter.keys.' . $k,
+ $places
+ ));
+ }
+
+ $filter = $filterBuilder
+ ->build();
+
+ ['to' => $endDate, 'from' => $startDate ] = $filter->getDateRangeData('dateRange');
+ $content = $filter->getQueryString();
+
+ $nb = $this->manager->countDocForPerson(
+ $person,
+ $startDate,
+ $endDate,
+ $content,
+ $filter->hasCheckBox('places') ? array_values($filter->getCheckboxData('places')) : []
+ );
+ $paginator = $this->paginator->create($nb);
+
+ $documents = $this->manager->findDocForPerson(
+ $person,
+ $paginator->getCurrentPageFirstItemNumber(),
+ $paginator->getItemsPerPage(),
+ $startDate,
+ $endDate,
+ $content,
+ $filter->hasCheckBox('places') ? array_values($filter->getCheckboxData('places')) : []
+ );
+
+ return new Response($this->twig->render(
+ '@ChillDocStore/GenericDoc/person_list.html.twig',
+ [
+ 'person' => $person,
+ 'pagination' => $paginator,
+ 'documents' => iterator_to_array($documents),
+ 'filter' => $filter,
+ ]
+ ));
+ }
+
+}
diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/FetchQuery.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/FetchQuery.php
new file mode 100644
index 000000000..30e07a841
--- /dev/null
+++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/FetchQuery.php
@@ -0,0 +1,233 @@
+
+ */
+ private array $joins = [];
+
+ /**
+ * @var list>
+ */
+ private array $joinParams = [];
+
+ /**
+ * @var array>
+ */
+ private array $joinTypes = [];
+
+ /**
+ * @var array
+ */
+ private array $wheres = [];
+
+ /**
+ * @var array>
+ */
+ private array $whereParams = [];
+
+ /**
+ * @var array>
+ */
+ private array $whereTypes = [];
+
+ public function __construct(
+ private readonly string $selectKeyString,
+ private readonly string $selectIdentifierJsonB,
+ private readonly string $selectDate,
+ private string $from = '',
+ private array $selectIdentifierParams = [],
+ private array $selectIdentifierTypes = [],
+ private array $selectDateParams = [],
+ private array $selectDateTypes = [],
+ ) {
+ }
+
+ public function addJoinClause(string $sql, array $params = [], array $types = []): int
+ {
+ $this->joins[] = $sql;
+ $this->joinParams[] = $params;
+ $this->joinTypes[] = $types;
+
+ return count($this->joins) - 1;
+ }
+
+ public function addWhereClause(string $sql, array $params = [], array $types = []): int
+ {
+ $this->wheres[] = $sql;
+ $this->whereParams[] = $params;
+ $this->whereTypes[] = $types;
+
+ return count($this->wheres) - 1;
+ }
+
+ public function removeWhereClause(int $index): void
+ {
+ if (!array_key_exists($index, $this->wheres)) {
+ throw new \UnexpectedValueException("this index does not exists");
+ }
+
+ unset($this->wheres[$index], $this->whereParams[$index], $this->whereTypes[$index]);
+
+ }
+
+ public function removeJoinClause(int $index): void
+ {
+ if (!array_key_exists($index, $this->joins)) {
+ throw new \UnexpectedValueException("this index does not exists");
+ }
+
+ unset($this->joins[$index], $this->joinParams[$index], $this->joinTypes[$index]);
+
+ }
+
+ public function getSelectKeyString(): string
+ {
+ return $this->selectKeyString;
+ }
+
+ public function getSelectIdentifierJsonB(): string
+ {
+ return $this->selectIdentifierJsonB;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getSelectIdentifierParams(): array
+ {
+ return $this->selectIdentifierParams;
+ }
+
+ public function getSelectIdentifiersTypes(): array
+ {
+ return $this->selectIdentifierTypes;
+ }
+
+ public function getSelectDate(): string
+ {
+ return $this->selectDate;
+ }
+
+ public function getSelectDateTypes(): array
+ {
+ return $this->selectDateTypes;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getSelectDateParams(): array
+ {
+ return $this->selectDateParams;
+ }
+
+ public function getFromQuery(): string
+ {
+ return $this->from . " " . implode(' ', $this->joins);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getFromQueryParams(): array
+ {
+ $result = [];
+
+ foreach ($this->joinParams as $params) {
+ $result = [...$result, ...$params];
+ }
+
+ return $result;
+ }
+
+ public function getFromQueryTypes(): array
+ {
+ $result = [];
+
+ foreach ($this->joinTypes as $types) {
+ $result = [...$result, ...$types];
+ }
+
+ return $result;
+ }
+
+ public function getWhereQuery(): string
+ {
+ return implode(' AND ', $this->wheres);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getWhereQueryParams(): array
+ {
+ $result = [];
+
+ foreach ($this->whereParams as $params) {
+ $result = [...$result, ...$params];
+ }
+
+ return $result;
+ }
+
+ public function getWhereQueryTypes(): array
+ {
+ $result = [];
+
+ foreach ($this->whereTypes as $types) {
+ $result = [...$result, ...$types];
+ }
+
+ return $result;
+ }
+
+ public function setSelectIdentifierParams(array $selectIdentifierParams): self
+ {
+ $this->selectIdentifierParams = $selectIdentifierParams;
+
+ return $this;
+ }
+
+ public function setSelectDateParams(array $selectDateParams): self
+ {
+ $this->selectDateParams = $selectDateParams;
+
+ return $this;
+ }
+
+ public function setFrom(string $from): self
+ {
+ $this->from = $from;
+
+ return $this;
+ }
+
+ public function setSelectIdentifierTypes(array $selectIdentifierTypes): self
+ {
+ $this->selectIdentifierTypes = $selectIdentifierTypes;
+
+ return $this;
+ }
+
+ public function setSelectDateTypes(array $selectDateTypes): self
+ {
+ $this->selectDateTypes = $selectDateTypes;
+
+ return $this;
+ }
+}
diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/FetchQueryInterface.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/FetchQueryInterface.php
new file mode 100644
index 000000000..e46795457
--- /dev/null
+++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/FetchQueryInterface.php
@@ -0,0 +1,67 @@
+
+ */
+ public function getSelectIdentifierParams(): array;
+
+ /**
+ * @return list
+ */
+ public function getSelectIdentifiersTypes(): array;
+
+ public function getSelectDate(): string;
+
+ /**
+ * @return list
+ */
+ public function getSelectDateParams(): array;
+
+ /**
+ * @return list
+ */
+ public function getSelectDateTypes(): array;
+
+ public function getFromQuery(): string;
+
+ /**
+ * @return list
+ */
+ public function getFromQueryParams(): array;
+
+ /**
+ * @return list
+ */
+ public function getFromQueryTypes(): array;
+
+ public function getWhereQuery(): string;
+
+ /**
+ * @return list
+ */
+ public function getWhereQueryParams(): array;
+
+ /**
+ * @return list
+ */
+ public function getWhereQueryTypes(): array;
+}
diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/FetchQueryToSqlBuilder.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/FetchQueryToSqlBuilder.php
new file mode 100644
index 000000000..2c0c59cff
--- /dev/null
+++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/FetchQueryToSqlBuilder.php
@@ -0,0 +1,57 @@
+, types: list}
+ */
+ public function toSql(FetchQueryInterface $query): array
+ {
+ $sql = strtr(self::SQL, [
+ '{{ key }}' => $query->getSelectKeyString(),
+ '{{ identifiers }}' => $query->getSelectIdentifierJsonB(),
+ '{{ date }}' => $query->getSelectDate(),
+ '{{ from }}' => $query->getFromQuery(),
+ '{{ where }}' => '' === ($w = $query->getWhereQuery()) ? '' : 'WHERE ' . $w,
+ ]);
+
+ $params = [
+ ...$query->getSelectIdentifierParams(),
+ ...$query->getSelectDateParams(),
+ ...$query->getFromQueryParams(),
+ ...$query->getWhereQueryParams()
+ ];
+
+ $types = [
+ ...$query->getSelectIdentifiersTypes(),
+ ...$query->getSelectDateTypes(),
+ ...$query->getFromQueryTypes(),
+ ...$query->getWhereQueryTypes(),
+ ];
+
+ return ['sql' => $sql, 'params' => $params, 'types' => $types];
+ }
+}
diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/GenericDocDTO.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/GenericDocDTO.php
new file mode 100644
index 000000000..fe9bf7e4f
--- /dev/null
+++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/GenericDocDTO.php
@@ -0,0 +1,31 @@
+linked instanceof AccompanyingPeriod ? 'accompanying-period' : 'person';
+ }
+}
diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/GenericDocForAccompanyingPeriodProviderInterface.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/GenericDocForAccompanyingPeriodProviderInterface.php
new file mode 100644
index 000000000..0d3cb1c32
--- /dev/null
+++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/GenericDocForAccompanyingPeriodProviderInterface.php
@@ -0,0 +1,31 @@
+
+ */
+ private iterable $providersForAccompanyingPeriod,
+
+ /**
+ * @var iterable
+ */
+ private iterable $providersForPerson,
+ private Connection $connection,
+ ) {
+ $this->builder = new FetchQueryToSqlBuilder();
+ }
+
+ /**
+ * @param list $places
+ * @throws Exception
+ */
+ public function countDocForAccompanyingPeriod(
+ AccompanyingPeriod $accompanyingPeriod,
+ ?\DateTimeImmutable $startDate = null,
+ ?\DateTimeImmutable $endDate = null,
+ ?string $content = null,
+ array $places = []
+ ): int {
+ ['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($accompanyingPeriod, $startDate, $endDate, $content, $places);
+
+ return $this->countDoc($sql, $params, $types);
+ }
+
+ private function countDoc(string $sql, array $params, array $types): int
+ {
+ if ($sql === '') {
+ return 0;
+ }
+
+ $countSql = "SELECT count(*) AS c FROM ({$sql}) AS sq";
+ $result = $this->connection->executeQuery($countSql, $params, $types);
+
+ $number = $result->fetchOne();
+
+ if (false === $number) {
+ throw new \UnexpectedValueException("number of documents failed to load");
+ }
+
+ return $number;
+ }
+
+ public function countDocForPerson(
+ Person $person,
+ ?\DateTimeImmutable $startDate = null,
+ ?\DateTimeImmutable $endDate = null,
+ ?string $content = null,
+ array $places = []
+ ): int {
+ ['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($person, $startDate, $endDate, $content, $places);
+
+ return $this->countDoc($sql, $params, $types);
+ }
+
+ /**
+ * @param list $places places to search. When empty, search in all places
+ * @return iterable
+ * @throws Exception
+ */
+ public function findDocForAccompanyingPeriod(
+ AccompanyingPeriod $accompanyingPeriod,
+ int $offset = 0,
+ int $limit = 20,
+ ?\DateTimeImmutable $startDate = null,
+ ?\DateTimeImmutable $endDate = null,
+ ?string $content = null,
+ array $places = []
+ ): iterable {
+ ['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($accompanyingPeriod, $startDate, $endDate, $content, $places);
+
+ return $this->findDocs($accompanyingPeriod, $sql, $params, $types, $offset, $limit);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws Exception
+ */
+ private function findDocs(AccompanyingPeriod|Person $linked, string $sql, array $params, array $types, int $offset, int $limit): iterable
+ {
+ if ($sql === '') {
+ return [];
+ }
+
+ $runSql = "{$sql} ORDER BY doc_date DESC LIMIT ? OFFSET ?";
+ $runParams = [...$params, ...[$limit, $offset]];
+ $runTypes = [...$types, ...[Types::INTEGER, Types::INTEGER]];
+
+ foreach ($this->connection->iterateAssociative($runSql, $runParams, $runTypes) as $row) {
+ yield new GenericDocDTO(
+ $row['key'],
+ json_decode($row['identifiers'], true, 512, JSON_THROW_ON_ERROR),
+ new \DateTimeImmutable($row['doc_date']),
+ $linked,
+ );
+ }
+ }
+
+ /**
+ * @param list $places places to search. When empty, search in all places
+ * @return iterable
+ */
+ public function findDocForPerson(
+ Person $person,
+ int $offset = 0,
+ int $limit = 20,
+ ?\DateTimeImmutable $startDate = null,
+ ?\DateTimeImmutable $endDate = null,
+ ?string $content = null,
+ array $places = []
+ ): iterable {
+ ['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($person, $startDate, $endDate, $content, $places);
+
+ return $this->findDocs($person, $sql, $params, $types, $offset, $limit);
+ }
+
+ public function placesForPerson(Person $person): array
+ {
+ ['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($person);
+
+ return $this->places($sql, $params, $types);
+ }
+
+ public function placesForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): array
+ {
+ ['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($accompanyingPeriod);
+
+ return $this->places($sql, $params, $types);
+ }
+
+ private function places(string $sql, array $params, array $types): array
+ {
+ if ($sql === '') {
+ return [];
+ }
+
+ $runSql = "SELECT DISTINCT key FROM ({$sql}) AS sq ORDER BY key";
+
+ $keys = [];
+
+ foreach ($this->connection->iterateAssociative($runSql, $params, $types) as $k) {
+ $keys[] = $k['key'];
+ }
+
+ return $keys;
+ }
+
+ /**
+ * @param list $places places to search. When empty, search in all places
+ */
+ private function buildUnionQuery(
+ AccompanyingPeriod|Person $linked,
+ ?\DateTimeImmutable $startDate = null,
+ ?\DateTimeImmutable $endDate = null,
+ ?string $content = null,
+ array $places = [],
+ ): array {
+ $queries = [];
+
+ if ($linked instanceof AccompanyingPeriod) {
+ foreach ($this->providersForAccompanyingPeriod as $provider) {
+ if (!$provider->isAllowedForAccompanyingPeriod($linked)) {
+ continue;
+ }
+ $queries[] = $provider->buildFetchQueryForAccompanyingPeriod($linked, $startDate, $endDate, $content);
+ }
+ } else {
+ foreach ($this->providersForPerson as $provider) {
+ if (!$provider->isAllowedForPerson($linked)) {
+ continue;
+ }
+ $queries[] = $provider->buildFetchQueryForPerson($linked, $startDate, $endDate, $content);
+ }
+ }
+ $sql = [];
+ $params = [];
+ $types = [];
+
+ foreach ($queries as $query) {
+ if ([] !== $places and !in_array($query->getSelectKeyString(), $places, true)) {
+ continue;
+ }
+
+ ['sql' => $q, 'params' => $p, 'types' => $t ] = $this->builder->toSql($query);
+
+ $sql[] = $q;
+ $params = [...$params, ...$p];
+ $types = [...$types, ...$t];
+ }
+
+ return ['sql' => implode(' UNION ', $sql), 'params' => $params, 'types' => $types];
+ }
+}
diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/Providers/AccompanyingCourseDocumentGenericDocProvider.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/Providers/AccompanyingCourseDocumentGenericDocProvider.php
new file mode 100644
index 000000000..fd36f7976
--- /dev/null
+++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/Providers/AccompanyingCourseDocumentGenericDocProvider.php
@@ -0,0 +1,147 @@
+entityManager->getClassMetadata(AccompanyingCourseDocument::class);
+
+ $query = new FetchQuery(
+ self::KEY,
+ sprintf('jsonb_build_object(\'id\', %s)', $classMetadata->getIdentifierColumnNames()[0]),
+ $classMetadata->getColumnName('date'),
+ $classMetadata->getSchemaName() . '.' . $classMetadata->getTableName()
+ );
+
+ $query->addWhereClause(
+ sprintf('%s = ?', $classMetadata->getSingleAssociationJoinColumnName('course')),
+ [$accompanyingPeriod->getId()],
+ [Types::INTEGER]
+ );
+
+ return $this->addWhereClause($query, $startDate, $endDate, $content);
+ }
+
+ public function isAllowedForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): bool
+ {
+ return $this->security->isGranted(AccompanyingCourseDocumentVoter::SEE, $accompanyingPeriod);
+ }
+
+ public function buildFetchQueryForPerson(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
+ {
+ $classMetadata = $this->entityManager->getClassMetadata(AccompanyingCourseDocument::class);
+
+ $query = new FetchQuery(
+ self::KEY,
+ sprintf('jsonb_build_object(\'id\', %s)', $classMetadata->getIdentifierColumnNames()[0]),
+ $classMetadata->getColumnName('date'),
+ $classMetadata->getSchemaName() . '.' . $classMetadata->getTableName() . ' AS acc_course_document'
+ );
+
+ $atLeastOne = false;
+ $or = [];
+ $orParams = [];
+ $orTypes = [];
+
+ foreach ($person->getAccompanyingPeriodParticipations() as $participation) {
+ if (!$this->security->isGranted(AccompanyingCourseDocumentVoter::SEE, $participation->getAccompanyingPeriod())) {
+ continue;
+ }
+
+ $atLeastOne = true;
+
+ $or[] = sprintf(
+ "(acc_course_document.%s = ? AND acc_course_document.%s BETWEEN ? AND COALESCE(?, 'infinity'::date))",
+ $classMetadata->getSingleAssociationJoinColumnName('course'),
+ $classMetadata->getColumnName('date')
+ );
+ $orParams = [...$orParams, $participation->getAccompanyingPeriod()->getId(), $participation->getStartDate(), $participation->getEndDate()];
+ $orTypes = [...$orTypes, Types::INTEGER, Types::DATE_MUTABLE, Types::DATE_MUTABLE];
+ }
+
+ if (!$atLeastOne) {
+ // there aren't any period allowed to be seen. Add an unreachable condition
+ $query->addWhereClause('TRUE = FALSE');
+
+ return $query;
+ }
+
+ $query->addWhereClause('(' . implode(' OR ', $or) . ')', $orParams, $orTypes);
+
+ return $this->addWhereClause($query, $startDate, $endDate, $content);
+ }
+
+ public function isAllowedForPerson(Person $person): bool
+ {
+ return $this->security->isGranted(AccompanyingPeriodVoter::SEE, $person);
+ }
+
+ private function addWhereClause(FetchQuery $query, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery
+ {
+ $classMetadata = $this->entityManager->getClassMetadata(AccompanyingCourseDocument::class);
+
+ if (null !== $startDate) {
+ $query->addWhereClause(
+ sprintf('? <= %s', $classMetadata->getColumnName('date')),
+ [$startDate],
+ [Types::DATE_IMMUTABLE]
+ );
+ }
+
+ if (null !== $endDate) {
+ $query->addWhereClause(
+ sprintf('? >= %s', $classMetadata->getColumnName('date')),
+ [$endDate],
+ [Types::DATE_IMMUTABLE]
+ );
+ }
+
+ if (null !== $content and '' !== $content) {
+ $query->addWhereClause(
+ sprintf(
+ '(%s ilike ? OR %s ilike ?)',
+ $classMetadata->getColumnName('title'),
+ $classMetadata->getColumnName('description')
+ ),
+ ['%' . $content . '%', '%' . $content . '%'],
+ [Types::STRING, Types::STRING]
+ );
+ }
+
+ return $query;
+ }
+
+}
diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/Providers/PersonDocumentGenericDocProvider.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/Providers/PersonDocumentGenericDocProvider.php
new file mode 100644
index 000000000..613f8d758
--- /dev/null
+++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/Providers/PersonDocumentGenericDocProvider.php
@@ -0,0 +1,65 @@
+personDocumentACLAwareRepository->buildFetchQueryForPerson(
+ $person,
+ $startDate,
+ $endDate,
+ $content
+ );
+ }
+
+ public function isAllowedForPerson(Person $person): bool
+ {
+ 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/GenericDoc/Renderer/AccompanyingCourseDocumentGenericDocRenderer.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/Renderer/AccompanyingCourseDocumentGenericDocRenderer.php
new file mode 100644
index 000000000..c32620030
--- /dev/null
+++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/Renderer/AccompanyingCourseDocumentGenericDocRenderer.php
@@ -0,0 +1,59 @@
+key === AccompanyingCourseDocumentGenericDocProvider::KEY
+ || $genericDocDTO->key === PersonDocumentGenericDocProvider::KEY;
+ }
+
+ public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string
+ {
+ return '@ChillDocStore/List/list_item.html.twig';
+ }
+
+ public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array
+ {
+ if (AccompanyingCourseDocumentGenericDocProvider::KEY === $genericDocDTO->key) {
+ return [
+ 'document' => $doc = $this->accompanyingCourseDocumentRepository->find($genericDocDTO->identifiers['id']),
+ 'accompanyingCourse' => $doc->getCourse(),
+ 'options' => $options,
+ 'context' => $genericDocDTO->getContext(),
+ ];
+ }
+ // this is a person
+ return [
+ 'document' => $doc = $this->personDocumentRepository->find($genericDocDTO->identifiers['id']),
+ 'person' => $doc->getPerson(),
+ 'options' => $options,
+ 'context' => $genericDocDTO->getContext(),
+ ];
+ }
+}
diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/Twig/GenericDocExtension.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/Twig/GenericDocExtension.php
new file mode 100644
index 000000000..308d85cd7
--- /dev/null
+++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/Twig/GenericDocExtension.php
@@ -0,0 +1,28 @@
+ true,
+ 'is_safe' => ['html'],
+ ])
+ ];
+ }
+}
diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/Twig/GenericDocExtensionRuntime.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/Twig/GenericDocExtensionRuntime.php
new file mode 100644
index 000000000..2dee0ed0b
--- /dev/null
+++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/Twig/GenericDocExtensionRuntime.php
@@ -0,0 +1,50 @@
+
+ */
+ private iterable $renderers,
+ ) {
+ }
+
+ /**
+ * @throws RuntimeError
+ * @throws SyntaxError
+ * @throws LoaderError
+ */
+ public function renderGenericDoc(Environment $twig, GenericDocDTO $genericDocDTO, array $options = []): string
+ {
+ foreach ($this->renderers as $renderer) {
+ if ($renderer->supports($genericDocDTO)) {
+ return $twig->render(
+ $renderer->getTemplate($genericDocDTO, $options),
+ $renderer->getTemplateData($genericDocDTO, $options),
+ );
+ }
+ }
+
+ throw new \LogicException("no renderer found");
+ }
+
+}
diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/Twig/GenericDocRendererInterface.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/Twig/GenericDocRendererInterface.php
new file mode 100644
index 000000000..940001f4a
--- /dev/null
+++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/Twig/GenericDocRendererInterface.php
@@ -0,0 +1,24 @@
+security->isGranted(AccompanyingCourseDocumentVoter::SEE, $course)) {
$menu->addChild($this->translator->trans('Documents'), [
- 'route' => 'accompanying_course_document_index',
+ 'route' => 'chill_docstore_generic-doc_by-period_index',
'routeParameters' => [
- 'course' => $course->getId(),
+ 'id' => $course->getId(),
],
])
->setExtras([
@@ -80,9 +80,9 @@ final class MenuBuilder implements LocalMenuBuilderInterface
if ($this->security->isGranted(PersonDocumentVoter::SEE, $person)) {
$menu->addChild($this->translator->trans('Documents'), [
- 'route' => 'person_document_index',
+ 'route' => 'chill_docstore_generic-doc_by-person_index',
'routeParameters' => [
- 'person' => $person->getId(),
+ 'id' => $person->getId(),
],
])
->setExtras([
diff --git a/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepository.php
index 23dcc4e0b..26a42b894 100644
--- a/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepository.php
+++ b/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepository.php
@@ -12,30 +12,33 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Repository;
use Chill\DocStoreBundle\Entity\PersonDocument;
+use Chill\DocStoreBundle\GenericDoc\FetchQuery;
+use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
+use Chill\DocStoreBundle\GenericDoc\Providers\PersonDocumentGenericDocProvider;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
+use Chill\MainBundle\Entity\Scope;
+use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
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;
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 AuthorizationHelperInterface $authorizationHelper;
-
- private CenterResolverDispatcher $centerResolverDispatcher;
-
- private EntityManagerInterface $em;
-
- private Security $security;
-
- public function __construct(EntityManagerInterface $em, AuthorizationHelperInterface $authorizationHelper, CenterResolverDispatcher $centerResolverDispatcher, Security $security)
- {
- $this->em = $em;
- $this->authorizationHelper = $authorizationHelper;
- $this->centerResolverDispatcher = $centerResolverDispatcher;
- $this->security = $security;
+ public function __construct(
+ private EntityManagerInterface $em,
+ private CenterResolverManagerInterface $centerResolverManager,
+ private AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser,
+ private Security $security,
+ ) {
}
public function buildQueryByPerson(Person $person): QueryBuilder
@@ -49,6 +52,128 @@ class PersonDocumentACLAwareRepository implements PersonDocumentACLAwareReposito
return $qb;
}
+
+ public function buildFetchQueryForPerson(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null): FetchQueryInterface
+ {
+ $query = $this->buildBaseFetchQueryForPerson($person, $startDate, $endDate, $content);
+
+ 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);
+
+ $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->addWhereClause(
+ sprintf('person_document.%s = ?', $personDocMetadata->getSingleAssociationJoinColumnName('person')),
+ [$person->getId()],
+ [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')),
+ [$startDate],
+ [Types::DATE_IMMUTABLE]
+ );
+ }
+
+ if (null !== $endDate) {
+ $query->addWhereClause(
+ sprintf('? >= %s', $personDocMetadata->getColumnName('date')),
+ [$endDate],
+ [Types::DATE_IMMUTABLE]
+ );
+ }
+
+ if (null !== $content and '' !== $content) {
+ $query->addWhereClause(
+ sprintf(
+ '(%s ilike ? OR %s ilike ?)',
+ $personDocMetadata->getColumnName('title'),
+ $personDocMetadata->getColumnName('description')
+ ),
+ ['%' . $content . '%', '%' . $content . '%'],
+ [Types::STRING, Types::STRING]
+ );
+ }
+ return $query;
+ }
+
public function countByPerson(Person $person): int
{
$qb = $this->buildQueryByPerson($person)->select('COUNT(d)');
@@ -75,16 +200,58 @@ class PersonDocumentACLAwareRepository implements PersonDocumentACLAwareReposito
private function addACL(QueryBuilder $qb, Person $person): void
{
- $center = $this->centerResolverDispatcher->resolveCenter($person);
+ $reachableScopes = [];
- $reachableScopes = $this->authorizationHelper
- ->getReachableScopes(
- $this->security->getUser(),
- PersonDocumentVoter::SEE,
- $center
- );
+ foreach ($this->centerResolverManager->resolveCenters($person) as $center) {
+ $reachableScopes = [
+ ...$reachableScopes,
+ ...$this->authorizationHelperForCurrentUser
+ ->getReachableScopes(
+ PersonDocumentVoter::SEE,
+ $center
+ )
+ ];
+ }
+
+ if ([] === $reachableScopes) {
+ $qb->andWhere("'FALSE' = 'TRUE'");
+
+ return;
+ }
$qb->andWhere($qb->expr()->in('d.scope', ':scopes'))
->setParameter('scopes', $reachableScopes);
}
+
+ private function addFetchQueryByPersonACL(FetchQuery $fetchQuery, Person $person): FetchQuery
+ {
+ $personDocMetadata = $this->em->getClassMetadata(PersonDocument::class);
+
+ $reachableScopes = [];
+
+ foreach ($this->centerResolverManager->resolveCenters($person) as $center) {
+ $reachableScopes = [
+ ...$reachableScopes,
+ ...$this->authorizationHelperForCurrentUser->getReachableScopes(PersonDocumentVoter::SEE, $center)
+ ];
+ }
+
+ if ([] === $reachableScopes) {
+ $fetchQuery->addWhereClause('FALSE = TRUE');
+
+ return $fetchQuery;
+ }
+
+ $fetchQuery->addWhereClause(
+ sprintf(
+ 'person_document.%s IN (%s)',
+ $personDocMetadata->getSingleAssociationJoinColumnName('scope'),
+ implode(', ', array_fill(0, count($reachableScopes), '?'))
+ ),
+ array_map(static fn (Scope $s) => $s->getId(), $reachableScopes),
+ array_fill(0, count($reachableScopes), Types::INTEGER)
+ );
+
+ return $fetchQuery;
+ }
}
diff --git a/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepositoryInterface.php b/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepositoryInterface.php
index 6c4bd2e9a..f1bc70812 100644
--- a/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepositoryInterface.php
+++ b/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepositoryInterface.php
@@ -11,11 +11,33 @@ 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
{
+ /**
+ * @deprecated use fetch query for listing and counting person documents
+ */
public function countByPerson(Person $person): int;
+ /**
+ * @deprecated use fetch query for listing and counting person documents
+ */
public function findByPerson(Person $person, array $orderBy = [], int $limit = 20, int $offset = 0): array;
+
+ public function buildFetchQueryForPerson(
+ Person $person,
+ ?\DateTimeImmutable $startDate = null,
+ ?\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/Repository/PersonDocumentRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentRepository.php
new file mode 100644
index 000000000..40afdc220
--- /dev/null
+++ b/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentRepository.php
@@ -0,0 +1,57 @@
+
+ */
+readonly class PersonDocumentRepository implements ObjectRepository
+{
+ private EntityRepository $repository;
+
+ public function __construct(
+ private EntityManagerInterface $entityManager
+ ) {
+ $this->repository = $this->entityManager->getRepository($this->getClassName());
+ }
+
+ public function find($id): ?PersonDocument
+ {
+ return $this->repository->find($id);
+ }
+
+ public function findAll()
+ {
+ return $this->repository->findAll();
+ }
+
+ public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null)
+ {
+ return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
+ }
+
+ public function findOneBy(array $criteria): ?PersonDocument
+ {
+ return $this->repository->findOneBy($criteria);
+ }
+
+ public function getClassName(): string
+ {
+ return PersonDocument::class;
+ }
+}
diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/delete.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/delete.html.twig
index c9bb608cf..d6f23d09d 100644
--- a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/delete.html.twig
+++ b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/delete.html.twig
@@ -31,8 +31,8 @@
'title' : 'Delete document ?'|trans,
'display_content' : block('docdescription'),
'confirm_question' : 'Are you sure you want to remove this document ?'|trans,
- 'cancel_route' : 'accompanying_course_document_index',
- 'cancel_parameters' : {'course' : accompanyingCourse.id, 'id': document.id},
+ 'cancel_route' : 'chill_docstore_generic-doc_by-period_index',
+ 'cancel_parameters' : {'id' : accompanyingCourse.id},
'form' : delete_form
} ) }}
{% endblock %}
diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/edit.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/edit.html.twig
index 0ca5661fc..326814502 100644
--- a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/edit.html.twig
+++ b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/edit.html.twig
@@ -21,7 +21,7 @@
diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/index.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/index.html.twig
deleted file mode 100644
index 7a013260c..000000000
--- a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/index.html.twig
+++ /dev/null
@@ -1,52 +0,0 @@
-{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %}
-
-{% set activeRouteKey = '' %}
-
-{% block title %}
- {{ 'Documents' }}
-{% endblock %}
-
-{% block js %}
- {{ parent() }}
- {{ encore_entry_script_tags('mod_docgen_picktemplate') }}
- {{ encore_entry_script_tags('mod_entity_workflow_pick') }}
- {{ encore_entry_script_tags('mod_document_action_buttons_group') }}
-{% endblock %}
-
-{% block css %}
- {{ parent() }}
- {{ encore_entry_link_tags('mod_docgen_picktemplate') }}
- {{ encore_entry_link_tags('mod_entity_workflow_pick') }}
- {{ encore_entry_link_tags('mod_document_action_buttons_group') }}
-{% endblock %}
-
-{% block content %}
-
-
{{ 'Documents' }}
-
- {% if documents|length == 0 %}
-
{{ 'No documents'|trans }}
- {% else %}
-
- {% for document in documents %}
- {% include '@ChillDocStore/List/list_item.html.twig' %}
- {% endfor %}
-
- {% endif %}
-
- {{ chill_pagination(pagination) }}
-
-
-
- {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE', accompanyingCourse) %}
-
- {% endif %}
-
-
-{% endblock %}
diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/new.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/new.html.twig
index 01be1a5d7..3fb692c78 100644
--- a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/new.html.twig
+++ b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/new.html.twig
@@ -25,7 +25,7 @@