mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-20 21:54:59 +00:00
Compare commits
148 Commits
Author | SHA1 | Date | |
---|---|---|---|
20291026fb
|
|||
85d6765178 | |||
30955752c3 | |||
f7f7e1cb1d | |||
651c455bdf | |||
d50d067bf7 | |||
46c647cbb7 | |||
da83b1e98c | |||
536c2622c7 | |||
cb70c322a4 | |||
89c231de41
|
|||
c773f9c6db
|
|||
4c05d1e026 | |||
3e2ff463bc
|
|||
59005e83c4 | |||
|
ab6feef371 | ||
8516e89c9c | |||
4091efc72e | |||
b577bd7123 | |||
dbb9feb129 | |||
e8b95f8491 | |||
8de37a9ef3 | |||
8fd6986c47
|
|||
807f1b4aa1
|
|||
f3002631ea
|
|||
9e667d4de4 | |||
fc88a5f40d | |||
9ff7aef3fc | |||
4f08019618
|
|||
2a58330832 | |||
a2cea3df02
|
|||
9ac43ecf5b | |||
f78f5e8419
|
|||
ccf3324bc2
|
|||
dfe780f0f5 | |||
dd056efa0d | |||
18c0b6a47f | |||
df0afcd228
|
|||
d66933c8b5 | |||
0ff51b0a5c | |||
d7f4895248
|
|||
7aee722957 | |||
5880858191 | |||
96105b101f | |||
d29415317b | |||
2ad3bbe96f | |||
1d636f5e9e
|
|||
f0dbb17172 | |||
f1dbc17dad | |||
09578a775c
|
|||
c888b5b84f | |||
27d76d9579 | |||
5b714f17be | |||
bbb167bb85 | |||
d713087dcb | |||
569aeeef87 | |||
97f2c75de8 | |||
4a2078dc65 | |||
00444e1e56 | |||
f02c5bca13
|
|||
0d56828ebd
|
|||
8b28667fe5
|
|||
72f73ec8e7
|
|||
b3d1320c94 | |||
2ed42e1a2c
|
|||
d0e5ba16fe | |||
8e65ad9476
|
|||
cf7338b690
|
|||
63dd71037a
|
|||
cc281762b3
|
|||
aa0cadfa84
|
|||
6e2cce9531
|
|||
1fbbf2b2ad
|
|||
e586b8ee5e
|
|||
6d04e477f8
|
|||
6b7b2ae522
|
|||
9b9c2774ad
|
|||
e902b6d409
|
|||
d8bf6a195f
|
|||
7c3152f277
|
|||
cef218fed5
|
|||
930a76cc66
|
|||
f11f7498d7
|
|||
1a9af6b0b1
|
|||
d347f6ae60
|
|||
3bb911b4d0
|
|||
f00b39980c
|
|||
09882bb4be
|
|||
1d21499eab
|
|||
8ef001e67e | |||
458df45fa5 | |||
2b968b9a5b | |||
f1fa4d415e
|
|||
2312a8d46f | |||
67c3de733f
|
|||
c05451bfe9
|
|||
8be9fb6553 | |||
f5f6eb78a2
|
|||
7a7e66146b | |||
4bbad4fc61
|
|||
86613a9be9
|
|||
21bd6478ad
|
|||
5849d8d670
|
|||
568ee079b5
|
|||
bf97b2a50c
|
|||
01785ed494
|
|||
97d401b7f6
|
|||
44ccfe92b6
|
|||
b6ea857389
|
|||
f8840d89bf
|
|||
813f2f1e12
|
|||
4a15a89102
|
|||
c707a34f16
|
|||
0c9010f065 | |||
3871299346 | |||
e2e0b08210
|
|||
4df0542932 | |||
13854e59de
|
|||
574ad42a76
|
|||
4736fca679
|
|||
32ae2f8f0d
|
|||
d58c0a867d
|
|||
15f8432ce0
|
|||
ae7637acc6 | |||
ce391a6de8 | |||
950835c10b
|
|||
9ba557a5bf | |||
439fecd69f
|
|||
f02168950f
|
|||
58c2235b88 | |||
42c5577027 | |||
036fe8d6f8
|
|||
51ebc253aa | |||
4fdc7fd210 | |||
0bf6c07e8d
|
|||
7a12602699
|
|||
15a927a9f8
|
|||
0a2805f23f | |||
27ce322690
|
|||
469e379166 | |||
f103b228e4 | |||
d2a31de1be | |||
138a537d2b | |||
c06c861e17 | |||
34cbd2605c | |||
044bab45ad | |||
b9890d1302 | |||
5b2a2a1bc5 |
15
.changes/v2.16.0.md
Normal file
15
.changes/v2.16.0.md
Normal file
@@ -0,0 +1,15 @@
|
||||
## v2.16.0 - 2024-02-08
|
||||
### Feature
|
||||
* ([#231](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/231)) Create new filter for persons having a participation in an accompanying period during a certain time span
|
||||
* ([#241](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/241)) [Export][List of accompanyign period] Add two columns: the list of persons participating to the period, and their ids
|
||||
* ([#244](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/244)) Add capability to generate export about change of steps of accompanying period, and generate exports for this
|
||||
* ([#253](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/253)) Export: group accompanying period by person participating
|
||||
* ([#243](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/243)) Export: add filter for courses not linked to a reference address
|
||||
* ([#229](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/229)) Allow to group activities linked with accompanying period by reason
|
||||
* ([#115](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/115)) Prevent social work to be saved when another user edited conccurently the social work
|
||||
* Modernize the event bundle, with some new fields and multiple improvements
|
||||
### Fixed
|
||||
* ([#220](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/220)) Fix error in logs about wrong typing of eventArgs in onEditNotificationComment method
|
||||
* ([#256](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/256)) Fix the conditions upon which social actions should be optional or required in relation to social issues within the activity creation form
|
||||
### UX
|
||||
* ([#260](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/260)) Order list of centers alphabetically in dropdown 'user' section admin.
|
3
.changes/v2.16.1.md
Normal file
3
.changes/v2.16.1.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## v2.16.1 - 2024-02-09
|
||||
### Fixed
|
||||
* Force bootstrap version to avoid error in builds with newer version
|
3
.changes/v2.16.2.md
Normal file
3
.changes/v2.16.2.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## v2.16.2 - 2024-02-21
|
||||
### Fixed
|
||||
* Check for null values in closing motive of parcours d'accompagnement for correct rendering of template
|
5
.changes/v2.16.3.md
Normal file
5
.changes/v2.16.3.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## v2.16.3 - 2024-02-26
|
||||
### Fixed
|
||||
* ([#236](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/236)) Fix translation of user job -> 'service' must be 'métier'
|
||||
### UX
|
||||
* ([#232](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/232)) Order user jobs and services alphabetically in export filters
|
9
.changes/v2.17.0.md
Normal file
9
.changes/v2.17.0.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## v2.17.0 - 2024-03-19
|
||||
### Feature
|
||||
* ([#237](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/237)) New export filter for social actions with an evaluation created between two dates
|
||||
* ([#258](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/258)) In the list of accompangying period, add the list of person's centers and the duration of the course
|
||||
* ([#238](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/238)) Allow to customize list person with new fields
|
||||
* ([#159](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/159)) Admin can publish news on the homepage
|
||||
### Fixed
|
||||
* ([#264](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/264)) Fix languages: load the languages in all availables languages configured for Chill
|
||||
* ([#259](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/259)) Keep a consistent behaviour between the filtering of activities within the document generation (model "accompanying period with activities"), and the same filter in the list of activities for an accompanying period
|
5
.changes/v2.18.0.md
Normal file
5
.changes/v2.18.0.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## v2.18.0 - 2024-03-26
|
||||
### Feature
|
||||
* ([#268](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/268)) Improve admin UX to configure document templates for document generation
|
||||
### Fixed
|
||||
* ([#267](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/267)) Fix the join between job and user in the user list (admin): show only the current user job
|
3
.changes/v2.18.1.md
Normal file
3
.changes/v2.18.1.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## v2.18.1 - 2024-03-26
|
||||
### Fixed
|
||||
* Fix layout issue in document generation for admin (minor)
|
3
.changes/v2.18.2.md
Normal file
3
.changes/v2.18.2.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## v2.18.2 - 2024-04-12
|
||||
### Fixed
|
||||
* ([#250](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/250)) Postal codes import : fix the source URL and the keys to handle each record
|
20
.changes/v2.19.0.md
Normal file
20
.changes/v2.19.0.md
Normal file
@@ -0,0 +1,20 @@
|
||||
## v2.19.0 - 2024-05-14
|
||||
### Feature
|
||||
* ([#197](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/197)) Make the script which subscribe to microsoft calendars changes more tolerant to errors or missing configuration on the microsoft side
|
||||
* ([#276](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/276)) Take closing date into account when computing the geographical unit on accompanying period. When a person moved after an accompanying period is closed, the date of closing accompanying period is took into account if it is earlier than the date given by the user.
|
||||
### Fixed
|
||||
* ([#270](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/270)) Fix broken link in homepage when a evaluation from a closed acc period was present in the homepage widget
|
||||
* ([#275](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/275)) Allow the filter "filter accompanying period by geographical unit" to take period's location on address into account
|
||||
### UX
|
||||
* Form for document generation moved to the top of document list page
|
||||
* ([#266](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/266)) Event bundle: adjust certain graphical issues for better user experience
|
||||
|
||||
|
||||
### Traduction francophone des principaux changements
|
||||
|
||||
- script de synchronisation des agendas de microsoft Outlook: le script est plus tolérant aux erreurs de configuration côté serveur (manque de droit d'accès);
|
||||
- dans les statistiques sur les parcours d'accompagnements, regroupement et filtre par unité géographique: lorsque la date de prise en compte de l'adresse est postérieure à la fermeture du parcours, c'est la date de fermeture du parcours qui est prise en compte (cela permet de tenir compte de la localisation de l'usager au moment de la fermeture dans le cas où celui-ci aurait déménagé par la suite);
|
||||
- sur la page d'accueil, il n'y a plus de rappel pour les évaluations pour les parcours cloturés;
|
||||
- correction du filtre "filtrer par zone géographique"
|
||||
- répétition du bouton pour générer un document en haut de la page "liste des documents", quand il y a plus de cinq documents;
|
||||
- module événement: améliorerations graphiques
|
@@ -23,3 +23,7 @@ max_line_length = 0
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
[.rst]
|
||||
ident_size = 3
|
||||
ident_style = space
|
||||
|
||||
|
@@ -34,6 +34,8 @@ variables:
|
||||
DEFAULT_CARRIER_CODE: BE
|
||||
# force a timezone
|
||||
TZ: Europe/Brussels
|
||||
# avoid direct deprecations (using symfony phpunit bridge: https://symfony.com/doc/4.x/components/phpunit_bridge.html#internal-deprecations
|
||||
SYMFONY_DEPRECATIONS_HELPER: max[total]=99999999&max[self]=0&max[direct]=0&verbose=1
|
||||
|
||||
stages:
|
||||
- Composer install
|
||||
|
75
CHANGELOG.md
75
CHANGELOG.md
@@ -6,6 +6,81 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
|
||||
|
||||
## v2.19.0 - 2024-05-14
|
||||
### Feature
|
||||
* ([#197](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/197)) Make the script which subscribe to microsoft calendars changes more tolerant to errors or missing configuration on the microsoft side
|
||||
* ([#276](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/276)) Take closing date into account when computing the geographical unit on accompanying period. When a person moved after an accompanying period is closed, the date of closing accompanying period is took into account if it is earlier than the date given by the user.
|
||||
### Fixed
|
||||
* ([#270](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/270)) Fix broken link in homepage when a evaluation from a closed acc period was present in the homepage widget
|
||||
* ([#275](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/275)) Allow the filter "filter accompanying period by geographical unit" to take period's location on address into account
|
||||
### UX
|
||||
* Form for document generation moved to the top of document list page
|
||||
* ([#266](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/266)) Event bundle: adjust certain graphical issues for better user experience
|
||||
|
||||
|
||||
### Traduction francophone des principaux changements
|
||||
|
||||
- script de synchronisation des agendas de microsoft Outlook: le script est plus tolérant aux erreurs de configuration côté serveur (manque de droit d'accès);
|
||||
- dans les statistiques sur les parcours d'accompagnements, regroupement et filtre par unité géographique: lorsque la date de prise en compte de l'adresse est postérieure à la fermeture du parcours, c'est la date de fermeture du parcours qui est prise en compte (cela permet de tenir compte de la localisation de l'usager au moment de la fermeture dans le cas où celui-ci aurait déménagé par la suite);
|
||||
- sur la page d'accueil, il n'y a plus de rappel pour les évaluations pour les parcours cloturés;
|
||||
- correction du filtre "filtrer par zone géographique"
|
||||
- répétition du bouton pour générer un document en haut de la page "liste des documents", quand il y a plus de cinq documents;
|
||||
- module événement: améliorerations graphiques
|
||||
|
||||
## v2.18.2 - 2024-04-12
|
||||
### Fixed
|
||||
* ([#250](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/250)) Postal codes import : fix the source URL and the keys to handle each record
|
||||
|
||||
## v2.18.1 - 2024-03-26
|
||||
### Fixed
|
||||
* Fix layout issue in document generation for admin (minor)
|
||||
|
||||
## v2.18.0 - 2024-03-26
|
||||
### Feature
|
||||
* ([#268](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/268)) Improve admin UX to configure document templates for document generation
|
||||
### Fixed
|
||||
* ([#267](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/267)) Fix the join between job and user in the user list (admin): show only the current user job
|
||||
|
||||
## v2.17.0 - 2024-03-19
|
||||
### Feature
|
||||
* ([#237](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/237)) New export filter for social actions with an evaluation created between two dates
|
||||
* ([#258](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/258)) In the list of accompangying period, add the list of person's centers and the duration of the course
|
||||
* ([#238](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/238)) Allow to customize list person with new fields
|
||||
* ([#159](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/159)) Admin can publish news on the homepage
|
||||
### Fixed
|
||||
* ([#264](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/264)) Fix languages: load the languages in all availables languages configured for Chill
|
||||
* ([#259](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/259)) Keep a consistent behaviour between the filtering of activities within the document generation (model "accompanying period with activities"), and the same filter in the list of activities for an accompanying period
|
||||
|
||||
## v2.16.3 - 2024-02-26
|
||||
### Fixed
|
||||
* ([#236](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/236)) Fix translation of user job -> 'service' must be 'métier'
|
||||
### UX
|
||||
* ([#232](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/232)) Order user jobs and services alphabetically in export filters
|
||||
|
||||
## v2.16.2 - 2024-02-21
|
||||
### Fixed
|
||||
* Check for null values in closing motive of parcours d'accompagnement for correct rendering of template
|
||||
|
||||
## v2.16.1 - 2024-02-09
|
||||
### Fixed
|
||||
* Force bootstrap version to avoid error in builds with newer version
|
||||
|
||||
## v2.16.0 - 2024-02-08
|
||||
### Feature
|
||||
* ([#231](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/231)) Create new filter for persons having a participation in an accompanying period during a certain time span
|
||||
* ([#241](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/241)) [Export][List of accompanyign period] Add two columns: the list of persons participating to the period, and their ids
|
||||
* ([#244](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/244)) Add capability to generate export about change of steps of accompanying period, and generate exports for this
|
||||
* ([#253](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/253)) Export: group accompanying period by person participating
|
||||
* ([#243](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/243)) Export: add filter for courses not linked to a reference address
|
||||
* ([#229](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/229)) Allow to group activities linked with accompanying period by reason
|
||||
* ([#115](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/115)) Prevent social work to be saved when another user edited conccurently the social work
|
||||
* Modernize the event bundle, with some new fields and multiple improvements
|
||||
### Fixed
|
||||
* ([#220](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/220)) Fix error in logs about wrong typing of eventArgs in onEditNotificationComment method
|
||||
* ([#256](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/256)) Fix the conditions upon which social actions should be optional or required in relation to social issues within the activity creation form
|
||||
### UX
|
||||
* ([#260](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/260)) Order list of centers alphabetically in dropdown 'user' section admin.
|
||||
|
||||
## v2.15.2 - 2024-01-11
|
||||
### Fixed
|
||||
* Fix the id_seq used when creating a new accompanying period participation during fusion of two person files
|
||||
|
@@ -242,3 +242,129 @@ This is an example of the *filter by birthdate*. This filter asks some informati
|
||||
Continue to explain the export framework
|
||||
|
||||
.. _main bundle: https://git.framasoft.org/Chill-project/Chill-Main
|
||||
|
||||
|
||||
With many-to-* relationship, why should we set WHERE clauses in an EXISTS subquery instead of a JOIN ?
|
||||
``````````````````````````````````````````````````````````````````````````````````````````````````````
|
||||
|
||||
As we described above, the doctrine builder is converted into a sql query. Let's see how to compute the "number of course
|
||||
which count at least one activity type with the id 7". For the purpose of this demonstration, we will restrict this on
|
||||
two accompanying period only: the ones with id 329 and 334.
|
||||
|
||||
Let's see the list of activities associated with those accompanying period:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
SELECT id, accompanyingperiod_id, type_id FROM activity WHERE accompanyingperiod_id IN (329, 334) AND type_id = 7
|
||||
ORDER BY accompanyingperiod_id;
|
||||
|
||||
We see that we have 6 activities for the accompanying period with id 329, and only one for the 334's one.
|
||||
|
||||
.. csv-table::
|
||||
:header: id, accompanyingperiod_id, type_id
|
||||
|
||||
990,329,7
|
||||
986,329,7
|
||||
987,329,7
|
||||
993,329,7
|
||||
991,329,7
|
||||
992,329,7
|
||||
1000,334,7
|
||||
|
||||
Let's calculate the average duration for those accompanying periods, and the number of period:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
SELECT AVG(age(COALESCE(closingdate, CURRENT_DATE), openingdate)), COUNT(id) from chill_person_accompanying_period WHERE id IN (329, 334);
|
||||
|
||||
The result of this query is:
|
||||
|
||||
.. csv-table::
|
||||
:header: AVG, COUNT
|
||||
|
||||
2 years 2 mons 21 days 12 hours 0 mins 0.0 secs,2
|
||||
|
||||
Now, we count the number of accompanying period, adding a :code:`JOIN` clause which make a link to the :code:`activity` table, and add a :code:`WHERE` clause to keep
|
||||
only the accompanying period which contains the given activity type:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
SELECT COUNT(chill_person_accompanying_period.id) from chill_person_accompanying_period
|
||||
JOIN activity ON chill_person_accompanying_period.id = activity.accompanyingperiod_id
|
||||
WHERE chill_person_accompanying_period.id IN (329, 334) AND activity.type_id = 7;
|
||||
|
||||
What are the results here ?
|
||||
|
||||
.. csv-table::
|
||||
:header: COUNT
|
||||
|
||||
7
|
||||
|
||||
:code:`7` ! Why this result ? Because the number of lines is duplicated for each activity. Let's see the list of rows which
|
||||
are taken into account for the computation:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
SELECT chill_person_accompanying_period.id, activity.id from chill_person_accompanying_period
|
||||
JOIN activity ON chill_person_accompanying_period.id = activity.accompanyingperiod_id
|
||||
WHERE chill_person_accompanying_period.id IN (329, 334) AND activity.type_id = 7;
|
||||
|
||||
.. csv-table::
|
||||
:header: accompanyingperiod.id, activity.id
|
||||
|
||||
329,993
|
||||
334,1000
|
||||
329,987
|
||||
329,990
|
||||
329,991
|
||||
329,992
|
||||
329,986
|
||||
|
||||
For each activity, a row is created and, as we count the number of non-null :code:`accompanyingperiod.id` columns, we
|
||||
count one entry for each activity (actually, we count the number of activities).
|
||||
|
||||
So, let's use the :code:`DISTINCT` keyword to count only once the equal ids:
|
||||
|
||||
.. code-block::
|
||||
|
||||
SELECT COUNT(DISTINCT chill_person_accompanying_period.id) from chill_person_accompanying_period
|
||||
JOIN activity ON chill_person_accompanying_period.id = activity.accompanyingperiod_id
|
||||
WHERE chill_person_accompanying_period.id IN (329, 334) AND activity.type_id = 7;
|
||||
|
||||
Now, it works again...
|
||||
|
||||
.. csv-table::
|
||||
:header: COUNT
|
||||
|
||||
2
|
||||
|
||||
But, for the average duration, this won't work: the duration which are equals (because the :code:`openingdate` is the same and
|
||||
:code:`closingdate` is still :code:`NULL`, for instance) will be counted only once, which will give unexpected result.
|
||||
|
||||
The solution is to move the condition "having an activity with activity type with id 7" in a :code:`EXISTS` clause:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
SELECT COUNT(chill_person_accompanying_period.id) from chill_person_accompanying_period
|
||||
WHERE chill_person_accompanying_period.id IN (329, 334) AND EXISTS (SELECT 1 FROM activity WHERE type_id = 7 AND accompanyingperiod_id = chill_person_accompanying_period.id);
|
||||
|
||||
The result is correct without :code:`DISTINCT` keyword:
|
||||
|
||||
.. csv-table::
|
||||
:header: COUNT
|
||||
|
||||
2
|
||||
|
||||
And we can now compute the average duration without fear:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
SELECT AVG(age(COALESCE(closingdate, CURRENT_DATE), openingdate)) from chill_person_accompanying_period
|
||||
WHERE chill_person_accompanying_period.id IN (329, 334) AND EXISTS (SELECT 1 FROM activity WHERE type_id = 7 AND accompanyingperiod_id = chill_person_accompanying_period.id);
|
||||
|
||||
Give the result:
|
||||
|
||||
.. csv-table::
|
||||
:header: AVG
|
||||
|
||||
2 years 2 mons 21 days 12 hours 0 mins 0.0 secs
|
||||
|
@@ -48,7 +48,7 @@ Clone or download the chill-skeleton project and `cd` into the main directory.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git clone https://gitlab.com/Chill-Projet/chill-skeleton-basic.git
|
||||
git clone https://gitea.champs-libres.be/Chill-project/chill-skeleton-basic.git
|
||||
cd chill-skeleton-basic
|
||||
|
||||
|
||||
|
@@ -8,6 +8,16 @@ Chill can store a list of geolocated address references, which are used to sugge
|
||||
|
||||
Those addresses may be load from a dedicated source.
|
||||
|
||||
Countries
|
||||
=========
|
||||
|
||||
In order to load addresses into the chill application we first have to make sure that a list of countries is present.
|
||||
To import the countries run the following command.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
bin/console chill:main:countries:populate
|
||||
|
||||
In France
|
||||
=========
|
||||
|
||||
|
@@ -5,72 +5,74 @@ Add condition with distinct alias on each export join clauses (Indicators + Filt
|
||||
|
||||
These are alias conventions :
|
||||
|
||||
| Entity | Join | Attribute | Alias |
|
||||
|:----------------------------------------|:----------------------------------------|:-------------------------------------------|:---------------------------------------|
|
||||
| AccompanyingPeriod::class | | | acp |
|
||||
| | AccompanyingPeriodWork::class | acp.works | acpw |
|
||||
| | AccompanyingPeriodParticipation::class | acp.participations | acppart |
|
||||
| | Location::class | acp.administrativeLocation | acploc |
|
||||
| | ClosingMotive::class | acp.closingMotive | acpmotive |
|
||||
| | UserJob::class | acp.job | acpjob |
|
||||
| | Origin::class | acp.origin | acporigin |
|
||||
| | Scope::class | acp.scopes | acpscope |
|
||||
| | 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 |
|
||||
| | SocialAction::class | acpw.socialAction | acpwsocialaction |
|
||||
| | Goal::class | acpw.goals | goal |
|
||||
| | Result::class | acpw.results | result |
|
||||
| AccompanyingPeriodParticipation::class | | | acppart |
|
||||
| | 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 |
|
||||
| | Center::class | person.center | center |
|
||||
| | HouseholdMember::class | partperson.householdParticipations | householdmember |
|
||||
| | MaritalStatus::class | person.maritalStatus | personmarital |
|
||||
| | VendeePerson::class | | vp |
|
||||
| | VendeePersonMineur::class | | vpm |
|
||||
| | CurrentPersonAddress::class | person.currentPersonAddress | currentPersonAddress (on a given date) |
|
||||
| ResidentialAddress::class | | | resaddr |
|
||||
| | ThirdParty::class | resaddr.hostThirdParty | tparty |
|
||||
| ThirdParty::class | | | tparty |
|
||||
| | ThirdPartyCategory::class | tparty.categories | tpartycat |
|
||||
| HouseholdMember::class | | | householdmember |
|
||||
| | Household::class | householdmember.household | household |
|
||||
| | Person::class | householdmember.person | memberperson |
|
||||
| | | memberperson.center | membercenter |
|
||||
| Household::class | | | household |
|
||||
| | HouseholdComposition::class | household.compositions | composition |
|
||||
| Activity::class | | | activity |
|
||||
| | Person::class | activity.person | actperson |
|
||||
| | AccompanyingPeriod::class | activity.accompanyingPeriod | acp |
|
||||
| | Person::class | activity\_person\_having\_activity.person | person\_person\_having\_activity |
|
||||
| | ActivityReason::class | activity\_person\_having\_activity.reasons | reasons\_person\_having\_activity |
|
||||
| | ActivityType::class | activity.activityType | acttype |
|
||||
| | Location::class | activity.location | actloc |
|
||||
| | SocialAction::class | activity.socialActions | actsocialaction |
|
||||
| | SocialIssue::class | activity.socialIssues | actsocialssue |
|
||||
| | ThirdParty::class | activity.thirdParties | acttparty |
|
||||
| | User::class | activity.user | actuser |
|
||||
| | User::class | activity.users | actusers |
|
||||
| | ActivityReason::class | activity.reasons | actreasons |
|
||||
| | Center::class | actperson.center | actcenter |
|
||||
| | Person::class | activity.createdBy | actcreator |
|
||||
| ActivityReason::class | | | actreasons |
|
||||
| | ActivityReasonCategory::class | actreason.category | actreasoncat |
|
||||
| Calendar::class | | | cal |
|
||||
| | CancelReason::class | cal.cancelReason | calcancel |
|
||||
| | Location::class | cal.location | calloc |
|
||||
| | User::class | cal.user | caluser |
|
||||
| VendeePerson::class | | | vp |
|
||||
| | SituationProfessionelle::class | vp.situationProfessionelle | vpprof |
|
||||
| | StatutLogement::class | vp.statutLogement | vplog |
|
||||
| | TempsDeTravail::class | vp.tempsDeTravail | vptt |
|
||||
| Entity | Join | Attribute | Alias |
|
||||
|:----------------------------------------|:----------------------------------------|:-------------------------------------------|:-------------------------------------------|
|
||||
| AccompanyingPeriodStepHistory::class | | | acpstephistory (contexte ACP_STEP_HISTORY) |
|
||||
| | AccompanyingPeriod::class | acpstephistory.period | acp |
|
||||
| AccompanyingPeriod::class | | | acp |
|
||||
| | AccompanyingPeriodWork::class | acp.works | acpw |
|
||||
| | AccompanyingPeriodParticipation::class | acp.participations | acppart |
|
||||
| | Location::class | acp.administrativeLocation | acploc |
|
||||
| | ClosingMotive::class | acp.closingMotive | acpmotive |
|
||||
| | UserJob::class | acp.job | acpjob |
|
||||
| | Origin::class | acp.origin | acporigin |
|
||||
| | Scope::class | acp.scopes | acpscope |
|
||||
| | 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 |
|
||||
| | SocialAction::class | acpw.socialAction | acpwsocialaction |
|
||||
| | Goal::class | acpw.goals | goal |
|
||||
| | Result::class | acpw.results | result |
|
||||
| AccompanyingPeriodParticipation::class | | | acppart |
|
||||
| | 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 |
|
||||
| | Center::class | person.center | center |
|
||||
| | HouseholdMember::class | partperson.householdParticipations | householdmember |
|
||||
| | MaritalStatus::class | person.maritalStatus | personmarital |
|
||||
| | VendeePerson::class | | vp |
|
||||
| | VendeePersonMineur::class | | vpm |
|
||||
| | CurrentPersonAddress::class | person.currentPersonAddress | currentPersonAddress (on a given date) |
|
||||
| ResidentialAddress::class | | | resaddr |
|
||||
| | ThirdParty::class | resaddr.hostThirdParty | tparty |
|
||||
| ThirdParty::class | | | tparty |
|
||||
| | ThirdPartyCategory::class | tparty.categories | tpartycat |
|
||||
| HouseholdMember::class | | | householdmember |
|
||||
| | Household::class | householdmember.household | household |
|
||||
| | Person::class | householdmember.person | memberperson |
|
||||
| | | memberperson.center | membercenter |
|
||||
| Household::class | | | household |
|
||||
| | HouseholdComposition::class | household.compositions | composition |
|
||||
| Activity::class | | | activity |
|
||||
| | Person::class | activity.person | actperson |
|
||||
| | AccompanyingPeriod::class | activity.accompanyingPeriod | acp |
|
||||
| | Person::class | activity\_person\_having\_activity.person | person\_person\_having\_activity |
|
||||
| | ActivityReason::class | activity\_person\_having\_activity.reasons | reasons\_person\_having\_activity |
|
||||
| | ActivityType::class | activity.activityType | acttype |
|
||||
| | Location::class | activity.location | actloc |
|
||||
| | SocialAction::class | activity.socialActions | actsocialaction |
|
||||
| | SocialIssue::class | activity.socialIssues | actsocialssue |
|
||||
| | ThirdParty::class | activity.thirdParties | acttparty |
|
||||
| | User::class | activity.user | actuser |
|
||||
| | User::class | activity.users | actusers |
|
||||
| | ActivityReason::class | activity.reasons | actreasons |
|
||||
| | Center::class | actperson.center | actcenter |
|
||||
| | Person::class | activity.createdBy | actcreator |
|
||||
| ActivityReason::class | | | actreasons |
|
||||
| | ActivityReasonCategory::class | actreason.category | actreasoncat |
|
||||
| Calendar::class | | | cal |
|
||||
| | CancelReason::class | cal.cancelReason | calcancel |
|
||||
| | Location::class | cal.location | calloc |
|
||||
| | User::class | cal.user | caluser |
|
||||
| VendeePerson::class | | | vp |
|
||||
| | SituationProfessionelle::class | vp.situationProfessionelle | vpprof |
|
||||
| | StatutLogement::class | vp.statutLogement | vplog |
|
||||
| | TempsDeTravail::class | vp.tempsDeTravail | vptt |
|
||||
|
@@ -15,7 +15,7 @@
|
||||
"@symfony/webpack-encore": "^4.1.0",
|
||||
"@tsconfig/node14": "^1.0.1",
|
||||
"bindings": "^1.5.0",
|
||||
"bootstrap": "^5.0.1",
|
||||
"bootstrap": "5.2.3",
|
||||
"chokidar": "^3.5.1",
|
||||
"fork-awesome": "^1.1.7",
|
||||
"jquery": "^3.6.0",
|
||||
|
@@ -291,7 +291,11 @@ class ActivityType
|
||||
public function checkSocialActionsVisibility(ExecutionContextInterface $context, mixed $payload)
|
||||
{
|
||||
if ($this->socialIssuesVisible !== $this->socialActionsVisible) {
|
||||
if (!(2 === $this->socialIssuesVisible && 1 === $this->socialActionsVisible)) {
|
||||
// if social issues are invisible then social actions cannot be optional or required + if social issues are optional then social actions shouldn't be required
|
||||
if (
|
||||
(0 === $this->socialIssuesVisible && (1 === $this->socialActionsVisible || 2 === $this->socialActionsVisible))
|
||||
|| (1 === $this->socialIssuesVisible && 2 === $this->socialActionsVisible)
|
||||
) {
|
||||
$context
|
||||
->buildViolation('The socialActionsVisible value is not compatible with the socialIssuesVisible value')
|
||||
->atPath('socialActionsVisible')
|
||||
|
@@ -57,7 +57,7 @@ final readonly class ByActivityTypeAggregator implements AggregatorInterface
|
||||
|
||||
public function getLabels($key, array $values, mixed $data)
|
||||
{
|
||||
return function (null|int|string $value): string {
|
||||
return function (int|string|null $value): string {
|
||||
if ('_header' === $value) {
|
||||
return 'export.aggregator.acp.by_activity_type.activity_type';
|
||||
}
|
||||
|
@@ -35,7 +35,7 @@ final readonly class ActivityPresenceAggregator implements AggregatorInterface
|
||||
|
||||
public function getLabels($key, array $values, mixed $data)
|
||||
{
|
||||
return function (null|int|string $value): string {
|
||||
return function (int|string|null $value): string {
|
||||
if ('_header' === $value) {
|
||||
return 'export.aggregator.activity.by_activity_presence.header';
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ declare(strict_types=1);
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\ActivityBundle\Export\Aggregator\PersonAggregators;
|
||||
namespace Chill\ActivityBundle\Export\Aggregator;
|
||||
|
||||
use Chill\ActivityBundle\Export\Declarations;
|
||||
use Chill\ActivityBundle\Repository\ActivityReasonCategoryRepository;
|
||||
@@ -25,8 +25,11 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
|
||||
class ActivityReasonAggregator implements AggregatorInterface, ExportElementValidatedInterface
|
||||
{
|
||||
public function __construct(protected ActivityReasonCategoryRepository $activityReasonCategoryRepository, protected ActivityReasonRepository $activityReasonRepository, protected TranslatableStringHelper $translatableStringHelper)
|
||||
{
|
||||
public function __construct(
|
||||
protected ActivityReasonCategoryRepository $activityReasonCategoryRepository,
|
||||
protected ActivityReasonRepository $activityReasonRepository,
|
||||
protected TranslatableStringHelper $translatableStringHelper
|
||||
) {
|
||||
}
|
||||
|
||||
public function addRole(): ?string
|
||||
@@ -51,7 +54,7 @@ class ActivityReasonAggregator implements AggregatorInterface, ExportElementVali
|
||||
|
||||
// make a jointure only if needed
|
||||
if (!\in_array('actreasons', $qb->getAllAliases(), true)) {
|
||||
$qb->innerJoin('activity.reasons', 'actreasons');
|
||||
$qb->leftJoin('activity.reasons', 'actreasons');
|
||||
}
|
||||
|
||||
// join category if necessary
|
||||
@@ -62,19 +65,12 @@ class ActivityReasonAggregator implements AggregatorInterface, ExportElementVali
|
||||
}
|
||||
}
|
||||
|
||||
// add the "group by" part
|
||||
$groupBy = $qb->getDQLPart('groupBy');
|
||||
|
||||
if (\count($groupBy) > 0) {
|
||||
$qb->addGroupBy($alias);
|
||||
} else {
|
||||
$qb->groupBy($alias);
|
||||
}
|
||||
$qb->addGroupBy($alias);
|
||||
}
|
||||
|
||||
public function applyOn(): string
|
||||
{
|
||||
return Declarations::ACTIVITY_PERSON;
|
||||
return Declarations::ACTIVITY;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
@@ -96,7 +92,9 @@ class ActivityReasonAggregator implements AggregatorInterface, ExportElementVali
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return [];
|
||||
return [
|
||||
'level' => 'reasons',
|
||||
];
|
||||
}
|
||||
|
||||
public function getLabels($key, array $values, $data)
|
@@ -58,7 +58,7 @@ class ActivityTypeAggregator implements AggregatorInterface
|
||||
|
||||
public function getLabels($key, array $values, $data): \Closure
|
||||
{
|
||||
return function (null|int|string $value): string {
|
||||
return function (int|string|null $value): string {
|
||||
if ('_header' === $value) {
|
||||
return 'Activity type';
|
||||
}
|
||||
|
@@ -80,7 +80,7 @@ final readonly class CreatorJobFilter implements FilterInterface
|
||||
{
|
||||
$builder
|
||||
->add('jobs', EntityType::class, [
|
||||
'choices' => $this->userJobRepository->findAllOrderedByName(),
|
||||
'choices' => $this->userJobRepository->findAllActive(),
|
||||
'class' => UserJob::class,
|
||||
'choice_label' => fn (UserJob $s) => $this->translatableStringHelper->localize(
|
||||
$s->getLabel()
|
||||
|
@@ -15,6 +15,7 @@ use Chill\ActivityBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Entity\User\UserScopeHistory;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
@@ -26,7 +27,8 @@ class CreatorScopeFilter implements FilterInterface
|
||||
private const PREFIX = 'acp_act_filter_creator_scope';
|
||||
|
||||
public function __construct(
|
||||
private readonly TranslatableStringHelper $translatableStringHelper
|
||||
private readonly TranslatableStringHelper $translatableStringHelper,
|
||||
private readonly ScopeRepositoryInterface $scopeRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -76,6 +78,7 @@ class CreatorScopeFilter implements FilterInterface
|
||||
$builder
|
||||
->add('scopes', EntityType::class, [
|
||||
'class' => Scope::class,
|
||||
'choices' => $this->scopeRepository->findAllActive(),
|
||||
'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize(
|
||||
$s->getName()
|
||||
),
|
||||
|
@@ -16,6 +16,7 @@ use Chill\ActivityBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Entity\User\UserJobHistory;
|
||||
use Chill\MainBundle\Entity\UserJob;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
@@ -27,7 +28,8 @@ class UsersJobFilter implements FilterInterface
|
||||
private const PREFIX = 'act_filter_user_job';
|
||||
|
||||
public function __construct(
|
||||
private readonly TranslatableStringHelperInterface $translatableStringHelper
|
||||
private readonly TranslatableStringHelperInterface $translatableStringHelper,
|
||||
private readonly UserJobRepositoryInterface $userJobRepository
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -69,6 +71,7 @@ class UsersJobFilter implements FilterInterface
|
||||
$builder
|
||||
->add('jobs', EntityType::class, [
|
||||
'class' => UserJob::class,
|
||||
'choices' => $this->userJobRepository->findAllActive(),
|
||||
'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()),
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
|
@@ -95,7 +95,7 @@ class ActivityType extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
/** @var \Chill\PersonBundle\Entity\AccompanyingPeriod|null $accompanyingPeriod */
|
||||
/** @var AccompanyingPeriod|null $accompanyingPeriod */
|
||||
$accompanyingPeriod = null;
|
||||
|
||||
if ($options['accompanyingPeriod'] instanceof AccompanyingPeriod) {
|
||||
|
@@ -243,7 +243,8 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos
|
||||
thirdparties.thirdpartyids,
|
||||
persons.personids,
|
||||
actions.socialactionids,
|
||||
issues.socialissueids
|
||||
issues.socialissueids,
|
||||
a.user_id
|
||||
|
||||
FROM activity a
|
||||
LEFT JOIN chill_main_location location ON a.location_id = location.id
|
||||
@@ -283,6 +284,7 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos
|
||||
->addJoinedEntityResult(ActivityPresence::class, 'activityPresence', 'a', 'attendee')
|
||||
->addFieldResult('activityPresence', 'presence_id', 'id')
|
||||
->addFieldResult('activityPresence', 'presence_name', 'name')
|
||||
->addScalarResult('user_id', 'userId', Types::INTEGER)
|
||||
|
||||
// results which cannot be mapped into entity
|
||||
->addScalarResult('comment_comment', 'comment', Types::TEXT)
|
||||
|
@@ -36,14 +36,14 @@ final readonly class ActivityDocumentACLAwareRepository implements ActivityDocum
|
||||
) {
|
||||
}
|
||||
|
||||
public function buildFetchQueryActivityDocumentLinkedToPersonFromPersonContext(Person $person, \DateTimeImmutable $startDate = null, \DateTimeImmutable $endDate = null, string $content = null): FetchQueryInterface
|
||||
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
|
||||
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);
|
||||
@@ -72,7 +72,7 @@ final readonly class ActivityDocumentACLAwareRepository implements ActivityDocum
|
||||
return $this->addWhereClauses($query, $startDate, $endDate, $content);
|
||||
}
|
||||
|
||||
public function buildFetchQueryActivityDocumentLinkedToAccompanyingPeriodFromPersonContext(Person $person, \DateTimeImmutable $startDate = null, \DateTimeImmutable $endDate = null, string $content = null): FetchQuery
|
||||
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);
|
||||
@@ -123,7 +123,7 @@ final readonly class ActivityDocumentACLAwareRepository implements ActivityDocum
|
||||
return $this->addWhereClauses($query, $startDate, $endDate, $content);
|
||||
}
|
||||
|
||||
private function addWhereClauses(FetchQuery $query, \DateTimeImmutable $startDate = null, \DateTimeImmutable $endDate = null, string $content = null): FetchQuery
|
||||
private function addWhereClauses(FetchQuery $query, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery
|
||||
{
|
||||
$storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class);
|
||||
|
||||
|
@@ -25,12 +25,12 @@ interface ActivityDocumentACLAwareRepositoryInterface
|
||||
*
|
||||
* 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;
|
||||
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;
|
||||
public function buildFetchQueryActivityDocumentLinkedToAccompanyingPeriodFromPersonContext(Person $person, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery;
|
||||
}
|
||||
|
@@ -34,7 +34,7 @@ class ActivityPresenceRepository implements ActivityPresenceRepositoryInterface
|
||||
return $this->repository->findAll();
|
||||
}
|
||||
|
||||
public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): array
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
return $this->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@ interface ActivityPresenceRepositoryInterface
|
||||
/**
|
||||
* @return array|ActivityPresence[]
|
||||
*/
|
||||
public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): array;
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;
|
||||
|
||||
public function findOneBy(array $criteria): ?ActivityPresence;
|
||||
|
||||
|
@@ -48,7 +48,7 @@ final class ActivityTypeRepository implements ActivityTypeRepositoryInterface
|
||||
/**
|
||||
* @return array|ActivityType[]
|
||||
*/
|
||||
public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): array
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\ActivityBundle\Service\DocGenerator;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\ActivityBundle\Entity\ActivityPresence;
|
||||
use Chill\ActivityBundle\Entity\ActivityType;
|
||||
use Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface;
|
||||
@@ -112,7 +113,7 @@ class ListActivitiesByAccompanyingPeriodContext implements
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list
|
||||
* @return list<Activity>
|
||||
*/
|
||||
private function filterActivitiesByUser(array $activities, User $user): array
|
||||
{
|
||||
@@ -120,6 +121,12 @@ class ListActivitiesByAccompanyingPeriodContext implements
|
||||
array_filter(
|
||||
$activities,
|
||||
function ($activity) use ($user) {
|
||||
$u = $activity['user'];
|
||||
|
||||
if (null !== $u && $u['username'] === $user->getUsername()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$activityUsernames = array_map(static fn ($user) => $user['username'], $activity['users'] ?? []);
|
||||
|
||||
return \in_array($user->getUsername(), $activityUsernames, true);
|
||||
@@ -129,7 +136,7 @@ class ListActivitiesByAccompanyingPeriodContext implements
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list
|
||||
* @return list<AccompanyingPeriod\AccompanyingPeriodWork>
|
||||
*/
|
||||
private function filterWorksByUser(array $works, User $user): array
|
||||
{
|
||||
@@ -216,6 +223,15 @@ class ListActivitiesByAccompanyingPeriodContext implements
|
||||
foreach ($activities as $row) {
|
||||
$activity = $row[0];
|
||||
|
||||
$user = match (null === $row['userId']) {
|
||||
false => $this->userRepository->find($row['userId']),
|
||||
true => null,
|
||||
};
|
||||
|
||||
$activity['user'] = $this->normalizer->normalize($user, 'docgen', [
|
||||
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => User::class,
|
||||
]);
|
||||
|
||||
$activity['date'] = $this->normalizer->normalize($activity['date'], 'docgen', [
|
||||
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => \DateTime::class,
|
||||
]);
|
||||
|
@@ -37,7 +37,7 @@ final readonly class AccompanyingPeriodActivityGenericDocProvider implements Gen
|
||||
) {
|
||||
}
|
||||
|
||||
public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, \DateTimeImmutable $startDate = null, \DateTimeImmutable $endDate = null, string $content = null, string $origin = null): FetchQueryInterface
|
||||
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);
|
||||
@@ -100,7 +100,7 @@ final readonly class AccompanyingPeriodActivityGenericDocProvider implements Gen
|
||||
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
|
||||
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);
|
||||
|
@@ -28,7 +28,7 @@ final readonly class PersonActivityGenericDocProvider implements GenericDocForPe
|
||||
) {
|
||||
}
|
||||
|
||||
public function buildFetchQueryForPerson(Person $person, \DateTimeImmutable $startDate = null, \DateTimeImmutable $endDate = null, string $content = null, string $origin = null): FetchQueryInterface
|
||||
public function buildFetchQueryForPerson(Person $person, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
|
||||
{
|
||||
return $this->personActivityDocumentACLAwareRepository->buildFetchQueryActivityDocumentLinkedToPersonFromPersonContext(
|
||||
$person,
|
||||
|
@@ -9,10 +9,10 @@ declare(strict_types=1);
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\ActivityBundle\Tests\Export\Aggregator\PersonAggregators;
|
||||
namespace Chill\ActivityBundle\Tests\Export\Aggregator;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\ActivityBundle\Export\Aggregator\PersonAggregators\ActivityReasonAggregator;
|
||||
use Chill\ActivityBundle\Export\Aggregator\ActivityReasonAggregator;
|
||||
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
@@ -33,14 +33,14 @@ final class ActivityReasonAggregatorTest extends AbstractAggregatorTest
|
||||
self::bootKernel();
|
||||
|
||||
$this->aggregator = self::$container->get(ActivityReasonAggregator::class);
|
||||
/*
|
||||
$request = $this->prophesize()
|
||||
->willExtend(\Symfony\Component\HttpFoundation\Request::class);
|
||||
|
||||
$request = $this->prophesize()
|
||||
->willExtend(\Symfony\Component\HttpFoundation\Request::class);
|
||||
$request->getLocale()->willReturn('fr');
|
||||
|
||||
$request->getLocale()->willReturn('fr');
|
||||
|
||||
self::$container->get('request_stack')
|
||||
->push($request->reveal());
|
||||
self::$container->get('request_stack')
|
||||
->push($request->reveal());*/
|
||||
}
|
||||
|
||||
public function getAggregator()
|
||||
@@ -65,7 +65,12 @@ final class ActivityReasonAggregatorTest extends AbstractAggregatorTest
|
||||
return [
|
||||
$em->createQueryBuilder()
|
||||
->select('count(activity.id)')
|
||||
->from(Activity::class, 'activity'),
|
||||
->from(Activity::class, 'activity')
|
||||
->join('activity.person', 'person'),
|
||||
$em->createQueryBuilder()
|
||||
->select('count(activity.id)')
|
||||
->from(Activity::class, 'activity')
|
||||
->join('activity.accompanyingPeriod', 'accompanyingPeriod'),
|
||||
$em->createQueryBuilder()
|
||||
->select('count(activity.id)')
|
||||
->from(Activity::class, 'activity')
|
@@ -91,6 +91,29 @@ class ActivityACLAwareRepositoryTest extends KernelTestCase
|
||||
self::assertIsArray($actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideDataFindByAccompanyingPeriod
|
||||
*/
|
||||
public function testfindByAccompanyingPeriodSimplified(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->findByAccompanyingPeriodSimplified($period);
|
||||
|
||||
self::assertIsArray($actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideDataFindByAccompanyingPeriod
|
||||
*/
|
||||
@@ -301,7 +324,10 @@ class ActivityACLAwareRepositoryTest extends KernelTestCase
|
||||
->getQuery()
|
||||
->getResult()
|
||||
) {
|
||||
throw new \RuntimeException('no jobs found');
|
||||
$job = new UserJob();
|
||||
$job->setLabel(['fr' => 'test']);
|
||||
$this->entityManager->persist($job);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
if (null === $user = $this->entityManager
|
||||
|
@@ -157,7 +157,7 @@ final class ActivityVoterTest extends KernelTestCase
|
||||
*
|
||||
* @return \Symfony\Component\Security\Core\Authentication\Token\TokenInterface
|
||||
*/
|
||||
protected function prepareToken(User $user = null)
|
||||
protected function prepareToken(?User $user = null)
|
||||
{
|
||||
$token = $this->prophet->prophesize();
|
||||
$token
|
||||
|
@@ -0,0 +1,139 @@
|
||||
<?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\Service\DocGenerator;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\ActivityBundle\Service\DocGenerator\ListActivitiesByAccompanyingPeriodContext;
|
||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class ListActivitiesByAccompanyingPeriodContextTest extends KernelTestCase
|
||||
{
|
||||
private ListActivitiesByAccompanyingPeriodContext $listActivitiesByAccompanyingPeriodContext;
|
||||
private AccompanyingPeriodRepository $accompanyingPeriodRepository;
|
||||
private UserRepositoryInterface $userRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->listActivitiesByAccompanyingPeriodContext = self::$container->get(ListActivitiesByAccompanyingPeriodContext::class);
|
||||
$this->accompanyingPeriodRepository = self::$container->get(AccompanyingPeriodRepository::class);
|
||||
$this->userRepository = self::$container->get(UserRepositoryInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideAccompanyingPeriod
|
||||
*/
|
||||
public function testGetDataWithoutFilteringActivityNorWorks(int $accompanyingPeriodId, int $userId): void
|
||||
{
|
||||
$context = $this->getContext();
|
||||
$template = new DocGeneratorTemplate();
|
||||
$template->setOptions([
|
||||
'mainPerson' => false,
|
||||
'person1' => false,
|
||||
'person2' => false,
|
||||
'thirdParty' => false,
|
||||
]);
|
||||
|
||||
$data = $context->getData(
|
||||
$template,
|
||||
$this->accompanyingPeriodRepository->find($accompanyingPeriodId),
|
||||
['myActivitiesOnly' => false, 'myWorksOnly' => false]
|
||||
);
|
||||
|
||||
self::assertIsArray($data);
|
||||
self::assertArrayHasKey('activities', $data);
|
||||
self::assertIsArray($data['activities']);
|
||||
self::assertGreaterThan(0, count($data['activities']));
|
||||
self::assertIsArray($data['activities'][0]);
|
||||
self::assertArrayHasKey('user', $data['activities'][0]);
|
||||
self::assertIsArray($data['activities'][0]['user']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideAccompanyingPeriod
|
||||
*/
|
||||
public function testGetDataWithoutFilteringActivityByUser(int $accompanyingPeriodId, int $userId): void
|
||||
{
|
||||
$context = $this->getContext();
|
||||
$template = new DocGeneratorTemplate();
|
||||
$template->setOptions([
|
||||
'mainPerson' => false,
|
||||
'person1' => false,
|
||||
'person2' => false,
|
||||
'thirdParty' => false,
|
||||
]);
|
||||
|
||||
$data = $context->getData(
|
||||
$template,
|
||||
$this->accompanyingPeriodRepository->find($accompanyingPeriodId),
|
||||
['myActivitiesOnly' => true, 'myWorksOnly' => false, 'creator' => $this->userRepository->find($userId)]
|
||||
);
|
||||
|
||||
self::assertIsArray($data);
|
||||
self::assertArrayHasKey('activities', $data);
|
||||
self::assertIsArray($data['activities']);
|
||||
self::assertGreaterThan(0, count($data['activities']));
|
||||
self::assertIsArray($data['activities'][0]);
|
||||
self::assertArrayHasKey('user', $data['activities'][0]);
|
||||
self::assertIsArray($data['activities'][0]['user']);
|
||||
}
|
||||
|
||||
public static function provideAccompanyingPeriod(): array
|
||||
{
|
||||
self::bootKernel();
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
|
||||
if (null === $period = $em->createQuery('SELECT a FROM '.AccompanyingPeriod::class.' a')
|
||||
->setMaxResults(1)
|
||||
->getSingleResult()) {
|
||||
throw new \RuntimeException('no period found');
|
||||
}
|
||||
|
||||
if (null === $user = $em->createQuery('SELECT u FROM '.User::class.' u')
|
||||
->setMaxResults(1)
|
||||
->getSingleResult()
|
||||
) {
|
||||
throw new \RuntimeException('no user found');
|
||||
}
|
||||
|
||||
$activity = new Activity();
|
||||
$activity
|
||||
->setAccompanyingPeriod($period)
|
||||
->setUser($user)
|
||||
->setDate(new \DateTime());
|
||||
|
||||
$em->persist($activity);
|
||||
$em->flush();
|
||||
|
||||
self::ensureKernelShutdown();
|
||||
|
||||
return [
|
||||
[$period->getId(), $user->getId()],
|
||||
];
|
||||
}
|
||||
|
||||
private function getContext(): ListActivitiesByAccompanyingPeriodContext
|
||||
{
|
||||
return $this->listActivitiesByAccompanyingPeriodContext;
|
||||
}
|
||||
}
|
@@ -153,7 +153,7 @@ services:
|
||||
|
||||
|
||||
## Aggregators
|
||||
Chill\ActivityBundle\Export\Aggregator\PersonAggregators\ActivityReasonAggregator:
|
||||
Chill\ActivityBundle\Export\Aggregator\ActivityReasonAggregator:
|
||||
tags:
|
||||
- { name: chill.export_aggregator, alias: activity_reason_aggregator }
|
||||
|
||||
|
@@ -396,7 +396,7 @@ export:
|
||||
by_creator_job:
|
||||
job_form_label: Métiers
|
||||
Filter activity by user job: Filtrer les échanges par métier du créateur de l'échange
|
||||
'Filtered activity by user job: only %jobs%': "Filtré par service du créateur de l'échange: uniquement %jobs%"
|
||||
'Filtered activity by user job: only %jobs%': "Filtré par métier du créateur de l'échange: uniquement %jobs%"
|
||||
by_persons:
|
||||
Filter activity by persons: Filtrer les échanges par usager participant
|
||||
'Filtered activity by persons: only %persons%': 'Échanges filtrés par usagers participants: seulement %persons%'
|
||||
|
@@ -49,7 +49,7 @@ final class AsideActivityController extends CRUDController
|
||||
return $asideActivity;
|
||||
}
|
||||
|
||||
protected function buildQueryEntities(string $action, Request $request, FilterOrderHelper $filterOrder = null)
|
||||
protected function buildQueryEntities(string $action, Request $request, ?FilterOrderHelper $filterOrder = null)
|
||||
{
|
||||
$qb = parent::buildQueryEntities($action, $request);
|
||||
|
||||
|
@@ -32,7 +32,7 @@ class AsideActivity implements TrackCreationInterface, TrackUpdateInterface
|
||||
*
|
||||
* @Assert\NotBlank
|
||||
*/
|
||||
private \Chill\MainBundle\Entity\User $agent;
|
||||
private User $agent;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime")
|
||||
@@ -44,7 +44,7 @@ class AsideActivity implements TrackCreationInterface, TrackUpdateInterface
|
||||
*
|
||||
* @ORM\JoinColumn(nullable=false)
|
||||
*/
|
||||
private \Chill\MainBundle\Entity\User $createdBy;
|
||||
private User $createdBy;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime")
|
||||
@@ -82,7 +82,7 @@ class AsideActivity implements TrackCreationInterface, TrackUpdateInterface
|
||||
*
|
||||
* @ORM\JoinColumn(nullable=false)
|
||||
*/
|
||||
private ?\Chill\AsideActivityBundle\Entity\AsideActivityCategory $type = null;
|
||||
private ?AsideActivityCategory $type = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime", nullable=true)
|
||||
@@ -92,7 +92,7 @@ class AsideActivity implements TrackCreationInterface, TrackUpdateInterface
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=User::class)
|
||||
*/
|
||||
private \Chill\MainBundle\Entity\User $updatedBy;
|
||||
private User $updatedBy;
|
||||
|
||||
public function getAgent(): ?User
|
||||
{
|
||||
|
@@ -16,6 +16,7 @@ use Chill\AsideActivityBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Entity\User\UserJobHistory;
|
||||
use Chill\MainBundle\Entity\UserJob;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
@@ -27,7 +28,8 @@ class ByUserJobFilter implements FilterInterface
|
||||
private const PREFIX = 'aside_act_filter_user_job';
|
||||
|
||||
public function __construct(
|
||||
private readonly TranslatableStringHelperInterface $translatableStringHelper
|
||||
private readonly TranslatableStringHelperInterface $translatableStringHelper,
|
||||
private readonly UserJobRepositoryInterface $userJobRepository
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -69,6 +71,7 @@ class ByUserJobFilter implements FilterInterface
|
||||
$builder
|
||||
->add('jobs', EntityType::class, [
|
||||
'class' => UserJob::class,
|
||||
'choices' => $this->userJobRepository->findAllActive(),
|
||||
'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()),
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
|
@@ -49,7 +49,7 @@ class AsideActivityCategoryRepository implements ObjectRepository
|
||||
*
|
||||
* @return AsideActivityCategory[]
|
||||
*/
|
||||
public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null): array
|
||||
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
|
||||
{
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
@@ -132,14 +132,14 @@ abstract class AbstractElement
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setComment(string $comment = null): self
|
||||
public function setComment(?string $comment = null): self
|
||||
{
|
||||
$this->comment = $comment;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setEndDate(\DateTimeInterface $endDate = null): self
|
||||
public function setEndDate(?\DateTimeInterface $endDate = null): self
|
||||
{
|
||||
if ($endDate instanceof \DateTime) {
|
||||
$this->endDate = \DateTimeImmutable::createFromMutable($endDate);
|
||||
|
@@ -66,7 +66,7 @@ final class ChargeKindRepository implements ChargeKindRepositoryInterface
|
||||
*
|
||||
* @return array<ChargeKind>
|
||||
*/
|
||||
public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): array
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
@@ -36,7 +36,7 @@ interface ChargeKindRepositoryInterface extends ObjectRepository
|
||||
/**
|
||||
* @return array<ChargeKind>
|
||||
*/
|
||||
public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): array;
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;
|
||||
|
||||
public function findOneBy(array $criteria): ?ChargeKind;
|
||||
|
||||
|
@@ -71,7 +71,7 @@ final class ResourceKindRepository implements ResourceKindRepositoryInterface
|
||||
*
|
||||
* @return list<ResourceKind>
|
||||
*/
|
||||
public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): array
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
@@ -36,7 +36,7 @@ interface ResourceKindRepositoryInterface extends ObjectRepository
|
||||
/**
|
||||
* @return list<ResourceKind>
|
||||
*/
|
||||
public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): array;
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;
|
||||
|
||||
public function findOneBy(array $criteria): ?ResourceKind;
|
||||
|
||||
|
@@ -32,7 +32,7 @@ final class Version20221207105407 extends AbstractMigration implements Container
|
||||
return 'Use new budget admin entities';
|
||||
}
|
||||
|
||||
public function setContainer(ContainerInterface $container = null)
|
||||
public function setContainer(?ContainerInterface $container = null)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
@@ -49,8 +49,6 @@ final class MapAndSubscribeUserCalendarCommand extends Command
|
||||
|
||||
$limit = 50;
|
||||
$offset = 0;
|
||||
/** @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')));
|
||||
$users = $this->userRepository->findAllAsArray('fr');
|
||||
$created = 0;
|
||||
@@ -93,7 +91,6 @@ final class MapAndSubscribeUserCalendarCommand extends Command
|
||||
} 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.
|
||||
|
@@ -13,7 +13,7 @@ namespace Chill\CalendarBundle\Exception;
|
||||
|
||||
class UserAbsenceSyncException extends \LogicException
|
||||
{
|
||||
public function __construct(string $message = '', int $code = 20_230_706, \Throwable $previous = null)
|
||||
public function __construct(string $message = '', int $code = 20_230_706, ?\Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@ use Chill\CalendarBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Entity\User\UserJobHistory;
|
||||
use Chill\MainBundle\Entity\UserJob;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
@@ -26,7 +27,8 @@ final readonly class JobFilter implements FilterInterface
|
||||
private const PREFIX = 'cal_filter_job';
|
||||
|
||||
public function __construct(
|
||||
private TranslatableStringHelper $translatableStringHelper
|
||||
private TranslatableStringHelper $translatableStringHelper,
|
||||
private UserJobRepositoryInterface $userJobRepository
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -74,6 +76,7 @@ final readonly class JobFilter implements FilterInterface
|
||||
$builder
|
||||
->add('job', EntityType::class, [
|
||||
'class' => UserJob::class,
|
||||
'choices' => $this->userJobRepository->findAllActive(),
|
||||
'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize(
|
||||
$j->getLabel()
|
||||
),
|
||||
|
@@ -15,6 +15,7 @@ use Chill\CalendarBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Entity\User\UserScopeHistory;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
@@ -28,7 +29,8 @@ class ScopeFilter implements FilterInterface
|
||||
|
||||
public function __construct(
|
||||
protected TranslatorInterface $translator,
|
||||
private readonly TranslatableStringHelper $translatableStringHelper
|
||||
private readonly TranslatableStringHelper $translatableStringHelper,
|
||||
private readonly ScopeRepositoryInterface $scopeRepository
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -76,6 +78,7 @@ class ScopeFilter implements FilterInterface
|
||||
$builder
|
||||
->add('scope', EntityType::class, [
|
||||
'class' => Scope::class,
|
||||
'choices' => $this->scopeRepository->findAllActive(),
|
||||
'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize(
|
||||
$s->getName()
|
||||
),
|
||||
|
@@ -33,7 +33,7 @@ final readonly class MSUserAbsenceReader implements MSUserAbsenceReaderInterface
|
||||
/**
|
||||
* @throw UserAbsenceSyncException when the data cannot be reached or is not valid from microsoft
|
||||
*/
|
||||
public function isUserAbsent(User $user): null|bool
|
||||
public function isUserAbsent(User $user): ?bool
|
||||
{
|
||||
$id = $this->mapCalendarToUser->getUserId($user);
|
||||
|
||||
|
@@ -18,5 +18,5 @@ interface MSUserAbsenceReaderInterface
|
||||
/**
|
||||
* @throw UserAbsenceSyncException when the data cannot be reached or is not valid from microsoft
|
||||
*/
|
||||
public function isUserAbsent(User $user): null|bool;
|
||||
public function isUserAbsent(User $user): ?bool;
|
||||
}
|
||||
|
@@ -29,7 +29,7 @@ class MachineHttpClient implements HttpClientInterface
|
||||
|
||||
private readonly HttpClientInterface $decoratedClient;
|
||||
|
||||
public function __construct(private readonly MachineTokenStorage $machineTokenStorage, HttpClientInterface $decoratedClient = null)
|
||||
public function __construct(private readonly MachineTokenStorage $machineTokenStorage, ?HttpClientInterface $decoratedClient = null)
|
||||
{
|
||||
$this->decoratedClient = $decoratedClient ?? \Symfony\Component\HttpClient\HttpClient::create();
|
||||
}
|
||||
@@ -68,7 +68,7 @@ class MachineHttpClient implements HttpClientInterface
|
||||
return $this->decoratedClient->request($method, $url, $options);
|
||||
}
|
||||
|
||||
public function stream($responses, float $timeout = null): ResponseStreamInterface
|
||||
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
|
||||
{
|
||||
return $this->decoratedClient->stream($responses, $timeout);
|
||||
}
|
||||
|
@@ -178,8 +178,8 @@ class MapCalendarToUser
|
||||
public function writeSubscriptionMetadata(
|
||||
User $user,
|
||||
int $expiration,
|
||||
string $id = null,
|
||||
string $secret = null
|
||||
?string $id = null,
|
||||
?string $secret = null
|
||||
): void {
|
||||
$user->setAttributeByDomain(self::METADATA_KEY, self::EXPIRATION_SUBSCRIPTION_EVENT, $expiration);
|
||||
|
||||
|
@@ -29,7 +29,7 @@ class OnBehalfOfUserHttpClient
|
||||
|
||||
private readonly HttpClientInterface $decoratedClient;
|
||||
|
||||
public function __construct(private readonly OnBehalfOfUserTokenStorage $tokenStorage, HttpClientInterface $decoratedClient = null)
|
||||
public function __construct(private readonly OnBehalfOfUserTokenStorage $tokenStorage, ?HttpClientInterface $decoratedClient = null)
|
||||
{
|
||||
$this->decoratedClient = $decoratedClient ?? \Symfony\Component\HttpClient\HttpClient::create();
|
||||
}
|
||||
@@ -63,7 +63,7 @@ class OnBehalfOfUserHttpClient
|
||||
return $this->decoratedClient->request($method, $url, $options);
|
||||
}
|
||||
|
||||
public function stream($responses, float $timeout = null): ResponseStreamInterface
|
||||
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
|
||||
{
|
||||
return $this->decoratedClient->stream($responses, $timeout);
|
||||
}
|
||||
|
@@ -171,7 +171,7 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
|
||||
}
|
||||
}
|
||||
|
||||
public function removeCalendar(string $remoteId, array $remoteAttributes, User $user, CalendarRange $associatedCalendarRange = null): void
|
||||
public function removeCalendar(string $remoteId, array $remoteAttributes, User $user, ?CalendarRange $associatedCalendarRange = null): void
|
||||
{
|
||||
if ('' === $remoteId) {
|
||||
return;
|
||||
|
@@ -46,7 +46,7 @@ class NullRemoteCalendarConnector implements RemoteCalendarConnectorInterface
|
||||
return [];
|
||||
}
|
||||
|
||||
public function removeCalendar(string $remoteId, array $remoteAttributes, User $user, CalendarRange $associatedCalendarRange = null): void
|
||||
public function removeCalendar(string $remoteId, array $remoteAttributes, User $user, ?CalendarRange $associatedCalendarRange = null): void
|
||||
{
|
||||
}
|
||||
|
||||
|
@@ -47,7 +47,7 @@ interface RemoteCalendarConnectorInterface
|
||||
*/
|
||||
public function listEventsForUser(User $user, \DateTimeImmutable $startDate, \DateTimeImmutable $endDate, ?int $offset = 0, ?int $limit = 50): array;
|
||||
|
||||
public function removeCalendar(string $remoteId, array $remoteAttributes, User $user, CalendarRange $associatedCalendarRange = null): void;
|
||||
public function removeCalendar(string $remoteId, array $remoteAttributes, User $user, ?CalendarRange $associatedCalendarRange = null): void;
|
||||
|
||||
public function removeCalendarRange(string $remoteId, array $remoteAttributes, User $user): void;
|
||||
|
||||
|
@@ -159,7 +159,7 @@ class CalendarACLAwareRepository implements CalendarACLAwareRepositoryInterface
|
||||
/**
|
||||
* @return array|Calendar[]
|
||||
*/
|
||||
public function findByAccompanyingPeriod(AccompanyingPeriod $period, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?array $orderBy = [], int $offset = null, int $limit = null): array
|
||||
public function findByAccompanyingPeriod(AccompanyingPeriod $period, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?array $orderBy = [], ?int $offset = null, ?int $limit = null): array
|
||||
{
|
||||
$qb = $this->buildQueryByAccompanyingPeriod($period, $startDate, $endDate)->select('c');
|
||||
|
||||
@@ -178,7 +178,7 @@ class CalendarACLAwareRepository implements CalendarACLAwareRepositoryInterface
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function findByPerson(Person $person, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?array $orderBy = [], int $offset = null, int $limit = null): array
|
||||
public function findByPerson(Person $person, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?array $orderBy = [], ?int $offset = null, ?int $limit = null): array
|
||||
{
|
||||
$qb = $this->buildQueryByPerson($person, $startDate, $endDate)
|
||||
->select('c');
|
||||
|
@@ -46,7 +46,7 @@ interface CalendarACLAwareRepositoryInterface
|
||||
/**
|
||||
* @return array|Calendar[]
|
||||
*/
|
||||
public function findByAccompanyingPeriod(AccompanyingPeriod $period, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?array $orderBy = [], int $offset = null, int $limit = null): array;
|
||||
public function findByAccompanyingPeriod(AccompanyingPeriod $period, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?array $orderBy = [], ?int $offset = null, ?int $limit = null): array;
|
||||
|
||||
/**
|
||||
* Return all the calendars which are associated with a person, either on @see{Calendar::person} or within.
|
||||
@@ -58,5 +58,5 @@ interface CalendarACLAwareRepositoryInterface
|
||||
*
|
||||
* @return array|Calendar[]
|
||||
*/
|
||||
public function findByPerson(Person $person, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?array $orderBy = [], int $offset = null, int $limit = null): array;
|
||||
public function findByPerson(Person $person, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?array $orderBy = [], ?int $offset = null, ?int $limit = null): array;
|
||||
}
|
||||
|
@@ -35,7 +35,7 @@ class CalendarDocRepository implements ObjectRepository, CalendarDocRepositoryIn
|
||||
return $this->repository->findAll();
|
||||
}
|
||||
|
||||
public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null)
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null)
|
||||
{
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@ interface CalendarDocRepositoryInterface
|
||||
/**
|
||||
* @return array|CalendarDoc[]
|
||||
*/
|
||||
public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null);
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null);
|
||||
|
||||
public function findOneBy(array $criteria): ?CalendarDoc;
|
||||
|
||||
|
@@ -52,7 +52,7 @@ class CalendarRangeRepository implements ObjectRepository
|
||||
/**
|
||||
* @return array|CalendarRange[]
|
||||
*/
|
||||
public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null)
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null)
|
||||
{
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
@@ -64,8 +64,8 @@ class CalendarRangeRepository implements ObjectRepository
|
||||
User $user,
|
||||
\DateTimeImmutable $from,
|
||||
\DateTimeImmutable $to,
|
||||
int $limit = null,
|
||||
int $offset = null
|
||||
?int $limit = null,
|
||||
?int $offset = null
|
||||
): array {
|
||||
$qb = $this->buildQueryAvailableRangesForUser($user, $from, $to);
|
||||
|
||||
|
@@ -46,7 +46,7 @@ class CalendarRepository implements ObjectRepository
|
||||
->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function createQueryBuilder(string $alias, string $indexBy = null): QueryBuilder
|
||||
public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder
|
||||
{
|
||||
return $this->repository->createQueryBuilder($alias, $indexBy);
|
||||
}
|
||||
@@ -67,7 +67,7 @@ class CalendarRepository implements ObjectRepository
|
||||
/**
|
||||
* @return array|Calendar[]
|
||||
*/
|
||||
public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): array
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
@@ -75,7 +75,7 @@ class CalendarRepository implements ObjectRepository
|
||||
/**
|
||||
* @return array|Calendar[]
|
||||
*/
|
||||
public function findByAccompanyingPeriod(AccompanyingPeriod $period, array $orderBy = null, int $limit = null, int $offset = null): array
|
||||
public function findByAccompanyingPeriod(AccompanyingPeriod $period, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
return $this->findBy(
|
||||
[
|
||||
@@ -87,7 +87,7 @@ class CalendarRepository implements ObjectRepository
|
||||
);
|
||||
}
|
||||
|
||||
public function findByNotificationAvailable(\DateTimeImmutable $startDate, \DateTimeImmutable $endDate, int $limit = null, int $offset = null): array
|
||||
public function findByNotificationAvailable(\DateTimeImmutable $startDate, \DateTimeImmutable $endDate, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
$qb = $this->queryByNotificationAvailable($startDate, $endDate)->select('c');
|
||||
|
||||
@@ -105,7 +105,7 @@ class CalendarRepository implements ObjectRepository
|
||||
/**
|
||||
* @return array|Calendar[]
|
||||
*/
|
||||
public function findByUser(User $user, \DateTimeImmutable $from, \DateTimeImmutable $to, int $limit = null, int $offset = null): array
|
||||
public function findByUser(User $user, \DateTimeImmutable $from, \DateTimeImmutable $to, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
$qb = $this->buildQueryByUser($user, $from, $to)->select('c');
|
||||
|
||||
|
@@ -41,7 +41,7 @@ class InviteRepository implements ObjectRepository
|
||||
/**
|
||||
* @return array|Invite[]
|
||||
*/
|
||||
public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null)
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null)
|
||||
{
|
||||
return $this->entityRepository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
@@ -44,7 +44,7 @@ final readonly class AccompanyingPeriodCalendarGenericDocProvider implements Gen
|
||||
/**
|
||||
* @throws MappingException
|
||||
*/
|
||||
public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, \DateTimeImmutable $startDate = null, \DateTimeImmutable $endDate = null, string $content = null, string $origin = null): FetchQueryInterface
|
||||
public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
|
||||
{
|
||||
$classMetadata = $this->em->getClassMetadata(CalendarDoc::class);
|
||||
$storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class);
|
||||
@@ -91,7 +91,7 @@ final readonly class AccompanyingPeriodCalendarGenericDocProvider implements Gen
|
||||
return $this->security->isGranted(CalendarVoter::SEE, $accompanyingPeriod);
|
||||
}
|
||||
|
||||
public function buildFetchQueryForPerson(Person $person, \DateTimeImmutable $startDate = null, \DateTimeImmutable $endDate = null, string $content = null, string $origin = null): FetchQueryInterface
|
||||
public function buildFetchQueryForPerson(Person $person, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
|
||||
{
|
||||
$classMetadata = $this->em->getClassMetadata(CalendarDoc::class);
|
||||
$storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class);
|
||||
|
@@ -40,7 +40,7 @@ final readonly class PersonCalendarGenericDocProvider implements GenericDocForPe
|
||||
) {
|
||||
}
|
||||
|
||||
private function addWhereClausesToQuery(FetchQuery $query, \DateTimeImmutable $startDate = null, \DateTimeImmutable $endDate = null, string $content = null): FetchQuery
|
||||
private function addWhereClausesToQuery(FetchQuery $query, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery
|
||||
{
|
||||
$storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class);
|
||||
|
||||
@@ -74,7 +74,7 @@ final readonly class PersonCalendarGenericDocProvider implements GenericDocForPe
|
||||
/**
|
||||
* @throws MappingException
|
||||
*/
|
||||
public function buildFetchQueryForPerson(Person $person, \DateTimeImmutable $startDate = null, \DateTimeImmutable $endDate = null, string $content = null, string $origin = null): FetchQueryInterface
|
||||
public function buildFetchQueryForPerson(Person $person, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface
|
||||
{
|
||||
$classMetadata = $this->em->getClassMetadata(CalendarDoc::class);
|
||||
$storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class);
|
||||
|
@@ -202,8 +202,8 @@ final class CalendarContextTest extends TestCase
|
||||
}
|
||||
|
||||
private function buildCalendarContext(
|
||||
EntityManagerInterface $entityManager = null,
|
||||
NormalizerInterface $normalizer = null
|
||||
?EntityManagerInterface $entityManager = null,
|
||||
?NormalizerInterface $normalizer = null
|
||||
): CalendarContext {
|
||||
$baseContext = $this->prophesize(BaseContextData::class);
|
||||
$baseContext->getData(null)->willReturn(['base_context' => 'data']);
|
||||
|
@@ -363,7 +363,7 @@ class CustomFieldsGroupController extends AbstractController
|
||||
*
|
||||
* @return \Symfony\Component\Form\Form
|
||||
*/
|
||||
private function createMakeDefaultForm(CustomFieldsGroup $group = null)
|
||||
private function createMakeDefaultForm(?CustomFieldsGroup $group = null)
|
||||
{
|
||||
return $this->createFormBuilder($group, [
|
||||
'method' => 'POST',
|
||||
|
@@ -156,7 +156,7 @@ class CustomFieldChoice extends AbstractCustomField
|
||||
return $serialized;
|
||||
}
|
||||
|
||||
public function extractOtherValue(CustomField $cf, array $data = null)
|
||||
public function extractOtherValue(CustomField $cf, ?array $data = null)
|
||||
{
|
||||
return $data['_other'];
|
||||
}
|
||||
@@ -355,7 +355,7 @@ class CustomFieldChoice extends AbstractCustomField
|
||||
* If the value had an 'allow_other' = true option, the returned value
|
||||
* **is not** the content of the _other field, but the `_other` string.
|
||||
*/
|
||||
private function guessValue(null|array|string $value)
|
||||
private function guessValue(array|string|null $value)
|
||||
{
|
||||
if (null === $value) {
|
||||
return null;
|
||||
|
@@ -38,7 +38,7 @@ class CustomField
|
||||
* targetEntity="Chill\CustomFieldsBundle\Entity\CustomFieldsGroup",
|
||||
* inversedBy="customFields")
|
||||
*/
|
||||
private ?\Chill\CustomFieldsBundle\Entity\CustomFieldsGroup $customFieldGroup = null;
|
||||
private ?CustomFieldsGroup $customFieldGroup = null;
|
||||
|
||||
/**
|
||||
* @ORM\Id
|
||||
@@ -212,7 +212,7 @@ class CustomField
|
||||
*
|
||||
* @return CustomField
|
||||
*/
|
||||
public function setCustomFieldsGroup(CustomFieldsGroup $customFieldGroup = null)
|
||||
public function setCustomFieldsGroup(?CustomFieldsGroup $customFieldGroup = null)
|
||||
{
|
||||
$this->customFieldGroup = $customFieldGroup;
|
||||
|
||||
|
@@ -63,7 +63,7 @@ class Option
|
||||
*
|
||||
* @ORM\JoinColumn(nullable=true)
|
||||
*/
|
||||
private ?\Chill\CustomFieldsBundle\Entity\CustomFieldLongChoice\Option $parent = null;
|
||||
private ?Option $parent = null;
|
||||
|
||||
/**
|
||||
* A json representation of text (multilingual).
|
||||
@@ -182,7 +182,7 @@ class Option
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setParent(Option $parent = null)
|
||||
public function setParent(?Option $parent = null)
|
||||
{
|
||||
$this->parent = $parent;
|
||||
$this->key = $parent->getKey();
|
||||
|
@@ -33,7 +33,7 @@ class CustomFieldsDefaultGroup
|
||||
*
|
||||
* sf4 check: option inversedBy="customFields" return inconsistent error mapping !!
|
||||
*/
|
||||
private ?\Chill\CustomFieldsBundle\Entity\CustomFieldsGroup $customFieldsGroup = null;
|
||||
private ?CustomFieldsGroup $customFieldsGroup = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255)
|
||||
|
@@ -139,7 +139,7 @@ class CustomFieldsGroup
|
||||
/**
|
||||
* Get name.
|
||||
*/
|
||||
public function getName(string $language = null): array|string
|
||||
public function getName(?string $language = null): array|string
|
||||
{
|
||||
// TODO set this in a service, PLUS twig function
|
||||
if (null !== $language) {
|
||||
|
@@ -84,7 +84,7 @@ class CustomFieldProvider implements ContainerAwareInterface
|
||||
*
|
||||
* @see \Symfony\Component\DependencyInjection\ContainerAwareInterface::setContainer()
|
||||
*/
|
||||
public function setContainer(ContainerInterface $container = null)
|
||||
public function setContainer(?ContainerInterface $container = null)
|
||||
{
|
||||
if (null === $container) {
|
||||
throw new \LogicException('container should not be null');
|
||||
|
@@ -25,7 +25,7 @@ final class CustomFieldsHelperTest extends KernelTestCase
|
||||
{
|
||||
private ?object $cfHelper = null;
|
||||
|
||||
private \Chill\CustomFieldsBundle\Entity\CustomField $randomCFText;
|
||||
private CustomField $randomCFText;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
|
@@ -16,29 +16,42 @@ use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface;
|
||||
use Chill\DocGeneratorBundle\Context\Exception\ContextNotFoundException;
|
||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
|
||||
use Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface;
|
||||
use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Serializer\Model\Collection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
// TODO à mettre dans services
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Constraints\NotNull;
|
||||
|
||||
final class DocGeneratorTemplateController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly ContextManager $contextManager, private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository, private readonly GeneratorInterface $generator, private readonly MessageBusInterface $messageBus, private readonly PaginatorFactory $paginatorFactory, private readonly EntityManagerInterface $entityManager)
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ContextManager $contextManager,
|
||||
private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
|
||||
private readonly MessageBusInterface $messageBus,
|
||||
private readonly PaginatorFactory $paginatorFactory,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly ClockInterface $clock,
|
||||
private readonly Security $security,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,9 +176,7 @@ final class DocGeneratorTemplateController extends AbstractController
|
||||
throw new NotFoundHttpException(sprintf('Entity with classname %s and id %s is not found', $context->getEntityClass(), $entityId));
|
||||
}
|
||||
|
||||
$contextGenerationData = [
|
||||
'test_file' => null,
|
||||
];
|
||||
$contextGenerationData = [];
|
||||
|
||||
if (
|
||||
$context instanceof DocGeneratorContextWithPublicFormInterface
|
||||
@@ -175,25 +186,39 @@ final class DocGeneratorTemplateController extends AbstractController
|
||||
$builder = $this->createFormBuilder(
|
||||
array_merge(
|
||||
$context->getFormData($template, $entity),
|
||||
$isTest ? ['test_file' => null, 'show_data' => false] : []
|
||||
$isTest ? ['creator' => null, 'dump_only' => false, 'send_result_to' => ''] : []
|
||||
)
|
||||
);
|
||||
|
||||
$context->buildPublicForm($builder, $template, $entity);
|
||||
} else {
|
||||
$builder = $this->createFormBuilder(
|
||||
['test_file' => null, 'show_data' => false]
|
||||
['creator' => null, 'show_data' => false, 'send_result_to' => '']
|
||||
);
|
||||
}
|
||||
|
||||
if ($isTest) {
|
||||
$builder->add('test_file', FileType::class, [
|
||||
'label' => 'Template file',
|
||||
$builder->add('dump_only', CheckboxType::class, [
|
||||
'label' => 'docgen.Show data instead of generating',
|
||||
'required' => false,
|
||||
]);
|
||||
$builder->add('show_data', CheckboxType::class, [
|
||||
'label' => 'Show data instead of generating',
|
||||
'required' => false,
|
||||
$builder->add('send_result_to', EmailType::class, [
|
||||
'label' => 'docgen.Send report to',
|
||||
'help' => 'docgen.Send report errors to this email address',
|
||||
'empty_data' => '',
|
||||
'required' => true,
|
||||
'constraints' => [
|
||||
new NotBlank(),
|
||||
new NotNull(),
|
||||
],
|
||||
]);
|
||||
$builder->add('creator', PickUserDynamicType::class, [
|
||||
'label' => 'docgen.Generate as creator',
|
||||
'help' => 'docgen.The document will be generated as the given creator',
|
||||
'multiple' => false,
|
||||
'constraints' => [
|
||||
new NotNull(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -204,8 +229,10 @@ final class DocGeneratorTemplateController extends AbstractController
|
||||
} elseif (!$form->isSubmitted() || ($form->isSubmitted() && !$form->isValid())) {
|
||||
$templatePath = '@ChillDocGenerator/Generator/basic_form.html.twig';
|
||||
$templateOptions = [
|
||||
'entity' => $entity, 'form' => $form->createView(),
|
||||
'template' => $template, 'context' => $context,
|
||||
'entity' => $entity,
|
||||
'form' => $form->createView(),
|
||||
'template' => $template,
|
||||
'context' => $context,
|
||||
];
|
||||
|
||||
return $this->render($templatePath, $templateOptions);
|
||||
@@ -218,60 +245,57 @@ final class DocGeneratorTemplateController extends AbstractController
|
||||
$context->contextGenerationDataNormalize($template, $entity, $contextGenerationData)
|
||||
: [];
|
||||
|
||||
// if is test, render the data or generate the doc
|
||||
if ($isTest && isset($form) && $form['show_data']->getData()) {
|
||||
return $this->render('@ChillDocGenerator/Generator/debug_value.html.twig', [
|
||||
'datas' => json_encode($context->getData($template, $entity, $contextGenerationData), \JSON_PRETTY_PRINT),
|
||||
]);
|
||||
}
|
||||
if ($isTest) {
|
||||
$generated = $this->generator->generateDocFromTemplate(
|
||||
$template,
|
||||
$entityId,
|
||||
$contextGenerationDataSanitized,
|
||||
null,
|
||||
true,
|
||||
isset($form) ? $form['test_file']->getData() : null
|
||||
);
|
||||
|
||||
return new Response(
|
||||
$generated,
|
||||
Response::HTTP_OK,
|
||||
[
|
||||
'Content-Transfer-Encoding', 'binary',
|
||||
'Content-Type' => 'application/vnd.oasis.opendocument.text',
|
||||
'Content-Disposition' => 'attachment; filename="generated.odt"',
|
||||
'Content-Length' => \strlen($generated),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// this is not a test
|
||||
// we prepare the object to store the document
|
||||
$storedObject = (new StoredObject())
|
||||
->setStatus(StoredObject::STATUS_PENDING)
|
||||
;
|
||||
|
||||
if ($isTest) {
|
||||
// document will be stored during 15 days, if generation is a test
|
||||
$storedObject->setDeleteAt($this->clock->now()->add(new \DateInterval('P15D')));
|
||||
}
|
||||
|
||||
$this->entityManager->persist($storedObject);
|
||||
|
||||
// we store the generated document
|
||||
$context
|
||||
->storeGenerated(
|
||||
$template,
|
||||
$storedObject,
|
||||
$entity,
|
||||
$contextGenerationData
|
||||
);
|
||||
// we store the generated document (associate with the original entity, etc.)
|
||||
// but only if this is not a test
|
||||
if (!$isTest) {
|
||||
$context
|
||||
->storeGenerated(
|
||||
$template,
|
||||
$storedObject,
|
||||
$entity,
|
||||
$contextGenerationData
|
||||
);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
if ($isTest) {
|
||||
$creator = $contextGenerationData['creator'];
|
||||
$sendResultTo = ($form ?? null)?->get('send_result_to')?->getData() ?? null;
|
||||
$dumpOnly = ($form ?? null)?->get('dump_only')?->getData() ?? false;
|
||||
} else {
|
||||
$creator = $this->security->getUser();
|
||||
|
||||
if (!$creator instanceof User) {
|
||||
throw new AccessDeniedHttpException('only authenticated user can request a generation');
|
||||
}
|
||||
|
||||
$sendResultTo = null;
|
||||
$dumpOnly = false;
|
||||
}
|
||||
|
||||
$this->messageBus->dispatch(
|
||||
new RequestGenerationMessage(
|
||||
$this->getUser(),
|
||||
$creator,
|
||||
$template,
|
||||
$entityId,
|
||||
$storedObject,
|
||||
$contextGenerationDataSanitized,
|
||||
$isTest,
|
||||
$sendResultTo,
|
||||
$dumpOnly,
|
||||
)
|
||||
);
|
||||
|
||||
|
@@ -69,7 +69,7 @@ class DocGeneratorTemplate
|
||||
*
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
private int $id;
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="json")
|
||||
|
@@ -13,5 +13,5 @@ namespace Chill\DocGeneratorBundle\GeneratorDriver;
|
||||
|
||||
interface DriverInterface
|
||||
{
|
||||
public function generateFromString(string $template, string $resourceType, array $data, string $templateName = null): string;
|
||||
public function generateFromString(string $template, string $resourceType, array $data, ?string $templateName = null): string;
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@ namespace Chill\DocGeneratorBundle\GeneratorDriver\Exception;
|
||||
*/
|
||||
class TemplateException extends \RuntimeException
|
||||
{
|
||||
public function __construct(private readonly array $errors, $code = 0, \Throwable $previous = null)
|
||||
public function __construct(private readonly array $errors, $code = 0, ?\Throwable $previous = null)
|
||||
{
|
||||
parent::__construct('Error while generating document from template', $code, $previous);
|
||||
}
|
||||
|
@@ -33,7 +33,7 @@ final class RelatorioDriver implements DriverInterface
|
||||
$this->url = $parameterBag->get('chill_doc_generator')['driver']['relatorio']['url'];
|
||||
}
|
||||
|
||||
public function generateFromString(string $template, string $resourceType, array $data, string $templateName = null): string
|
||||
public function generateFromString(string $template, string $resourceType, array $data, ?string $templateName = null): string
|
||||
{
|
||||
$form = new FormDataPart(
|
||||
[
|
||||
|
@@ -14,10 +14,9 @@ namespace Chill\DocGeneratorBundle\Repository;
|
||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
final class DocGeneratorTemplateRepository implements ObjectRepository
|
||||
final class DocGeneratorTemplateRepository implements DocGeneratorTemplateRepositoryInterface
|
||||
{
|
||||
private readonly EntityRepository $repository;
|
||||
|
||||
@@ -58,7 +57,7 @@ final class DocGeneratorTemplateRepository implements ObjectRepository
|
||||
*
|
||||
* @return DocGeneratorTemplate[]
|
||||
*/
|
||||
public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null): array
|
||||
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
|
||||
{
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
@@ -85,7 +84,7 @@ final class DocGeneratorTemplateRepository implements ObjectRepository
|
||||
->getResult();
|
||||
}
|
||||
|
||||
public function findOneBy(array $criteria, array $orderBy = null): ?DocGeneratorTemplate
|
||||
public function findOneBy(array $criteria, ?array $orderBy = null): ?DocGeneratorTemplate
|
||||
{
|
||||
return $this->repository->findOneBy($criteria, $orderBy);
|
||||
}
|
||||
|
@@ -0,0 +1,23 @@
|
||||
<?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\DocGeneratorBundle\Repository;
|
||||
|
||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
/**
|
||||
* @extends ObjectRepository<DocGeneratorTemplate>
|
||||
*/
|
||||
interface DocGeneratorTemplateRepositoryInterface extends ObjectRepository
|
||||
{
|
||||
public function countByEntity(string $entity): int;
|
||||
}
|
@@ -1,36 +1,62 @@
|
||||
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block admin_content %}
|
||||
{% embed '@ChillMain/CRUD/_index.html.twig' %}
|
||||
{% block table_entities_thead_tr %}
|
||||
<th></th>
|
||||
<th>{{ 'Title'|trans }}</th>
|
||||
<th>{{ 'docgen.Context'|trans }}</th>
|
||||
<th>{{ 'docgen.test generate'|trans }}</th>
|
||||
<th>{{ 'Edit'|trans }}</th>
|
||||
{% endblock %}
|
||||
|
||||
{% block table_entities_tbody %}
|
||||
{% for entity in entities %}
|
||||
<tr>
|
||||
<td>{{ entity.id }}</td>
|
||||
<td>{{ entity.name|localize_translatable_string}}</td>
|
||||
<td>{{ contextManager.getContextByKey(entity.context).name|trans }}</td>
|
||||
<td>
|
||||
<form method="get" action="{{ path('chill_docgenerator_test_generate_redirect') }}">
|
||||
<input type="hidden" name="returnPath" value="{{ app.request.query.get('returnPath', '/')|e('html_attr') }}" />
|
||||
<input type="hidden" name="template" value="{{ entity.id|e('html_attr') }}" />
|
||||
<input type="hidden" name="entityClassName" value="{{ contextManager.getContextByKey(entity.context).entityClass|e('html_attr') }}" />
|
||||
<input type="text" name="entityId" />
|
||||
{% if entities|length == 0 %}
|
||||
<p class="chill-no-data-statement">{{ 'docgen.Any template configured'|trans }}</p>
|
||||
{% else %}
|
||||
<div class="flex-table">
|
||||
{% for entity in entities %}
|
||||
<div class="item-bloc">
|
||||
<div class="item-row">
|
||||
<div class="item-col" style="flex-basis:100%;">
|
||||
<h2>{{ entity.name|localize_translatable_string }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-row">
|
||||
<p><span class="badge bg-chill-green-dark">{{ contextManager.getContextByKey(entity.context).name|trans }}</span></p>
|
||||
</div>
|
||||
<div class="item-row">
|
||||
<div class="item-col"></div>
|
||||
<ul class="record_actions item-col flex-shrink-1">
|
||||
<li>
|
||||
<form method="get" action="{{ path('chill_docgenerator_test_generate_redirect') }}">
|
||||
<input type="hidden" name="returnPath" value="{{ app.request.query.get('returnPath', app.request.uri)|e('html_attr') }}" />
|
||||
<input type="hidden" name="template" value="{{ entity.id|e('html_attr') }}" />
|
||||
<input type="hidden" name="entityClassName" value="{{ contextManager.getContextByKey(entity.context).entityClass|e('html_attr') }}" />
|
||||
<input type="text" name="entityId" placeholder="{{ 'docgen.entity_id_placeholder'|trans }}" required />
|
||||
|
||||
<button type="submit" class="btn btn-mini btn-misc"><i class="fa fa-cog"></i>{{ 'docgen.test generate'|trans }}</button>
|
||||
</form>
|
||||
</li>
|
||||
<li>
|
||||
{{ entity.file|chill_document_button_group('Template file', true) }}
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_crud_docgen_template_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'Edit'|trans }}"></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<button type="submit" class="btn btn-mini btn-misc"><i class="fa fa-cog"></i>{{ 'docgen.test generate'|trans }}</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ chill_path_add_return_path('chill_crud_docgen_template_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'Edit'|trans }}"></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block actions_before %}
|
||||
|
@@ -6,18 +6,20 @@
|
||||
<div class="col-md-10 col-xxl">
|
||||
|
||||
<h1>{{ block('title') }}</h1>
|
||||
<div class="container">
|
||||
<div class="container overflow-hidden">
|
||||
{% for key, context in contexts %}
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="row g-3" style="margin-top: 1rem;">
|
||||
<div class="col-4 offset-1 text-center">
|
||||
<a
|
||||
href="{{ path('chill_crud_docgen_template_new', { 'context': key }) }}"
|
||||
class="btn btn-outline-chill-green-dark">
|
||||
{{ context.name|trans }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
{{ context.description|trans|nl2br }}
|
||||
<div class="col">
|
||||
<div>
|
||||
{{ context.description|trans|nl2br }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{{ creator.label }},
|
||||
{% if creator is not same as null %}{{ creator.label }},{% endif %}
|
||||
|
||||
{{ 'docgen.failure_email.The generation of the document {template_name} failed'|trans({'{template_name}': template.name|localize_translatable_string}) }}
|
||||
{{ 'docgen.failure_email.The generation of the document %template_name% failed'|trans({'%template_name%': template.name|localize_translatable_string}) }}
|
||||
|
||||
{{ 'docgen.failure_email.Forward this email to your administrator for solving'|trans }}
|
||||
|
||||
|
@@ -0,0 +1,7 @@
|
||||
{{ 'docgen.data_dump_email.Dear'|trans }}
|
||||
|
||||
{{ 'docgen.data_dump_email.data_dump_ready_and_link'|trans }}
|
||||
|
||||
{{ link }}
|
||||
|
||||
{{ 'docgen.data_dump_email.link_valid_until'|trans({validity: validity}) }}
|
@@ -20,7 +20,7 @@ class NormalizeNullValueHelper
|
||||
{
|
||||
}
|
||||
|
||||
public function normalize(array $attributes, string $format = 'docgen', ?array $context = [], ClassMetadataInterface $classMetadata = null)
|
||||
public function normalize(array $attributes, string $format = 'docgen', ?array $context = [], ?ClassMetadataInterface $classMetadata = null)
|
||||
{
|
||||
$data = [];
|
||||
$data['isNull'] = true;
|
||||
|
@@ -21,7 +21,7 @@ class BaseContextData
|
||||
{
|
||||
}
|
||||
|
||||
public function getData(User $user = null): array
|
||||
public function getData(?User $user = null): array
|
||||
{
|
||||
$data = [];
|
||||
|
||||
|
@@ -17,54 +17,88 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||
use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface;
|
||||
use Chill\DocGeneratorBundle\GeneratorDriver\Exception\TemplateException;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\File\File;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class Generator implements GeneratorInterface
|
||||
{
|
||||
private const LOG_PREFIX = '[docgen generator] ';
|
||||
|
||||
public function __construct(private readonly ContextManagerInterface $contextManager, private readonly DriverInterface $driver, private readonly EntityManagerInterface $entityManager, private readonly LoggerInterface $logger, private readonly StoredObjectManagerInterface $storedObjectManager)
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ContextManagerInterface $contextManager,
|
||||
private readonly DriverInterface $driver,
|
||||
private readonly ManagerRegistry $objectManagerRegistry,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly StoredObjectManagerInterface $storedObjectManager
|
||||
) {
|
||||
}
|
||||
|
||||
public function generateDataDump(
|
||||
DocGeneratorTemplate $template,
|
||||
int $entityId,
|
||||
array $contextGenerationDataNormalized,
|
||||
StoredObject $destinationStoredObject,
|
||||
User $creator,
|
||||
bool $clearEntityManagerDuringProcess = true,
|
||||
): StoredObject {
|
||||
return $this->generateFromTemplate(
|
||||
$template,
|
||||
$entityId,
|
||||
$contextGenerationDataNormalized,
|
||||
$destinationStoredObject,
|
||||
$creator,
|
||||
$clearEntityManagerDuringProcess,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of File|null
|
||||
* @template B of bool
|
||||
*
|
||||
* @param B $isTest
|
||||
* @param (B is true ? T : null) $testFile
|
||||
*
|
||||
* @psalm-return (B is true ? string : null)
|
||||
*
|
||||
* @throws \Symfony\Component\Serializer\Exception\ExceptionInterface|\Throwable
|
||||
*/
|
||||
public function generateDocFromTemplate(
|
||||
DocGeneratorTemplate $template,
|
||||
int $entityId,
|
||||
array $contextGenerationDataNormalized,
|
||||
StoredObject $destinationStoredObject = null,
|
||||
bool $isTest = false,
|
||||
File $testFile = null,
|
||||
User $creator = null
|
||||
): ?string {
|
||||
if ($destinationStoredObject instanceof StoredObject && StoredObject::STATUS_PENDING !== $destinationStoredObject->getStatus()) {
|
||||
StoredObject $destinationStoredObject,
|
||||
User $creator,
|
||||
bool $clearEntityManagerDuringProcess = true,
|
||||
): StoredObject {
|
||||
return $this->generateFromTemplate(
|
||||
$template,
|
||||
$entityId,
|
||||
$contextGenerationDataNormalized,
|
||||
$destinationStoredObject,
|
||||
$creator,
|
||||
$clearEntityManagerDuringProcess,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
private function generateFromTemplate(
|
||||
DocGeneratorTemplate $template,
|
||||
int $entityId,
|
||||
array $contextGenerationDataNormalized,
|
||||
StoredObject $destinationStoredObject,
|
||||
User $creator,
|
||||
bool $clearEntityManagerDuringProcess = true,
|
||||
bool $generateDumpOnly = false,
|
||||
): StoredObject {
|
||||
if (StoredObject::STATUS_PENDING !== $destinationStoredObject->getStatus()) {
|
||||
$this->logger->info(self::LOG_PREFIX.'Aborting generation of an already generated document');
|
||||
throw new ObjectReadyException();
|
||||
}
|
||||
|
||||
$this->logger->info(self::LOG_PREFIX.'Starting generation of a document', [
|
||||
'entity_id' => $entityId,
|
||||
'destination_stored_object' => null === $destinationStoredObject ? null : $destinationStoredObject->getId(),
|
||||
'destination_stored_object' => $destinationStoredObject->getId(),
|
||||
]);
|
||||
|
||||
$context = $this->contextManager->getContextByDocGeneratorTemplate($template);
|
||||
|
||||
$entity = $this
|
||||
->entityManager
|
||||
->objectManagerRegistry
|
||||
->getManagerForClass($context->getEntityClass())
|
||||
->find($context->getEntityClass(), $entityId)
|
||||
;
|
||||
|
||||
@@ -82,17 +116,47 @@ class Generator implements GeneratorInterface
|
||||
|
||||
$data = $context->getData($template, $entity, $contextGenerationDataNormalized);
|
||||
|
||||
$destinationStoredObjectId = $destinationStoredObject instanceof StoredObject ? $destinationStoredObject->getId() : null;
|
||||
$this->entityManager->clear();
|
||||
gc_collect_cycles();
|
||||
if (null !== $destinationStoredObjectId) {
|
||||
$destinationStoredObject = $this->entityManager->find(StoredObject::class, $destinationStoredObjectId);
|
||||
$destinationStoredObjectId = $destinationStoredObject->getId();
|
||||
|
||||
if ($clearEntityManagerDuringProcess) {
|
||||
// we clean the entity manager
|
||||
$this->objectManagerRegistry->getManagerForClass($context->getEntityClass())?->clear();
|
||||
|
||||
// this will force php to clean the memory
|
||||
gc_collect_cycles();
|
||||
}
|
||||
|
||||
if ($isTest && ($testFile instanceof File)) {
|
||||
$templateDecrypted = file_get_contents($testFile->getPathname());
|
||||
} else {
|
||||
// as we potentially deleted the storedObject from memory, we have to restore it
|
||||
$destinationStoredObject = $this->objectManagerRegistry
|
||||
->getManagerForClass(StoredObject::class)
|
||||
->find(StoredObject::class, $destinationStoredObjectId);
|
||||
|
||||
if ($generateDumpOnly) {
|
||||
$content = Yaml::dump($data, 6);
|
||||
/* @var StoredObject $destinationStoredObject */
|
||||
$destinationStoredObject
|
||||
->setType('application/yaml')
|
||||
->setFilename(sprintf('%s_yaml', uniqid('doc_', true)))
|
||||
->setStatus(StoredObject::STATUS_READY)
|
||||
;
|
||||
|
||||
try {
|
||||
$this->storedObjectManager->write($destinationStoredObject, $content);
|
||||
} catch (StoredObjectManagerException $e) {
|
||||
$destinationStoredObject->addGenerationErrors($e->getMessage());
|
||||
|
||||
throw new GeneratorException([$e->getMessage()], $e);
|
||||
}
|
||||
|
||||
return $destinationStoredObject;
|
||||
}
|
||||
|
||||
try {
|
||||
$templateDecrypted = $this->storedObjectManager->read($template->getFile());
|
||||
} catch (StoredObjectManagerException $e) {
|
||||
$destinationStoredObject->addGenerationErrors($e->getMessage());
|
||||
|
||||
throw new GeneratorException([$e->getMessage()], $e);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -105,19 +169,10 @@ class Generator implements GeneratorInterface
|
||||
$template->getFile()->getFilename()
|
||||
);
|
||||
} catch (TemplateException $e) {
|
||||
$destinationStoredObject->addGenerationErrors(implode("\n", $e->getErrors()));
|
||||
throw new GeneratorException($e->getErrors(), $e);
|
||||
}
|
||||
|
||||
if (true === $isTest) {
|
||||
$this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [
|
||||
'is_test' => true,
|
||||
'entity_id' => $entityId,
|
||||
'destination_stored_object' => null === $destinationStoredObject ? null : $destinationStoredObject->getId(),
|
||||
]);
|
||||
|
||||
return $generatedResource;
|
||||
}
|
||||
|
||||
/* @var StoredObject $destinationStoredObject */
|
||||
$destinationStoredObject
|
||||
->setType($template->getFile()->getType())
|
||||
@@ -125,15 +180,19 @@ class Generator implements GeneratorInterface
|
||||
->setStatus(StoredObject::STATUS_READY)
|
||||
;
|
||||
|
||||
$this->storedObjectManager->write($destinationStoredObject, $generatedResource);
|
||||
try {
|
||||
$this->storedObjectManager->write($destinationStoredObject, $generatedResource);
|
||||
} catch (StoredObjectManagerException $e) {
|
||||
$destinationStoredObject->addGenerationErrors($e->getMessage());
|
||||
|
||||
$this->entityManager->flush();
|
||||
throw new GeneratorException([$e->getMessage()], $e);
|
||||
}
|
||||
|
||||
$this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [
|
||||
'entity_id' => $entityId,
|
||||
'destination_stored_object' => $destinationStoredObject->getId(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
return $destinationStoredObject;
|
||||
}
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@ class GeneratorException extends \RuntimeException
|
||||
/**
|
||||
* @param string[] $errors
|
||||
*/
|
||||
public function __construct(private readonly array $errors = [], \Throwable $previous = null)
|
||||
public function __construct(private readonly array $errors = [], ?\Throwable $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
'Could not generate the document',
|
||||
|
@@ -13,29 +13,48 @@ namespace Chill\DocGeneratorBundle\Service\Generator;
|
||||
|
||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Symfony\Component\HttpFoundation\File\File;
|
||||
|
||||
interface GeneratorInterface
|
||||
{
|
||||
/**
|
||||
* @template T of File|null
|
||||
* @template B of bool
|
||||
* Generate a document and store the document on disk.
|
||||
*
|
||||
* @param B $isTest
|
||||
* @param (B is true ? T : null) $testFile
|
||||
* The given $destinationStoredObject will be updated with filename, status, and eventually errors will be stored
|
||||
* into the object. The number of generation trial will also be incremented.
|
||||
*
|
||||
* @psalm-return (B is true ? string : null)
|
||||
* This process requires a huge amount of data. For this reason, the entity manager will be cleaned during the process,
|
||||
* unless the paarameter `$clearEntityManagerDuringProcess` is set on false.
|
||||
*
|
||||
* @throws \Symfony\Component\Serializer\Exception\ExceptionInterface|\Throwable
|
||||
* As the entity manager might be cleaned, the new instance of the stored object will be returned by this method.
|
||||
*
|
||||
* Ensure to store change in the database after each generation trial (call `EntityManagerInterface::flush`).
|
||||
*
|
||||
* @phpstan-impure
|
||||
*
|
||||
* @param StoredObject $destinationStoredObject will be update with filename, status and incremented of generation trials
|
||||
*
|
||||
* @throws StoredObjectManagerException if unable to decrypt the template or store the document
|
||||
*/
|
||||
public function generateDocFromTemplate(
|
||||
DocGeneratorTemplate $template,
|
||||
int $entityId,
|
||||
array $contextGenerationDataNormalized,
|
||||
StoredObject $destinationStoredObject = null,
|
||||
bool $isTest = false,
|
||||
File $testFile = null,
|
||||
User $creator = null
|
||||
): ?string;
|
||||
StoredObject $destinationStoredObject,
|
||||
User $creator,
|
||||
bool $clearEntityManagerDuringProcess = true,
|
||||
): StoredObject;
|
||||
|
||||
/**
|
||||
* Generate a data dump, and store it within the `$destinationStoredObject`.
|
||||
*/
|
||||
public function generateDataDump(
|
||||
DocGeneratorTemplate $template,
|
||||
int $entityId,
|
||||
array $contextGenerationDataNormalized,
|
||||
StoredObject $destinationStoredObject,
|
||||
User $creator,
|
||||
bool $clearEntityManagerDuringProcess = true,
|
||||
): StoredObject;
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@ namespace Chill\DocGeneratorBundle\Service\Generator;
|
||||
|
||||
class RelatedEntityNotFoundException extends \RuntimeException
|
||||
{
|
||||
public function __construct(string $relatedEntityClass, int $relatedEntityId, \Throwable $previous = null)
|
||||
public function __construct(string $relatedEntityClass, int $relatedEntityId, ?\Throwable $previous = null)
|
||||
{
|
||||
parent::__construct(
|
||||
sprintf('Related entity not found: %s, %s', $relatedEntityClass, $relatedEntityId),
|
||||
|
@@ -0,0 +1,64 @@
|
||||
<?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\DocGeneratorBundle\Service\Messenger;
|
||||
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
||||
use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent;
|
||||
|
||||
/**
|
||||
* The OnAfterMessageHandledClearStoredObjectCache class is an event subscriber that clears the stored object cache
|
||||
* after a specific message is handled or fails.
|
||||
*/
|
||||
final readonly class OnAfterMessageHandledClearStoredObjectCache implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private StoredObjectManagerInterface $storedObjectManager,
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
WorkerMessageHandledEvent::class => [
|
||||
['afterHandling', 0],
|
||||
],
|
||||
WorkerMessageFailedEvent::class => [
|
||||
['afterFails', 0],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function afterHandling(WorkerMessageHandledEvent $event): void
|
||||
{
|
||||
if ($event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) {
|
||||
$this->clearStoredObjectCache();
|
||||
}
|
||||
}
|
||||
|
||||
public function afterFails(WorkerMessageFailedEvent $event): void
|
||||
{
|
||||
if ($event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) {
|
||||
$this->clearStoredObjectCache();
|
||||
}
|
||||
}
|
||||
|
||||
private function clearStoredObjectCache(): void
|
||||
{
|
||||
$this->logger->debug('clear the cache after generation of a document');
|
||||
|
||||
$this->storedObjectManager->clearCache();
|
||||
}
|
||||
}
|
@@ -11,10 +11,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocGeneratorBundle\Service\Messenger;
|
||||
|
||||
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
|
||||
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface;
|
||||
use Chill\DocGeneratorBundle\Service\Generator\GeneratorException;
|
||||
use Chill\DocGeneratorBundle\tests\Service\Messenger\OnGenerationFailsTest;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
|
||||
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
|
||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
@@ -24,12 +25,22 @@ use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @see OnGenerationFailsTest for test suite
|
||||
*/
|
||||
final readonly class OnGenerationFails implements EventSubscriberInterface
|
||||
{
|
||||
public const LOG_PREFIX = '[docgen failed] ';
|
||||
|
||||
public function __construct(private DocGeneratorTemplateRepository $docGeneratorTemplateRepository, private EntityManagerInterface $entityManager, private LoggerInterface $logger, private MailerInterface $mailer, private StoredObjectRepository $storedObjectRepository, private TranslatorInterface $translator, private UserRepositoryInterface $userRepository)
|
||||
{
|
||||
public function __construct(
|
||||
private DocGeneratorTemplateRepositoryInterface $docGeneratorTemplateRepository,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private LoggerInterface $logger,
|
||||
private MailerInterface $mailer,
|
||||
private StoredObjectRepositoryInterface $storedObjectRepository,
|
||||
private TranslatorInterface $translator,
|
||||
private UserRepositoryInterface $userRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
@@ -45,13 +56,12 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) {
|
||||
$message = $event->getEnvelope()->getMessage();
|
||||
|
||||
if (!$message instanceof RequestGenerationMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var RequestGenerationMessage $message */
|
||||
$message = $event->getEnvelope()->getMessage();
|
||||
|
||||
$this->logger->error(self::LOG_PREFIX.'Docgen failed', [
|
||||
'stored_object_id' => $message->getDestinationStoredObjectId(),
|
||||
'entity_id' => $message->getEntityId(),
|
||||
@@ -79,16 +89,8 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
|
||||
|
||||
private function warnCreator(RequestGenerationMessage $message, WorkerMessageFailedEvent $event): void
|
||||
{
|
||||
$creatorId = $message->getCreatorId();
|
||||
|
||||
if (null === $creator = $this->userRepository->find($creatorId)) {
|
||||
$this->logger->error(self::LOG_PREFIX.'Creator not found with given id', ['creator_id', $creatorId]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (null === $creator->getEmail() || '' === $creator->getEmail()) {
|
||||
$this->logger->info(self::LOG_PREFIX.'Creator does not have any email', ['user' => $creator->getUsernameCanonical()]);
|
||||
if (null === $message->getSendResultToEmail() || '' === $message->getSendResultToEmail()) {
|
||||
$this->logger->info(self::LOG_PREFIX.'No email associated with this request generation');
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -96,7 +98,7 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
|
||||
// if the exception is not a GeneratorException, we try the previous one...
|
||||
$throwable = $event->getThrowable();
|
||||
if (!$throwable instanceof GeneratorException) {
|
||||
$throwable = $throwable->getPrevious();
|
||||
$throwable = $throwable->getPrevious() ?? $throwable;
|
||||
}
|
||||
|
||||
if ($throwable instanceof GeneratorException) {
|
||||
@@ -111,8 +113,14 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
|
||||
return;
|
||||
}
|
||||
|
||||
if (null === $creator = $this->userRepository->find($message->getCreatorId())) {
|
||||
$this->logger->error(self::LOG_PREFIX.'Creator not found');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$email = (new TemplatedEmail())
|
||||
->to($creator->getEmail())
|
||||
->to($message->getSendResultToEmail())
|
||||
->subject($this->translator->trans('docgen.failure_email.The generation of a document failed'))
|
||||
->textTemplate('@ChillDocGenerator/Email/on_generation_failed_email.txt.twig')
|
||||
->context([
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user