diff --git a/.changes/unreleased/DX-20230623-122408.yaml b/.changes/unreleased/DX-20230623-122408.yaml
deleted file mode 100644
index 58dd96180..000000000
--- a/.changes/unreleased/DX-20230623-122408.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-kind: DX
-body: '[FilterOrderHelper] add entity choice and singleCheckbox'
-time: 2023-06-23T12:24:08.133491895+02:00
-custom:
- Issue: ""
diff --git a/.changes/unreleased/Feature-20230623-122530.yaml b/.changes/unreleased/Feature-20230623-122530.yaml
deleted file mode 100644
index 922750ea8..000000000
--- a/.changes/unreleased/Feature-20230623-122530.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-kind: Feature
-body: '[activity list] add filtering for activities list'
-time: 2023-06-23T12:25:30.49643551+02:00
-custom:
- Issue: ""
diff --git a/.changes/unreleased/Feature-20230623-122702.yaml b/.changes/unreleased/Feature-20230623-122702.yaml
deleted file mode 100644
index e1d1b0e1f..000000000
--- a/.changes/unreleased/Feature-20230623-122702.yaml
+++ /dev/null
@@ -1,6 +0,0 @@
-kind: Feature
-body: '[activity list] in person context, show also the activities from the accompanying
- periods where the person participates'
-time: 2023-06-23T12:27:02.159041095+02:00
-custom:
- Issue: ""
diff --git a/.changes/unreleased/Feature-20230623-124438.yaml b/.changes/unreleased/Feature-20230623-124438.yaml
deleted file mode 100644
index bc199d3bb..000000000
--- a/.changes/unreleased/Feature-20230623-124438.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-kind: Feature
-body: '[activity list] add pagination to the list of activities'
-time: 2023-06-23T12:44:38.879098862+02:00
-custom:
- Issue: ""
diff --git a/.changes/unreleased/Feature-20230707-123609.yaml b/.changes/unreleased/Feature-20230707-123609.yaml
new file mode 100644
index 000000000..51ff94d4c
--- /dev/null
+++ b/.changes/unreleased/Feature-20230707-123609.yaml
@@ -0,0 +1,5 @@
+kind: Feature
+body: '[export] Add a list for people with their associated course'
+time: 2023-07-07T12:36:09.596469063+02:00
+custom:
+ Issue: "125"
diff --git a/.changes/unreleased/Feature-20230707-124132.yaml b/.changes/unreleased/Feature-20230707-124132.yaml
new file mode 100644
index 000000000..4ad93ad22
--- /dev/null
+++ b/.changes/unreleased/Feature-20230707-124132.yaml
@@ -0,0 +1,6 @@
+kind: Feature
+body: '[export] Add ordering by person''s lastname or course opening date in list
+ which concerns accompanying course or peoples'
+time: 2023-07-07T12:41:32.112725962+02:00
+custom:
+ Issue: ""
diff --git a/.changes/unreleased/Feature-20230711-150055.yaml b/.changes/unreleased/Feature-20230711-150055.yaml
new file mode 100644
index 000000000..ecee61b49
--- /dev/null
+++ b/.changes/unreleased/Feature-20230711-150055.yaml
@@ -0,0 +1,5 @@
+kind: Feature
+body: '[Export] allow to group activities by localisation'
+time: 2023-07-11T15:00:55.770070399+02:00
+custom:
+ Issue: "128"
diff --git a/.changes/unreleased/Feature-20230711-155929.yaml b/.changes/unreleased/Feature-20230711-155929.yaml
new file mode 100644
index 000000000..329bbb677
--- /dev/null
+++ b/.changes/unreleased/Feature-20230711-155929.yaml
@@ -0,0 +1,5 @@
+kind: Feature
+body: '[export] Add a filter "filter course having an activity between two dates"'
+time: 2023-07-11T15:59:29.065329834+02:00
+custom:
+ Issue: "129"
diff --git a/.changes/unreleased/Fixed-20230628-170055.yaml b/.changes/unreleased/Fixed-20230628-170055.yaml
deleted file mode 100644
index 7f9ec3028..000000000
--- a/.changes/unreleased/Fixed-20230628-170055.yaml
+++ /dev/null
@@ -1,6 +0,0 @@
-kind: Fixed
-body: '[export] Rename label for CurrentActionFilter (on accompanying period work)
- to make precision between "ouvert" and "sans date de fin"'
-time: 2023-06-28T17:00:55.206937751+02:00
-custom:
- Issue: ""
diff --git a/.changes/unreleased/Fixed-20230629-124412.yaml b/.changes/unreleased/Fixed-20230629-124412.yaml
deleted file mode 100644
index 7fc3d3eb0..000000000
--- a/.changes/unreleased/Fixed-20230629-124412.yaml
+++ /dev/null
@@ -1,6 +0,0 @@
-kind: Fixed
-body: Force the db to have either a person_location or a address_location, and avoid
- to have both also internally in the entity
-time: 2023-06-29T12:44:12.019663991+02:00
-custom:
- Issue: ""
diff --git a/.changes/unreleased/Fixed-20230629-231503.yaml b/.changes/unreleased/Fixed-20230629-231503.yaml
deleted file mode 100644
index e021d1fda..000000000
--- a/.changes/unreleased/Fixed-20230629-231503.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-kind: Fixed
-body: '[export] set rolling date on person age aggregator'
-time: 2023-06-29T23:15:03.20841309+02:00
-custom:
- Issue: ""
diff --git a/.changes/unreleased/Fixed-20230630-171119.yaml b/.changes/unreleased/Fixed-20230630-171119.yaml
deleted file mode 100644
index f3185ace2..000000000
--- a/.changes/unreleased/Fixed-20230630-171119.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-kind: Fixed
-body: '[export] fix list when a person locating a course is without address'
-time: 2023-06-30T17:11:19.454081914+02:00
-custom:
- Issue: ""
diff --git a/.changes/unreleased/Fixed-20230630-171153.yaml b/.changes/unreleased/Fixed-20230630-171153.yaml
deleted file mode 100644
index c09bd93d0..000000000
--- a/.changes/unreleased/Fixed-20230630-171153.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-kind: Fixed
-body: '[export] remove unused condition on course about duration participation'
-time: 2023-06-30T17:11:53.076615549+02:00
-custom:
- Issue: ""
diff --git a/.changes/unreleased/Fixed-20230712-090514.yaml b/.changes/unreleased/Fixed-20230712-090514.yaml
new file mode 100644
index 000000000..51a8b9317
--- /dev/null
+++ b/.changes/unreleased/Fixed-20230712-090514.yaml
@@ -0,0 +1,5 @@
+kind: Fixed
+body: reimplement the visualization of all calculator results (specific to AMLI)
+time: 2023-07-12T09:05:14.416268226+02:00
+custom:
+ Issue: ""
diff --git a/.changes/unreleased/Fixed-20230713-102640.yaml b/.changes/unreleased/Fixed-20230713-102640.yaml
new file mode 100644
index 000000000..e731e5252
--- /dev/null
+++ b/.changes/unreleased/Fixed-20230713-102640.yaml
@@ -0,0 +1,6 @@
+kind: Fixed
+body: |
+ Correct bug in thirdparty API search query: simplify address joins clause for child and parent kind
+time: 2023-07-13T10:26:40.503796155+02:00
+custom:
+ Issue: "126"
diff --git a/.changes/v2.4.0.md b/.changes/v2.4.0.md
new file mode 100644
index 000000000..522957300
--- /dev/null
+++ b/.changes/v2.4.0.md
@@ -0,0 +1,36 @@
+## v2.4.0 - 2023-07-07
+
+### Feature
+* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] on "filter by user working" on accompanying period, add two dates to filters intervention within a period
+* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] Add an aggregator by user's job working on a course
+* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] add an aggregator by user's scope working on a course
+* [export] on aggregator "user working on a course"
+* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] add a center aggregator for Person
+* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] add a filter on "job working on a course"
+* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] Add a filter on "scope working on a course"
+* ([#121](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/121)) Create a role "See Confidential Periods", separated from the "Reassign courses" role
+* ([#124](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/124)) Sync user absence / presence through microsoft outlook / graph api.
+
+### Fixed
+* ([#116](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/116)) On the accompanying course page, open the action on view mode if the user does not have right to update them (i.e. if the accompanying period is closed)
+* [export] Rename label for CurrentActionFilter (on accompanying period work) to make precision between "ouvert" and "sans date de fin"
+* Force the db to have either a person_location or a address_location, and avoid to have both also internally in the entity
+* [export] set rolling date on person age aggregator
+* [export] fix list when a person locating a course is without address
+* [export] remove unused condition on course about duration participation
+* Command to subscribe on MS Graph users calendars: improve the loop to be more efficient
+
+### DX
+* Rolling Date: can receive a null parameter
+
+### Traduction francophone des principaux changements
+
+- sur le "filtre par intervenant", ajoute deux dates pour limiter la période d'intervention;
+- ajout d'un regroupement par métier des intervenants sur un parcours;
+- ajout d'un regroupement par service des intervenants sur un parcours;
+- ajout d'un regroupement par utilisateur intervenant sur un parcours
+- ajout d'un regroupement "par centre de l'usager";
+- ajout d'un filtre "par métier intervenant sur un parcours";
+- ajout d'un filtre "par service intervenant sur un parcours";
+- création d'un rôle spécifique pour voir les parcours confidentiels (et séparer de celui de la liste qui permet de ré-assigner les parcours en lot);
+- synchronisation de l'absence des utilisateurs par microsoft graph api
diff --git a/.changie.yaml b/.changie.yaml
index 8a25ed695..cda69de65 100644
--- a/.changie.yaml
+++ b/.changie.yaml
@@ -30,6 +30,8 @@ kinds:
auto: patch
- label: DX
auto: patch
+ - label: UX
+ auto: patch
newlines:
afterChangelogHeader: 1
beforeChangelogVersion: 1
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 93ff93556..1bb9a8ee3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,42 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
+## v2.4.0 - 2023-07-07
+### Feature
+* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] on "filter by user working" on accompanying period, add two dates to filters intervention within a period
+* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] Add an aggregator by user's job working on a course
+* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] add an aggregator by user's scope working on a course
+* [export] on aggregator "user working on a course"
+* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] add a center aggregator for Person
+* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] add a filter on "job working on a course"
+* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] Add a filter on "scope working on a course"
+* ([#121](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/121)) Create a role "See Confidential Periods", separated from the "Reassign courses" role
+* ([#124](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/124)) Sync user absence / presence through microsoft outlook / graph api.
+
+### Fixed
+* ([#116](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/116)) On the accompanying course page, open the action on view mode if the user does not have right to update them (i.e. if the accompanying period is closed)
+* [export] Rename label for CurrentActionFilter (on accompanying period work) to make precision between "ouvert" and "sans date de fin"
+* Force the db to have either a person_location or a address_location, and avoid to have both also internally in the entity
+* [export] set rolling date on person age aggregator
+* [export] fix list when a person locating a course is without address
+* [export] remove unused condition on course about duration participation
+* Command to subscribe on MS Graph users calendars: improve the loop to be more efficient
+
+### DX
+* Rolling Date: can receive a null parameter
+
+### Traduction francophone des principaux changements
+
+- sur le "filtre par intervenant", ajoute deux dates pour limiter la période d'intervention;
+- ajout d'un regroupement par métier des intervenants sur un parcours;
+- ajout d'un regroupement par service des intervenants sur un parcours;
+- ajout d'un regroupement par utilisateur intervenant sur un parcours
+- ajout d'un regroupement "par centre de l'usager";
+- ajout d'un filtre "par métier intervenant sur un parcours";
+- ajout d'un filtre "par service intervenant sur un parcours";
+- création d'un rôle spécifique pour voir les parcours confidentiels (et séparer de celui de la liste qui permet de ré-assigner les parcours en lot);
+- synchronisation de l'absence des utilisateurs par microsoft graph api
+
## v2.3.0 - 2023-06-27
### Feature
* ([#110](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/110)) Edit saved exports options: the saved exports options (forms, filters, aggregators) are now editable.
diff --git a/exports_alias_conventions.md b/exports_alias_conventions.md
index fd7844691..64df91030 100644
--- a/exports_alias_conventions.md
+++ b/exports_alias_conventions.md
@@ -18,6 +18,7 @@ These are alias conventions :
| | SocialIssue::class | acp.socialIssues | acpsocialissue |
| | User::class | acp.user | acpuser |
| | AccompanyingPeriopStepHistory::class | acp.stepHistories | acpstephistories |
+| | AccompanyingPeriodInfo::class | not existing (using custom WITH clause) | acpinfo |
| AccompanyingPeriodWork::class | | | acpw |
| | AccompanyingPeriodWorkEvaluation::class | acpw.accompanyingPeriodWorkEvaluations | workeval |
| | User::class | acpw.referrers | acpwuser |
@@ -28,6 +29,8 @@ These are alias conventions :
| | Person::class | acppart.person | partperson |
| AccompanyingPeriodWorkEvaluation::class | | | workeval |
| | Evaluation::class | workeval.evaluation | eval |
+| AccompanyingPeriodInfo::class | | | acpinfo |
+| | User::class | acpinfo.user | acpinfo_user |
| Goal::class | | | goal |
| | Result::class | goal.results | goalresult |
| Person::class | | | person |
diff --git a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php
index 63639c149..1e911ff08 100644
--- a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php
+++ b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php
@@ -259,8 +259,8 @@ final class ActivityController extends AbstractController
$filterArgs = [
'my_activities' => $filter->getSingleCheckboxData('my_activities'),
- 'types' => $filter->getEntityChoiceData('activity_types'),
- 'jobs' => $filter->getEntityChoiceData('jobs'),
+ 'types' => $filter->hasEntityChoice('activity_types') ? $filter->getEntityChoiceData('activity_types') : [],
+ 'jobs' => $filter->hasEntityChoice('jobs') ? $filter->getEntityChoiceData('jobs') : [],
'before' => $filter->getDateRangeData('activity_date')['to'],
'after' => $filter->getDateRangeData('activity_date')['from'],
];
@@ -327,21 +327,28 @@ final class ActivityController extends AbstractController
$filterBuilder
->addDateRange('activity_date', 'activity.date')
- ->addSingleCheckbox('my_activities', 'activity_filter.My activities')
- ->addEntityChoice('activity_types', 'activity_filter.Types', \Chill\ActivityBundle\Entity\ActivityType::class, $types, [
- 'choice_label' => function (\Chill\ActivityBundle\Entity\ActivityType $activityType) {
- $text = match ($activityType->hasCategory()) {
- true => $this->translatableStringHelper->localize($activityType->getCategory()->getName()) . ' > ',
- false => '',
- };
+ ->addSingleCheckbox('my_activities', 'activity_filter.My activities');
- return $text . $this->translatableStringHelper->localize($activityType->getName());
- }
- ])
- ->addEntityChoice('jobs', 'activity_filter.Jobs', UserJob::class, $jobs, [
- 'choice_label' => fn (UserJob $u) => $this->translatableStringHelper->localize($u->getLabel())
- ])
- ;
+ if (1 < count($types)) {
+ $filterBuilder
+ ->addEntityChoice('activity_types', 'activity_filter.Types', \Chill\ActivityBundle\Entity\ActivityType::class, $types, [
+ 'choice_label' => function (\Chill\ActivityBundle\Entity\ActivityType $activityType) {
+ $text = match ($activityType->hasCategory()) {
+ true => $this->translatableStringHelper->localize($activityType->getCategory()->getName()) . ' > ',
+ false => '',
+ };
+
+ return $text . $this->translatableStringHelper->localize($activityType->getName());
+ }
+ ]);
+ }
+
+ if (1 < count($jobs)) {
+ $filterBuilder
+ ->addEntityChoice('jobs', 'activity_filter.Jobs', UserJob::class, $jobs, [
+ 'choice_label' => fn (UserJob $u) => $this->translatableStringHelper->localize($u->getLabel())
+ ]);
+ }
return $filterBuilder->build();
}
diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityLocationAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityLocationAggregator.php
new file mode 100644
index 000000000..9103943e4
--- /dev/null
+++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityLocationAggregator.php
@@ -0,0 +1,80 @@
+getAllAliases(), true)) {
+ $qb->leftJoin('activity.location', 'actloc');
+ }
+ $qb->addSelect(sprintf('actloc.name AS %s', self::KEY));
+ $qb->addGroupBy(self::KEY);
+ }
+
+ public function applyOn(): string
+ {
+ return Declarations::ACTIVITY;
+ }
+
+ public function buildForm(FormBuilderInterface $builder)
+ {
+ // no form required for this aggregator
+ }
+ public function getFormDefaultData(): array
+ {
+ return [];
+ }
+
+ public function getLabels($key, array $values, $data): Closure
+ {
+ return function ($value): string {
+ if ('_header' === $value) {
+ return 'export.aggregator.activity.by_location.Activity Location';
+ }
+
+ if (null === $value || '' === $value) {
+ return '';
+ }
+
+ return $value;
+ };
+ }
+
+ public function getQueryKeys($data): array
+ {
+ return [self::KEY];
+ }
+
+ public function getTitle()
+ {
+ return 'export.aggregator.activity.by_location.Title';
+ }
+}
diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/PeriodHavingActivityBetweenDatesFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/PeriodHavingActivityBetweenDatesFilter.php
new file mode 100644
index 000000000..27e012d0b
--- /dev/null
+++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/PeriodHavingActivityBetweenDatesFilter.php
@@ -0,0 +1,90 @@
+add('start_date', PickRollingDateType::class, [
+ 'label' => 'export.filter.activity.course_having_activity_between_date.Receiving an activity after'
+ ])
+ ->add('end_date', PickRollingDateType::class, [
+ 'label' => 'export.filter.activity.course_having_activity_between_date.Receiving an activity before'
+ ]);
+ }
+
+ public function getFormDefaultData(): array
+ {
+ return [
+ 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
+ 'end_date' => new RollingDate(RollingDate::T_TODAY)
+ ];
+ }
+
+ public function describeAction($data, $format = 'string')
+ {
+ return [
+ 'export.filter.activity.course_having_activity_between_date.Only course having an activity between from and to',
+ [
+ 'from' => $this->rollingDateConverter->convert($data['start_date']),
+ 'to' => $this->rollingDateConverter->convert($data['end_date']),
+ ]
+ ];
+ }
+
+ public function addRole(): ?string
+ {
+ return null;
+ }
+
+ public function alterQuery(QueryBuilder $qb, $data)
+ {
+ $alias = 'act_period_having_act_betw_date_alias';
+ $from = 'act_period_having_act_betw_date_start';
+ $to = 'act_period_having_act_betw_date_end';
+
+ $qb->andWhere(
+ $qb->expr()->exists(
+ 'SELECT 1 FROM ' . Activity::class . " {$alias} WHERE {$alias}.date >= :{$from} AND {$alias}.date < :{$to} AND {$alias}.accompanyingPeriod = acp"
+ )
+ );
+
+ $qb
+ ->setParameter($from, $this->rollingDateConverter->convert($data['start_date']))
+ ->setParameter($to, $this->rollingDateConverter->convert($data['end_date']));
+ }
+
+ public function applyOn()
+ {
+ return \Chill\PersonBundle\Export\Declarations::ACP_TYPE;
+ }
+}
diff --git a/src/Bundle/ChillActivityBundle/Timeline/TimelineActivityProvider.php b/src/Bundle/ChillActivityBundle/Timeline/TimelineActivityProvider.php
index dad597676..2c9272b08 100644
--- a/src/Bundle/ChillActivityBundle/Timeline/TimelineActivityProvider.php
+++ b/src/Bundle/ChillActivityBundle/Timeline/TimelineActivityProvider.php
@@ -161,6 +161,7 @@ class TimelineActivityProvider implements TimelineProviderInterface
// loop on reachable scopes
foreach ($reachableScopes as $scope) {
+ /** @phpstan-ignore-next-line */
if (in_array($scope->getId(), $scopes_ids, true)) {
continue;
}
diff --git a/src/Bundle/ChillActivityBundle/config/services/export.yaml b/src/Bundle/ChillActivityBundle/config/services/export.yaml
index 09817d80e..5af2895e9 100644
--- a/src/Bundle/ChillActivityBundle/config/services/export.yaml
+++ b/src/Bundle/ChillActivityBundle/config/services/export.yaml
@@ -135,6 +135,10 @@ services:
tags:
- { name: chill.export_filter, alias: 'accompanyingcourse_has_no_activity_filter' }
+ Chill\ActivityBundle\Export\Filter\ACPFilters\PeriodHavingActivityBetweenDatesFilter:
+ tags:
+ - { name: chill.export_filter, alias: 'period_having_activity_betw_dates_filter' }
+
## Aggregators
Chill\ActivityBundle\Export\Aggregator\PersonAggregators\ActivityReasonAggregator:
tags:
@@ -144,6 +148,10 @@ services:
tags:
- { name: chill.export_aggregator, alias: activity_common_type_aggregator }
+ Chill\ActivityBundle\Export\Aggregator\ActivityLocationAggregator:
+ tags:
+ - { name: chill.export_aggregator, alias: activity_common_location_aggregator }
+
chill.activity.export.user_aggregator:
class: Chill\ActivityBundle\Export\Aggregator\ActivityUserAggregator
tags:
diff --git a/src/Bundle/ChillActivityBundle/translations/messages+intl-icu.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages+intl-icu.fr.yml
new file mode 100644
index 000000000..ab3b963ab
--- /dev/null
+++ b/src/Bundle/ChillActivityBundle/translations/messages+intl-icu.fr.yml
@@ -0,0 +1,5 @@
+export:
+ filter:
+ activity:
+ course_having_activity_between_date:
+ Only course having an activity between from and to: Seulement les parcours ayant reçu au moins un échange entre le {from, date, short} et le {to, date, short}
diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml
index 3099e99b0..4ddad2292 100644
--- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml
+++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml
@@ -96,9 +96,6 @@ activity_filter:
My activities: Mes échanges (où j'interviens)
Types: Par type d'échange
Jobs: Par métier impliqué
- By: Filtrer par
- Search: Chercher dans la liste
- By date: Filtrer par date
#timeline
'%user% has done an %activity_type%': '%user% a effectué un échange de type "%activity_type%"'
@@ -376,6 +373,12 @@ export:
by_usersscope:
Filter by users scope: Filtrer les échanges par services d'au moins un utilisateur participant
'Filtered activity by users scope: only %scopes%': 'Filtré par service d''au moins un utilisateur participant: seulement %scopes%'
+ course_having_activity_between_date:
+ Title: Filtre les parcours ayant reçu un échange entre deux dates
+ Receiving an activity after: Ayant reçu un échange après le
+ Receiving an activity before: Ayant reçu un échange avant le
+
+
aggregator:
activity:
by_sent_received:
@@ -383,6 +386,9 @@ export:
is sent: envoyé
is received: reçu
Group activity by sentreceived: Grouper les échanges par envoyé / reçu
+ by_location:
+ Activity Location: Localisation de l'échange
+ Title: Grouper les échanges par localisation de l'échange
generic_doc:
filter:
diff --git a/src/Bundle/ChillBudgetBundle/Resources/views/Budget/_macros.html.twig b/src/Bundle/ChillBudgetBundle/Resources/views/Budget/_macros.html.twig
index dfa286af4..a1fee19ce 100644
--- a/src/Bundle/ChillBudgetBundle/Resources/views/Budget/_macros.html.twig
+++ b/src/Bundle/ChillBudgetBundle/Resources/views/Budget/_macros.html.twig
@@ -1,11 +1,12 @@
-{% macro table_elements(elements, family) %}
+{% macro table_elements(elements, type) %}
+
-
{{ 'Budget element type'|trans }}
-
{{ 'Amount'|trans }}
-
{{ 'Validity period'|trans }}
-
+
{{ 'Budget element type'|trans }}
+
{{ 'Amount'|trans }}
+
{{ 'Validity period'|trans }}
+
@@ -38,17 +39,17 @@
{% if is_granted('CHILL_BUDGET_ELEMENT_SEE', f) %}
-
+
{% endif %}
{% if is_granted('CHILL_BUDGET_ELEMENT_UPDATE', f) %}
-
+
{% endif %}
{% if is_granted('CHILL_BUDGET_ELEMENT_DELETE', f) %}
-
+
{% endif %}
@@ -69,7 +70,7 @@
{% endmacro %}
-{% macro table_results(actualCharges, actualResources) %}
+{% macro table_results(actualCharges, actualResources, results) %}
{% set totalCharges = 0 %}
{% for c in actualCharges %}
@@ -97,6 +98,20 @@
{{ result|format_currency('EUR') }}
+ {% for result in results %}
+
{% if is_granted('CHILL_BUDGET_ELEMENT_CREATE', person) %}
diff --git a/src/Bundle/ChillCalendarBundle/Command/MapAndSubscribeUserCalendarCommand.php b/src/Bundle/ChillCalendarBundle/Command/MapAndSubscribeUserCalendarCommand.php
index b90fb83d4..193b04934 100644
--- a/src/Bundle/ChillCalendarBundle/Command/MapAndSubscribeUserCalendarCommand.php
+++ b/src/Bundle/ChillCalendarBundle/Command/MapAndSubscribeUserCalendarCommand.php
@@ -18,9 +18,12 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Command;
+use Chill\CalendarBundle\Exception\UserAbsenceSyncException;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\EventsOnUserSubscriptionCreator;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MapCalendarToUser;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MSGraphUserRepository;
+use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MSUserAbsenceSync;
+use Chill\MainBundle\Repository\UserRepositoryInterface;
use DateInterval;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
@@ -30,32 +33,17 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
-class MapAndSubscribeUserCalendarCommand extends Command
+final class MapAndSubscribeUserCalendarCommand extends Command
{
- private EntityManagerInterface $em;
-
- private EventsOnUserSubscriptionCreator $eventsOnUserSubscriptionCreator;
-
- private LoggerInterface $logger;
-
- private MapCalendarToUser $mapCalendarToUser;
-
- private MSGraphUserRepository $userRepository;
-
public function __construct(
- EntityManagerInterface $em,
- EventsOnUserSubscriptionCreator $eventsOnUserSubscriptionCreator,
- LoggerInterface $logger,
- MapCalendarToUser $mapCalendarToUser,
- MSGraphUserRepository $userRepository
+ private readonly EntityManagerInterface $em,
+ private readonly EventsOnUserSubscriptionCreator $eventsOnUserSubscriptionCreator,
+ private readonly LoggerInterface $logger,
+ private readonly MapCalendarToUser $mapCalendarToUser,
+ private readonly UserRepositoryInterface $userRepository,
+ private readonly MSUserAbsenceSync $userAbsenceSync,
) {
parent::__construct('chill:calendar:msgraph-user-map-subscribe');
-
- $this->em = $em;
- $this->eventsOnUserSubscriptionCreator = $eventsOnUserSubscriptionCreator;
- $this->logger = $logger;
- $this->mapCalendarToUser = $mapCalendarToUser;
- $this->userRepository = $userRepository;
}
public function execute(InputInterface $input, OutputInterface $output): int
@@ -67,83 +55,109 @@ class MapAndSubscribeUserCalendarCommand extends Command
/** @var DateInterval $interval the interval before the end of the expiration */
$interval = new DateInterval('P1D');
$expiration = (new DateTimeImmutable('now'))->add(new DateInterval($input->getOption('subscription-duration')));
- $total = $this->userRepository->countByMostOldSubscriptionOrWithoutSubscriptionOrData($interval);
+ $users = $this->userRepository->findAllAsArray('fr');
$created = 0;
$renewed = 0;
- $this->logger->info(self::class . ' the number of user to get - renew', [
- 'total' => $total,
+ $this->logger->info(self::class . ' start user to get - renew', [
'expiration' => $expiration->format(DateTimeImmutable::ATOM),
]);
- while ($offset < $total) {
- $users = $this->userRepository->findByMostOldSubscriptionOrWithoutSubscriptionOrData(
- $interval,
- $limit,
- $offset
- );
+ foreach ($users as $u) {
+ ++$offset;
- foreach ($users as $user) {
- if (!$this->mapCalendarToUser->hasUserId($user)) {
- $this->mapCalendarToUser->writeMetadata($user);
- }
-
- if ($this->mapCalendarToUser->hasUserId($user)) {
- // we first try to renew an existing subscription, if any.
- // if not, or if it fails, we try to create a new one
- if ($this->mapCalendarToUser->hasActiveSubscription($user)) {
- $this->logger->debug(self::class . ' renew a subscription for', [
- 'userId' => $user->getId(),
- 'username' => $user->getUsernameCanonical(),
- ]);
-
- ['secret' => $secret, 'id' => $id, 'expiration' => $expirationTs]
- = $this->eventsOnUserSubscriptionCreator->renewSubscriptionForUser($user, $expiration);
- $this->mapCalendarToUser->writeSubscriptionMetadata($user, $expirationTs, $id, $secret);
-
- if (0 !== $expirationTs) {
- ++$renewed;
- } else {
- $this->logger->warning(self::class . ' could not renew subscription for a user', [
- 'userId' => $user->getId(),
- 'username' => $user->getUsernameCanonical(),
- ]);
- }
- }
-
- if (!$this->mapCalendarToUser->hasActiveSubscription($user)) {
- $this->logger->debug(self::class . ' create a subscription for', [
- 'userId' => $user->getId(),
- 'username' => $user->getUsernameCanonical(),
- ]);
-
- ['secret' => $secret, 'id' => $id, 'expiration' => $expirationTs]
- = $this->eventsOnUserSubscriptionCreator->createSubscriptionForUser($user, $expiration);
- $this->mapCalendarToUser->writeSubscriptionMetadata($user, $expirationTs, $id, $secret);
-
- if (0 !== $expirationTs) {
- ++$created;
- } else {
- $this->logger->warning(self::class . ' could not create subscription for a user', [
- 'userId' => $user->getId(),
- 'username' => $user->getUsernameCanonical(),
- ]);
- }
- }
- }
-
- ++$offset;
+ if (false === $u['enabled']) {
+ continue;
}
- $this->em->flush();
- $this->em->clear();
+ $user = $this->userRepository->find($u['id']);
+
+ if (null === $user) {
+ $this->logger->error("could not find user by id", ['uid' => $u['id']]);
+ $output->writeln("could not find user by id : " . $u['id']);
+ continue;
+ }
+
+ if (!$this->mapCalendarToUser->hasUserId($user)) {
+ $user = $this->mapCalendarToUser->writeMetadata($user);
+
+ // if user still does not have userid, continue
+ if (!$this->mapCalendarToUser->hasUserId($user)) {
+ $this->logger->warning("user does not have a counterpart on ms api", ['userId' => $user->getId(), 'email' => $user->getEmail()]);
+ $output->writeln(sprintf("giving up for user with email %s and id %s", $user->getEmail(), $user->getId()));
+
+ continue;
+ }
+ }
+
+ // sync user absence
+ try {
+ $this->userAbsenceSync->syncUserAbsence($user);
+ } catch (UserAbsenceSyncException $e) {
+ $this->logger->error("could not sync user absence", ['userId' => $user->getId(), 'email' => $user->getEmail(), 'exception' => $e->getTraceAsString(), "message" => $e->getMessage()]);
+ $output->writeln(sprintf("Could not sync user absence: id: %s and email: %s", $user->getId(), $user->getEmail()));
+ throw $e;
+ }
+
+ // we first try to renew an existing subscription, if any.
+ // if not, or if it fails, we try to create a new one
+ if ($this->mapCalendarToUser->hasActiveSubscription($user)) {
+ $this->logger->debug(self::class . ' renew a subscription for', [
+ 'userId' => $user->getId(),
+ 'username' => $user->getUsernameCanonical(),
+ ]);
+
+ ['secret' => $secret, 'id' => $id, 'expiration' => $expirationTs]
+ = $this->eventsOnUserSubscriptionCreator->renewSubscriptionForUser($user, $expiration);
+ $this->mapCalendarToUser->writeSubscriptionMetadata($user, $expirationTs, $id, $secret);
+
+ if (0 !== $expirationTs) {
+ ++$renewed;
+ } else {
+ $this->logger->warning(self::class . ' could not renew subscription for a user', [
+ 'userId' => $user->getId(),
+ 'username' => $user->getUsernameCanonical(),
+ ]);
+ }
+ }
+
+ if (!$this->mapCalendarToUser->hasActiveSubscription($user)) {
+ $this->logger->debug(self::class . ' create a subscription for', [
+ 'userId' => $user->getId(),
+ 'username' => $user->getUsernameCanonical(),
+ ]);
+
+ ['secret' => $secret, 'id' => $id, 'expiration' => $expirationTs]
+ = $this->eventsOnUserSubscriptionCreator->createSubscriptionForUser($user, $expiration);
+ $this->mapCalendarToUser->writeSubscriptionMetadata($user, $expirationTs, $id, $secret);
+
+ if (0 !== $expirationTs) {
+ ++$created;
+ } else {
+ $this->logger->warning(self::class . ' could not create subscription for a user', [
+ 'userId' => $user->getId(),
+ 'username' => $user->getUsernameCanonical(),
+ ]);
+ }
+ }
+
+
+ if (0 === $offset % $limit) {
+ $this->em->flush();
+ $this->em->clear();
+ }
}
+ $this->em->flush();
+ $this->em->clear();
+
$this->logger->warning(self::class . ' process executed', [
'created' => $created,
'renewed' => $renewed,
]);
+ $output->writeln("users synchronized");
+
return 0;
}
@@ -152,7 +166,7 @@ class MapAndSubscribeUserCalendarCommand extends Command
parent::configure();
$this
- ->setDescription('MSGraph: collect user metadata and create subscription on events for users')
+ ->setDescription('MSGraph: collect user metadata and create subscription on events for users, and sync the user absence-presence')
->addOption(
'renew-before-end-interval',
'r',
diff --git a/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php b/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php
new file mode 100644
index 000000000..a5e5a679a
--- /dev/null
+++ b/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php
@@ -0,0 +1,20 @@
+'msgraph' ?? 'subscription_events_expiration'
- OR (attributes->'msgraph' ?? 'subscription_events_expiration' AND (attributes->'msgraph'->>'subscription_events_expiration')::int < EXTRACT(EPOCH FROM (NOW() + :interval::interval)))
- LIMIT :limit OFFSET :offset
- ;
- SQL;
-
- private EntityManagerInterface $entityManager;
-
- public function __construct(EntityManagerInterface $entityManager)
- {
- $this->entityManager = $entityManager;
- }
-
- public function countByMostOldSubscriptionOrWithoutSubscriptionOrData(DateInterval $interval): int
- {
- $rsm = new ResultSetMapping();
- $rsm->addScalarResult('c', 'c');
-
- $sql = strtr(self::MOST_OLD_SUBSCRIPTION_OR_ANY_MS_GRAPH, [
- '{select}' => 'COUNT(u) AS c',
- 'LIMIT :limit OFFSET :offset' => '',
- ]);
-
- return $this->entityManager->createNativeQuery($sql, $rsm)->setParameters([
- 'interval' => $interval,
- ])->getSingleScalarResult();
- }
-
- /**
- * @return array|User[]
- */
- public function findByMostOldSubscriptionOrWithoutSubscriptionOrData(DateInterval $interval, int $limit = 50, int $offset = 0): array
- {
- $rsm = new ResultSetMappingBuilder($this->entityManager);
- $rsm->addRootEntityFromClassMetadata(User::class, 'u');
-
- return $this->entityManager->createNativeQuery(
- strtr(self::MOST_OLD_SUBSCRIPTION_OR_ANY_MS_GRAPH, ['{select}' => $rsm->generateSelectClause()]),
- $rsm
- )->setParameters([
- 'interval' => $interval,
- 'limit' => $limit,
- 'offset' => $offset,
- ])->getResult();
- }
-}
diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php
new file mode 100644
index 000000000..c70072a47
--- /dev/null
+++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php
@@ -0,0 +1,69 @@
+mapCalendarToUser->getUserId($user);
+
+ if (null === $id) {
+ return null;
+ }
+
+ try {
+ $automaticRepliesSettings = $this->machineHttpClient
+ ->request('GET', 'users/' . $id . '/mailboxSettings/automaticRepliesSetting')
+ ->toArray(true);
+ } catch (ClientExceptionInterface|DecodingExceptionInterface|RedirectionExceptionInterface|TransportExceptionInterface $e) {
+ throw new UserAbsenceSyncException("Error receiving response for mailboxSettings", 0, $e);
+ } catch (ServerExceptionInterface $e) {
+ throw new UserAbsenceSyncException("Server error receiving response for mailboxSettings", 0, $e);
+ }
+
+ if (!array_key_exists("status", $automaticRepliesSettings)) {
+ throw new \LogicException("no key \"status\" on automatic replies settings: " . json_encode($automaticRepliesSettings, JSON_THROW_ON_ERROR));
+ }
+
+ return match ($automaticRepliesSettings['status']) {
+ 'disabled' => false,
+ 'alwaysEnabled' => true,
+ 'scheduled' =>
+ RemoteEventConverter::convertStringDateWithoutTimezone($automaticRepliesSettings['scheduledStartDateTime']['dateTime']) < $this->clock->now()
+ && RemoteEventConverter::convertStringDateWithoutTimezone($automaticRepliesSettings['scheduledEndDateTime']['dateTime']) > $this->clock->now(),
+ default => throw new UserAbsenceSyncException("this status is not documented by Microsoft")
+ };
+ }
+
+}
diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderInterface.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderInterface.php
new file mode 100644
index 000000000..a918bb7ea
--- /dev/null
+++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderInterface.php
@@ -0,0 +1,22 @@
+absenceReader->isUserAbsent($user);
+
+ if (null === $absence) {
+ return;
+ }
+
+ if ($absence === $user->isAbsent()) {
+ // nothing to do
+ return;
+ }
+
+ $this->logger->info("will change user absence", ['userId' => $user->getId()]);
+
+ if ($absence) {
+ $this->logger->debug("make user absent", ['userId' => $user->getId()]);
+ $user->setAbsenceStart($this->clock->now());
+ } else {
+ $this->logger->debug("make user present", ['userId' => $user->getId()]);
+ $user->setAbsenceStart(null);
+ }
+ }
+}
diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php
index f56735de7..6a986c9db 100644
--- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php
+++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php
@@ -23,6 +23,8 @@ use Chill\CalendarBundle\Command\MapAndSubscribeUserCalendarCommand;
use Chill\CalendarBundle\Controller\RemoteCalendarConnectAzureController;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MachineHttpClient;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MachineTokenStorage;
+use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MSUserAbsenceReaderInterface;
+use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MSUserAbsenceSync;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraphRemoteCalendarConnector;
use Chill\CalendarBundle\RemoteCalendar\Connector\NullRemoteCalendarConnector;
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
@@ -37,17 +39,13 @@ class RemoteCalendarCompilerPass implements CompilerPassInterface
public function process(ContainerBuilder $container)
{
$config = $container->getParameter('chill_calendar');
- $connector = null;
- if (!$config['remote_calendars_sync']['enabled']) {
- $connector = NullRemoteCalendarConnector::class;
- }
-
- if ($config['remote_calendars_sync']['microsoft_graph']['enabled']) {
+ if (true === $config['remote_calendars_sync']['microsoft_graph']['enabled']) {
$connector = MSGraphRemoteCalendarConnector::class;
$container->setAlias(HttpClientInterface::class . ' $machineHttpClient', MachineHttpClient::class);
} else {
+ $connector = NullRemoteCalendarConnector::class;
// remove services which cannot be loaded
$container->removeDefinition(MapAndSubscribeUserCalendarCommand::class);
$container->removeDefinition(AzureGrantAdminConsentAndAcquireToken::class);
@@ -55,16 +53,14 @@ class RemoteCalendarCompilerPass implements CompilerPassInterface
$container->removeDefinition(MachineTokenStorage::class);
$container->removeDefinition(MachineHttpClient::class);
$container->removeDefinition(MSGraphRemoteCalendarConnector::class);
+ $container->removeDefinition(MSUserAbsenceReaderInterface::class);
+ $container->removeDefinition(MSUserAbsenceSync::class);
}
if (!$container->hasAlias(Azure::class) && $container->hasDefinition('knpu.oauth2.client.azure')) {
$container->setAlias(Azure::class, 'knpu.oauth2.provider.azure');
}
- if (null === $connector) {
- throw new RuntimeException('Could not configure remote calendar');
- }
-
foreach ([
NullRemoteCalendarConnector::class,
MSGraphRemoteCalendarConnector::class, ] as $serviceId) {
diff --git a/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderTest.php b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderTest.php
new file mode 100644
index 000000000..089477fda
--- /dev/null
+++ b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderTest.php
@@ -0,0 +1,176 @@
+prophesize(MapCalendarToUser::class);
+ $mapUser->getUserId($user)->willReturn('1234');
+ $clock = new MockClock(new \DateTimeImmutable('2023-07-07T12:00:00'));
+
+ $absenceReader = new MSUserAbsenceReader($client, $mapUser->reveal(), $clock);
+
+ self::assertEquals($expected, $absenceReader->isUserAbsent($user), $message);
+ }
+
+ public function testIsUserAbsentWithoutRemoteId(): void
+ {
+ $user = new User();
+ $client = new MockHttpClient();
+
+ $mapUser = $this->prophesize(MapCalendarToUser::class);
+ $mapUser->getUserId($user)->willReturn(null);
+ $clock = new MockClock(new \DateTimeImmutable('2023-07-07T12:00:00'));
+
+ $absenceReader = new MSUserAbsenceReader($client, $mapUser->reveal(), $clock);
+
+ self::assertNull($absenceReader->isUserAbsent($user), "when no user found, absence should be null");
+ }
+
+ public function provideDataTestUserAbsence(): iterable
+ {
+ // contains data that was retrieved from microsoft graph api on 2023-07-06
+
+ yield [
+ <<<'JSON'
+ {
+ "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/mailboxSettings/automaticRepliesSetting",
+ "status": "disabled",
+ "externalAudience": "none",
+ "internalReplyMessage": "Je suis en congé.",
+ "externalReplyMessage": "",
+ "scheduledStartDateTime": {
+ "dateTime": "2023-07-06T12:00:00.0000000",
+ "timeZone": "UTC"
+ },
+ "scheduledEndDateTime": {
+ "dateTime": "2023-07-07T12:00:00.0000000",
+ "timeZone": "UTC"
+ }
+ }
+ JSON,
+ false,
+ "User is present"
+ ];
+
+ yield [
+ <<<'JSON'
+ {
+ "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/mailboxSettings/automaticRepliesSetting",
+ "status": "scheduled",
+ "externalAudience": "none",
+ "internalReplyMessage": "Je suis en congé.",
+ "externalReplyMessage": "",
+ "scheduledStartDateTime": {
+ "dateTime": "2023-07-06T11:00:00.0000000",
+ "timeZone": "UTC"
+ },
+ "scheduledEndDateTime": {
+ "dateTime": "2023-07-21T11:00:00.0000000",
+ "timeZone": "UTC"
+ }
+ }
+ JSON,
+ true,
+ 'User is absent with absence scheduled, we are within this period'
+ ];
+
+ yield [
+ <<<'JSON'
+ {
+ "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/mailboxSettings/automaticRepliesSetting",
+ "status": "scheduled",
+ "externalAudience": "none",
+ "internalReplyMessage": "Je suis en congé.",
+ "externalReplyMessage": "",
+ "scheduledStartDateTime": {
+ "dateTime": "2023-07-08T11:00:00.0000000",
+ "timeZone": "UTC"
+ },
+ "scheduledEndDateTime": {
+ "dateTime": "2023-07-21T11:00:00.0000000",
+ "timeZone": "UTC"
+ }
+ }
+ JSON,
+ false,
+ 'User is present: absence is scheduled for later'
+ ];
+
+ yield [
+ <<<'JSON'
+ {
+ "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/mailboxSettings/automaticRepliesSetting",
+ "status": "scheduled",
+ "externalAudience": "none",
+ "internalReplyMessage": "Je suis en congé.",
+ "externalReplyMessage": "",
+ "scheduledStartDateTime": {
+ "dateTime": "2023-07-05T11:00:00.0000000",
+ "timeZone": "UTC"
+ },
+ "scheduledEndDateTime": {
+ "dateTime": "2023-07-06T11:00:00.0000000",
+ "timeZone": "UTC"
+ }
+ }
+ JSON,
+ false,
+ 'User is present: absence is past'
+ ];
+
+ yield [
+ <<<'JSON'
+ {
+ "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/mailboxSettings/automaticRepliesSetting",
+ "status": "alwaysEnabled",
+ "externalAudience": "none",
+ "internalReplyMessage": "Je suis en congé.",
+ "externalReplyMessage": "",
+ "scheduledStartDateTime": {
+ "dateTime": "2023-07-06T12:00:00.0000000",
+ "timeZone": "UTC"
+ },
+ "scheduledEndDateTime": {
+ "dateTime": "2023-07-07T12:00:00.0000000",
+ "timeZone": "UTC"
+ }
+ }
+ JSON,
+ true,
+ "User is absent: absence is always enabled"
+ ];
+ }
+
+}
diff --git a/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSyncTest.php b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSyncTest.php
new file mode 100644
index 000000000..1b0f1e416
--- /dev/null
+++ b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSyncTest.php
@@ -0,0 +1,68 @@
+prophesize(MSUserAbsenceReaderInterface::class);
+ $userAbsenceReader->isUserAbsent($user)->willReturn($absenceFromMicrosoft);
+
+ $clock = new MockClock(new \DateTimeImmutable('2023-07-01T12:00:00'));
+
+ $syncer = new MSUserAbsenceSync($userAbsenceReader->reveal(), $clock, new NullLogger());
+
+ $syncer->syncUserAbsence($user);
+
+ self::assertEquals($expectedAbsence, $user->isAbsent(), $message);
+ self::assertEquals($expectedAbsenceStart, $user->getAbsenceStart(), $message);
+ }
+
+ public function provideDataTestSyncUserAbsence(): iterable
+ {
+ yield [new User(), false, false, null, "user present remains present"];
+ yield [new User(), true, true, new \DateTimeImmutable('2023-07-01T12:00:00'), "user present becomes absent"];
+
+ $user = new User();
+ $user->setAbsenceStart($abs = new \DateTimeImmutable("2023-07-01T12:00:00"));
+ yield [$user, true, true, $abs, "user absent remains absent"];
+
+ $user = new User();
+ $user->setAbsenceStart($abs = new \DateTimeImmutable("2023-07-01T12:00:00"));
+ yield [$user, false, false, null, "user absent becomes present"];
+
+ yield [new User(), null, false, null, "user not syncable: presence do not change"];
+
+ $user = new User();
+ $user->setAbsenceStart($abs = new \DateTimeImmutable("2023-07-01T12:00:00"));
+ yield [$user, null, true, $abs, "user not syncable: absence do not change"];
+ }
+}
diff --git a/src/Bundle/ChillEventBundle/Controller/ParticipationController.php b/src/Bundle/ChillEventBundle/Controller/ParticipationController.php
index 1929beac1..a2a82c88f 100644
--- a/src/Bundle/ChillEventBundle/Controller/ParticipationController.php
+++ b/src/Bundle/ChillEventBundle/Controller/ParticipationController.php
@@ -16,6 +16,8 @@ use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\Participation;
use Chill\EventBundle\Form\ParticipationType;
use Chill\EventBundle\Security\Authorization\ParticipationVoter;
+use Doctrine\Common\Collections\Collection;
+use Doctrine\Common\Collections\ReadableCollection;
use LogicException;
use Psr\Log\LoggerInterface;
use RuntimeException;
@@ -509,7 +511,7 @@ class ParticipationController extends AbstractController
/**
* @return \Symfony\Component\Form\FormInterface
*/
- protected function createEditFormMultiple(ArrayIterator $participations, Event $event)
+ protected function createEditFormMultiple(Collection $participations, Event $event)
{
$form = $this->createForm(
\Symfony\Component\Form\Extension\Core\Type\FormType::class,
@@ -638,6 +640,7 @@ class ParticipationController extends AbstractController
$ignoredParticipations = $newParticipations = [];
foreach ($participations as $participation) {
+ /** @var Participation $participation */
// check for authorization
$this->denyAccessUnlessGranted(
ParticipationVoter::CREATE,
diff --git a/src/Bundle/ChillEventBundle/Entity/Event.php b/src/Bundle/ChillEventBundle/Entity/Event.php
index d55a7b8a8..cc762e979 100644
--- a/src/Bundle/ChillEventBundle/Entity/Event.php
+++ b/src/Bundle/ChillEventBundle/Entity/Event.php
@@ -160,11 +160,11 @@ class Event implements HasCenterInterface, HasScopeInterface
}
/**
- * @return ArrayIterator|Collection|Traversable
+ * @return Collection
*/
public function getParticipations()
{
- return $this->getParticipationsOrdered();
+ return new ArrayCollection(iterator_to_array($this->getParticipationsOrdered()));
}
/**
diff --git a/src/Bundle/ChillMainBundle/Export/ExportInterface.php b/src/Bundle/ChillMainBundle/Export/ExportInterface.php
index f357a9fdb..a11a51746 100644
--- a/src/Bundle/ChillMainBundle/Export/ExportInterface.php
+++ b/src/Bundle/ChillMainBundle/Export/ExportInterface.php
@@ -97,7 +97,7 @@ interface ExportInterface extends ExportElementInterface
* @param mixed[] $values The values from the result. if there are duplicates, those might be given twice. Example: array('FR', 'BE', 'CZ', 'FR', 'BE', 'FR')
* @param mixed $data The data from the export's form (as defined in `buildForm`)
*
- * @return callable(null|string|int|float|'_header' $value): string|int|\DateTimeInterface where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }`
+ * @return (callable(null|string|int|float|'_header' $value): string|int|\DateTimeInterface) where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }`
*/
public function getLabels($key, array $values, $data);
diff --git a/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php b/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php
index 4e41a1740..51d1b3974 100644
--- a/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php
+++ b/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php
@@ -40,7 +40,7 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType
'label' => false,
'required' => false,
'attr' => [
- 'placeholder' => 'activity_filter.Search',
+ 'placeholder' => 'filter_order.Search',
]
]);
}
@@ -48,16 +48,7 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType
$checkboxesBuilder = $builder->create('checkboxes', null, ['compound' => true]);
foreach ($helper->getCheckboxes() as $name => $c) {
- $choices = array_combine(
- array_map(static function ($c, $t) {
- if (null !== $t) {
- return $t;
- }
-
- return $c;
- }, $c['choices'], $c['trans']),
- $c['choices']
- );
+ $choices = self::buildCheckboxChoices($c['choices'], $c['trans']);
$checkboxesBuilder->add($name, ChoiceType::class, [
'choices' => $choices,
@@ -148,6 +139,20 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType
}
+ public static function buildCheckboxChoices(array $choices, array $trans = []): array
+ {
+ return array_combine(
+ array_map(static function ($c, $t) {
+ if (null !== $t) {
+ return $t;
+ }
+
+ return $c;
+ }, $choices, $trans),
+ $choices
+ );
+ }
+
public function buildView(FormView $view, FormInterface $form, array $options)
{
/** @var FilterOrderHelper $helper */
diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/forms.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/forms.scss
index cd81f36dc..28c597bc0 100644
--- a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/forms.scss
+++ b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/forms.scss
@@ -44,13 +44,5 @@ form {
}
.chill_filter_order {
- background: $gray-100; /*
- border: 3px dashed $white;
- background: repeating-linear-gradient(
- -45deg,
- $gray-100,
- $gray-100 2px,
- $white 2px,
- $white 6px
- ); */
+ background: $gray-100;
}
\ No newline at end of file
diff --git a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig
index 9a9a11fbd..adf67b81b 100644
--- a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig
+++ b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig
@@ -1,110 +1,144 @@
{{ form_start(form) }}
- {% set btnSubmit = 0 %}
-
-
- {% if form.vars.has_search_box %}
-
-
- {{ form_widget(form.q) }}
-
-
-
- {% endif %}
-
- {% if form.dateRanges is defined %}
- {% set btnSubmit = 1 %}
- {% if form.dateRanges|length > 0 %}
- {% for dateRangeName, _o in form.dateRanges %}
-
- {% if form.dateRanges[dateRangeName].vars.label is not same as(false) %}
- {{ form_label(form.dateRanges[dateRangeName])}}
- {% else %}
-
+
+ {% if form.dateRanges is defined %}
+ {% set btnSubmit = 1 %}
+ {% if form.dateRanges|length > 0 %}
+ {% for dateRangeName, _o in form.dateRanges %}
+
+ {% if form.dateRanges[dateRangeName].vars.label is not same as(false) %}
+ {{ form_label(form.dateRanges[dateRangeName])}}
+ {% else %}
+
- {% endfor %}
+ {% endfor %}
+ {% endif %}
{% endif %}
- {% endif %}
- {% if form.checkboxes is defined %}
- {% set btnSubmit = 1 %}
- {% if form.checkboxes|length > 0 %}
- {% for checkbox_name, options in form.checkboxes %}
-
-
{{ 'activity_filter.By'|trans }}
-
- {% for c in form['checkboxes'][checkbox_name].children %}
- {{ form_widget(c) }}
- {{ form_label(c) }}
- {% endfor %}
-
-
- {% endfor %}
- {% endif %}
- {% endif %}
- {% if form.entity_choices is defined %}
- {% set btnSubmit = 1 %}
- {% if form.entity_choices |length > 0 %}
- {% for checkbox_name, options in form.entity_choices %}
-
- {% if form.entity_choices[checkbox_name].vars.label is not same as(false) %}
- {{ form_label(form.entity_choices[checkbox_name])}}
- {% endif %}
-
- {% for c in form['entity_choices'][checkbox_name].children %}
- {{ form_widget(c) }}
- {{ form_label(c) }}
- {% endfor %}
-
-
- {% endfor %}
- {% endif %}
- {% endif %}
- {% if form.user_pickers is defined %}
- {% set btnSubmit = 1 %}
- {% if form.user_pickers.children|length > 0 %}
- {% for name, options in form.user_pickers %}
-
- {% if form.user_pickers[name].vars.label is not same as(false) %}
- {{ form_label(form.user_pickers[name]) }}
- {% else %}
- {{ form_label(form.user_pickers[name].vars.label) }}
- {% endif %}
-
- {{ form_widget(form.user_pickers[name]) }}
-
-
- {% endfor %}
- {% endif %}
- {% endif %}
- {% if form.single_checkboxes is defined %}
- {% set btnSubmit = 1 %}
- {% for name, _o in form.single_checkboxes %}
+ {% if form.checkboxes is defined %}
+ {% set btnSubmit = 1 %}
+ {% if form.checkboxes|length > 0 %}
+ {% for checkbox_name, options in form.checkboxes %}
+
+
{{ 'filter_order.By'|trans }}
+
+ {% for c in form['checkboxes'][checkbox_name].children %}
+ {{ form_widget(c) }}
+ {{ form_label(c) }}
+ {% endfor %}
+
+
+ {% endfor %}
+ {% endif %}
+ {% endif %}
+
+ {% if form.entity_choices is defined %}
+ {% set btnSubmit = 1 %}
+ {% if form.entity_choices |length > 0 %}
+ {% for checkbox_name, options in form.entity_choices %}
+
+ {% if form.entity_choices[checkbox_name].vars.label is not same as(false) %}
+ {{ form_label(form.entity_choices[checkbox_name])}}
+ {% endif %}
+
+ {% for c in form['entity_choices'][checkbox_name].children %}
+ {{ form_widget(c) }}
+ {{ form_label(c) }}
+ {% endfor %}
+
+
+ {% endfor %}
+ {% endif %}
+ {% endif %}
+
+ {% if form.user_pickers is defined %}
+ {% set btnSubmit = 1 %}
+ {% if form.user_pickers.children|length > 0 %}
+ {% for name, options in form.user_pickers %}
+
+ {% if form.user_pickers[name].vars.label is not same as(false) %}
+ {{ form_label(form.user_pickers[name]) }}
+ {% else %}
+ {{ form_label(form.user_pickers[name].vars.label) }}
+ {% endif %}
+
+ {{ form_widget(form.user_pickers[name]) }}
+
+
+ {% endfor %}
+ {% endif %}
+ {% endif %}
+
+ {% if form.single_checkboxes is defined %}
+ {% set btnSubmit = 1 %}
+ {% for name, _o in form.single_checkboxes %}
+
diff --git a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php
index 709624b54..795a921e7 100644
--- a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php
+++ b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php
@@ -42,11 +42,6 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH
self::RE_OPEN_COURSE,
];
- /**
- * Give the ability to see all confidential courses.
- */
- public const CONFIDENTIAL_CRUD = 'CHILL_PERSON_ACCOMPANYING_PERIOD_CRUD_CONFIDENTIAL';
-
public const CREATE = 'CHILL_PERSON_ACCOMPANYING_PERIOD_CREATE';
/**
@@ -107,6 +102,11 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH
*/
public const TOGGLE_INTENSITY = 'CHILL_PERSON_ACCOMPANYING_PERIOD_TOGGLE_INTENSITY';
+ /**
+ * Right to see confidential period even if not referrer
+ */
+ public const SEE_CONFIDENTIAL_ALL = 'CHILL_PERSON_ACCOMPANYING_PERIOD_SEE_CONFIDENTIAL';
+
private Security $security;
private VoterHelperInterface $voterHelper;
@@ -131,7 +131,6 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH
return [
self::SEE,
self::SEE_DETAILS,
- self::CONFIDENTIAL_CRUD,
self::CREATE,
self::EDIT,
self::DELETE,
@@ -139,6 +138,7 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH
self::TOGGLE_CONFIDENTIAL_ALL,
self::REASSIGN_BULK,
self::STATS,
+ self::SEE_CONFIDENTIAL_ALL,
];
}
@@ -149,7 +149,7 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH
public function getRolesWithoutScope(): array
{
- return [self::REASSIGN_BULK];
+ return [];
}
protected function supports($attribute, $subject)
@@ -216,7 +216,7 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH
// if confidential, only the referent can see it
if ($subject->isConfidential()) {
- if ($this->voterHelper->voteOnAttribute(self::CONFIDENTIAL_CRUD, $subject, $token)) {
+ if ($this->voterHelper->voteOnAttribute(self::SEE_CONFIDENTIAL_ALL, $subject, $token)) {
return true;
}
diff --git a/src/Bundle/ChillPersonBundle/Service/DocGenerator/PersonContext.php b/src/Bundle/ChillPersonBundle/Service/DocGenerator/PersonContext.php
index 9d7f1cdd5..5b5773922 100644
--- a/src/Bundle/ChillPersonBundle/Service/DocGenerator/PersonContext.php
+++ b/src/Bundle/ChillPersonBundle/Service/DocGenerator/PersonContext.php
@@ -360,10 +360,12 @@ final class PersonContext implements PersonContextInterface
private function isScopeNecessary(Person $person): bool
{
- if ($this->showScopes && 1 < $this->authorizationHelper->getReachableScopes(
- $this->security->getUser(),
- PersonDocumentVoter::CREATE,
- $this->centerResolverManager->resolveCenters($person)
+ if ($this->showScopes && 1 < count(
+ $this->authorizationHelper->getReachableScopes(
+ $this->security->getUser(),
+ PersonDocumentVoter::CREATE,
+ $this->centerResolverManager->resolveCenters($person)
+ )
)) {
return true;
}
diff --git a/src/Bundle/ChillPersonBundle/Tests/Repository/AccompanyingPeriodACLAwareRepositoryTest.php b/src/Bundle/ChillPersonBundle/Tests/Repository/AccompanyingPeriodACLAwareRepositoryTest.php
new file mode 100644
index 000000000..cb8d56ba3
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Tests/Repository/AccompanyingPeriodACLAwareRepositoryTest.php
@@ -0,0 +1,517 @@
+accompanyingPeriodRepository = self::$container->get(AccompanyingPeriodRepository::class);
+ $this->centerRepository = self::$container->get(CenterRepositoryInterface::class);
+ $this->centerResolverManager = self::$container->get(CenterResolverManagerInterface::class);
+ $this->entityManager = self::$container->get(EntityManagerInterface::class);
+ $this->scopeRepository = self::$container->get(ScopeRepositoryInterface::class);
+ $this->registry = self::$container->get(Registry::class);
+ }
+
+ public static function tearDownAfterClass(): void
+ {
+ self::bootKernel();
+ $em = self::$container->get(EntityManagerInterface::class);
+ $repository = self::$container->get(AccompanyingPeriodRepository::class);
+
+ foreach (self::$periodsIdsToDelete as $id) {
+ if (null === $period = $repository->find($id)) {
+ throw new \RuntimeException("period not found while trying to delete it");
+ }
+
+ foreach ($period->getParticipations() as $participation) {
+ $em->remove($participation);
+ }
+ $em->remove($period);
+ }
+
+ //$em->flush();
+ }
+
+ /**
+ * @dataProvider provideDataFindByUserAndPostalCodesOpenedAccompanyingPeriod
+ * @param list, scopeCanSeeConfidential: list}> $centerScopes
+ * @param list $expectedContains
+ * @param list $expectedNotContains
+ */
+ public function testFindByUserAndPostalCodesOpenedAccompanyingPeriod(User $user, User $searched, array $centerScopes, array $expectedContains, array $expectedNotContains, string $message): void
+ {
+ $security = $this->prophesize(Security::class);
+ $security->getUser()->willReturn($user);
+
+ $authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class);
+ $centers = [];
+
+ foreach ($centerScopes as ['center' => $center, 'scopeOnRole' => $scopes, 'scopeCanSeeConfidential' => $scopesCanSeeConfidential]) {
+ $centers[spl_object_hash($center)] = $center;
+ $authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE, $center)
+ ->willReturn($scopes);
+ $authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, $center)
+ ->willReturn($scopesCanSeeConfidential);
+ }
+ $authorizationHelper->getReachableCenters(AccompanyingPeriodVoter::SEE)->willReturn(array_values($centers));
+
+ $repository = new AccompanyingPeriodACLAwareRepository(
+ $this->accompanyingPeriodRepository,
+ $security->reveal(),
+ $authorizationHelper->reveal(),
+ $this->centerResolverManager
+ );
+
+ $actual = array_map(
+ fn (AccompanyingPeriod $period) => $period->getId(),
+ $repository->findByUserAndPostalCodesOpenedAccompanyingPeriod($searched, [], ['id' => 'DESC'], 20, 0)
+ );
+
+ foreach ($expectedContains as $expected) {
+ self::assertContains($expected->getId(), $actual, $message);
+ }
+ foreach ($expectedNotContains as $expected) {
+ self::assertNotContains($expected->getId(), $actual, $message);
+ }
+ }
+
+ public function provideDataFindByUserAndPostalCodesOpenedAccompanyingPeriod(): iterable
+ {
+ $this->setUp();
+
+ if (null === $user = $this->entityManager->createQuery("SELECT u FROM " . User::class . " u")->setMaxResults(1)->getSingleResult()) {
+ throw new \RuntimeException("no user found");
+ }
+
+ if (null === $anotherUser = $this->entityManager->createQuery("SELECT u FROM " . User::class . " u WHERE u.id != :uid")->setParameter('uid', $user->getId())
+ ->setMaxResults(1)->getSingleResult()) {
+ throw new \RuntimeException("no user found");
+ }
+
+ /** @var Person $person */
+ [$person, $anotherPerson, $person2, $person3] = $this->entityManager
+ ->createQuery("SELECT p FROM " . Person::class . " p JOIN p.centerCurrent current_center")
+ ->setMaxResults(4)
+ ->getResult();
+
+ if (null === $person || null === $anotherPerson || null === $person2 || null === $person3) {
+ throw new \RuntimeException("no person found");
+ }
+
+ $scopes = $this->scopeRepository->findAll();
+
+ if (3 > count($scopes)) {
+ throw new \RuntimeException("not enough scopes for this test");
+ }
+ $scopesCanSee = [ $scopes[0] ];
+ $scopesGroup2 = [ $scopes[1] ];
+
+ $centers = $this->centerRepository->findActive();
+ $aCenterNotAssociatedToPerson = array_values(array_filter($centers, fn (Center $c) => $c !== $person->getCenter()))[0];
+
+ if (2 > count($centers)) {
+ throw new \RuntimeException("not enough centers for this test");
+ }
+
+ $period = $this->buildPeriod($person, $scopesCanSee, $user, true);
+ $period->setUser($user);
+
+ yield [
+ $anotherUser,
+ $user,
+ [
+ [
+ 'center' => $person->getCenter(),
+ 'scopeOnRole' => $scopesCanSee,
+ 'scopeCanSeeConfidential' => [],
+ ],
+ ],
+ [$period],
+ [],
+ "period should be visible with expected scopes",
+ ];
+
+ yield [
+ $anotherUser,
+ $user,
+ [
+ [
+ 'center' => $person->getCenter(),
+ 'scopeOnRole' => $scopesGroup2,
+ 'scopeCanSeeConfidential' => [],
+ ],
+ ],
+ [],
+ [$period],
+ "period should not be visible without expected scopes",
+ ];
+
+ yield [
+ $anotherUser,
+ $user,
+ [
+ [
+ 'center' => $person->getCenter(),
+ 'scopeOnRole' => $scopesGroup2,
+ 'scopeCanSeeConfidential' => [],
+ ],
+ [
+ 'center' => $aCenterNotAssociatedToPerson,
+ 'scopeOnRole' => $scopesCanSee,
+ 'scopeCanSeeConfidential' => [],
+ ],
+ ],
+ [],
+ [$period],
+ "period should not be visible for user having right in another scope (with multiple centers)"
+ ];
+
+ $period = $this->buildPeriod($person, $scopesCanSee, $user, true);
+ $period->setUser($user);
+ $period->setConfidential(true);
+
+ yield [
+ $anotherUser,
+ $user,
+ [
+ [
+ 'center' => $person->getCenter(),
+ 'scopeOnRole' => $scopesCanSee,
+ 'scopeCanSeeConfidential' => [],
+ ],
+ ],
+ [],
+ [$period],
+ "period confidential should not be visible",
+ ];
+
+ yield [
+ $anotherUser,
+ $user,
+ [
+ [
+ 'center' => $person->getCenter(),
+ 'scopeOnRole' => $scopesCanSee,
+ 'scopeCanSeeConfidential' => $scopesCanSee,
+ ],
+ ],
+ [$period],
+ [],
+ "period confidential be visible if user has required scopes",
+ ];
+
+ $this->entityManager->flush();
+ }
+
+ /**
+ * @dataProvider provideDataFindByUndispatched
+ * @param list, scopeCanSeeConfidential: list}> $centerScopes
+ * @param list $expectedContains
+ * @param list $expectedNotContains
+ */
+ public function testFindByUndispatched(User $user, array $centerScopes, array $expectedContains, array $expectedNotContains, string $message): void
+ {
+ $security = $this->prophesize(Security::class);
+ $security->getUser()->willReturn($user);
+
+ $authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class);
+ $centers = [];
+
+ foreach ($centerScopes as ['center' => $center, 'scopeOnRole' => $scopes, 'scopeCanSeeConfidential' => $scopesCanSeeConfidential]) {
+ $centers[spl_object_hash($center)] = $center;
+ $authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE, $center)
+ ->willReturn($scopes);
+ $authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, $center)
+ ->willReturn($scopesCanSeeConfidential);
+ }
+ $authorizationHelper->getReachableCenters(AccompanyingPeriodVoter::SEE)->willReturn(array_values($centers));
+
+ $repository = new AccompanyingPeriodACLAwareRepository(
+ $this->accompanyingPeriodRepository,
+ $security->reveal(),
+ $authorizationHelper->reveal(),
+ $this->centerResolverManager
+ );
+
+ $actual = array_map(
+ fn (AccompanyingPeriod $period) => $period->getId(),
+ $repository->findByUnDispatched([], [], [], ['id' => 'DESC'], 20, 0)
+ );
+
+ foreach ($expectedContains as $expected) {
+ self::assertContains($expected->getId(), $actual, $message);
+ }
+ foreach ($expectedNotContains as $expected) {
+ self::assertNotContains($expected->getId(), $actual, $message);
+ }
+ }
+
+ public function provideDataFindByUndispatched(): iterable
+ {
+ $this->setUp();
+
+ if (null === $user = $this->entityManager->createQuery("SELECT u FROM " . User::class . " u")->setMaxResults(1)->getSingleResult()) {
+ throw new \RuntimeException("no user found");
+ }
+
+ if (null === $anotherUser = $this->entityManager->createQuery("SELECT u FROM " . User::class . " u WHERE u.id != :uid")->setParameter('uid', $user->getId())
+ ->setMaxResults(1)->getSingleResult()) {
+ throw new \RuntimeException("no user found");
+ }
+
+ /** @var Person $person */
+ [$person, $anotherPerson, $person2, $person3] = $this->entityManager
+ ->createQuery("SELECT p FROM " . Person::class . " p ")
+ ->setMaxResults(4)
+ ->getResult();
+
+ if (null === $person || null === $anotherPerson || null === $person2 || null === $person3) {
+ throw new \RuntimeException("no person found");
+ }
+
+ $scopes = $this->scopeRepository->findAll();
+
+ if (3 > count($scopes)) {
+ throw new \RuntimeException("not enough scopes for this test");
+ }
+ $scopesCanSee = [ $scopes[0] ];
+ $scopesGroup2 = [ $scopes[1] ];
+
+ $centers = $this->centerRepository->findActive();
+
+ if (2 > count($centers)) {
+ throw new \RuntimeException("not enough centers for this test");
+ }
+
+ $period = $this->buildPeriod($person, $scopesCanSee, $user, true);
+
+
+ // expected scope: can see the period
+ yield [
+ $anotherUser,
+ [
+ [
+ 'center' => $person->getCenter(),
+ 'scopeOnRole' => $scopesCanSee,
+ 'scopeCanSeeConfidential' => [],
+ ],
+ ],
+ [$period],
+ [],
+ "period should be visible with expected scopes",
+ ];
+
+ // no scope visible
+ yield [
+ $anotherUser,
+ [
+ [
+ 'center' => $person->getCenter(),
+ 'scopeOnRole' => $scopesGroup2,
+ 'scopeCanSeeConfidential' => [],
+ ],
+ ],
+ [],
+ [$period],
+ "period should not be visible without expected scopes",
+ ];
+
+ // another center
+ yield [
+ $anotherUser,
+ [
+ [
+ 'center' => $person->getCenter(),
+ 'scopeOnRole' => $scopesGroup2,
+ 'scopeCanSeeConfidential' => [],
+ ],
+ [
+ 'center' => array_values(array_filter($centers, fn (Center $c) => $c !== $person->getCenter()))[0],
+ 'scopeOnRole' => $scopesCanSee,
+ 'scopeCanSeeConfidential' => [],
+ ],
+ ],
+ [],
+ [$period],
+ "period should not be visible for user having right in another scope (with multiple centers)"
+ ];
+
+ $this->entityManager->flush();
+ }
+
+ /**
+ * For testing this method, we mock the authorization helper to return different Scope that a user
+ * can see, or that a user can see confidential periods.
+ *
+ * @param array $scopeUserCanSee
+ * @param array $scopeUserCanSeeConfidential
+ * @param array $expectedPeriod
+ * @dataProvider provideDataForFindByPerson
+ */
+ public function testFindByPersonTestUser(User $user, Person $person, array $scopeUserCanSee, array $scopeUserCanSeeConfidential, array $expectedPeriod, string $message): void
+ {
+ $security = $this->prophesize(Security::class);
+ $security->getUser()->willReturn($user);
+
+ $authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class);
+ $authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE, Argument::any())
+ ->willReturn($scopeUserCanSee);
+ $authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, Argument::any())
+ ->willReturn($scopeUserCanSeeConfidential);
+
+ $repository = new AccompanyingPeriodACLAwareRepository(
+ $this->accompanyingPeriodRepository,
+ $security->reveal(),
+ $authorizationHelper->reveal(),
+ $this->centerResolverManager
+ );
+
+ $actuals = $repository->findByPerson($person, AccompanyingPeriodVoter::SEE);
+ $expectedIds = array_map(fn (AccompanyingPeriod $period) => $period->getId(), $expectedPeriod);
+
+ self::assertCount(count($expectedPeriod), $actuals, $message);
+ foreach ($actuals as $actual) {
+ self::assertContains($actual->getId(), $expectedIds);
+ }
+ }
+
+ public function provideDataForFindByPerson(): iterable
+ {
+ $this->setUp();
+
+ if (null === $user = $this->entityManager->createQuery("SELECT u FROM " . User::class . " u")->setMaxResults(1)->getSingleResult()) {
+ throw new \RuntimeException("no user found");
+ }
+
+ if (null === $anotherUser = $this->entityManager->createQuery("SELECT u FROM " . User::class . " u WHERE u.id != :uid")->setParameter('uid', $user->getId())
+ ->setMaxResults(1)->getSingleResult()) {
+ throw new \RuntimeException("no user found");
+ }
+
+ [$person, $anotherPerson, $person2, $person3] = $this->entityManager
+ ->createQuery("SELECT p FROM " . Person::class . " p WHERE SIZE(p.accompanyingPeriodParticipations) = 0")
+ ->setMaxResults(4)
+ ->getResult();
+
+ if (null === $person || null === $anotherPerson || null === $person2 || null === $person3) {
+ throw new \RuntimeException("no person found");
+ }
+
+ $scopes = $this->scopeRepository->findAll();
+
+ if (3 > count($scopes)) {
+ throw new \RuntimeException("not enough scopes for this test");
+ }
+ $scopesCanSee = [ $scopes[0] ];
+ $scopesGroup2 = [ $scopes[1] ];
+
+ // case: a period is in draft state
+ $period = $this->buildPeriod($person, $scopesCanSee, $user, false);
+
+ yield [$user, $person, $scopesCanSee, [], [$period], "a user can see his period during draft state"];
+
+ // another user is not allowed to see this period, because it is in DRAFT state
+ yield [$anotherUser, $person, $scopesCanSee, [], [], "another user is not allowed to see the period of someone else in draft state"];
+
+ // the period is confirmed
+ $period = $this->buildPeriod($anotherPerson, $scopesCanSee, $user, true);
+
+ // the other user can now see it
+ yield [$user, $anotherPerson, $scopesCanSee, [], [$period], "a user see his period when confirmed"];
+ yield [$anotherUser, $anotherPerson, $scopesCanSee, [], [$period], "another user with required scopes is allowed to see the period when not draft"];
+ yield [$anotherUser, $anotherPerson, $scopesGroup2, [], [], "another user without the required scopes is not allowed to see the period when not draft"];
+
+ // this period will be confidential
+ $period = $this->buildPeriod($person2, $scopesCanSee, $user, true);
+ $period->setConfidential(true)->setUser($user, true);
+
+ yield [$user, $person2, $scopesCanSee, [], [$period], "a user see his period when confirmed and confidential with required scopes"];
+ yield [$user, $person2, $scopesGroup2, [], [$period], "a user see his period when confirmed and confidential without required scopes"];
+ yield [$anotherUser, $person2, $scopesCanSee, [], [], "a user don't see a confidential period, even if he has required scopes"];
+ yield [$anotherUser, $person2, $scopesCanSee, $scopesCanSee, [$period], "a user see the period when confirmed and confidential if he has required scope to see the period"];
+
+ // period draft with creator = null
+ $period = $this->buildPeriod($person3, $scopesCanSee, null, false);
+ yield [$user, $person3, $scopesCanSee, [], [$period], "a user see a period when draft if no creator on the period"];
+ $this->entityManager->flush();
+ }
+
+ /**
+ * @param Person $person
+ * @param array $scopes
+ * @return AccompanyingPeriod
+ */
+ private function buildPeriod(Person $person, array $scopes, User|null $creator, bool $confirm): AccompanyingPeriod
+ {
+ $period = new AccompanyingPeriod();
+ $period->addPerson($person);
+ if (null !== $creator) {
+ $period->setCreatedBy($creator);
+ }
+
+ foreach ($scopes as $scope) {
+ $period->addScope($scope);
+ }
+
+ $this->entityManager->persist($period);
+ self::$periodsIdsToDelete[] = $period->getId();
+
+ if ($confirm) {
+ $workflow = $this->registry->get($period, 'accompanying_period_lifecycle');
+ $workflow->apply($period, 'confirm');
+ }
+
+ return $period;
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml b/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml
index c299bbaaa..c5a2dba68 100644
--- a/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml
+++ b/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml
@@ -135,6 +135,14 @@ services:
tags:
- { name: chill.export_filter, alias: accompanyingcourse_user_working_on_filter }
+ Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\JobWorkingOnCourseFilter:
+ tags:
+ - { name: chill.export_filter, alias: accompanyingcourse_job_working_on_filter }
+
+ Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\ScopeWorkingOnCourseFilter:
+ tags:
+ - { name: chill.export_filter, alias: accompanyingcourse_scope_working_on_filter }
+
Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\HavingAnAccompanyingPeriodInfoWithinDatesFilter:
tags:
- { name: chill.export_filter, alias: accompanyingcourse_info_within_filter }
@@ -231,3 +239,15 @@ services:
Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\CreatorJobAggregator:
tags:
- { name: chill.export_aggregator, alias: accompanyingcourse_creator_job_aggregator }
+
+ Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\UserWorkingOnCourseAggregator:
+ tags:
+ - { name: chill.export_aggregator, alias: accompanyingcourse_user_working_on_course_aggregator }
+
+ Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\JobWorkingOnCourseAggregator:
+ tags:
+ - { name: chill.export_aggregator, alias: accompanyingcourse_job_working_on_course_aggregator }
+
+ Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\ScopeWorkingOnCourseAggregator:
+ tags:
+ - { name: chill.export_aggregator, alias: accompanyingcourse_scope_working_on_course_aggregator }
diff --git a/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml b/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml
index bf372ea06..43f5556cf 100644
--- a/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml
+++ b/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml
@@ -4,35 +4,27 @@ services:
autowire: true
## Indicators
- chill.person.export.count_person:
- class: Chill\PersonBundle\Export\Export\CountPerson
- autowire: true
- autoconfigure: true
+ Chill\PersonBundle\Export\Export\CountPerson:
tags:
- { name: chill.export, alias: count_person }
- chill.person.export.count_person_with_accompanying_course:
- class: Chill\PersonBundle\Export\Export\CountPersonWithAccompanyingCourse
- autowire: true
- autoconfigure: true
+ Chill\PersonBundle\Export\Export\CountPersonWithAccompanyingCourse:
tags:
- { name: chill.export, alias: count_person_with_accompanying_course }
Chill\PersonBundle\Export\Export\ListPerson:
- autowire: true
- autoconfigure: true
tags:
- { name: chill.export, alias: list_person }
- Chill\PersonBundle\Export\Export\ListPersonWithAccompanyingPeriod:
- autowire: true
- autoconfigure: true
+ Chill\PersonBundle\Export\Export\ListPersonHavingAccompanyingPeriod:
tags:
- { name: chill.export, alias: list_person_with_acp }
+ Chill\PersonBundle\Export\Export\ListPersonWithAccompanyingPeriodDetails:
+ tags:
+ - { name: chill.export, alias: list_person_with_acp_details }
+
Chill\PersonBundle\Export\Export\ListAccompanyingPeriod:
- autowire: true
- autoconfigure: true
tags:
- { name: chill.export, alias: list_acp }
@@ -177,3 +169,8 @@ services:
tags:
- { name: chill.export_aggregator, alias: person_household_compo_aggregator }
+ Chill\PersonBundle\Export\Aggregator\PersonAggregators\CenterAggregator:
+ tags:
+ - { name: chill.export_aggregator, alias: person_center_aggregator }
+
+
diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml
index f783c403e..892bb0dfd 100644
--- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml
+++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml
@@ -329,8 +329,9 @@ CHILL_PERSON_ACCOMPANYING_PERIOD_CREATE: Créer un parcours d'accompagnement
CHILL_PERSON_ACCOMPANYING_PERIOD_UPDATE: Modifier un parcours d'accompagnement
CHILL_PERSON_ACCOMPANYING_PERIOD_FULL: Voir les détails, créer, supprimer et mettre à jour un parcours d'accompagnement
CHILL_PERSON_ACCOMPANYING_COURSE_REASSIGN_BULK: Réassigner les parcours en lot
-CHILL_PERSON_ACCOMPANYING_PERIOD_SEE_DETAILS: Voir les détails d'un parcours d'accompagnement
+CHILL_PERSON_ACCOMPANYING_PERIOD_SEE_DETAILS: Voir les détails d'un parcours d'accompagnement
CHILL_PERSON_ACCOMPANYING_PERIOD_STATS: Statistiques sur les parcours d'accompagnement
+CHILL_PERSON_ACCOMPANYING_PERIOD_SEE_CONFIDENTIAL: Voir les parcours confidentiels
CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_CREATE: Créer une action d'accompagnement
CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_DELETE: Supprimer une action d'accompagnement
@@ -372,7 +373,7 @@ Count people participating in an accompanying course by various parameters.: Com
Exports of accompanying courses: Exports des parcours d'accompagnement
Count accompanying courses: Nombre de parcours
Count accompanying courses by various parameters: Compte le nombre de parcours en fonction de différents filtres.
-Accompanying courses participation duration and number of participations: Durée moyenne et nombre des participation des usagers aux parcours
+Accompanying courses participation duration and number of participations: Durée moyenne et nombre des participations des usagers aux parcours
Create an average of accompanying courses duration of each person participation to accompanying course, according to filters on persons, accompanying course: Crée un rapport qui comptabilise la moyenne de la durée de participation de chaque usager concerné aux parcours, avec différents filtres, notamment sur les usagers concernés.
Closingdate to apply: Date de fin à prendre en compte lorsque le parcours n'est pas clotûré
@@ -1000,6 +1001,8 @@ notification:
Notify referrer: Notifier le référent
Notify any: Notifier d'autres utilisateurs
+personId: Identifiant de l'usager
+
export:
export:
acp_stats:
@@ -1019,6 +1022,11 @@ export:
Household composition: Composition du ménage
Group course by household composition: Grouper les usagers par composition familiale
Calc date: Date de calcul de la composition du ménage
+ by_center:
+ title: Grouper les usagers par centre
+ at_date: Date de calcul du centre
+ center: Centre de l'usager
+
course:
by_referrer:
Computation date for referrer: Date à laquelle le référent était actif
@@ -1035,6 +1043,15 @@ export:
Number of actions: Nombre d'actions
by_creator_job:
Creator's job: Métier du créateur
+ by_user_working:
+ title: Grouper les parcours par intervenant
+ user: Intervenant
+ by_job_working:
+ title: Grouper les parcours par métier de l'intervenant
+ job: Métier de l'intervenant
+ by_scope_working:
+ title: Grouper les parcours par service de l'intervenant
+ scope: Service de l'intervenant
course_work:
by_current_action:
Current action ?: Action en cours ?
@@ -1084,8 +1101,20 @@ export:
end_date: Fin de la période
Only course with events between %startDate% and %endDate%: Seulement les parcours ayant reçu une intervention entre le %startDate% et le %endDate%
by_user_working:
- title: Filter les parcours par intervenant
- 'Filtered by user working on course: only %users%': 'Filtré par intervenants sur le parcours: seulement %users%'
+ title: Filter les parcours par intervenant, entre deux dates
+ 'Filtered by user working on course: only %users%, between %start_date% and %end_date%': 'Filtré par intervenants sur le parcours: seulement %users%, entre le %start_date% et le %end_date%'
+ User working after: Intervention après le
+ User working before: Intervention avant le
+ by_job_working:
+ title: Filtrer les parcours par métier de l'intervenant, entre deux dates
+ 'Filtered by job working on course: only %jobs%, between %start_date% and %end_date%': 'Filtré par métier des intervenants sur le parcours: seulement %jobs%, entre le %start_date% et le %end_date%'
+ Job working after: Intervention après le
+ Job working before: Intervention avant le
+ by_scope_working:
+ title: Filtrer les parcours par service de l'intervenant, entre deux dates
+ 'Filtered by scope working on course: only %scopes%, between %start_date% and %end_date%': 'Filtré par service des intervenants sur le parcours: seulement %scopes%, entre le %start_date% et le %end_date%'
+ Scope working after: Intervention après le
+ Scope working before: Intervention avant le
by_step:
Filter by step: Filtrer les parcours par statut du parcours
Filter by step between dates: Filtrer les parcours par statut du parcours entre deux dates
@@ -1123,13 +1152,15 @@ export:
list:
person_with_acp:
List peoples having an accompanying period: Liste des usagers ayant un parcours d'accompagnement
+ List peoples having an accompanying period with period details: Liste des usagers concernés avec détail de chaque parcours
Create a list of people having an accompaying periods, according to various filters.: Génère une liste des usagers ayant un parcours d'accompagnement, selon différents critères liés au parcours ou à l'usager
+ Create a list of people having an accompaying periods with details of period, according to various filters.: Génère une liste des usagers ayant un parcours d'accompagnement, selon différents critères liés au parcours ou à l'usager. Ajoute les détails du parcours à la liste.
acp:
List of accompanying periods: Liste des parcours d'accompagnements
Generate a list of accompanying periods, filtered on different parameters.: Génère une liste des parcours d'accompagnement, filtrée sur différents paramètres.
Date of calculation for associated elements: Date de calcul des éléments associés
The associated referree, localisation, and other elements will be valid at this date: Les éléments associés, comme la localisation, le référent et d'autres éléments seront valides à cette date
- id: Identifiant du parcours
+ acpId: Identifiant du parcours
openingDate: Date d'ouverture du parcours
closingDate: Date de fermeture du parcours
closingMotive: Motif de cloture
@@ -1137,14 +1168,14 @@ export:
confidential: Confidentiel
emergency: Urgent
intensity: Intensité
- createdAt: Créé le
- updatedAt: Dernière mise à jour le
+ acpCreatedAt: Créé le
+ acpUpdatedAt: Dernière mise à jour le
acpOrigin: Origine du parcours
origin: Origine du parcours
acpClosingMotive: Motif de fermeture
acpJob: Métier du parcours
- createdBy: Créé par
- updatedBy: Dernière modification par
+ acpCreatedBy: Créé par
+ acpUpdatedBy: Dernière modification par
administrativeLocation: Location administrative
step: Etape
stepSince: Dernière modification de l'étape
@@ -1152,7 +1183,7 @@ export:
referrerSince: Référent depuis le
locationIsPerson: Parcours localisé auprès d'un usager concerné
locationIsTemp: Parcours avec une localisation temporaire
- acpLocationPersonName: Usager auprès duquel le parcours est localisé
+ locationPersonName: Usager auprès duquel le parcours est localisé
locationPersonId: Identifiant de l'usager auprès duquel le parcours est localisé
acpaddress_fieldscountry: Pays de l'adresse
isRequestorPerson: Le demandeur est-il un usager ?
diff --git a/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php b/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php
index 22e49704f..3523c7a58 100644
--- a/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php
+++ b/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php
@@ -26,6 +26,7 @@ use Chill\TaskBundle\Event\TaskEvent;
use Chill\TaskBundle\Event\UI\UIEvent;
use Chill\TaskBundle\Form\SingleTaskType;
use Chill\TaskBundle\Repository\SingleTaskAclAwareRepositoryInterface;
+use Chill\TaskBundle\Repository\SingleTaskStateRepository;
use Chill\TaskBundle\Security\Authorization\TaskVoter;
use LogicException;
use Psr\Log\LoggerInterface;
@@ -71,7 +72,8 @@ final class SingleTaskController extends AbstractController
EventDispatcherInterface $eventDispatcher,
TimelineBuilder $timelineBuilder,
LoggerInterface $logger,
- FilterOrderHelperFactoryInterface $filterOrderHelperFactory
+ FilterOrderHelperFactoryInterface $filterOrderHelperFactory,
+ private SingleTaskStateRepository $singleTaskStateRepository
) {
$this->eventDispatcher = $eventDispatcher;
$this->timelineBuilder = $timelineBuilder;
@@ -452,7 +454,7 @@ final class SingleTaskController extends AbstractController
{
$this->denyAccessUnlessGranted('ROLE_USER');
- $filterOrder = $this->buildFilterOrder();
+ $filterOrder = $this->buildFilterOrder(false);
$flags = array_merge(
$filterOrder->getCheckboxData('status'),
array_map(static fn ($i) => 'state_' . $i, $filterOrder->getCheckboxData('states'))
@@ -667,7 +669,7 @@ final class SingleTaskController extends AbstractController
return $form;
}
- private function buildFilterOrder(): FilterOrderHelper
+ private function buildFilterOrder($includeFilterByUser = true): FilterOrderHelper
{
$statuses = ['no-alert', 'warning', 'alert'];
$statusTrans = [
@@ -675,18 +677,22 @@ final class SingleTaskController extends AbstractController
'Tasks near deadline',
'Tasks over deadline',
];
- $states = [
- // todo: get a list of possible states dynamically
- 'new', 'in_progress', 'closed', 'canceled',
- ];
+ $states = $this->singleTaskStateRepository->findAllExistingStates();
+ $checked = array_values(array_filter($states, fn (string $state) => !in_array($state, ['closed', 'canceled', 'validated'], true)));
- return $this->filterOrderHelperFactory
+ $filterBuilder = $this->filterOrderHelperFactory
->create(self::class)
->addSearchBox()
->addCheckbox('status', $statuses, $statuses, $statusTrans)
- ->addCheckbox('states', $states, ['new', 'in_progress'])
- ->addUserPicker('userPicker', 'Filter by user', ['multiple' => true, 'required' => false])
- ->build();
+ ->addCheckbox('states', $states, $checked)
+ ;
+
+ if ($includeFilterByUser) {
+ $filterBuilder
+ ->addUserPicker('userPicker', 'Filter by user', ['multiple' => true, 'required' => false]);
+ }
+
+ return $filterBuilder->build();
}
/**
diff --git a/src/Bundle/ChillTaskBundle/Repository/SingleTaskStateRepository.php b/src/Bundle/ChillTaskBundle/Repository/SingleTaskStateRepository.php
new file mode 100644
index 000000000..2d64c69a0
--- /dev/null
+++ b/src/Bundle/ChillTaskBundle/Repository/SingleTaskStateRepository.php
@@ -0,0 +1,47 @@
+
+ * @throws Exception
+ */
+ public function findAllExistingStates(): array
+ {
+ $states = [];
+
+ foreach ($this->connection->fetchAllNumeric(self::FIND_ALL_STATES) as $row) {
+ if ('' !== $row[0] && null !== $row[0]) {
+ $states[] = $row[0];
+ }
+ }
+
+ return $states;
+ }
+
+}
diff --git a/src/Bundle/ChillTaskBundle/config/services/repositories.yaml b/src/Bundle/ChillTaskBundle/config/services/repositories.yaml
index 7bee5abd0..9681e5d5c 100644
--- a/src/Bundle/ChillTaskBundle/config/services/repositories.yaml
+++ b/src/Bundle/ChillTaskBundle/config/services/repositories.yaml
@@ -1,4 +1,8 @@
services:
+ _defaults:
+ autowire: true
+ autoconfigure: true
+
chill_task.single_task_repository:
class: Chill\TaskBundle\Repository\SingleTaskRepository
factory: ['@doctrine.orm.entity_manager', getRepository]
@@ -10,8 +14,8 @@ services:
- "@chill.main.security.authorization.helper"
Chill\TaskBundle\Repository\SingleTaskRepository: '@chill_task.single_task_repository'
- Chill\TaskBundle\Repository\SingleTaskAclAwareRepository:
- autowire: true
- autoconfigure: true
+ Chill\TaskBundle\Repository\SingleTaskAclAwareRepository: ~
Chill\TaskBundle\Repository\SingleTaskAclAwareRepositoryInterface: '@Chill\TaskBundle\Repository\SingleTaskAclAwareRepository'
+
+ Chill\TaskBundle\Repository\SingleTaskStateRepository: ~
diff --git a/src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php b/src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php
index 86c0fa9db..1d4e12074 100644
--- a/src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php
+++ b/src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php
@@ -71,12 +71,9 @@ class ThirdPartyApiSearch implements SearchApiInterface
->setSelectKey('tparty')
->setSelectJsonbMetadata("jsonb_build_object('id', tparty.id)")
->setFromClause('chill_3party.third_party AS tparty
- LEFT JOIN chill_main_address cma ON cma.id = tparty.address_id
- LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id
LEFT JOIN chill_3party.third_party AS parent ON tparty.parent_id = parent.id
- LEFT JOIN chill_main_address cma_p ON parent.address_id = cma_p.id
- LEFT JOIN chill_main_postal_code cmpc_p ON cma_p.postcode_id = cmpc.id')
- ->andWhereClause('tparty.active IS TRUE');
+ LEFT JOIN chill_main_address cma ON cma.id = COALESCE(parent.address_id, tparty.address_id)
+ LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id');
$strs = explode(' ', $pattern);
$wheres = [];
@@ -102,9 +99,8 @@ class ThirdPartyApiSearch implements SearchApiInterface
(parent.canonicalized LIKE '%s' || LOWER(UNACCENT(?)) || '%')::int
) + " .
// take postcode label into account, but lower than the canonicalized field
- "COALESCE((LOWER(UNACCENT(cmpc.label)) LIKE '%' || LOWER(UNACCENT(?)) || '%')::int * 0.3, 0) + " .
- "COALESCE((LOWER(UNACCENT(cmpc_p.label)) LIKE '%' || LOWER(UNACCENT(?)) || '%')::int * 0.3, 0)";
- $pertinenceArgs[] = [$str, $str, $str, $str, $str, $str];
+ "COALESCE((LOWER(UNACCENT(cmpc.label)) LIKE '%' || LOWER(UNACCENT(?)) || '%')::int * 0.3, 0)";
+ $pertinenceArgs[] = [$str, $str, $str, $str, $str];
}
}