Compare commits

...

172 Commits

Author SHA1 Message Date
801f799e45 Merge branch '125-list-person-on-course' into testing 2023-07-11 17:04:17 +02:00
98b8f3dcff Merge branch '118-design-filterOrder' into testing 2023-07-11 17:03:23 +02:00
a333a0312a Merge branch 'issue719_filter_activities_version_2' into testing 2023-07-11 17:02:44 +02:00
0288fd22cf Merge branch '128-export-group-activity-by-localisation' into testing 2023-07-11 17:02:33 +02:00
1dd4398c43 Merge branch '129-export-filter-course-having-activity' into testing 2023-07-11 17:02:19 +02:00
2882038efc [export] Add a filter "filter course having an activity between two dates" 2023-07-11 16:00:40 +02:00
6065680e1e Feature: [export] allow to group activities by location 2023-07-11 15:01:32 +02:00
88114e3ba6 Fixed: [filterOrder] refactor active filter helper to a dedicated class and fix loading of multiple entity choices 2023-07-11 14:17:02 +02:00
bf93c1ddb2 fix label color in active filters pills 2023-07-11 14:06:10 +02:00
0d365e16e5 add missing translations 2023-07-10 15:59:33 +02:00
802ff20b5c Merge branch '118-design-filterOrder' of gitlab.com:Chill-Projet/chill-bundles into 118-design-filterOrder 2023-07-10 15:55:18 +02:00
cdfe201574 render active filters like pills 2023-07-10 15:55:05 +02:00
43419f9f15 [filterOrder] fix error in method getActiveFilters when dealing with entityChoice with incorrect number of translation 2023-07-10 15:39:00 +02:00
39896ea6e2 [FilterOrder] add a method to get all the active filters 2023-07-10 15:26:54 +02:00
ca62c3fd0b Merge remote-tracking branch 'origin/master' into 118-design-filterOrder 2023-07-10 11:51:39 +02:00
b3b84c5dc0 Merge branch 'issue719_filter_activities_version_2' into 118-design-filterOrder 2023-07-10 11:49:24 +02:00
6bdb3e9695 fix typo which prevent to apply a filter on activity types 2023-07-07 21:49:36 +02:00
20e64e8768 test filterOrder in an accordion 2023-07-07 15:41:29 +02:00
4f4b3dbb44 Merge branch 'issue719_filter_activities_version_2' into testing 2023-07-07 13:29:28 +02:00
1c3e6e0dba Merge remote-tracking branch 'origin/118-design-filterOrder' into testing 2023-07-07 13:29:11 +02:00
e7ca81e057 Merge branch 'master' into testing 2023-07-07 13:27:02 +02:00
197d69ef4a release v2.4.0 2023-07-07 13:21:22 +02:00
9423f4d055 Merge branch '124-sync-user-absence-ms-graph' into 'master'
Feature: sync user absence with microsoft graph api

Closes #124

See merge request Chill-Projet/chill-bundles!571
2023-07-07 11:07:08 +00:00
99d6e9e6b8 Merge branch 'accompanying_period_voter' into 'master'
rights for reassigning and accessing confidential parcours

Closes #99 and #121

See merge request Chill-Projet/chill-bundles!483
2023-07-07 11:06:37 +00:00
63f9bd5548 [export] Add ordering by person''s lastname or course opening date in list which concerns accompanying course or people 2023-07-07 12:42:32 +02:00
c8146ded17 Feature: add a list for people with their associated accompanying course 2023-07-07 12:36:24 +02:00
17d2b795b4 update changelog 2023-07-07 11:38:00 +02:00
7f30742fc3 Rename ListPersonWithAccompanyingPeriod to ListPersonHavingAccompanyingPeriod 2023-07-07 09:36:39 +02:00
56d9072abe change id, to avoid collision between ListPersonHelper and ListAccompanyingPeriodHelper 2023-07-07 09:33:03 +02:00
7ccff61c25 Refactor ListAccompanyingPeriod to use a helper for most of the work 2023-07-07 09:17:36 +02:00
8929f4b8a3 Merge branch '116-course-index-link-action-to-view-if-closed' into 'master'
Fixed: rights on the action list in accompanying period main's page

Closes #116

See merge request Chill-Projet/chill-bundles!566
2023-07-06 20:05:25 +00:00
43b7139488 One more changie [ci-skip] 2023-07-06 22:01:45 +02:00
d3251075e9 fix loading of kernel if ms calendar is not created 2023-07-06 21:55:29 +02:00
93a598b549 improve php applying rector rules 2023-07-06 21:45:29 +02:00
9b6e6ec20f add a changie 2023-07-06 21:34:43 +02:00
77d4b13c1b Sync user absence / presence within MapAndSubscribeUserCalendarCommand 2023-07-06 21:33:01 +02:00
2861945a52 Syncer for user absence, from the msgraph reader 2023-07-06 17:29:10 +02:00
5b42b85b50 Read absence from MS graph api 2023-07-06 16:58:24 +02:00
e40b1b9853 Merge branch 'master' into accompanying_period_voter 2023-07-05 22:39:22 +02:00
c19232de35 DX: fix phpstan issues 2023-07-05 22:37:51 +02:00
c95dc23c51 Merge remote-tracking branch 'origin/master' into accompanying_period_voter 2023-07-05 22:27:47 +02:00
c04fd66163 do not show filter on job or activity type if less than 2 possibilities 2023-07-05 22:20:27 +02:00
0361743ae0 Merge branch '113-add-missing-filters' into 'master'
Add missing aggregators and filters

Closes #113

See merge request Chill-Projet/chill-bundles!567
2023-07-05 20:08:20 +00:00
af4e7f1226 Add changie entry 2023-07-05 22:06:36 +02:00
ff1629cbb7 Separate role "see confidential course" from "reassign bulk" 2023-07-05 22:06:21 +02:00
779eb812b0 Add new role to see confidential right on method AccompanyingPeriodACLAwareRepositoryInterface::findByUserAndPostalCodeOpenedAccompanyingPeriod 2023-07-05 21:56:50 +02:00
a990591e0c handle right to see confidential course on regulation list 2023-07-05 16:23:14 +02:00
145c1df313 cleaning 2023-07-05 09:43:13 +02:00
7f9738975c UX: improve FilterOrder box design 2023-07-04 17:53:08 +02:00
a56370d851 DX: fix phpstan issues with more strict type hinting in AuthorizationHelperInterface 2023-07-04 17:02:36 +02:00
3e63b4abf3 UX: improve FilterOrder box design 2023-07-04 16:42:56 +02:00
dd344aed52 Implements right "see confidential course" on method findByPerson
Add unit tests for that
2023-07-04 15:59:39 +02:00
1485d1ce7a Merge branch 'master' into 118-design-filterOrder 2023-07-04 14:56:00 +02:00
a7dbdc2b9d FEATURE [voter][confidential] voter adapted. repository changes left to do 2023-07-03 13:35:34 +02:00
b3d993165d FEATURE: [confidential][voter] bulk assign right should also give right to access confidential parcours 2023-07-03 13:35:34 +02:00
9ccc57bbcb FEATURE [config][voter] config set for relation between bulk_assign and see_confidential 2023-07-03 13:35:34 +02:00
cc0e832cc9 FEATURE [voter][confidential] added right to see confidential periods 2023-07-03 13:35:33 +02:00
c8b62d990a fixes for exports and list 2023-06-30 17:12:09 +02:00
b7df62d4f5 [export] use a rolling date on age aggregator (Person)
This query allow to detects the saved export which won't work any more:

```sql
select
s.id, user_id, description, title, u.label
from chill_main_saved_export s join users u on u.id = s.user_id
WHERE
options->'export'->'export'->'aggregators'->'person_age_aggregator'->'enabled' is not null;
```
2023-06-29 23:15:15 +02:00
5a395b160f [export] add aggregator and filter related to AccompanyingPeriodInfo +
Center aggregator for Person

see #113
2023-06-29 17:53:47 +02:00
393e59e22b DX: Rolling date: allow to receive a null parameter (RollingDate)
When receiving a null parameter (a null rolling date), it will return
null
2023-06-29 17:53:47 +02:00
4a5ac170ba [export] add dates for filter "user working on course" 2023-06-29 17:53:47 +02:00
c019fffbe7 fix cs 2023-06-29 17:53:15 +02:00
31745bc252 [export] order center alphabetically when generating an export 2023-06-29 17:53:15 +02:00
56940d830c fix typos 2023-06-29 17:45:50 +02:00
347eda05df Fix: force the consistency of location in accompanying period
- internally, the entity remove the addressLocation when the
  personLocation is set, and vice-versa;
- this commit add a migration which may solve the case when both case
  happens (priority to personLocation + keep the history)
- add a constraint on the database to avoid such situation
2023-06-29 12:44:28 +02:00
90e8687799 Fixed: [export] rename label on CurrentActionFilter 2023-06-28 17:01:42 +02:00
9687debb57 Merge remote-tracking branch 'origin/master' into testing 2023-06-27 21:11:09 +02:00
da50fbc1fb update for release v2.3.0 2023-06-27 18:46:04 +02:00
5bbc50976e Merge branch '103-document-page' into 'master'
Liste unifiée des documents

Closes #103

See merge request Chill-Projet/chill-bundles!545
2023-06-27 16:35:29 +00:00
01dee54fab Merge branch '110-export-editable' into 'master'
Feature: make saved export editables ( OP#203 )

Closes #110

See merge request Chill-Projet/chill-bundles!554
2023-06-27 16:32:37 +00:00
abe020f116 Merge remote-tracking branch 'origin/master' into 103-document-page 2023-06-27 18:31:14 +02:00
4632c18d93 restore feature: generate a document from period 2023-06-27 18:26:41 +02:00
7a1feaa8cb show documents from person in list of document from course 2023-06-27 18:20:41 +02:00
687ff63ce7 Rename label of filter in French: "parcours actif" => "parcours ouvert", and "filtrer les parcours ouverts" => "Filtrer les parcours dont la date d''ouverture" 2023-06-27 16:00:05 +02:00
a7c3089736 Feature: avoid duplicates for the same period in acc period user history 2023-06-27 15:47:48 +02:00
90be68002a add possibility to add long text in changelog 2023-06-27 15:47:48 +02:00
a93051d157 [export] set the default date for accompanying period list 2023-06-27 15:14:19 +02:00
f19b939bd4 Fixed: rights on the action list in accompanying period main's page
Add is_granted check on the action:

- if update action is allowed, open in update mode;
- if see action is allowed, open in see mode;
- fallback to an inactive link (should not happens)
2023-06-27 11:04:22 +02:00
9f0fdb031a Merge remote-tracking branch 'origin/master' into 110-export-editable 2023-06-26 11:06:52 +02:00
0e9597bf77 fix handling of DirectExportInterface 2023-06-25 15:47:30 +02:00
769504c497 Merge branch 'issue719_filter_activities_version_2' into testing 2023-06-23 13:10:17 +02:00
811364e139 Merge branch '110-export-editable' into testing 2023-06-23 13:09:43 +02:00
9978b6a6e4 Fix: fix the loading of pickCenterType when no regroupments exists and
defaults of SocialWorkTypeFilter
2023-06-23 13:09:19 +02:00
0e5f1b4ab9 Feature: [activity list] add pagination 2023-06-23 12:44:54 +02:00
f7c11d3567 Feature: Add filters on activity list 2023-06-23 12:27:18 +02:00
51544cfc48 DX: improve typing of a property in UserJob 2023-06-23 12:24:40 +02:00
659dff3d2c DX: Add features to filterOrder
Allow to add single checkboxes and entitychoices to filter order
2023-06-23 12:24:32 +02:00
deffc5e4db Merge branch '103-document-page' into testing 2023-06-18 22:03:51 +02:00
40ecaab5b4 Merge branch '110-export-editable' into testing 2023-06-18 22:03:36 +02:00
f7be53f790 Merge remote-tracking branch 'origin/master' into testing 2023-06-18 22:02:14 +02:00
9073f118db [rector] add form default data rule: handle case when there are method call on builder without add 2023-06-18 21:37:52 +02:00
21f11fb034 execute rector run on filters and aggregators 2023-06-18 21:36:40 +02:00
50de389bc7 Revert "apply fixes for list"
This reverts commit 938027cc
2023-06-18 11:58:42 +02:00
960acb8c0a Revert "apply rector rules for exports"
This reverts commit 3adf3625
2023-06-18 11:57:26 +02:00
c52ba06ea0 adapt rector rules for chained builder->add 2023-06-16 15:26:49 +02:00
3fb97c3945 DX: comment the examples in rule definition
As this is genearte error in both phpunits and php-cs-fixer
2023-06-16 14:07:49 +02:00
5495b1cb44 fix condition 2023-06-13 15:33:10 +02:00
24049b9dfc remove unused file 2023-06-13 15:33:03 +02:00
34a333f6a3 Merge branch 'master' into 103-document-page 2023-06-13 15:27:58 +02:00
29140d9374 add changelog entry 2023-06-13 15:16:03 +02:00
5cbdea29e9 Merge remote-tracking branch 'origin/master' into 103-document-page 2023-06-13 15:14:17 +02:00
909d2dfb60 layout for rendering list 2023-06-13 15:00:16 +02:00
1f1ebb6adb fix mismatch for key in different providers 2023-06-13 11:30:00 +02:00
4456fb3749 Fixing generic doc providers from calendar + fix cs 2023-06-13 11:01:40 +02:00
727e9d0f74 WIP: fix loading of activity document 2023-06-12 17:58:27 +02:00
c2a734b30b Merge branch '110-adapt-exports' into '110-export-editable'
Apply rector rules to make saved exports editable

See merge request Chill-Projet/chill-bundles!555
2023-06-12 15:54:55 +00:00
68c850026b Merge branch '103b-document-page-julie' into '103-document-page'
103b document page julie

See merge request Chill-Projet/chill-bundles!551
2023-06-08 10:01:52 +00:00
aa6479fbf9 doc for rector rule [ci-skip] 2023-06-06 11:14:14 +02:00
05822e7e4a add changelog 2023-06-06 10:52:12 +02:00
b4614974c0 allow to edit existing saved export with new export options 2023-06-06 10:49:48 +02:00
c3ac382711 link to update aggregator and filters from saved export and then execute 2023-06-06 10:49:34 +02:00
ad82685c02 allow to edit existing saved export with new export options 2023-06-06 10:49:00 +02:00
7e8dbbe873 link to update aggregator and filters from saved export and then execute 2023-06-06 09:57:06 +02:00
12fdfd81c4 remove empty data which sparkle a bug in RadioListMapper 2023-06-06 09:37:43 +02:00
cf576dca7b fix missing choices in getFormDefaultData with automatic generation 2023-06-05 18:16:32 +02:00
7fab411b96 remove stubs 2023-06-05 18:10:55 +02:00
88d363fc0c apply rector rules on list interfaces 2023-06-05 18:05:23 +02:00
a28740c46c change chill rule namespace 2023-06-05 18:03:54 +02:00
1a03718014 fix cs 2023-06-05 18:02:49 +02:00
1e8fec5cbd move rector rules to a shareable namespace 2023-06-05 18:02:41 +02:00
fb1b28407a publish namespace to every app using this package 2023-06-05 18:02:31 +02:00
cc30c81fd7 change chill rule namespace 2023-06-05 17:54:25 +02:00
938027cc1e apply fixes for list 2023-06-05 17:54:00 +02:00
db14221729 fix cs 2023-06-05 17:42:37 +02:00
f10c50231f apply rector rules on list interfaces 2023-06-05 17:40:44 +02:00
73bc95306e move rector rules to a shareable namespace 2023-06-05 17:29:37 +02:00
933e9f75b3 publish namespace to every app using this package 2023-06-05 17:21:43 +02:00
3adf3625dc apply rector rules for exports 2023-06-05 17:21:21 +02:00
ea77adc640 [edit export] add required method on export's interface
The method "getFormDefaultData" is applyied on every interface which
will use it:

- ExportInterface
- AggregatorInterface
- DirectExportInterface
- FilterInterface

The method buildForm is moved to those interfaces.

[ci-skip]
2023-06-05 17:09:58 +02:00
d5ee158caa [export][rector] rector rules to add get form default data on export, aggregator, direct export 2023-06-05 16:41:55 +02:00
02afcb30d4 [export][rector] first rector rule to add new method to filters 2023-06-04 01:11:38 +02:00
cb0a6bbd21 Regenerate data from saved export on formatter test
[ci-skip]
2023-06-04 01:10:50 +02:00
fb0afc7e0a [WIP] get default data from saved exports for center and export steps 2023-06-02 15:32:38 +02:00
d1e1b1c4ce [export form] decouple data from PickCenter form 2023-06-01 14:07:31 +02:00
2aeb72811a [activity][docs] attempt to implement generic doc for activity documents in person context 2023-06-01 13:31:35 +02:00
7eb4fb4e56 FEATURE [calendar][docs] fix query to display rendez-vous documents from person and parcours contexts 2023-06-01 13:09:51 +02:00
5dc1cbce48 FEATURE [activity][docs] generic doc for activity documents in person context 2023-06-01 11:01:29 +02:00
59e1e02b92 FEATURE [calendar][docs] try to implement showing calendar docs from parcours context in person context 2023-06-01 10:45:55 +02:00
5196d26a3e FEATURE [translations] add translations 2023-06-01 10:44:14 +02:00
9a3fcf081e FEATURE [personCalendar][genericDoc] implement genericDoc for calendar objects linked to a person 2023-05-31 17:10:38 +02:00
ef04a04056 FEATURE [genericDoc][calendar] minor changes to template and provider 2023-05-31 16:54:21 +02:00
ba55fa349b FEATURE [genericDoc][activity] finalize implementation 2023-05-31 16:53:38 +02:00
eea5cedc5f Merge branch '103b-document-page-julie' of gitlab.com:Chill-Projet/chill-bundles into 103b-document-page-julie 2023-05-31 14:38:35 +02:00
c07e26785e WIP [genericDoc][activity] add repository method to get activity linked to storedObject 2023-05-31 14:38:05 +02:00
4155af6686 FEATURE [genericDoc][calendar] use metadatas 2023-05-31 14:38:05 +02:00
9eb9a9a214 WIP [genericDoc][activity] implementing generic doc for activities 2023-05-31 14:38:05 +02:00
47a3e30ec5 FEATURE [genericDoc] generic doc interface implemented for rendez-vous 2023-05-31 14:38:05 +02:00
20489813f0 FIX [route] adjust to using new route name in redirect 2023-05-31 14:38:05 +02:00
cb718a80de Add accompanying period work evaluation documents to the list of documents for person 2023-05-31 12:23:34 +02:00
2b57807565 show generic doc accompanying course document in person generic doc list 2023-05-30 22:14:13 +02:00
da36c59616 refactor: rename class providing AccompanyingCourseDocument Generic doc 2023-05-30 21:52:36 +02:00
40ddd1f1ee Add license 2023-05-30 21:25:33 +02:00
e9fdabf931 Remove old list for person document 2023-05-30 21:24:53 +02:00
40af1e64ac [generic doc] listing generic doc for person 2023-05-30 21:24:04 +02:00
c245ffe559 WIP [genericDoc][activity] add repository method to get activity linked to storedObject 2023-05-30 18:14:32 +02:00
bd074ebade FEATURE [genericDoc][calendar] use metadatas 2023-05-30 17:50:32 +02:00
d09e5d33db WIP [genericDoc][activity] implementing generic doc for activities 2023-05-30 16:59:16 +02:00
101cca8662 FEATURE [genericDoc] generic doc interface implemented for rendez-vous 2023-05-30 16:58:36 +02:00
90a5a735aa FIX [route] adjust to using new route name in redirect 2023-05-30 14:38:16 +02:00
eb107f5a15 add filter for generic doc + fix issues in filter 2023-05-30 12:46:05 +02:00
a3d3588b75 DX: fix json_decode signature 2023-05-26 22:01:02 +02:00
08874d734e [generic doc] add doc provider and renderer for evaluation document 2023-05-26 21:58:52 +02:00
2b5d007fda Remove old doc index page, replace by the generic doc index page 2023-05-25 11:09:26 +02:00
e550817ded Render for generic doc 2023-05-25 09:55:46 +02:00
8dbe2d6ec2 GenericDoc: add provider for AccompanyingCourseDocument, without filtering 2023-05-24 17:09:29 +02:00
afcd6e0605 bootstrap generic doc manager and associated services 2023-05-24 17:05:56 +02:00
6fb01b19ec Merge branch 'feature/change-parcours-status' into testing 2023-04-28 15:55:51 +02:00
b8ecff4f08 Merge branch 'exports/filters-on-work-date' into testing 2023-04-28 14:38:36 +02:00
4c340dd086 Merge branch 'feature/change-parcours-status' into testing 2023-04-28 14:38:28 +02:00
8f1955c536 Merge branch 'master' into testing 2023-04-28 14:38:08 +02:00
c9c15cdd56 Merge remote-tracking branch 'origin/testing' into testing 2023-04-28 14:38:00 +02:00
363 changed files with 10932 additions and 1533 deletions

View File

@@ -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"

View File

@@ -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: ""

View File

@@ -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"

View File

@@ -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"

42
.changes/v2.3.0.md Normal file
View File

@@ -0,0 +1,42 @@
## 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.
* ([#103](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/103)) Get an unified list of document in person and accompanying period context
* [export] Set the default date of calculation of the accompanying period's list as "today"
* Force accompanying period user history to be unique for the same period and stardate/enddate [:warning: may encounter migration issue]
If some issue is encountered during migration, use this SQL to find the line which are in conflict, examine the problem and delete some of the concerning line
*
```sql
-- to see the line which are in conflict with another one
SELECT o.*
FROM chill_person_accompanying_period_user_history o
JOIN chill_person_accompanying_period_user_history c ON o.id < c.id AND o.accompanyingperiod_id = c.accompanyingperiod_id
WHERE tsrange(o.startdate, o.enddate, '[)') && tsrange(c.startdate, c.enddate, '[)')
ORDER BY accompanyingperiod_id;
-- to examine line in conflict for a given accompanyingperiod_id (given by the previous query)
SELECT * FROM chill_person_accompanying_period_user_history WHERE accompanyingperiod_id = IIIIDDDD order by startdate, enddate;
```
* Rename label of filter in French: "parcours actif" => "parcours ouvert", and "filtrer les parcours ouverts" => "Filtrer les parcours dont la date d'ouverture"
### Traduction francophone des principaux changements
* Les exports enregistrés sont éditables par l'utilisateur;
* L'onglet "Document" dans les parcours et les dossiers d'usager affiche désormais les documents ajoutés à différents endroits.
Pour les parcours, il s'agit de:
- documents ajoutés directement dans le parcours;
- documents des échanges;
- documents des rendez-vous;
- documents des évaluations;
- documents directement ajoutés dans le dossier des usagers concernés par le parcours;
Pour les usagers, il s'agit de:
- documents des échanges;
- documents des parcours;
- documents des rendez-vous;
- documents des actions, des échanges, des rendez-vous, des évaluations ajoutés dans les parcours.
* Dans la liste des parcours, la date de calcul des éléments associés est "aujourd'hui" par défaut.
* Dans les exports, renommage des libellés des filtres: "parcours actif" => "parcours ouvert", et "filtrer les parcours ouverts" => "Filtrer les parcours dont la date d'ouverture"

36
.changes/v2.4.0.md Normal file
View File

@@ -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

View File

@@ -5,8 +5,11 @@ changelogPath: CHANGELOG.md
versionExt: md
versionFormat: '## {{.Version}} - {{.Time.Format "2006-01-02"}}'
kindFormat: '### {{.Kind}}'
# Note: it is possible to add a `.custom.Long` text manually into the yaml file produced by `changie new`. This will add a long description.
changeFormat: >-
* {{ if not (eq .Custom.Issue "") }}([#{{ .Custom.Issue }}](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/{{ .Custom.Issue }})) {{ end }}{{.Body}}
* {{ if not (eq .Custom.Issue "") }}([#{{ .Custom.Issue }}](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/{{ .Custom.Issue }})) {{ end }}{{.Body}} {{ if not (eq .Custom.Long "") }}
{{ .Custom.Long }}{{ end }}
custom:
- key: Issue
label: Issue number (on chill-bundles repository) (optional)
@@ -27,6 +30,8 @@ kinds:
auto: patch
- label: DX
auto: patch
- label: UX
auto: patch
newlines:
afterChangelogHeader: 1
beforeChangelogVersion: 1

View File

@@ -13,6 +13,7 @@ $finder = PhpCsFixer\Finder::create();
$finder
->in(__DIR__.'/src')
->in(__DIR__.'/utils')
->append([__FILE__])
->exclude(['docs/', 'tests/app'])
->notPath('tests/app')

View File

@@ -6,6 +6,86 @@ 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.
* ([#103](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/103)) Get an unified list of document in person and accompanying period context
* [export] Set the default date of calculation of the accompanying period's list as "today"
* Force accompanying period user history to be unique for the same period and stardate/enddate [:warning: may encounter migration issue]
If some issue is encountered during migration, use this SQL to find the line which are in conflict, examine the problem and delete some of the concerning line
*
```sql
-- to see the line which are in conflict with another one
SELECT o.*
FROM chill_person_accompanying_period_user_history o
JOIN chill_person_accompanying_period_user_history c ON o.id < c.id AND o.accompanyingperiod_id = c.accompanyingperiod_id
WHERE tsrange(o.startdate, o.enddate, '[)') && tsrange(c.startdate, c.enddate, '[)')
ORDER BY accompanyingperiod_id;
-- to examine line in conflict for a given accompanyingperiod_id (given by the previous query)
SELECT * FROM chill_person_accompanying_period_user_history WHERE accompanyingperiod_id = IIIIDDDD order by startdate, enddate;
```
* Rename label of filter in French: "parcours actif" => "parcours ouvert", and "filtrer les parcours ouverts" => "Filtrer les parcours dont la date d'ouverture"
### Traduction francophone des principaux changements
* Les exports enregistrés sont éditables par l'utilisateur;
* L'onglet "Document" dans les parcours et les dossiers d'usager affiche désormais les documents ajoutés à différents endroits.
Pour les parcours, il s'agit de:
- documents ajoutés directement dans le parcours;
- documents des échanges;
- documents des rendez-vous;
- documents des évaluations;
- documents directement ajoutés dans le dossier des usagers concernés par le parcours;
Pour les usagers, il s'agit de:
- documents des échanges;
- documents des parcours;
- documents des rendez-vous;
- documents des actions, des échanges, des rendez-vous, des évaluations ajoutés dans les parcours.
* Dans la liste des parcours, la date de calcul des éléments associés est "aujourd'hui" par défaut.
* Dans les exports, renommage des libellés des filtres: "parcours actif" => "parcours ouvert", et "filtrer les parcours ouverts" => "Filtrer les parcours dont la date d'ouverture"
## v2.2.2 - 2023-06-26
### Fixed
* [Accompanying period comments]: order comments from the most recent to the oldest, in the list

View File

@@ -67,6 +67,7 @@
"fakerphp/faker": "^1.13",
"jangregor/phpstan-prophecy": "^1.0",
"nelmio/alice": "^3.8",
"nikic/php-parser": "^4.15",
"phpspec/prophecy-phpunit": "^2.0",
"phpstan/extension-installer": "^1.2",
"phpstan/phpstan": "^1.9",
@@ -103,14 +104,16 @@
"Chill\\ReportBundle\\": "src/Bundle/ChillReportBundle",
"Chill\\TaskBundle\\": "src/Bundle/ChillTaskBundle",
"Chill\\ThirdPartyBundle\\": "src/Bundle/ChillThirdPartyBundle",
"Chill\\WopiBundle\\": "src/Bundle/ChillWopiBundle/src"
"Chill\\WopiBundle\\": "src/Bundle/ChillWopiBundle/src",
"Chill\\Utils\\Rector\\": "utils/rector/src"
}
},
"autoload-dev": {
"psr-4": {
"App\\": "tests/app/src/",
"Chill\\DocGeneratorBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests",
"Chill\\WopiBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests"
"Chill\\WopiBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests",
"Chill\\Utils\\Rector\\Tests\\": "utils/rector/tests"
}
},
"config": {

View File

@@ -62,7 +62,6 @@ class BirthdateFilter implements ExportElementValidatedInterface, FilterInterfac
{
$builder->add('date_from', DateType::class, [
'label' => 'Born after this date',
'data' => new DateTime(),
'attr' => ['class' => 'datepicker'],
'widget' => 'single_text',
'format' => 'dd-MM-yyyy',
@@ -70,12 +69,15 @@ class BirthdateFilter implements ExportElementValidatedInterface, FilterInterfac
$builder->add('date_to', DateType::class, [
'label' => 'Born before this date',
'data' => new DateTime(),
'attr' => ['class' => 'datepicker'],
'widget' => 'single_text',
'format' => 'dd-MM-yyyy',
]);
}
public function getFormDefaultData(): array
{
return ['date_from' => new DateTime(), 'date_to' => new DateTime()];
}
// here, we create a simple string which will describe the action of
// the filter in the Response

View File

@@ -36,6 +36,10 @@ class CountPerson implements ExportInterface
{
// this export does not add any form
}
public function getFormDefaultData(): array
{
return [];
}
public function getAllowedFormattersTypes()
{

View File

@@ -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 |

View File

@@ -2,6 +2,7 @@ parameters:
level: 5
paths:
- src/
- utils/
tmpDir: .cache/
reportUnmatchedIgnoredErrors: false
excludePaths:

29
phpunit.rector.xml Normal file
View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
bootstrap="tests/app/vendor/autoload.php"
cacheResultFile=".cache/phpunit/test-results-rector"
executionOrder="depends,defects"
forceCoversAnnotation="true"
beStrictAboutCoversAnnotation="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTodoAnnotatedTests="true"
convertDeprecationsToExceptions="true"
failOnRisky="true"
failOnWarning="true"
verbose="true"
colors="true"
>
<testsuites>
<testsuite name="default">
<directory>utils/rector/tests</directory>
</testsuite>
</testsuites>
<coverage cacheDirectory=".cache/phpunit/code-coverage-rector"
processUncoveredFiles="true">
<include>
<directory suffix=".php">utils/rector/src</directory>
</include>
</coverage>
</phpunit>

View File

@@ -24,6 +24,9 @@ return static function (RectorConfig $rectorConfig): void {
LevelSetList::UP_TO_PHP_74
]);
// chill rules
$rectorConfig->rule(\Chill\Utils\Rector\Rector\ChillBundleAddFormDefaultDataOnExportFilterAggregatorRector::class);
// skip some path...
$rectorConfig->skip([
// make rector stuck for some files

View File

@@ -18,11 +18,17 @@ use Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface;
use Chill\ActivityBundle\Repository\ActivityRepository;
use Chill\ActivityBundle\Repository\ActivityTypeCategoryRepository;
use Chill\ActivityBundle\Repository\ActivityTypeRepositoryInterface;
use Chill\ActivityBundle\Repository\ActivityUserJobRepository;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\LocationRepository;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Privacy\PrivacyEvent;
@@ -47,68 +53,26 @@ use function array_key_exists;
final class ActivityController extends AbstractController
{
private AccompanyingPeriodRepository $accompanyingPeriodRepository;
private ActivityACLAwareRepositoryInterface $activityACLAwareRepository;
private ActivityRepository $activityRepository;
private ActivityTypeCategoryRepository $activityTypeCategoryRepository;
private ActivityTypeRepositoryInterface $activityTypeRepository;
private CenterResolverManagerInterface $centerResolver;
private EntityManagerInterface $entityManager;
private EventDispatcherInterface $eventDispatcher;
private LocationRepository $locationRepository;
private LoggerInterface $logger;
private PersonRepository $personRepository;
private SerializerInterface $serializer;
private ThirdPartyRepository $thirdPartyRepository;
private TranslatorInterface $translator;
private UserRepositoryInterface $userRepository;
public function __construct(
ActivityACLAwareRepositoryInterface $activityACLAwareRepository,
ActivityTypeRepositoryInterface $activityTypeRepository,
ActivityTypeCategoryRepository $activityTypeCategoryRepository,
PersonRepository $personRepository,
ThirdPartyRepository $thirdPartyRepository,
LocationRepository $locationRepository,
ActivityRepository $activityRepository,
AccompanyingPeriodRepository $accompanyingPeriodRepository,
EntityManagerInterface $entityManager,
EventDispatcherInterface $eventDispatcher,
LoggerInterface $logger,
SerializerInterface $serializer,
UserRepositoryInterface $userRepository,
CenterResolverManagerInterface $centerResolver,
TranslatorInterface $translator
private readonly ActivityACLAwareRepositoryInterface $activityACLAwareRepository,
private readonly ActivityTypeRepositoryInterface $activityTypeRepository,
private readonly ActivityTypeCategoryRepository $activityTypeCategoryRepository,
private readonly PersonRepository $personRepository,
private readonly ThirdPartyRepository $thirdPartyRepository,
private readonly LocationRepository $locationRepository,
private readonly ActivityRepository $activityRepository,
private readonly AccompanyingPeriodRepository $accompanyingPeriodRepository,
private readonly EntityManagerInterface $entityManager,
private readonly EventDispatcherInterface $eventDispatcher,
private readonly LoggerInterface $logger,
private readonly SerializerInterface $serializer,
private readonly UserRepositoryInterface $userRepository,
private readonly CenterResolverManagerInterface $centerResolver,
private readonly TranslatorInterface $translator,
private readonly FilterOrderHelperFactoryInterface $filterOrderHelperFactory,
private readonly TranslatableStringHelperInterface $translatableStringHelper,
private readonly PaginatorFactory $paginatorFactory,
) {
$this->activityACLAwareRepository = $activityACLAwareRepository;
$this->activityTypeRepository = $activityTypeRepository;
$this->activityTypeCategoryRepository = $activityTypeCategoryRepository;
$this->personRepository = $personRepository;
$this->thirdPartyRepository = $thirdPartyRepository;
$this->locationRepository = $locationRepository;
$this->activityRepository = $activityRepository;
$this->accompanyingPeriodRepository = $accompanyingPeriodRepository;
$this->entityManager = $entityManager;
$this->eventDispatcher = $eventDispatcher;
$this->logger = $logger;
$this->serializer = $serializer;
$this->userRepository = $userRepository;
$this->centerResolver = $centerResolver;
$this->translator = $translator;
}
/**
@@ -289,14 +253,31 @@ final class ActivityController extends AbstractController
{
$view = null;
$activities = [];
// TODO: add pagination
[$person, $accompanyingPeriod] = $this->getEntity($request);
$filter = $this->buildFilterOrder($person ?? $accompanyingPeriod);
$filterArgs = [
'my_activities' => $filter->getSingleCheckboxData('my_activities'),
'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'],
];
if ($person instanceof Person) {
$this->denyAccessUnlessGranted(ActivityVoter::SEE, $person);
$count = $this->activityACLAwareRepository->countByPerson($person, ActivityVoter::SEE, $filterArgs);
$paginator = $this->paginatorFactory->create($count);
$activities = $this->activityACLAwareRepository
->findByPerson($person, ActivityVoter::SEE, 0, null, ['date' => 'DESC', 'id' => 'DESC']);
->findByPerson(
$person,
ActivityVoter::SEE,
$paginator->getCurrentPageFirstItemNumber(),
$paginator->getItemsPerPage(),
['date' => 'DESC', 'id' => 'DESC'],
$filterArgs
);
$event = new PrivacyEvent($person, [
'element_class' => Activity::class,
@@ -308,10 +289,21 @@ final class ActivityController extends AbstractController
} elseif ($accompanyingPeriod instanceof AccompanyingPeriod) {
$this->denyAccessUnlessGranted(ActivityVoter::SEE, $accompanyingPeriod);
$count = $this->activityACLAwareRepository->countByAccompanyingPeriod($accompanyingPeriod, ActivityVoter::SEE, $filterArgs);
$paginator = $this->paginatorFactory->create($count);
$activities = $this->activityACLAwareRepository
->findByAccompanyingPeriod($accompanyingPeriod, ActivityVoter::SEE, 0, null, ['date' => 'DESC', 'id' => 'DESC']);
->findByAccompanyingPeriod(
$accompanyingPeriod,
ActivityVoter::SEE,
$paginator->getCurrentPageFirstItemNumber(),
$paginator->getItemsPerPage(),
['date' => 'DESC', 'id' => 'DESC'],
$filterArgs
);
$view = 'ChillActivityBundle:Activity:listAccompanyingCourse.html.twig';
} else {
throw new \LogicException("Unsupported");
}
return $this->render(
@@ -320,10 +312,47 @@ final class ActivityController extends AbstractController
'activities' => $activities,
'person' => $person,
'accompanyingCourse' => $accompanyingPeriod,
'filter' => $filter,
'paginator' => $paginator,
]
);
}
private function buildFilterOrder(AccompanyingPeriod|Person $associated): FilterOrderHelper
{
$filterBuilder = $this->filterOrderHelperFactory->create(self::class);
$types = $this->activityACLAwareRepository->findActivityTypeByAssociated($associated);
$jobs = $this->activityACLAwareRepository->findUserJobByAssociated($associated);
$filterBuilder
->addDateRange('activity_date', 'activity.date')
->addSingleCheckbox('my_activities', 'activity_filter.My activities');
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();
}
public function newAction(Request $request): Response
{
$view = null;

View File

@@ -40,6 +40,10 @@ class ByActivityNumberAggregator implements AggregatorInterface
{
// No form needed
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data)
{

View File

@@ -52,6 +52,10 @@ class ByCreatorAggregator implements AggregatorInterface
{
// no form
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data)
{

View File

@@ -57,6 +57,10 @@ class BySocialActionAggregator implements AggregatorInterface
{
// no form
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data)
{

View File

@@ -57,6 +57,10 @@ class BySocialIssueAggregator implements AggregatorInterface
{
// no form
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data)
{

View File

@@ -57,6 +57,10 @@ class ByThirdpartyAggregator implements AggregatorInterface
{
// no form
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data)
{

View File

@@ -57,6 +57,10 @@ class CreatorScopeAggregator implements AggregatorInterface
{
// no form
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data)
{

View File

@@ -29,14 +29,6 @@ class DateAggregator implements AggregatorInterface
private const DEFAULT_CHOICE = 'year';
private TranslatorInterface $translator;
public function __construct(
TranslatorInterface $translator
) {
$this->translator = $translator;
}
public function addRole(): ?string
{
return null;
@@ -84,9 +76,12 @@ class DateAggregator implements AggregatorInterface
'multiple' => false,
'expanded' => true,
'empty_data' => self::DEFAULT_CHOICE,
'data' => self::DEFAULT_CHOICE,
]);
}
public function getFormDefaultData(): array
{
return ['frequency' => self::DEFAULT_CHOICE];
}
public function getLabels($key, array $values, $data)
{

View File

@@ -57,6 +57,10 @@ class LocationTypeAggregator implements AggregatorInterface
{
// no form
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data)
{

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ActivityBundle\Export\Aggregator;
use Chill\ActivityBundle\Export\Declarations;
use Chill\ActivityBundle\Repository\ActivityTypeRepositoryInterface;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Repository\LocationRepository;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Closure;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
use function in_array;
final readonly class ActivityLocationAggregator implements AggregatorInterface
{
public const KEY = 'activity_location_aggregator';
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
if (!in_array('actloc', $qb->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';
}
}

View File

@@ -60,6 +60,10 @@ class ActivityTypeAggregator implements AggregatorInterface
{
// no form required for this aggregator
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data): Closure
{

View File

@@ -58,6 +58,10 @@ class ActivityUserAggregator implements AggregatorInterface
{
// nothing to add
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, $values, $data): Closure
{

View File

@@ -56,6 +56,10 @@ class ActivityUsersAggregator implements AggregatorInterface
{
// nothing to add on the form
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data)
{

View File

@@ -55,6 +55,10 @@ class ActivityUsersJobAggregator implements \Chill\MainBundle\Export\AggregatorI
{
// nothing to add in the form
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data)
{

View File

@@ -55,6 +55,10 @@ class ActivityUsersScopeAggregator implements \Chill\MainBundle\Export\Aggregato
{
// nothing to add in the form
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data)
{

View File

@@ -110,6 +110,10 @@ class ActivityReasonAggregator implements AggregatorInterface, ExportElementVali
]
);
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data)
{

View File

@@ -47,6 +47,10 @@ class SentReceivedAggregator implements AggregatorInterface
{
// No form needed
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data): callable
{

View File

@@ -39,6 +39,10 @@ class AvgActivityDuration implements ExportInterface, GroupedExportInterface
public function buildForm(FormBuilderInterface $builder)
{
}
public function getFormDefaultData(): array
{
return [];
}
public function getAllowedFormattersTypes(): array
{

View File

@@ -40,6 +40,10 @@ class AvgActivityVisitDuration implements ExportInterface, GroupedExportInterfac
{
// TODO: Implement buildForm() method.
}
public function getFormDefaultData(): array
{
return [];
}
public function getAllowedFormattersTypes(): array
{

View File

@@ -39,6 +39,10 @@ class CountActivity implements ExportInterface, GroupedExportInterface
public function buildForm(FormBuilderInterface $builder)
{
}
public function getFormDefaultData(): array
{
return [];
}
public function getAllowedFormattersTypes(): array
{

View File

@@ -44,6 +44,10 @@ class ListActivity implements ListInterface, GroupedExportInterface
{
$this->helper->buildForm($builder);
}
public function getFormDefaultData(): array
{
return [];
}
public function getAllowedFormattersTypes()
{

View File

@@ -40,6 +40,10 @@ class SumActivityDuration implements ExportInterface, GroupedExportInterface
{
// TODO: Implement buildForm() method.
}
public function getFormDefaultData(): array
{
return [];
}
public function getAllowedFormattersTypes(): array
{

View File

@@ -40,6 +40,10 @@ class SumActivityVisitDuration implements ExportInterface, GroupedExportInterfac
{
// TODO: Implement buildForm() method.
}
public function getFormDefaultData(): array
{
return [];
}
public function getAllowedFormattersTypes(): array
{

View File

@@ -35,6 +35,10 @@ class CountActivity implements ExportInterface, GroupedExportInterface
public function buildForm(FormBuilderInterface $builder)
{
}
public function getFormDefaultData(): array
{
return [];
}
public function getAllowedFormattersTypes()
{

View File

@@ -88,6 +88,10 @@ class ListActivity implements ListInterface, GroupedExportInterface
])],
]);
}
public function getFormDefaultData(): array
{
return [];
}
public function getAllowedFormattersTypes()
{

View File

@@ -53,6 +53,10 @@ class StatActivityDuration implements ExportInterface, GroupedExportInterface
public function buildForm(FormBuilderInterface $builder)
{
}
public function getFormDefaultData(): array
{
return [];
}
public function getAllowedFormattersTypes()
{

View File

@@ -68,6 +68,10 @@ class ActivityTypeFilter implements FilterInterface
'expanded' => true,
]);
}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string'): array
{

View File

@@ -52,6 +52,10 @@ class ByCreatorFilter implements FilterInterface
'multiple' => true,
]);
}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string'): array
{

View File

@@ -60,6 +60,10 @@ class BySocialActionFilter implements FilterInterface
'multiple' => true,
]);
}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string'): array
{

View File

@@ -60,6 +60,10 @@ class BySocialIssueFilter implements FilterInterface
'multiple' => true,
]);
}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string'): array
{

View File

@@ -68,9 +68,12 @@ class EmergencyFilter implements FilterInterface
'multiple' => false,
'expanded' => true,
'empty_data' => self::DEFAULT_CHOICE,
'data' => self::DEFAULT_CHOICE,
]);
}
public function getFormDefaultData(): array
{
return ['accepted_emergency' => self::DEFAULT_CHOICE];
}
public function describeAction($data, $format = 'string'): array
{

View File

@@ -44,6 +44,10 @@ class HasNoActivityFilter implements FilterInterface
{
//no form needed
}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string'): array
{

View File

@@ -46,6 +46,10 @@ class LocationFilter implements FilterInterface
'label' => 'pick location',
]);
}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string'): array
{

View File

@@ -65,6 +65,10 @@ class LocationTypeFilter implements FilterInterface
//'label' => false,
]);
}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string'): array
{

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ActivityBundle\Export\Filter\ACPFilters;
use Chill\ActivityBundle\Entity\Activity;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
final readonly class PeriodHavingActivityBetweenDatesFilter implements FilterInterface
{
public function __construct(
private RollingDateConverterInterface $rollingDateConverter,
) {
}
public function getTitle()
{
return 'export.filter.activity.course_having_activity_between_date.Title';
}
public function buildForm(FormBuilderInterface $builder)
{
$builder
->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;
}
}

View File

@@ -69,9 +69,12 @@ class SentReceivedFilter implements FilterInterface
'multiple' => false,
'expanded' => true,
'empty_data' => self::DEFAULT_CHOICE,
'data' => self::DEFAULT_CHOICE,
]);
}
public function getFormDefaultData(): array
{
return ['accepted_sentreceived' => self::DEFAULT_CHOICE];
}
public function describeAction($data, $format = 'string'): array
{

View File

@@ -61,6 +61,10 @@ class UserFilter implements FilterInterface
'label' => 'Creators',
]);
}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string'): array
{

View File

@@ -71,6 +71,10 @@ class UserScopeFilter implements FilterInterface
'expanded' => true,
]);
}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string'): array
{

View File

@@ -80,11 +80,9 @@ class ActivityDateFilter implements FilterInterface
$builder
->add('date_from', PickRollingDateType::class, [
'label' => 'Activities after this date',
'data' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START),
])
->add('date_to', PickRollingDateType::class, [
'label' => 'Activities before this date',
'data' => new RollingDate(RollingDate::T_TODAY),
]);
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) {
@@ -127,6 +125,10 @@ class ActivityDateFilter implements FilterInterface
}
});
}
public function getFormDefaultData(): array
{
return ['date_from' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), 'date_to' => new RollingDate(RollingDate::T_TODAY)];
}
public function describeAction($data, $format = 'string')
{

View File

@@ -78,6 +78,10 @@ class ActivityTypeFilter implements ExportElementValidatedInterface, FilterInter
],
]);
}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string')
{

View File

@@ -56,6 +56,10 @@ class ActivityUsersFilter implements FilterInterface
'label' => 'Users',
]);
}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string')
{

View File

@@ -82,6 +82,10 @@ class ActivityReasonFilter implements ExportElementValidatedInterface, FilterInt
'expanded' => false,
]);
}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string')
{

View File

@@ -112,7 +112,6 @@ class PersonHavingActivityBetweenDateFilter implements ExportElementValidatedInt
{
$builder->add('date_from', DateType::class, [
'label' => 'Implied in an activity after this date',
'data' => new DateTime(),
'attr' => ['class' => 'datepicker'],
'widget' => 'single_text',
'format' => 'dd-MM-yyyy',
@@ -120,7 +119,6 @@ class PersonHavingActivityBetweenDateFilter implements ExportElementValidatedInt
$builder->add('date_to', DateType::class, [
'label' => 'Implied in an activity before this date',
'data' => new DateTime(),
'attr' => ['class' => 'datepicker'],
'widget' => 'single_text',
'format' => 'dd-MM-yyyy',
@@ -130,7 +128,6 @@ class PersonHavingActivityBetweenDateFilter implements ExportElementValidatedInt
'class' => ActivityReason::class,
'choice_label' => fn (ActivityReason $reason): ?string => $this->translatableStringHelper->localize($reason->getName()),
'group_by' => fn (ActivityReason $reason): ?string => $this->translatableStringHelper->localize($reason->getCategory()->getName()),
'data' => $this->activityReasonRepository->findAll(),
'multiple' => true,
'expanded' => false,
'label' => 'Activity reasons for those activities',
@@ -176,6 +173,10 @@ class PersonHavingActivityBetweenDateFilter implements ExportElementValidatedInt
}
});
}
public function getFormDefaultData(): array
{
return ['date_from' => new DateTime(), 'date_to' => new DateTime(), 'reasons' => $this->activityReasonRepository->findAll()];
}
public function describeAction($data, $format = 'string')
{

View File

@@ -60,6 +60,10 @@ class UsersJobFilter implements FilterInterface
'expanded' => true,
]);
}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string')
{

View File

@@ -67,6 +67,10 @@ class UsersScopeFilter implements FilterInterface
'expanded' => true,
]);
}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string')
{

View File

@@ -18,67 +18,193 @@ use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\LocationType;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\NoResultException;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Role\Role;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Security;
use function count;
use function in_array;
final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInterface
final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInterface
{
private AuthorizationHelper $authorizationHelper;
private CenterResolverDispatcherInterface $centerResolverDispatcher;
private EntityManagerInterface $em;
private ActivityRepository $repository;
private Security $security;
private TokenStorageInterface $tokenStorage;
public function __construct(
AuthorizationHelper $authorizationHelper,
CenterResolverDispatcherInterface $centerResolverDispatcher,
TokenStorageInterface $tokenStorage,
ActivityRepository $repository,
EntityManagerInterface $em,
Security $security
private AuthorizationHelperForCurrentUserInterface $authorizationHelper,
private CenterResolverManagerInterface $centerResolverManager,
private ActivityRepository $repository,
private EntityManagerInterface $em,
private Security $security,
private RequestStack $requestStack,
) {
$this->authorizationHelper = $authorizationHelper;
$this->centerResolverDispatcher = $centerResolverDispatcher;
$this->tokenStorage = $tokenStorage;
$this->repository = $repository;
$this->em = $em;
$this->security = $security;
}
public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array
/**
* @throws NonUniqueResultException
* @throws NoResultException
*/
public function countByAccompanyingPeriod(AccompanyingPeriod $period, string $role, array $filters = []): int
{
$user = $this->security->getUser();
$center = $this->centerResolverDispatcher->resolveCenter($period);
$qb = $this->buildBaseQuery($filters);
if (0 === count($orderBy)) {
$orderBy = ['date' => 'DESC'];
$qb
->select('COUNT(a)')
->andWhere('a.accompanyingPeriod = :period')->setParameter('period', $period);
return $qb->getQuery()->getSingleScalarResult();
}
public function countByPerson(Person $person, string $role, array $filters = []): int
{
$qb = $this->buildBaseQuery($filters);
$qb = $this->filterBaseQueryByPerson($qb, $person, $role);
$qb->select('COUNT(a)');
return $qb->getQuery()->getSingleScalarResult();
}
public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): array
{
$qb = $this->buildBaseQuery($filters);
$qb->andWhere('a.accompanyingPeriod = :period')->setParameter('period', $period);
foreach ($orderBy as $field => $order) {
$qb->addOrderBy('a.' . $field, $order);
}
$scopes = $this->authorizationHelper
->getReachableCircles($user, $role, $center);
if (null !== $start) {
$qb->setFirstResult($start);
}
if (null !== $limit) {
$qb->setMaxResults($limit);
}
return $this->em->getRepository(Activity::class)
->findByAccompanyingPeriod($period, $scopes, true, $limit, $start, $orderBy);
return $qb->getQuery()->getResult();
}
public function buildBaseQuery(array $filters): QueryBuilder
{
$qb = $this->repository
->createQueryBuilder('a')
;
if (($filters['my_activities'] ?? false) and ($user = $this->security->getUser()) instanceof User) {
$qb->andWhere(
$qb->expr()->orX(
'a.createdBy = :user',
'a.user = :user',
':user MEMBER OF a.users'
)
)->setParameter('user', $user);
}
if ([] !== ($types = $filters['types'] ?? [])) {
$qb->andWhere('a.activityType IN (:types)')->setParameter('types', $types);
}
if ([] !== ($jobs = $filters['jobs'] ?? [])) {
$qb
->leftJoin('a.createdBy', 'creator')
->leftJoin('a.user', 'activity_u')
->andWhere(
$qb->expr()->orX(
'creator.userJob IN (:jobs)',
'activity_u.userJob IN (:jobs)',
'EXISTS (SELECT 1 FROM ' . User::class . ' activity_user WHERE activity_user MEMBER OF a.users AND activity_user.userJob IN (:jobs))'
)
)
->setParameter('jobs', $jobs);
}
if (null !== ($after = $filters['after'] ?? null)) {
$qb->andWhere('a.date >= :after')->setParameter('after', $after);
}
if (null !== ($before = $filters['before'] ?? null)) {
$qb->andWhere('a.date <= :before')->setParameter('before', $before);
}
return $qb;
}
/**
* @param AccompanyingPeriod|Person $associated
* @return array<ActivityType>
*/
public function findActivityTypeByAssociated(AccompanyingPeriod|Person $associated): array
{
$in = $this->em->createQueryBuilder();
$in
->select('1')
->from(Activity::class, 'a');
if ($associated instanceof Person) {
$in = $this->filterBaseQueryByPerson($in, $associated, ActivityVoter::SEE);
} else {
$in->andWhere('a.accompanyingPeriod = :period')->setParameter('period', $associated);
}
// join between the embedded exist query and the main query
$in->andWhere('a.activityType = t');
$qb = $this->em->createQueryBuilder()->setParameters($in->getParameters());
$qb
->select('t')
->from(ActivityType::class, 't')
->where(
$qb->expr()->exists($in->getDQL())
);
return $qb->getQuery()->getResult();
}
public function findUserJobByAssociated(Person|AccompanyingPeriod $associated): array
{
$in = $this->em->createQueryBuilder();
$in->select('IDENTITY(u.userJob)')
->from(User::class, 'u')
->join(
Activity::class,
'a',
Join::WITH,
'a.createdBy = u OR a.user = u OR u MEMBER OF a.users'
);
if ($associated instanceof Person) {
$in = $this->filterBaseQueryByPerson($in, $associated, ActivityVoter::SEE);
} else {
$in->andWhere('a.accompanyingPeriod = :associated');
$in->setParameter('associated', $associated);
}
$qb = $this->em->createQueryBuilder()->setParameters($in->getParameters());
$qb->select('ub', 'JSON_EXTRACT(ub.label, :lang) AS HIDDEN lang')
->from(UserJob::class, 'ub')
->where($qb->expr()->in('ub.id', $in->getDQL()))
->setParameter('lang', $this->requestStack->getCurrentRequest()->getLocale())
->orderBy('lang')
;
return $qb->getQuery()->getResult();
}
public function findByAccompanyingPeriodSimplified(AccompanyingPeriod $period, ?int $limit = 1000): array
{
$rsm = new ResultSetMappingBuilder($this->em);
@@ -159,25 +285,73 @@ final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInte
return $nq->getResult(AbstractQuery::HYDRATE_ARRAY);
}
/**
* @param array $orderBy
*
* @return Activity[]|array
*/
public function findByPerson(Person $person, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array
public function findByPerson(Person $person, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = [], array $filters = []): array
{
$user = $this->security->getUser();
$center = $this->centerResolverDispatcher->resolveCenter($person);
$qb = $this->buildBaseQuery($filters);
if (0 === count($orderBy)) {
$orderBy = ['date' => 'DESC'];
$qb = $this->filterBaseQueryByPerson($qb, $person, $role);
foreach ($orderBy as $field => $direction) {
$qb->addOrderBy('a.' . $field, $direction);
}
$reachableScopes = $this->authorizationHelper
->getReachableCircles($user, $role, $center);
if (null !== $start) {
$qb->setFirstResult($start);
}
if (null !== $limit) {
$qb->setMaxResults($limit);
}
return $this->em->getRepository(Activity::class)
->findByPersonImplied($person, $reachableScopes, $orderBy, $limit, $start);
return $qb->getQuery()->getResult();
}
private function filterBaseQueryByPerson(QueryBuilder $qb, Person $person, string $role): QueryBuilder
{
$orX = $qb->expr()->orX();
$counter = 0;
foreach ($this->centerResolverManager->resolveCenters($person) as $center) {
$scopes = $this->authorizationHelper->getReachableScopes($role, $center);
if ([] === $scopes) {
continue;
}
$orX->add(sprintf('a.person = :person AND a.scope IN (:scopes_%d)', $counter));
$qb->setParameter(sprintf('scopes_%d', $counter), $scopes);
$qb->setParameter('person', $person);
$counter++;
}
foreach ($person->getAccompanyingPeriodParticipations() as $participation) {
if (!$this->security->isGranted(ActivityVoter::SEE, $participation->getAccompanyingPeriod())) {
continue;
}
$and = $qb->expr()->andX(
sprintf('a.accompanyingPeriod = :period_%d', $counter),
sprintf('a.date >= :participation_start_%d', $counter)
);
$qb
->setParameter(sprintf('period_%d', $counter), $participation->getAccompanyingPeriod())
->setParameter(sprintf('participation_start_%d', $counter), $participation->getStartDate());
if (null !== $participation->getEndDate()) {
$and->add(sprintf('a.date < :participation_end_%d', $counter));
$qb
->setParameter(sprintf('participation_end_%d', $counter), $participation->getEndDate());
}
$orX->add($and);
$counter++;
}
if (0 === $orX->count()) {
$qb->andWhere('FALSE = TRUE');
} else {
$qb->andWhere($orX);
}
return $qb;
}
public function queryTimelineIndexer(string $context, array $args = []): array
@@ -226,7 +400,6 @@ final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInte
// acls:
$reachableCenters = $this->authorizationHelper->getReachableCenters(
$this->tokenStorage->getToken()->getUser(),
ActivityVoter::SEE
);
@@ -251,7 +424,7 @@ final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInte
continue;
}
// we get all the reachable scopes for this center
$reachableScopes = $this->authorizationHelper->getReachableScopes($this->tokenStorage->getToken()->getUser(), ActivityVoter::SEE, $center);
$reachableScopes = $this->authorizationHelper->getReachableScopes(ActivityVoter::SEE, $center);
// we get the ids for those scopes
$reachablesScopesId = array_map(
static fn (Scope $scope) => $scope->getId(),

View File

@@ -11,15 +11,32 @@ declare(strict_types=1);
namespace Chill\ActivityBundle\Repository;
use Chill\ActivityBundle\Entity\Activity;
use Chill\ActivityBundle\Entity\ActivityType;
use Chill\MainBundle\Entity\UserJob;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
interface ActivityACLAwareRepositoryInterface
{
/**
* @return Activity[]|array
* Return all the activities associated to an accompanying period and that the user is allowed to apply the given role.
*
*
* @param array{my_activities?: bool, types?: array<ActivityType>, jobs?: array<UserJob>, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters
* @return array<Activity>
*/
public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array;
public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): array;
/**
* @param array{my_activities?: bool, types?: array<ActivityType>, jobs?: array<UserJob>, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters
*/
public function countByAccompanyingPeriod(AccompanyingPeriod $period, string $role, array $filters = []): int;
/**
* @param array{my_activities?: bool, types?: array<ActivityType>, jobs?: array<UserJob>, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters
*/
public function countByPerson(Person $person, string $role, array $filters = []): int;
/**
* Return a list of activities, simplified as array (not object).
@@ -31,7 +48,28 @@ interface ActivityACLAwareRepositoryInterface
public function findByAccompanyingPeriodSimplified(AccompanyingPeriod $period, ?int $limit = 1000): array;
/**
* @return Activity[]|array
* @param array{my_activities?: bool, types?: array<ActivityType>, jobs?: array<UserJob>, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters
* @return array<Activity>
*/
public function findByPerson(Person $person, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array;
public function findByPerson(Person $person, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): array;
/**
* Return a list of the type for the activities associated to person or accompanying period
*
* @return array<ActivityType>
*/
public function findActivityTypeByAssociated(AccompanyingPeriod|Person $associated): array;
/**
* Return a list of the user job for the activities associated to person or accompanying period
*
* Associated mean the job:
* - of the creator;
* - of the user (activity.user)
* - of all the users
*
* @return array<UserJob>
*/
public function findUserJobByAssociated(AccompanyingPeriod|Person $associated): array;
}

View File

@@ -0,0 +1,198 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ActivityBundle\Repository;
use Chill\ActivityBundle\Entity\Activity;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\ActivityBundle\Service\GenericDoc\Providers\AccompanyingPeriodActivityGenericDocProvider;
use Chill\ActivityBundle\Service\GenericDoc\Providers\PersonActivityGenericDocProvider;
use Chill\DocStoreBundle\Entity\PersonDocument;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\GenericDoc\FetchQuery;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\DocStoreBundle\GenericDoc\Providers\PersonDocumentGenericDocProvider;
use Chill\DocStoreBundle\Repository\PersonDocumentACLAwareRepositoryInterface;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter;
use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\HttpKernel\HttpCache\Store;
use Symfony\Component\Security\Core\Security;
final readonly class ActivityDocumentACLAwareRepository implements ActivityDocumentACLAwareRepositoryInterface
{
public function __construct(
private EntityManagerInterface $em,
private CenterResolverManagerInterface $centerResolverManager,
private AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser,
private Security $security
) {
}
public function buildFetchQueryActivityDocumentLinkedToPersonFromPersonContext(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null): FetchQueryInterface
{
$query = $this->buildBaseFetchQueryActivityDocumentLinkedToPersonFromPersonContext($person, $startDate, $endDate, $content);
return $this->addFetchQueryByPersonACL($query, $person);
}
public function buildBaseFetchQueryActivityDocumentLinkedToPersonFromPersonContext(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery
{
$storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class);
$activityMetadata = $this->em->getClassMetadata(Activity::class);
$query = new FetchQuery(
PersonActivityGenericDocProvider::KEY,
sprintf('jsonb_build_object(\'id\', stored_obj.%s, \'activity_id\', activity.%s)', $storedObjectMetadata->getSingleIdentifierColumnName(), $activityMetadata->getSingleIdentifierColumnName()),
sprintf('stored_obj.%s', $storedObjectMetadata->getColumnName('createdAt')),
sprintf('%s AS stored_obj', $storedObjectMetadata->getSchemaName().'.'.$storedObjectMetadata->getTableName())
);
$query->addJoinClause(
'JOIN public.activity_storedobject activity_doc ON activity_doc.storedobject_id = stored_obj.id'
);
$query->addJoinClause(
'JOIN public.activity activity ON activity.id = activity_doc.activity_id'
);
$query->addWhereClause(
sprintf('activity.%s = ?', $activityMetadata->getSingleAssociationJoinColumnName('person')),
[$person->getId()],
[Types::INTEGER]
);
return $this->addWhereClauses($query, $startDate, $endDate, $content);
}
public function buildFetchQueryActivityDocumentLinkedToAccompanyingPeriodFromPersonContext(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery
{
$storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class);
$activityMetadata = $this->em->getClassMetadata(Activity::class);
$query = new FetchQuery(
AccompanyingPeriodActivityGenericDocProvider::KEY,
sprintf('jsonb_build_object(\'id\', stored_obj.%s, \'activity_id\', activity.%s)', $storedObjectMetadata->getSingleIdentifierColumnName(), $activityMetadata->getSingleIdentifierColumnName()),
sprintf('stored_obj.%s', $storedObjectMetadata->getColumnName('createdAt')),
sprintf('%s AS stored_obj', $storedObjectMetadata->getSchemaName().'.'.$storedObjectMetadata->getTableName())
);
$query->addJoinClause(
'JOIN public.activity_storedobject activity_doc ON activity_doc.storedobject_id = stored_obj.id'
);
$query->addJoinClause(
'JOIN public.activity activity ON activity.id = activity_doc.activity_id'
);
// add documents of activities from parcours context
$or = [];
$orParams = [];
$orTypes = [];
foreach ($person->getAccompanyingPeriodParticipations() as $participation) {
if (!$this->security->isGranted(ActivityVoter::SEE, $participation->getAccompanyingPeriod())) {
continue;
}
$or[] = sprintf(
'(activity.%s = ? AND stored_obj.%s BETWEEN ?::date AND COALESCE(?::date, \'infinity\'::date))',
$activityMetadata->getSingleAssociationJoinColumnName('accompanyingPeriod'),
$storedObjectMetadata->getColumnName('createdAt')
);
$orParams = [...$orParams, $participation->getAccompanyingPeriod()->getId(),
DateTimeImmutable::createFromInterface($participation->getStartDate()),
null === $participation->getEndDate() ? null : DateTimeImmutable::createFromInterface($participation->getEndDate())];
$orTypes = [...$orTypes, Types::INTEGER, Types::DATE_IMMUTABLE, Types::DATE_IMMUTABLE];
}
if ([] === $or) {
$query->addWhereClause('TRUE = FALSE');
return $query;
}
$query->addWhereClause(sprintf('(%s)', implode(' OR ', $or)), $orParams, $orTypes);
return $this->addWhereClauses($query, $startDate, $endDate, $content);
}
private function addWhereClauses(FetchQuery $query, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery
{
$storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class);
if (null !== $startDate) {
$query->addWhereClause(
sprintf('stored_obj.%s >= ?', $storedObjectMetadata->getColumnName('createdAt')),
[$startDate],
[Types::DATE_IMMUTABLE]
);
}
if (null !== $endDate) {
$query->addWhereClause(
sprintf('stored_obj.%s < ?', $storedObjectMetadata->getColumnName('createdAt')),
[$endDate],
[Types::DATE_IMMUTABLE]
);
}
if (null !== $content and '' !== $content) {
$query->addWhereClause(
'stored_obj.title ilike ?',
['%' . $content . '%'],
[Types::STRING]
);
}
return $query;
}
private function addFetchQueryByPersonACL(FetchQuery $fetchQuery, Person $person): FetchQuery
{
$activityMetadata = $this->em->getClassMetadata(Activity::class);
$reachableScopes = [];
foreach ($this->centerResolverManager->resolveCenters($person) as $center) {
$reachableScopes = [
...$reachableScopes,
...$this->authorizationHelperForCurrentUser->getReachableScopes(ActivityVoter::SEE, $center)
];
}
if ([] === $reachableScopes) {
$fetchQuery->addWhereClause('FALSE = TRUE');
return $fetchQuery;
}
$fetchQuery->addWhereClause(
sprintf(
'activity.%s IN (%s)',
$activityMetadata->getSingleAssociationJoinColumnName('scope'),
implode(', ', array_fill(0, count($reachableScopes), '?'))
),
array_map(static fn (Scope $s) => $s->getId(), $reachableScopes),
array_fill(0, count($reachableScopes), Types::INTEGER)
);
return $fetchQuery;
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ActivityBundle\Repository;
use Chill\DocStoreBundle\GenericDoc\FetchQuery;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\PersonBundle\Entity\Person;
use DateTimeImmutable;
/**
* Gives queries usable for fetching documents, with ACL aware
*/
interface ActivityDocumentACLAwareRepositoryInterface
{
/**
* Return a fetch query for querying document's activities for a person
*
* This method must check the rights to see a document: the user must be allowed to see the given activities
*/
public function buildFetchQueryActivityDocumentLinkedToPersonFromPersonContext(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null): FetchQueryInterface;
/**
* Return a fetch query for querying document's activities for an activity in accompanying periods, but for a given person
*
* This method must check the rights to see a document: the user must be allowed to see the given accompanying periods
*/
public function buildFetchQueryActivityDocumentLinkedToAccompanyingPeriodFromPersonContext(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery;
}

View File

@@ -11,9 +11,13 @@ declare(strict_types=1);
namespace Chill\ActivityBundle\Repository;
use Chill\ActivityBundle\Entity\Activity;
use Chill\ActivityBundle\Entity\ActivityType;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\Expr\Join;
final class ActivityTypeRepository implements ActivityTypeRepositoryInterface
{

View File

@@ -12,12 +12,14 @@ declare(strict_types=1);
namespace Chill\ActivityBundle\Repository;
use Chill\ActivityBundle\Entity\ActivityType;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Doctrine\Persistence\ObjectRepository;
interface ActivityTypeRepositoryInterface extends ObjectRepository
{
/**
* @return array|ActivityType[]
* @return array<ActivityType>
*/
public function findAllActive(): array;
}

View File

@@ -1,5 +1,7 @@
// Access to Bootstrap variables and mixins
@import '~ChillMainAssets/module/bootstrap/shared';
@import '~ChillPersonAssets/chill/scss/mixins.scss';
@import 'bootstrap/scss/_badge.scss';
//// ACTIVITY CREATION
// first step: select type page
@@ -96,3 +98,25 @@ li.document-list-item {
justify-content: space-between;
margin-bottom: 0.3rem;
}
.badge-activity-type {
display: inline-block;
background-color: #f3f3f3;
.title_label {
@include chill_badge(#9acd32);
}
.title_action {
padding: var(--bs-badge-padding-y) var(--bs-badge-padding-x);
margin-right: 1rem;
font-size: var(--bs-badge-font-size);
font-weight: var(--bs-badge-font-weight);
line-height: 1;
color: var(--bs-badge-color);
text-align: center;
white-space: nowrap;
vertical-align: baseline;
}
}

View File

@@ -80,12 +80,15 @@
<div class="context-{{ context }}">
{{ filter|chill_render_filter_order_helper }}
{% if activities|length == 0 %}
<p class="chill-no-data-statement">
{{ "There isn't any activities."|trans }}
</p>
{% else %}
<div class="flex-table activity-list">
{% for activity in activities %}
{% include 'ChillActivityBundle:Activity:_list_item.html.twig' with {
@@ -96,4 +99,6 @@
</div>
{% endif %}
{{ chill_pagination(paginator) }}
</div>

View File

@@ -0,0 +1,83 @@
{% import "@ChillDocStore/Macro/macro.html.twig" as m %}
{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %}
{% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %}
{% set person_id = null %}
{% if activity.person %}
{% set person_id = activity.person.id %}
{% endif %}
{% set accompanying_course_id = null %}
{% if activity.accompanyingPeriod %}
{% set accompanying_course_id = activity.accompanyingPeriod.id %}
{% endif %}
<div class="item-bloc activity-item{% if itemBlocClass is defined %} {{ itemBlocClass }}{% endif %}">
<div class="item-row">
<div class="item-col" style="width: unset">
{% if document.isPending %}
<div class="badge text-bg-info" data-docgen-is-pending="{{ document.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
{% elseif document.isFailure %}
<div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
{% endif %}
<div>
{% if activity.accompanyingPeriod is not null and context == 'person' %}
<span class="badge bg-primary">
<i class="fa fa-random"></i> {{ activity.accompanyingPeriod.id }}
</span>&nbsp;
{% endif %}
<div class="badge-activity-type">
<span class="title_label"></span>
<span class="title_action">
{{ activity.type.name | localize_translatable_string }}
{% if activity.emergency %}
<span class="badge bg-danger rounded-pill fs-6 float-end">{{ 'Emergency'|trans|upper }}</span>
{% endif %}
</span>
</div>
</div>
<div class="denomination h2">
{{ document.title|chill_print_or_message("No title") }}
</div>
{% if document.hasTemplate %}
<div>
<p>{{ document.template.name|localize_translatable_string }}</p>
</div>
{% endif %}
</div>
<div class="item-col">
<div class="container">
<div class="dates row text-end">
<span>{{ document.createdAt|format_date('short') }}</span>
</div>
</div>
</div>
</div>
<div class="item-row separator">
<div class="item-col item-meta">
{{ mmm.createdBy(document) }}
</div>
<ul class="item-col record_actions flex-shrink-1">
{% if is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) %}
<li>
{{ document|chill_document_button_group(document.title, is_granted('CHILL_ACTIVITY_UPDATE', activity), {small: false}) }}
</li>
{% endif %}
{% if is_granted('CHILL_ACTIVITY_SEE', activity)%}
<li>
<a href="{{ chill_path_add_return_path('chill_activity_activity_show', {'id': activity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id}) }}" class="btn btn-show"></a>
</li>
{% endif %}
{% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %}
<li>
<a href="{{ chill_path_add_return_path('chill_activity_activity_edit', {'id': activity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-edit"></a>
</li>
{% endif %}
</ul>
</div>
</div>

View File

@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ActivityBundle\Service\GenericDoc\Providers;
use Chill\ActivityBundle\Entity\Activity;
use Chill\ActivityBundle\Repository\ActivityDocumentACLAwareRepositoryInterface;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\GenericDoc\FetchQuery;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\MappingException;
use Symfony\Component\Security\Core\Security;
final class AccompanyingPeriodActivityGenericDocProvider implements GenericDocForAccompanyingPeriodProviderInterface, GenericDocForPersonProviderInterface
{
public const KEY = 'accompanying_period_activity_document';
public function __construct(
private EntityManagerInterface $em,
private Security $security,
private ActivityDocumentACLAwareRepositoryInterface $activityDocumentACLAwareRepository,
) {
}
public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
{
$storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class);
$activityMetadata = $this->em->getClassMetadata(Activity::class);
$query = new FetchQuery(
self::KEY,
sprintf("jsonb_build_object('id', doc_obj.%s, 'activity_id', activity.%s)", $storedObjectMetadata->getSingleIdentifierColumnName(), $activityMetadata->getSingleIdentifierColumnName()),
'doc_obj.'.$storedObjectMetadata->getColumnName('createdAt'),
$storedObjectMetadata->getSchemaName().'.'.$storedObjectMetadata->getTableName().' AS doc_obj'
);
$query->addJoinClause(
'JOIN public.activity_storedobject activity_doc ON activity_doc.storedobject_id = doc_obj.id'
);
$query->addJoinClause(
'JOIN public.activity activity ON activity.id = activity_doc.activity_id'
);
$query->addWhereClause(
'activity.accompanyingperiod_id = ?',
[$accompanyingPeriod->getId()],
[Types::INTEGER]
);
if (null !== $startDate) {
$query->addWhereClause(
sprintf('doc_obj.%s >= ?', $storedObjectMetadata->getColumnName('createdAt')),
[$startDate],
[Types::DATE_IMMUTABLE]
);
}
if (null !== $endDate) {
$query->addWhereClause(
sprintf('doc_obj.%s < ?', $storedObjectMetadata->getColumnName('createdAt')),
[$endDate],
[Types::DATE_IMMUTABLE]
);
}
if (null !== $content) {
$query->addWhereClause(
'doc_obj.title ilike ?',
['%' . $content . '%'],
[Types::STRING]
);
}
return $query;
}
/**
* @param AccompanyingPeriod $accompanyingPeriod
* @return bool
*/
public function isAllowedForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): bool
{
return $this->security->isGranted(ActivityVoter::SEE, $accompanyingPeriod);
}
public function isAllowedForPerson(Person $person): bool
{
return $this->security->isGranted(AccompanyingPeriodVoter::SEE, $person);
}
public function buildFetchQueryForPerson(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
{
return $this->activityDocumentACLAwareRepository
->buildFetchQueryActivityDocumentLinkedToAccompanyingPeriodFromPersonContext($person, $startDate, $endDate, $content);
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ActivityBundle\Service\GenericDoc\Providers;
use Chill\ActivityBundle\Repository\ActivityDocumentACLAwareRepository;
use Chill\ActivityBundle\Repository\ActivityDocumentACLAwareRepositoryInterface;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface;
use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface;
use Chill\DocStoreBundle\Repository\PersonDocumentACLAwareRepositoryInterface;
use Chill\PersonBundle\Entity\Person;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Security;
final readonly class PersonActivityGenericDocProvider implements GenericDocForPersonProviderInterface
{
public const KEY = 'person_activity_document';
public function __construct(
private Security $security,
private ActivityDocumentACLAwareRepositoryInterface $personActivityDocumentACLAwareRepository,
) {
}
public function buildFetchQueryForPerson(Person $person, ?DateTimeImmutable $startDate = null, ?DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
{
return $this->personActivityDocumentACLAwareRepository->buildFetchQueryActivityDocumentLinkedToPersonFromPersonContext(
$person,
$startDate,
$endDate,
$content
);
}
/**
* @param Person $person
* @return bool
*/
public function isAllowedForPerson(Person $person): bool
{
return $this->security->isGranted(ActivityVoter::SEE, $person);
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ActivityBundle\Service\GenericDoc\Renderers;
use Chill\ActivityBundle\Repository\ActivityRepository;
use Chill\ActivityBundle\Service\GenericDoc\Providers\AccompanyingPeriodActivityGenericDocProvider;
use Chill\ActivityBundle\Service\GenericDoc\Providers\PersonActivityGenericDocProvider;
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface;
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
final class AccompanyingPeriodActivityGenericDocRenderer implements GenericDocRendererInterface
{
private StoredObjectRepository $objectRepository;
private ActivityRepository $activityRepository;
public function __construct(StoredObjectRepository $storedObjectRepository, ActivityRepository $activityRepository)
{
$this->objectRepository = $storedObjectRepository;
$this->activityRepository = $activityRepository;
}
public function supports(GenericDocDTO $genericDocDTO, $options = []): bool
{
return $genericDocDTO->key === AccompanyingPeriodActivityGenericDocProvider::KEY || $genericDocDTO->key === PersonActivityGenericDocProvider::KEY;
}
public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string
{
return '@ChillActivity/GenericDoc/activity_document.html.twig';
}
public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array
{
return [
'activity' => $this->activityRepository->find($genericDocDTO->identifiers['activity_id']),
'document' => $this->objectRepository->find($genericDocDTO->identifiers['id']),
'context' => $genericDocDTO->getContext(),
];
}
}

View File

@@ -0,0 +1,325 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ActivityBundle\Tests\Repository;
use Chill\ActivityBundle\Entity\ActivityType;
use Chill\ActivityBundle\Repository\ActivityACLAwareRepository;
use Chill\ActivityBundle\Repository\ActivityRepository;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\EntityManagerInterface;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Security;
/**
* @internal
* @coversNothing
*/
class ActivityACLAwareRepositoryTest extends KernelTestCase
{
use ProphecyTrait;
private AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser;
private CenterResolverManagerInterface $centerResolverManager;
private ActivityRepository $activityRepository;
private EntityManagerInterface $entityManager;
private Security $security;
private RequestStack $requestStack;
protected function setUp(): void
{
self::bootKernel();
$this->authorizationHelperForCurrentUser = self::$container->get(AuthorizationHelperForCurrentUserInterface::class);
$this->centerResolverManager = self::$container->get(CenterResolverManagerInterface::class);
$this->activityRepository = self::$container->get(ActivityRepository::class);
$this->entityManager = self::$container->get(EntityManagerInterface::class);
$this->security = self::$container->get(Security::class);
$this->requestStack = $requestStack = new RequestStack();
$request = $this->prophesize(Request::class);
$request->getLocale()->willReturn('fr');
$request->getDefaultLocale()->willReturn('fr');
$requestStack->push($request->reveal());
}
/**
* @dataProvider provideDataFindByAccompanyingPeriod
*/
public function testFindByAccompanyingPeriod(AccompanyingPeriod $period, User $user, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): void
{
$security = $this->prophesize(Security::class);
$security->isGranted($role, $period)->willReturn(true);
$security->getUser()->willReturn($user);
$repository = new ActivityACLAwareRepository(
$this->authorizationHelperForCurrentUser,
$this->centerResolverManager,
$this->activityRepository,
$this->entityManager,
$security->reveal(),
$this->requestStack
);
$actual = $repository->findByAccompanyingPeriod($period, $role, $start, $limit, $orderBy, $filters);
self::assertIsArray($actual);
}
/**
* @dataProvider provideDataFindByAccompanyingPeriod
*/
public function testFindActivityTypeByAccompanyingPeriod(AccompanyingPeriod $period, User $user, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): void
{
$security = $this->prophesize(Security::class);
$security->isGranted($role, $period)->willReturn(true);
$security->getUser()->willReturn($user);
$repository = new ActivityACLAwareRepository(
$this->authorizationHelperForCurrentUser,
$this->centerResolverManager,
$this->activityRepository,
$this->entityManager,
$security->reveal(),
$this->requestStack
);
$actual = $repository->findActivityTypeByAssociated($period);
self::assertIsArray($actual);
}
/**
* @dataProvider provideDataFindByPerson
*/
public function testFindActivityTypeByPerson(Person $person, User $user, array $centers, array $scopes, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = [], array $filters = []): void
{
$role = ActivityVoter::SEE;
$centerResolver = $this->prophesize(CenterResolverManagerInterface::class);
$centerResolver->resolveCenters($person)->willReturn($centers);
$authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class);
$authorizationHelper->getReachableScopes($role, Argument::type(Center::class))
->willReturn($scopes);
$security = $this->prophesize(Security::class);
$security->isGranted($role, Argument::type(AccompanyingPeriod::class))->willReturn(true);
$security->getUser()->willReturn($user);
$repository = new ActivityACLAwareRepository(
$authorizationHelper->reveal(),
$centerResolver->reveal(),
$this->activityRepository,
$this->entityManager,
$security->reveal(),
$this->requestStack
);
$actual = $repository->findByPerson($person, $role, $start, $limit, $orderBy, $filters);
self::assertIsArray($actual);
}
/**
* @dataProvider provideDataFindByPerson
*/
public function testFindByPerson(Person $person, User $user, array $centers, array $scopes, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = [], array $filters = []): void
{
$centerResolver = $this->prophesize(CenterResolverManagerInterface::class);
$centerResolver->resolveCenters($person)->willReturn($centers);
$authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class);
$authorizationHelper->getReachableScopes($role, Argument::type(Center::class))
->willReturn($scopes);
$security = $this->prophesize(Security::class);
$security->isGranted($role, Argument::type(AccompanyingPeriod::class))->willReturn(true);
$security->getUser()->willReturn($user);
$repository = new ActivityACLAwareRepository(
$authorizationHelper->reveal(),
$centerResolver->reveal(),
$this->activityRepository,
$this->entityManager,
$security->reveal(),
$this->requestStack
);
$actual = $repository->findByPerson($person, $role, $start, $limit, $orderBy, $filters);
self::assertIsArray($actual);
}
public function provideDataFindByPerson(): iterable
{
$this->setUp();
/** @var Person $person */
if (null === $person = $this->entityManager->createQueryBuilder()
->select('p')->from(Person::class, 'p')->setMaxResults(1)
->getQuery()->getSingleResult()) {
throw new \RuntimeException("person not found");
}
/** @var AccompanyingPeriod $period1 */
if (null === $period1 = $this->entityManager
->createQueryBuilder()
->select('a')
->from(AccompanyingPeriod::class, 'a')
->setMaxResults(1)
->getQuery()
->getSingleResult()) {
throw new \RuntimeException("no period found");
}
/** @var AccompanyingPeriod $period2 */
if (null === $period2 = $this->entityManager
->createQueryBuilder()
->select('a')
->from(AccompanyingPeriod::class, 'a')
->where('a.id > :pid')
->setParameter('pid', $period1->getId())
->setMaxResults(1)
->getQuery()
->getSingleResult()) {
throw new \RuntimeException("no second period found");
}
// add a period
$period1->addPerson($person);
$period2->addPerson($person);
$period1->getParticipationsContainsPerson($person)->first()->setEndDate(
(new \DateTime('now'))->add(new \DateInterval('P1M'))
);
if ([] === $types = $this->entityManager
->createQueryBuilder()
->select('t')
->from(ActivityType::class, 't')
->setMaxResults(2)
->getQuery()
->getResult()) {
throw new \RuntimeException("no types");
}
if ([] === $jobs = $this->entityManager
->createQueryBuilder()
->select('j')
->from(UserJob::class, 'j')
->setMaxResults(2)
->getQuery()
->getResult()
) {
throw new \RuntimeException("no jobs found");
}
if (null === $user = $this->entityManager
->createQueryBuilder()
->select('u')
->from(User::class, 'u')
->setMaxResults(1)
->getQuery()
->getSingleResult()
) {
throw new \RuntimeException("no user found");
}
if ([] === $centers = $this->entityManager->createQueryBuilder()
->select('c')->from(Center::class, 'c')->setMaxResults(2)->getQuery()
->getResult()) {
throw new \RuntimeException("no centers found");
}
if ([] === $scopes = $this->entityManager->createQueryBuilder()
->select('s')->from(Scope::class, 's')->setMaxResults(2)->getQuery()
->getResult()) {
throw new \RuntimeException("no scopes found");
}
yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], []];
yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['my_activities' => true]];
yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['types' => $types]];
yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['jobs' => $jobs]];
yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago')]];
yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['before' => new \DateTimeImmutable('1 year ago')]];
yield [$person, $user, $centers, $scopes, ActivityVoter::SEE, 0, 5, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago'), 'before' => new \DateTimeImmutable('1 month ago')]];
}
public function provideDataFindByAccompanyingPeriod(): iterable
{
$this->setUp();
if (null === $period = $this->entityManager
->createQueryBuilder()
->select('a')
->from(AccompanyingPeriod::class, 'a')
->setMaxResults(1)
->getQuery()
->getSingleResult()) {
throw new \RuntimeException("no period found");
}
if ([] === $types = $this->entityManager
->createQueryBuilder()
->select('t')
->from(ActivityType::class, 't')
->setMaxResults(2)
->getQuery()
->getResult()) {
throw new \RuntimeException("no types");
}
if ([] === $jobs = $this->entityManager
->createQueryBuilder()
->select('j')
->from(UserJob::class, 'j')
->setMaxResults(2)
->getQuery()
->getResult()
) {
throw new \RuntimeException("no jobs found");
}
if (null === $user = $this->entityManager
->createQueryBuilder()
->select('u')
->from(User::class, 'u')
->setMaxResults(1)
->getQuery()
->getSingleResult()
) {
throw new \RuntimeException("no user found");
}
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], []];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['my_activities' => true]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['types' => $types]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['jobs' => $jobs]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago')]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['before' => new \DateTimeImmutable('1 year ago')]];
yield [$period, $user, ActivityVoter::SEE, 0, 10, ['date' => 'DESC'], ['after' => new \DateTimeImmutable('1 year ago'), 'before' => new \DateTimeImmutable('1 month ago')]];
}
}

View File

@@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ActivityBundle\Tests\Repository;
use Chill\ActivityBundle\Repository\ActivityDocumentACLAwareRepository;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\DocStoreBundle\GenericDoc\FetchQueryToSqlBuilder;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\EntityManagerInterface;
use phpseclib3\Math\BinaryField;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Security\Core\Security;
/**
* @internal
* @coversNothing
*/
class ActivityDocumentACLAwareRepositoryTest extends KernelTestCase
{
use ProphecyTrait;
private EntityManagerInterface $entityManager;
private CenterResolverManagerInterface $centerResolverManager;
private AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser;
private Security $security;
protected function setUp(): void
{
self::bootKernel();
$this->entityManager = self::$container->get(EntityManagerInterface::class);
$this->centerResolverManager = self::$container->get(CenterResolverManagerInterface::class);
$this->authorizationHelperForCurrentUser = self::$container->get(AuthorizationHelperForCurrentUserInterface::class);
$this->security = self::$container->get(Security::class);
}
/**
* @dataProvider provideDataForPerson
*/
public function testBuildFetchQueryActivityDocumentLinkedToPersonFromPersonContext(Person $person, array $reachableScopes, bool $_unused, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?string $content): void
{
$authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class);
$authorizationHelper->getReachableScopes(ActivityVoter::SEE, Argument::any())
->willReturn($reachableScopes);
$repository = new ActivityDocumentACLAwareRepository(
$this->entityManager,
$this->centerResolverManager,
$authorizationHelper->reveal(),
$this->security
);
$query = $repository->buildFetchQueryActivityDocumentLinkedToPersonFromPersonContext($person, $startDate, $endDate, $content);
['sql' => $sql, 'params' => $params, 'types' => $types] = (new FetchQueryToSqlBuilder())->toSql($query);
$nb = $this->entityManager->getConnection()->fetchOne("SELECT COUNT(*) FROM ({$sql}) sq", $params, $types);
self::assertIsInt($nb);
}
/**
* @dataProvider provideDataForPerson
*/
public function testBuildFetchQueryActivityDocumentLinkedToAccompanyingPeriodFromPersonContext(Person $person, array $_unused, bool $canSeePeriod, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?string $content): void
{
$security = $this->prophesize(Security::class);
$security->isGranted(ActivityVoter::SEE, Argument::type(AccompanyingPeriod::class))
->willReturn($canSeePeriod);
$repository = new ActivityDocumentACLAwareRepository(
$this->entityManager,
$this->centerResolverManager,
$this->authorizationHelperForCurrentUser,
$security->reveal()
);
$query = $repository->buildFetchQueryActivityDocumentLinkedToAccompanyingPeriodFromPersonContext($person, $startDate, $endDate, $content);
['sql' => $sql, 'params' => $params, 'types' => $types] = (new FetchQueryToSqlBuilder())->toSql($query);
$nb = $this->entityManager->getConnection()->fetchOne("SELECT COUNT(*) FROM ({$sql}) sq", $params, $types);
self::assertIsInt($nb);
}
public function provideDataForPerson(): iterable
{
$this->setUp();
if (null === $person = $this->entityManager->createQuery("SELECT p FROM " . Person::class . " p WHERE SIZE(p.accompanyingPeriodParticipations) > 0 ")
->setMaxResults(1)
->getSingleResult()) {
throw new \RuntimeException("no person in dtabase");
}
if ([] === $scopes = $this->entityManager->createQuery("SELECT s FROM " . Scope::class . " s ")->setMaxResults(5)->getResult()) {
throw new \RuntimeException("no scopes in database");
}
yield [$person, [], true, null, null, null];
yield [$person, $scopes, true, null, null, null];
yield [$person, $scopes, true, new \DateTimeImmutable("1 month ago"), null, null];
yield [$person, $scopes, true, new \DateTimeImmutable("1 month ago"), new \DateTimeImmutable("1 week ago"), null];
yield [$person, $scopes, true, new \DateTimeImmutable("1 month ago"), new \DateTimeImmutable("1 week ago"), "content"];
yield [$person, $scopes, true, null, new \DateTimeImmutable("1 week ago"), "content"];
yield [$person, [], true, new \DateTimeImmutable("1 month ago"), new \DateTimeImmutable("1 week ago"), "content"];
}
}

View File

@@ -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;
}

View File

@@ -38,3 +38,6 @@ services:
Chill\ActivityBundle\Service\EntityInfo\:
resource: '../Service/EntityInfo/'
Chill\ActivityBundle\Service\GenericDoc\:
resource: '../Service/GenericDoc/'

View File

@@ -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:

View File

@@ -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}

View File

@@ -83,12 +83,20 @@ Third persons: Tiers non-pro.
Others persons: Usagers
Third parties: Tiers professionnels
Users concerned: T(M)S
activity:
date: Date de l'échange
Insert a document: Insérer un document
Remove a document: Supprimer le document
comment: Commentaire
No documents: Aucun document
# activity filter in list page
activity_filter:
My activities: Mes échanges (où j'interviens)
Types: Par type d'échange
Jobs: Par métier impliqué
#timeline
'%user% has done an %activity_type%': '%user% a effectué un échange de type "%activity_type%"'
@@ -365,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:
@@ -372,3 +386,11 @@ 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:
keys:
accompanying_period_activity_document: Document des échanges des parcours

View File

@@ -50,6 +50,10 @@ class ByActivityTypeAggregator implements AggregatorInterface
{
// No form needed
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data)
{

View File

@@ -57,6 +57,10 @@ class ByUserJobAggregator implements AggregatorInterface
{
// nothing to add in the form
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data)
{

View File

@@ -57,6 +57,10 @@ class ByUserScopeAggregator implements AggregatorInterface
{
// nothing to add in the form
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data)
{

View File

@@ -34,6 +34,10 @@ class AvgAsideActivityDuration implements ExportInterface, GroupedExportInterfac
public function buildForm(FormBuilderInterface $builder)
{
}
public function getFormDefaultData(): array
{
return [];
}
public function getAllowedFormattersTypes(): array
{

View File

@@ -34,6 +34,10 @@ class CountAsideActivity implements ExportInterface, GroupedExportInterface
public function buildForm(FormBuilderInterface $builder)
{
}
public function getFormDefaultData(): array
{
return [];
}
public function getAllowedFormattersTypes(): array
{

View File

@@ -73,6 +73,10 @@ final class ListAsideActivity implements ListInterface, GroupedExportInterface
public function buildForm(FormBuilderInterface $builder)
{
}
public function getFormDefaultData(): array
{
return [];
}
public function getAllowedFormattersTypes()
{

View File

@@ -34,6 +34,10 @@ class SumAsideActivityDuration implements ExportInterface, GroupedExportInterfac
public function buildForm(FormBuilderInterface $builder)
{
}
public function getFormDefaultData(): array
{
return [];
}
public function getAllowedFormattersTypes(): array
{

View File

@@ -76,6 +76,10 @@ class ByActivityTypeFilter implements FilterInterface
},
]);
}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string'): array
{

View File

@@ -72,11 +72,9 @@ class ByDateFilter implements FilterInterface
$builder
->add('date_from', PickRollingDateType::class, [
'label' => 'export.filter.Aside activities after this date',
'data' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START),
])
->add('date_to', PickRollingDateType::class, [
'label' => 'export.filter.Aside activities before this date',
'data' => new RollingDate(RollingDate::T_TODAY),
]);
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) {
@@ -119,6 +117,10 @@ class ByDateFilter implements FilterInterface
}
});
}
public function getFormDefaultData(): array
{
return ['date_from' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), 'date_to' => new RollingDate(RollingDate::T_TODAY)];
}
public function describeAction($data, $format = 'string'): array
{

View File

@@ -53,6 +53,10 @@ class ByUserFilter implements FilterInterface
'label' => 'Creators',
]);
}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string'): array
{

View File

@@ -60,6 +60,10 @@ class ByUserJobFilter implements FilterInterface
'expanded' => true,
]);
}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string')
{

View File

@@ -67,6 +67,10 @@ class ByUserScopeFilter implements FilterInterface
'expanded' => true,
]);
}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string')
{

View File

@@ -176,11 +176,12 @@ export:
agent_id: Utilisateur
creator_id: Créateur
main_scope: Service principal de l'utilisateur
main_center: Centre principal de l'utilisteur
main_center: Centre principal de l'utilisateur
aside_activity_type: Catégorie d'activité annexe
date: Date
duration: Durée
note: Note
id: Identifiant
Exports of aside activities: Exports des activités annexes
Count aside activities: Nombre d'activités annexes

View File

@@ -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',

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\CalendarBundle\Exception;
class UserAbsenceSyncException extends \LogicException
{
public function __construct(string $message = "", int $code = 20_230_706, ?\Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View File

@@ -58,6 +58,10 @@ final class AgentAggregator implements AggregatorInterface
{
// no form
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data): Closure
{

View File

@@ -59,6 +59,10 @@ class CancelReasonAggregator implements AggregatorInterface
{
// no form
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data): Closure
{

View File

@@ -58,6 +58,10 @@ final class JobAggregator implements AggregatorInterface
{
// no form
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data): Closure
{

View File

@@ -52,6 +52,10 @@ final class LocationAggregator implements AggregatorInterface
{
// no form
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data): Closure
{

View File

@@ -58,6 +58,10 @@ final class LocationTypeAggregator implements AggregatorInterface
{
// no form
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data): Closure
{

Some files were not shown because too many files have changed in this diff Show More