mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-19 13:15:00 +00:00
Compare commits
92 Commits
testing-20
...
2.19.0
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 |
@@ -1,6 +0,0 @@
|
||||
kind: Feature
|
||||
body: Create new filter for persons having a participation in an accompanying period
|
||||
during a certain time span
|
||||
time: 2023-12-18T15:31:51.489901829+01:00
|
||||
custom:
|
||||
Issue: "231"
|
@@ -1,6 +0,0 @@
|
||||
kind: Feature
|
||||
body: '[Export][List of accompanyign period] Add two columns: the list of persons
|
||||
participating to the period, and their ids'
|
||||
time: 2024-01-22T12:48:49.824833412+01:00
|
||||
custom:
|
||||
Issue: "241"
|
@@ -1,5 +0,0 @@
|
||||
kind: Feature
|
||||
body: 'Add capability to generate export about change of steps of accompanying period, and generate exports for this'
|
||||
time: 2024-01-29T13:33:19.190365565+01:00
|
||||
custom:
|
||||
Issue: "244"
|
@@ -1,5 +0,0 @@
|
||||
kind: Feature
|
||||
body: 'Export: group accompanying period by person participating'
|
||||
time: 2024-02-07T10:39:51.97331052+01:00
|
||||
custom:
|
||||
Issue: "253"
|
@@ -1,5 +0,0 @@
|
||||
kind: Feature
|
||||
body: 'Export: add filter for courses not linked to a reference address'
|
||||
time: 2024-02-07T11:46:29.491027007+01:00
|
||||
custom:
|
||||
Issue: "243"
|
@@ -1,5 +0,0 @@
|
||||
kind: Feature
|
||||
body: Allow to group activities linked with accompanying period by reason
|
||||
time: 2024-02-07T16:40:38.408575109+01:00
|
||||
custom:
|
||||
Issue: "229"
|
@@ -1,6 +0,0 @@
|
||||
kind: Fixed
|
||||
body: Fix error in logs about wrong typing of eventArgs in onEditNotificationComment
|
||||
method
|
||||
time: 2023-11-29T11:31:38.933538592+01:00
|
||||
custom:
|
||||
Issue: "220"
|
@@ -1,6 +0,0 @@
|
||||
kind: Fixed
|
||||
body: Fix the conditions upon which social actions should be optional or required
|
||||
in relation to social issues within the activity creation form
|
||||
time: 2024-01-30T14:03:01.942955636+01:00
|
||||
custom:
|
||||
Issue: "256"
|
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
|
||||
|
||||
|
@@ -35,7 +35,7 @@ variables:
|
||||
# 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=0
|
||||
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
|
||||
|
@@ -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
|
||||
=========
|
||||
|
||||
|
@@ -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",
|
||||
|
@@ -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)
|
||||
|
@@ -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,
|
||||
]);
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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%'
|
||||
|
@@ -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,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.
|
||||
|
@@ -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): bool|null
|
||||
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): bool|null;
|
||||
public function isUserAbsent(User $user): ?bool;
|
||||
}
|
||||
|
@@ -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")
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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}) }}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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([
|
||||
|
@@ -11,15 +11,21 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocGeneratorBundle\Service\Messenger;
|
||||
|
||||
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
|
||||
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
|
||||
use Chill\DocGeneratorBundle\Service\Generator\Generator;
|
||||
use Chill\DocGeneratorBundle\Service\Generator\GeneratorException;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
|
||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* Handle the request of document generation.
|
||||
@@ -30,8 +36,17 @@ class RequestGenerationHandler implements MessageHandlerInterface
|
||||
|
||||
private const LOG_PREFIX = '[docgen message handler] ';
|
||||
|
||||
public function __construct(private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository, private readonly EntityManagerInterface $entityManager, private readonly Generator $generator, private readonly LoggerInterface $logger, private readonly StoredObjectRepository $storedObjectRepository, private readonly UserRepositoryInterface $userRepository)
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly Generator $generator,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly StoredObjectRepository $storedObjectRepository,
|
||||
private readonly UserRepositoryInterface $userRepository,
|
||||
private readonly MailerInterface $mailer,
|
||||
private readonly TempUrlGeneratorInterface $tempUrlGenerator,
|
||||
private readonly TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(RequestGenerationMessage $message)
|
||||
@@ -45,25 +60,59 @@ class RequestGenerationHandler implements MessageHandlerInterface
|
||||
}
|
||||
|
||||
if ($destinationStoredObject->getGenerationTrialsCounter() >= self::AUTHORIZED_TRIALS) {
|
||||
$this->logger->error(self::LOG_PREFIX.'Request generation abandoned: maximum number of retry reached', [
|
||||
'template_id' => $message->getTemplateId(),
|
||||
'destination_stored_object' => $message->getDestinationStoredObjectId(),
|
||||
'trial' => $destinationStoredObject->getGenerationTrialsCounter(),
|
||||
]);
|
||||
|
||||
throw new UnrecoverableMessageHandlingException('maximum number of retry reached');
|
||||
}
|
||||
|
||||
$creator = $this->userRepository->find($message->getCreatorId());
|
||||
|
||||
// we increase the number of generation trial in the object, and, in the same time, update the counter
|
||||
// on the database side. This ensure that, if the script fails for any reason (memory limit reached), the
|
||||
// counter is inscreased
|
||||
$destinationStoredObject->addGenerationTrial();
|
||||
$this->entityManager->createQuery('UPDATE '.StoredObject::class.' s SET s.generationTrialsCounter = s.generationTrialsCounter + 1 WHERE s.id = :id')
|
||||
->setParameter('id', $destinationStoredObject->getId())
|
||||
->execute();
|
||||
|
||||
$this->generator->generateDocFromTemplate(
|
||||
$template,
|
||||
$message->getEntityId(),
|
||||
$message->getContextGenerationData(),
|
||||
$destinationStoredObject,
|
||||
false,
|
||||
null,
|
||||
$creator
|
||||
);
|
||||
try {
|
||||
if ($message->isDumpOnly()) {
|
||||
$destinationStoredObject = $this->generator->generateDataDump(
|
||||
$template,
|
||||
$message->getEntityId(),
|
||||
$message->getContextGenerationData(),
|
||||
$destinationStoredObject,
|
||||
$creator
|
||||
);
|
||||
|
||||
$this->sendDataDump($destinationStoredObject, $message);
|
||||
} else {
|
||||
$destinationStoredObject = $this->generator->generateDocFromTemplate(
|
||||
$template,
|
||||
$message->getEntityId(),
|
||||
$message->getContextGenerationData(),
|
||||
$destinationStoredObject,
|
||||
$creator
|
||||
);
|
||||
}
|
||||
} catch (StoredObjectManagerException|GeneratorException $e) {
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->logger->error(self::LOG_PREFIX.'Request generation failed', [
|
||||
'template_id' => $message->getTemplateId(),
|
||||
'destination_stored_object' => $message->getDestinationStoredObjectId(),
|
||||
'trial' => $destinationStoredObject->getGenerationTrialsCounter(),
|
||||
'error' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->logger->info(self::LOG_PREFIX.'Request generation finished', [
|
||||
'template_id' => $message->getTemplateId(),
|
||||
@@ -71,4 +120,23 @@ class RequestGenerationHandler implements MessageHandlerInterface
|
||||
'duration_int' => (new \DateTimeImmutable('now'))->getTimestamp() - $message->getCreatedAt()->getTimestamp(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function sendDataDump(StoredObject $destinationStoredObject, RequestGenerationMessage $message): void
|
||||
{
|
||||
$url = $this->tempUrlGenerator->generate('GET', $destinationStoredObject->getFilename(), 3600);
|
||||
$parts = [];
|
||||
parse_str(parse_url((string) $url->url)['query'], $parts);
|
||||
$validity = \DateTimeImmutable::createFromFormat('U', $parts['temp_url_expires']);
|
||||
|
||||
$email = (new TemplatedEmail())
|
||||
->to($message->getSendResultToEmail())
|
||||
->textTemplate('@ChillDocGenerator/Email/send_data_dump_to_admin.txt.twig')
|
||||
->context([
|
||||
'link' => $url->url,
|
||||
'validity' => $validity,
|
||||
])
|
||||
->subject($this->translator->trans('docgen.data_dump_email.subject'));
|
||||
|
||||
$this->mailer->send($email);
|
||||
}
|
||||
}
|
||||
|
@@ -15,27 +15,33 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
|
||||
class RequestGenerationMessage
|
||||
final readonly class RequestGenerationMessage
|
||||
{
|
||||
private readonly int $creatorId;
|
||||
private int $creatorId;
|
||||
|
||||
private readonly int $templateId;
|
||||
private int $templateId;
|
||||
|
||||
private readonly int $destinationStoredObjectId;
|
||||
private int $destinationStoredObjectId;
|
||||
|
||||
private readonly \DateTimeImmutable $createdAt;
|
||||
private \DateTimeImmutable $createdAt;
|
||||
|
||||
private ?string $sendResultToEmail;
|
||||
|
||||
public function __construct(
|
||||
User $creator,
|
||||
DocGeneratorTemplate $template,
|
||||
private readonly int $entityId,
|
||||
private int $entityId,
|
||||
StoredObject $destinationStoredObject,
|
||||
private readonly array $contextGenerationData
|
||||
private array $contextGenerationData,
|
||||
private bool $isTest = false,
|
||||
?string $sendResultToEmail = null,
|
||||
private bool $dumpOnly = false,
|
||||
) {
|
||||
$this->creatorId = $creator->getId();
|
||||
$this->templateId = $template->getId();
|
||||
$this->destinationStoredObjectId = $destinationStoredObject->getId();
|
||||
$this->createdAt = new \DateTimeImmutable('now');
|
||||
$this->sendResultToEmail = $sendResultToEmail ?? $creator->getEmail();
|
||||
}
|
||||
|
||||
public function getCreatorId(): int
|
||||
@@ -67,4 +73,19 @@ class RequestGenerationMessage
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function isTest(): bool
|
||||
{
|
||||
return $this->isTest;
|
||||
}
|
||||
|
||||
public function getSendResultToEmail(): ?string
|
||||
{
|
||||
return $this->sendResultToEmail;
|
||||
}
|
||||
|
||||
public function isDumpOnly(): bool
|
||||
{
|
||||
return $this->dumpOnly;
|
||||
}
|
||||
}
|
||||
|
@@ -20,7 +20,9 @@ use Chill\DocGeneratorBundle\Service\Generator\ObjectReadyException;
|
||||
use Chill\DocGeneratorBundle\Service\Generator\RelatedEntityNotFoundException;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
@@ -66,7 +68,11 @@ class GeneratorTest extends TestCase
|
||||
$entityManager->find('DummyClass', Argument::type('int'))
|
||||
->willReturn($entity);
|
||||
$entityManager->clear()->shouldBeCalled();
|
||||
$entityManager->flush()->shouldBeCalled();
|
||||
$entityManager->flush()->shouldNotBeCalled();
|
||||
|
||||
$managerRegistry = $this->prophesize(ManagerRegistry::class);
|
||||
$managerRegistry->getManagerForClass('DummyClass')->willReturn($entityManager->reveal());
|
||||
$managerRegistry->getManagerForClass(StoredObject::class)->willReturn($entityManager->reveal());
|
||||
|
||||
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
|
||||
$storedObjectManager->read($templateStoredObject)->willReturn('template');
|
||||
@@ -75,7 +81,7 @@ class GeneratorTest extends TestCase
|
||||
$generator = new Generator(
|
||||
$contextManagerInterface->reveal(),
|
||||
$driver->reveal(),
|
||||
$entityManager->reveal(),
|
||||
$managerRegistry->reveal(),
|
||||
new NullLogger(),
|
||||
$storedObjectManager->reveal()
|
||||
);
|
||||
@@ -84,7 +90,8 @@ class GeneratorTest extends TestCase
|
||||
$template,
|
||||
1,
|
||||
[],
|
||||
$destinationStoredObject
|
||||
$destinationStoredObject,
|
||||
new User()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,7 +102,7 @@ class GeneratorTest extends TestCase
|
||||
$generator = new Generator(
|
||||
$this->prophesize(ContextManagerInterface::class)->reveal(),
|
||||
$this->prophesize(DriverInterface::class)->reveal(),
|
||||
$this->prophesize(EntityManagerInterface::class)->reveal(),
|
||||
$this->prophesize(ManagerRegistry::class)->reveal(),
|
||||
new NullLogger(),
|
||||
$this->prophesize(StoredObjectManagerInterface::class)->reveal()
|
||||
);
|
||||
@@ -108,7 +115,8 @@ class GeneratorTest extends TestCase
|
||||
$template,
|
||||
1,
|
||||
[],
|
||||
$destinationStoredObject
|
||||
$destinationStoredObject,
|
||||
new User()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -136,10 +144,14 @@ class GeneratorTest extends TestCase
|
||||
$entityManager->find(Argument::type('string'), Argument::type('int'))
|
||||
->willReturn(null);
|
||||
|
||||
$managerRegistry = $this->prophesize(ManagerRegistry::class);
|
||||
$managerRegistry->getManagerForClass('DummyClass')->willReturn($entityManager->reveal());
|
||||
$managerRegistry->getManagerForClass(StoredObject::class)->willReturn($entityManager->reveal());
|
||||
|
||||
$generator = new Generator(
|
||||
$contextManagerInterface->reveal(),
|
||||
$this->prophesize(DriverInterface::class)->reveal(),
|
||||
$entityManager->reveal(),
|
||||
$managerRegistry->reveal(),
|
||||
new NullLogger(),
|
||||
$this->prophesize(StoredObjectManagerInterface::class)->reveal()
|
||||
);
|
||||
@@ -148,7 +160,8 @@ class GeneratorTest extends TestCase
|
||||
$template,
|
||||
1,
|
||||
[],
|
||||
$destinationStoredObject
|
||||
$destinationStoredObject,
|
||||
new User()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,107 @@
|
||||
<?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\tests\Service\Messenger;
|
||||
|
||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||
use Chill\DocGeneratorBundle\Service\Messenger\OnAfterMessageHandledClearStoredObjectCache;
|
||||
use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
||||
use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class OnAfterMessageHandledClearStoredObjectCacheTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testThatNotGenerationMessageDoesNotCallAClearCache(): void
|
||||
{
|
||||
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
|
||||
$storedObjectManager->clearCache()->shouldNotBeCalled();
|
||||
|
||||
$eventSubscriber = $this->buildEventSubscriber($storedObjectManager->reveal());
|
||||
|
||||
$eventSubscriber->afterHandling($this->buildEventSuccess(new \stdClass()));
|
||||
$eventSubscriber->afterFails($this->buildEventFailed(new \stdClass()));
|
||||
}
|
||||
|
||||
public function testThatConcernedEventCallAClearCache(): void
|
||||
{
|
||||
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
|
||||
$storedObjectManager->clearCache()->shouldBeCalledTimes(2);
|
||||
|
||||
$eventSubscriber = $this->buildEventSubscriber($storedObjectManager->reveal());
|
||||
|
||||
$eventSubscriber->afterHandling($this->buildEventSuccess($this->buildRequestGenerationMessage()));
|
||||
$eventSubscriber->afterFails($this->buildEventFailed($this->buildRequestGenerationMessage()));
|
||||
}
|
||||
|
||||
private function buildRequestGenerationMessage(
|
||||
): RequestGenerationMessage {
|
||||
$creator = new User();
|
||||
$creator->setEmail('fake@example.com');
|
||||
|
||||
$class = new \ReflectionClass($creator);
|
||||
$property = $class->getProperty('id');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue($creator, 1);
|
||||
|
||||
$template ??= new DocGeneratorTemplate();
|
||||
$class = new \ReflectionClass($template);
|
||||
$property = $class->getProperty('id');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue($template, 2);
|
||||
|
||||
$destinationStoredObject = new StoredObject();
|
||||
$class = new \ReflectionClass($destinationStoredObject);
|
||||
$property = $class->getProperty('id');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue($destinationStoredObject, 3);
|
||||
|
||||
return new RequestGenerationMessage(
|
||||
$creator,
|
||||
$template,
|
||||
1,
|
||||
$destinationStoredObject,
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
private function buildEventSubscriber(StoredObjectManagerInterface $storedObjectManager): OnAfterMessageHandledClearStoredObjectCache
|
||||
{
|
||||
return new OnAfterMessageHandledClearStoredObjectCache($storedObjectManager, new NullLogger());
|
||||
}
|
||||
|
||||
private function buildEventFailed(object $message): WorkerMessageFailedEvent
|
||||
{
|
||||
$envelope = new Envelope($message);
|
||||
|
||||
return new WorkerMessageFailedEvent($envelope, 'testing', new \RuntimeException());
|
||||
}
|
||||
|
||||
private function buildEventSuccess(object $message): WorkerMessageHandledEvent
|
||||
{
|
||||
$envelope = new Envelope($message);
|
||||
|
||||
return new WorkerMessageHandledEvent($envelope, 'test_receiver');
|
||||
}
|
||||
}
|
@@ -0,0 +1,226 @@
|
||||
<?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\tests\Service\Messenger;
|
||||
|
||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface;
|
||||
use Chill\DocGeneratorBundle\Service\Messenger\OnGenerationFails;
|
||||
use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Mime\RawMessage;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class OnGenerationFailsTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testNotConcernedMessageAreNotHandled(): void
|
||||
{
|
||||
$entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||
$entityManager->flush()->shouldNotBeCalled();
|
||||
|
||||
$mailer = $this->prophesize(MailerInterface::class);
|
||||
$mailer->send()->shouldNotBeCalled();
|
||||
|
||||
$eventSubscriber = $this->buildOnGenerationFailsEventSubscriber(
|
||||
entityManager: $entityManager->reveal(),
|
||||
mailer: $mailer->reveal()
|
||||
);
|
||||
|
||||
$event = $this->buildEvent(new \stdClass());
|
||||
|
||||
$eventSubscriber->onMessageFailed($event);
|
||||
}
|
||||
|
||||
public function testMessageThatWillBeRetriedAreNotHandled(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
|
||||
$entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||
$entityManager->flush()->shouldNotBeCalled();
|
||||
|
||||
$mailer = $this->prophesize(MailerInterface::class);
|
||||
$mailer->send()->shouldNotBeCalled();
|
||||
|
||||
$eventSubscriber = $this->buildOnGenerationFailsEventSubscriber(
|
||||
entityManager: $entityManager->reveal(),
|
||||
mailer: $mailer->reveal()
|
||||
);
|
||||
|
||||
$event = $this->buildEvent($this->buildRequestGenerationMessage($storedObject));
|
||||
$event->setForRetry();
|
||||
|
||||
$eventSubscriber->onMessageFailed($event);
|
||||
}
|
||||
|
||||
public function testThatANotRetriyableEventWillMarkObjectAsFailed(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
|
||||
$entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||
$entityManager->flush()->shouldBeCalled();
|
||||
|
||||
$mailer = $this->prophesize(MailerInterface::class);
|
||||
$mailer->send(Argument::type(RawMessage::class), Argument::any())->shouldBeCalled();
|
||||
|
||||
$eventSubscriber = $this->buildOnGenerationFailsEventSubscriber(
|
||||
entityManager: $entityManager->reveal(),
|
||||
mailer: $mailer->reveal(),
|
||||
storedObject: $storedObject
|
||||
);
|
||||
|
||||
$event = $this->buildEvent($this->buildRequestGenerationMessage($storedObject));
|
||||
|
||||
$eventSubscriber->onMessageFailed($event);
|
||||
|
||||
self::assertEquals(StoredObject::STATUS_FAILURE, $storedObject->getStatus());
|
||||
}
|
||||
|
||||
public function testThatANonRetryableEventSendAnEmail(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
|
||||
$entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||
$entityManager->flush()->shouldBeCalled();
|
||||
|
||||
$mailer = $this->prophesize(MailerInterface::class);
|
||||
$mailer->send(
|
||||
Argument::that(function ($arg): bool {
|
||||
if (!$arg instanceof Email) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($arg->getTo() as $to) {
|
||||
if ('test@test.com' === $to->getAddress()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
Argument::any()
|
||||
)
|
||||
->shouldBeCalled();
|
||||
|
||||
$eventSubscriber = $this->buildOnGenerationFailsEventSubscriber(
|
||||
entityManager: $entityManager->reveal(),
|
||||
mailer: $mailer->reveal(),
|
||||
storedObject: $storedObject
|
||||
);
|
||||
|
||||
$event = $this->buildEvent($this->buildRequestGenerationMessage($storedObject, sendResultToEmail: 'test@test.com'));
|
||||
|
||||
$eventSubscriber->onMessageFailed($event);
|
||||
}
|
||||
|
||||
private function buildRequestGenerationMessage(
|
||||
StoredObject $destinationStoredObject,
|
||||
?User $creator = null,
|
||||
?DocGeneratorTemplate $template = null,
|
||||
array $contextGenerationData = [],
|
||||
bool $isTest = false,
|
||||
?string $sendResultToEmail = null,
|
||||
): RequestGenerationMessage {
|
||||
if (null === $creator) {
|
||||
$creator = new User();
|
||||
$creator->setEmail('fake@example.com');
|
||||
}
|
||||
|
||||
if (null === $creator->getId()) {
|
||||
$class = new \ReflectionClass($creator);
|
||||
$property = $class->getProperty('id');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue($creator, 1);
|
||||
}
|
||||
|
||||
$template ??= new DocGeneratorTemplate();
|
||||
$class = new \ReflectionClass($template);
|
||||
$property = $class->getProperty('id');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue($template, 2);
|
||||
|
||||
$class = new \ReflectionClass($destinationStoredObject);
|
||||
$property = $class->getProperty('id');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue($destinationStoredObject, 3);
|
||||
|
||||
return new RequestGenerationMessage(
|
||||
$creator,
|
||||
$template,
|
||||
1,
|
||||
$destinationStoredObject,
|
||||
$contextGenerationData,
|
||||
$isTest,
|
||||
$sendResultToEmail
|
||||
);
|
||||
}
|
||||
|
||||
private function buildOnGenerationFailsEventSubscriber(
|
||||
?StoredObject $storedObject = null,
|
||||
?EntityManagerInterface $entityManager = null,
|
||||
?MailerInterface $mailer = null,
|
||||
): OnGenerationFails {
|
||||
$storedObjectRepository = $this->prophesize(StoredObjectRepositoryInterface::class);
|
||||
$storedObjectRepository->find(Argument::type('int'))->willReturn($storedObject ?? new StoredObject());
|
||||
|
||||
if (null === $entityManager) {
|
||||
$entityManagerProphecy = $this->prophesize(EntityManagerInterface::class);
|
||||
}
|
||||
|
||||
if (null === $mailer) {
|
||||
$mailerProphecy = $this->prophesize(MailerInterface::class);
|
||||
}
|
||||
|
||||
$translator = $this->prophesize(TranslatorInterface::class);
|
||||
$translator->trans(Argument::type('string'))->will(fn ($args) => $args[0]);
|
||||
|
||||
$userRepository = $this->prophesize(UserRepositoryInterface::class);
|
||||
$userRepository->find(Argument::type('int'))->willReturn(new User());
|
||||
|
||||
$docGeneratorTemplateRepository = $this->prophesize(DocGeneratorTemplateRepositoryInterface::class);
|
||||
$docGeneratorTemplateRepository->find(Argument::type('int'))->willReturn(new DocGeneratorTemplate());
|
||||
|
||||
return new OnGenerationFails(
|
||||
$docGeneratorTemplateRepository->reveal(),
|
||||
$entityManager ?? $entityManagerProphecy->reveal(),
|
||||
new NullLogger(),
|
||||
$mailer ?? $mailerProphecy->reveal(),
|
||||
$storedObjectRepository->reveal(),
|
||||
$translator->reveal(),
|
||||
$userRepository->reveal()
|
||||
);
|
||||
}
|
||||
|
||||
private function buildEvent(object $message): WorkerMessageFailedEvent
|
||||
{
|
||||
$envelope = new Envelope($message);
|
||||
|
||||
return new WorkerMessageFailedEvent($envelope, 'testing', new \RuntimeException());
|
||||
}
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
docgen:
|
||||
data_dump_email:
|
||||
link_valid_until: >-
|
||||
Ce lien est valide jusqu'au {validity, date, full}, {validity, time, medium}
|
@@ -14,13 +14,31 @@ docgen:
|
||||
Doc generation is pending: La génération de ce document est en cours
|
||||
Come back later: Revenir plus tard
|
||||
|
||||
Send report to: Envoyer le rapport à
|
||||
Send report errors to this email address: Les rapports d'erreurs seront envoyés à l'adresse email indiquée
|
||||
Generate as creator: Générer en tant que
|
||||
The document will be generated as the given creator: Le document sera généré à la place de l'utilisateur indiqué
|
||||
Show data instead of generating: Montrer les données au lieu de générer le document
|
||||
|
||||
Any template configured: Aucun gabarit de document configuré
|
||||
|
||||
entity_id_placeholder: Identifiant de l'entité
|
||||
|
||||
failure_email:
|
||||
The generation of a document failed: La génération d'un document a échoué
|
||||
The generation of the document {template_name} failed: La génération d'un document à partir du modèle {{ template_name }} a échoué.
|
||||
The generation of the document %template_name% failed: La génération d'un document à partir du modèle {{ template_name }} a échoué.
|
||||
The following errors were encoutered: Les erreurs suivantes ont été rencontrées
|
||||
Forward this email to your administrator for solving: Faites suivre ce message vers votre administrateur pour la résolution du problème.
|
||||
References: Références
|
||||
|
||||
data_dump_email:
|
||||
subject: Contenu des données de génération de document disponible
|
||||
Dear: Cher
|
||||
data_dump_ready_and_link: >-
|
||||
Le contenu des données est disponible. Vous pouvez le télécharger à l'aide du lien suivant:
|
||||
|
||||
|
||||
|
||||
crud:
|
||||
docgen_template:
|
||||
index:
|
||||
@@ -28,5 +46,4 @@ crud:
|
||||
add_new: Créer
|
||||
|
||||
|
||||
Show data instead of generating: Montrer les données au lieu de générer le document
|
||||
Template file: Fichier modèle
|
||||
|
@@ -25,6 +25,11 @@ use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
/**
|
||||
* Represent a document stored in an object store.
|
||||
*
|
||||
* StoredObjects 's content should be read and written using the @see{StoredObjectManagerInterface}.
|
||||
*
|
||||
* The property `$deleteAt` allow a deletion of the document after the given date. But this property should
|
||||
* be set before the document is actually written by the StoredObjectManager.
|
||||
*
|
||||
* @ORM\Entity
|
||||
*
|
||||
* @ORM\Table("chill_doc.stored_object")
|
||||
@@ -117,6 +122,16 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa
|
||||
*/
|
||||
private int $generationTrialsCounter = 0;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime_immutable", nullable=true, options={"default": null})
|
||||
*/
|
||||
private ?\DateTimeImmutable $deleteAt = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="text", nullable=false, options={"default": ""})
|
||||
*/
|
||||
private string $generationErrors = '';
|
||||
|
||||
/**
|
||||
* @param StoredObject::STATUS_* $status
|
||||
*/
|
||||
@@ -144,6 +159,11 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa
|
||||
*/
|
||||
public function getCreationDate(): \DateTime
|
||||
{
|
||||
if (null === $this->createdAt) {
|
||||
// this scenario will quite never happens
|
||||
return new \DateTime('now');
|
||||
}
|
||||
|
||||
return \DateTime::createFromImmutable($this->createdAt);
|
||||
}
|
||||
|
||||
@@ -303,4 +323,37 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa
|
||||
{
|
||||
return self::STATUS_FAILURE === $this->getStatus();
|
||||
}
|
||||
|
||||
public function getDeleteAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->deleteAt;
|
||||
}
|
||||
|
||||
public function setDeleteAt(?\DateTimeImmutable $deleteAt): StoredObject
|
||||
{
|
||||
$this->deleteAt = $deleteAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getGenerationErrors(): string
|
||||
{
|
||||
return $this->generationErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds generation errors to the stored object.
|
||||
*
|
||||
* The existing generation errors are not removed
|
||||
*
|
||||
* @param string $generationErrors the generation errors to be added
|
||||
*
|
||||
* @return StoredObject the modified StoredObject instance
|
||||
*/
|
||||
public function addGenerationErrors(string $generationErrors): StoredObject
|
||||
{
|
||||
$this->generationErrors = $this->generationErrors.$generationErrors."\n";
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@@ -14,11 +14,10 @@ namespace Chill\DocStoreBundle\Repository;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
final class StoredObjectRepository implements ObjectRepository
|
||||
final readonly class StoredObjectRepository implements StoredObjectRepositoryInterface
|
||||
{
|
||||
private readonly EntityRepository $repository;
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
|
@@ -0,0 +1,22 @@
|
||||
<?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\DocStoreBundle\Repository;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
/**
|
||||
* @extends ObjectRepository<StoredObject>
|
||||
*/
|
||||
interface StoredObjectRepositoryInterface extends ObjectRepository
|
||||
{
|
||||
}
|
@@ -1,54 +1,62 @@
|
||||
{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %}
|
||||
{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %} {% set
|
||||
activeRouteKey = '' %} {% block title %}
|
||||
{{ "Documents" }}
|
||||
{% endblock %} {% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags("mod_docgen_picktemplate") }}
|
||||
{{ encore_entry_script_tags("mod_entity_workflow_pick") }}
|
||||
{{ encore_entry_script_tags("mod_document_action_buttons_group") }}
|
||||
{% endblock %} {% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags("mod_docgen_picktemplate") }}
|
||||
{{ encore_entry_link_tags("mod_entity_workflow_pick") }}
|
||||
{{ encore_entry_link_tags("mod_document_action_buttons_group") }}
|
||||
{% endblock %} {% block content %}
|
||||
<div class="document-list">
|
||||
<h1>{{ "Documents" }}</h1>
|
||||
|
||||
{% set activeRouteKey = '' %}
|
||||
|
||||
{% block title %}
|
||||
{{ 'Documents' }}
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_docgen_picktemplate') }}
|
||||
{{ encore_entry_script_tags('mod_entity_workflow_pick') }}
|
||||
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_docgen_picktemplate') }}
|
||||
{{ encore_entry_link_tags('mod_entity_workflow_pick') }}
|
||||
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="document-list">
|
||||
<h1>{{ 'Documents' }}</h1>
|
||||
|
||||
{{ filter|chill_render_filter_order_helper }}
|
||||
|
||||
{% if documents|length == 0 %}
|
||||
<p class="chill-no-data-statement">{{ 'No documents'|trans }}</p>
|
||||
{% else %}
|
||||
<div class="flex-table chill-task-list">
|
||||
{% for document in documents %}
|
||||
{{ document|chill_generic_doc_render }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{{ chill_pagination(pagination) }}
|
||||
|
||||
<div data-docgen-template-picker="data-docgen-template-picker" data-entity-class="Chill\PersonBundle\Entity\AccompanyingPeriod" data-entity-id="{{ accompanyingCourse.id }}"></div>
|
||||
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE', accompanyingCourse) %}
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="create">
|
||||
<a href="{{ path('accompanying_course_document_new', {'course': accompanyingCourse.id}) }}" class="btn btn-create">
|
||||
{{ 'Create'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{{ filter | chill_render_filter_order_helper }}
|
||||
|
||||
{% if documents|length > 5 %}
|
||||
<div
|
||||
data-docgen-template-picker="data-docgen-template-picker"
|
||||
data-entity-class="Chill\PersonBundle\Entity\AccompanyingPeriod"
|
||||
data-entity-id="{{ accompanyingCourse.id }}"
|
||||
></div>
|
||||
{% endif %} {% if documents|length == 0 %}
|
||||
<p class="chill-no-data-statement">{{ "No documents" | trans }}</p>
|
||||
{% else %}
|
||||
<div class="flex-table chill-task-list">
|
||||
{% for document in documents %}
|
||||
{{ document | chill_generic_doc_render }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{{ chill_pagination(pagination) }}
|
||||
|
||||
<div
|
||||
data-docgen-template-picker="data-docgen-template-picker"
|
||||
data-entity-class="Chill\PersonBundle\Entity\AccompanyingPeriod"
|
||||
data-entity-id="{{ accompanyingCourse.id }}"
|
||||
></div>
|
||||
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE',
|
||||
accompanyingCourse) %}
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="create">
|
||||
<a
|
||||
href="{{
|
||||
path('accompanying_course_document_new', {
|
||||
course: accompanyingCourse.id
|
||||
})
|
||||
}}"
|
||||
class="btn btn-create"
|
||||
>
|
||||
{{ "Create" | trans }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@@ -1,74 +1,70 @@
|
||||
{#
|
||||
* Copyright (C) 2018, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#}
|
||||
|
||||
{% extends "@ChillPerson/Person/layout.html.twig" %}
|
||||
|
||||
{% set activeRouteKey = '' %}
|
||||
|
||||
{% import "@ChillDocStore/Macro/macro.html.twig" as m %}
|
||||
|
||||
{% block title %}
|
||||
{{ 'Documents for %name%'|trans({ '%name%': person|chill_entity_render_string } ) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_docgen_picktemplate') }}
|
||||
{{ encore_entry_script_tags('mod_entity_workflow_pick') }}
|
||||
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_docgen_picktemplate') }}
|
||||
{{ encore_entry_link_tags('mod_entity_workflow_pick') }}
|
||||
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{# * Copyright (C) 2018, Champs Libres Cooperative SCRLFS,
|
||||
<http://www.champs-libres.coop> * * This program is free software: you can
|
||||
redistribute it and/or modify * it under the terms of the GNU Affero General
|
||||
Public License as * published by the Free Software Foundation, either version 3
|
||||
of the * License, or (at your option) any later version. * * This program is
|
||||
distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY;
|
||||
without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. See the * GNU Affero General Public License for more
|
||||
details. * * You should have received a copy of the GNU Affero General Public
|
||||
License * along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#} {% extends "@ChillPerson/Person/layout.html.twig" %} {% set activeRouteKey =
|
||||
'' %} {% import "@ChillDocStore/Macro/macro.html.twig" as m %} {% block title %}
|
||||
{{ 'Documents for %name%'|trans({ '%name%': person|chill_entity_render_string } ) }}
|
||||
{% endblock %} {% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags("mod_docgen_picktemplate") }}
|
||||
{{ encore_entry_script_tags("mod_entity_workflow_pick") }}
|
||||
{{ encore_entry_script_tags("mod_document_action_buttons_group") }}
|
||||
{% endblock %} {% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags("mod_docgen_picktemplate") }}
|
||||
{{ encore_entry_link_tags("mod_entity_workflow_pick") }}
|
||||
{{ encore_entry_link_tags("mod_document_action_buttons_group") }}
|
||||
{% endblock %} {% block content %}
|
||||
|
||||
<div class="col-md-10 col-xxl">
|
||||
<h1>{{ 'Documents for %name%'|trans({ '%name%': person|chill_entity_render_string } ) }}</h1>
|
||||
<h1>
|
||||
{{ 'Documents for %name%'|trans({ '%name%': person|chill_entity_render_string } ) }}
|
||||
</h1>
|
||||
|
||||
{{ filter|chill_render_filter_order_helper }}
|
||||
{{ filter | chill_render_filter_order_helper }}
|
||||
|
||||
{% if documents|length == 0 %}
|
||||
<p class="chill-no-data-statement">{{ 'No documents'|trans }}</p>
|
||||
{% if documents|length > 5 %}
|
||||
<div
|
||||
data-docgen-template-picker="data-docgen-template-picker"
|
||||
data-entity-class="Chill\PersonBundle\Entity\Person"
|
||||
data-entity-id="{{ person.id }}"
|
||||
></div>
|
||||
{% endif %} {% if documents|length == 0 %}
|
||||
<p class="chill-no-data-statement">{{ "No documents" | trans }}</p>
|
||||
{% else %}
|
||||
<div class="flex-table chill-task-list">
|
||||
{% for document in documents %}
|
||||
{{ document|chill_generic_doc_render }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="flex-table chill-task-list">
|
||||
{% for document in documents %}
|
||||
{{ document | chill_generic_doc_render }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{{ chill_pagination(pagination) }}
|
||||
{{ chill_pagination(pagination) }}
|
||||
|
||||
<div data-docgen-template-picker="data-docgen-template-picker" data-entity-class="Chill\PersonBundle\Entity\Person" data-entity-id="{{ person.id }}"></div>
|
||||
|
||||
{% if is_granted('CHILL_PERSON_DOCUMENT_CREATE', person) %}
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="create">
|
||||
<a href="{{ path('person_document_new', {'person': person.id}) }}" class="btn btn-create">
|
||||
{{ 'Create new document' | trans }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
<div
|
||||
data-docgen-template-picker="data-docgen-template-picker"
|
||||
data-entity-class="Chill\PersonBundle\Entity\Person"
|
||||
data-entity-id="{{ person.id }}"
|
||||
></div>
|
||||
|
||||
{% if is_granted('CHILL_PERSON_DOCUMENT_CREATE', person) %}
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="create">
|
||||
<a
|
||||
href="{{ path('person_document_new', { person: person.id }) }}"
|
||||
class="btn btn-create"
|
||||
>
|
||||
{{ "Create new document" | trans }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@@ -104,6 +104,12 @@ final class StoredObjectManager implements StoredObjectManagerInterface
|
||||
)
|
||||
: $clearContent;
|
||||
|
||||
$headers = [];
|
||||
|
||||
if (null !== $document->getDeleteAt()) {
|
||||
$headers['X-Delete-At'] = $document->getDeleteAt()->getTimestamp();
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this
|
||||
->client
|
||||
@@ -118,6 +124,7 @@ final class StoredObjectManager implements StoredObjectManagerInterface
|
||||
->url,
|
||||
[
|
||||
'body' => $encryptedContent,
|
||||
'headers' => $headers,
|
||||
]
|
||||
);
|
||||
} catch (TransportExceptionInterface $exception) {
|
||||
@@ -129,6 +136,11 @@ final class StoredObjectManager implements StoredObjectManagerInterface
|
||||
}
|
||||
}
|
||||
|
||||
public function clearCache(): void
|
||||
{
|
||||
$this->inMemory = [];
|
||||
}
|
||||
|
||||
private function extractLastModifiedFromResponse(ResponseInterface $response): \DateTimeImmutable
|
||||
{
|
||||
$lastModifiedString = (($response->getHeaders()['last-modified'] ?? [])[0] ?? '');
|
||||
|
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\DocStoreBundle\Service;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||
|
||||
interface StoredObjectManagerInterface
|
||||
{
|
||||
@@ -23,6 +24,8 @@ interface StoredObjectManagerInterface
|
||||
* @param StoredObject $document the document
|
||||
*
|
||||
* @return string the retrieved content in clear
|
||||
*
|
||||
* @throws StoredObjectManagerException if unable to read or decrypt the content
|
||||
*/
|
||||
public function read(StoredObject $document): string;
|
||||
|
||||
@@ -31,6 +34,10 @@ interface StoredObjectManagerInterface
|
||||
*
|
||||
* @param StoredObject $document the document
|
||||
* @param $clearContent The content to store in clear
|
||||
*
|
||||
* @throws StoredObjectManagerException
|
||||
*/
|
||||
public function write(StoredObject $document, string $clearContent): void;
|
||||
|
||||
public function clearCache(): void;
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ declare(strict_types=1);
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests;
|
||||
namespace Chill\DocStoreBundle\Tests\Service;
|
||||
|
||||
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
@@ -117,6 +117,41 @@ final class StoredObjectManagerTest extends TestCase
|
||||
self::assertEquals($clearContent, $storedObjectManager->read($storedObject));
|
||||
}
|
||||
|
||||
public function testWriteWithDeleteAt()
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
|
||||
$expectedRequests = [
|
||||
function ($method, $url, $options): MockResponse {
|
||||
self::assertEquals('PUT', $method);
|
||||
self::assertArrayHasKey('headers', $options);
|
||||
self::assertIsArray($options['headers']);
|
||||
self::assertCount(0, array_filter($options['headers'], fn (string $header) => str_contains($header, 'X-Delete-At')));
|
||||
|
||||
return new MockResponse('', ['http_code' => 201]);
|
||||
},
|
||||
|
||||
function ($method, $url, $options): MockResponse {
|
||||
self::assertEquals('PUT', $method);
|
||||
self::assertArrayHasKey('headers', $options);
|
||||
self::assertIsArray($options['headers']);
|
||||
self::assertCount(1, array_filter($options['headers'], fn (string $header) => str_contains($header, 'X-Delete-At')));
|
||||
self::assertContains('X-Delete-At: 1711014260', $options['headers']);
|
||||
|
||||
return new MockResponse('', ['http_code' => 201]);
|
||||
},
|
||||
];
|
||||
$client = new MockHttpClient($expectedRequests);
|
||||
|
||||
$manager = new StoredObjectManager($client, $this->getTempUrlGenerator($storedObject));
|
||||
|
||||
$manager->write($storedObject, 'ok');
|
||||
|
||||
// with a deletedAt date
|
||||
$storedObject->setDeleteAt(\DateTimeImmutable::createFromFormat('U', '1711014260'));
|
||||
$manager->write($storedObject, 'ok');
|
||||
}
|
||||
|
||||
private function getHttpClient(string $encodedContent): HttpClientInterface
|
||||
{
|
||||
$callback = static function ($method, $url, $options) use ($encodedContent) {
|
@@ -0,0 +1,36 @@
|
||||
<?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\Migrations\DocStore;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20240322100107 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'StoredObject: add deleteAt and generationErrors columns';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object ADD deleteAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object ADD generationErrors TEXT DEFAULT \'\' NOT NULL');
|
||||
$this->addSql('COMMENT ON COLUMN chill_doc.stored_object.deleteAt IS \'(DC2Type:datetime_immutable)\'');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object DROP deleteAt');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object DROP generationErrors');
|
||||
}
|
||||
}
|
@@ -13,7 +13,7 @@ Update document: Modifier le document
|
||||
Edit attributes: Modifier les propriétés du document
|
||||
Existing document: Document existant
|
||||
No document to download: Aucun document à télécharger
|
||||
'Choose a document category': Choisissez une catégorie de document
|
||||
"Choose a document category": Choisissez une catégorie de document
|
||||
No document found: Aucun document trouvé
|
||||
The document is successfully registered: Le document est enregistré
|
||||
The document is successfully updated: Le document est mis à jour
|
||||
@@ -36,7 +36,6 @@ Delete document ?: Supprimer le document ?
|
||||
Are you sure you want to remove this document ?: Êtes-vous sûr·e de vouloir supprimer ce document ?
|
||||
The document is successfully removed: Le document a été supprimé
|
||||
|
||||
|
||||
# dropzone upload
|
||||
File too big: Fichier trop volumineux
|
||||
Drop your file or click here: Cliquez ici ou faites glissez votre nouveau fichier dans cette zone
|
||||
|
@@ -17,10 +17,11 @@ use Chill\EventBundle\Form\EventType;
|
||||
use Chill\EventBundle\Form\Type\PickEventType;
|
||||
use Chill\EventBundle\Security\Authorization\EventVoter;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Form\Type\PickPersonType;
|
||||
use Chill\PersonBundle\Form\Type\PickPersonDynamicType;
|
||||
use Chill\PersonBundle\Privacy\PrivacyEvent;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Csv;
|
||||
@@ -37,53 +38,26 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* Class EventController.
|
||||
*/
|
||||
class EventController extends AbstractController
|
||||
final class EventController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var AuthorizationHelper
|
||||
*/
|
||||
protected $authorizationHelper;
|
||||
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $eventDispatcher;
|
||||
|
||||
/**
|
||||
* @var FormFactoryInterface
|
||||
*/
|
||||
protected $formFactoryInterface;
|
||||
|
||||
/**
|
||||
* @var PaginatorFactory
|
||||
*/
|
||||
protected $paginator;
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
protected $translator;
|
||||
|
||||
/**
|
||||
* EventController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
EventDispatcherInterface $eventDispatcher,
|
||||
AuthorizationHelper $authorizationHelper,
|
||||
FormFactoryInterface $formFactoryInterface,
|
||||
TranslatorInterface $translator,
|
||||
PaginatorFactory $paginator
|
||||
private readonly EventDispatcherInterface $eventDispatcher,
|
||||
private readonly AuthorizationHelperInterface $authorizationHelper,
|
||||
private readonly FormFactoryInterface $formFactoryInterface,
|
||||
private readonly TranslatorInterface $translator,
|
||||
private readonly PaginatorFactory $paginator,
|
||||
private readonly Security $security,
|
||||
) {
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->authorizationHelper = $authorizationHelper;
|
||||
$this->formFactoryInterface = $formFactoryInterface;
|
||||
$this->translator = $translator;
|
||||
$this->paginator = $paginator;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,7 +155,7 @@ class EventController extends AbstractController
|
||||
|
||||
$this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person);
|
||||
|
||||
$reachablesCircles = $this->authorizationHelper->getReachableCircles(
|
||||
$reachablesCircles = $this->authorizationHelper->getReachableScopes(
|
||||
$this->getUser(),
|
||||
EventVoter::SEE,
|
||||
$person->getCenter()
|
||||
@@ -233,6 +207,12 @@ class EventController extends AbstractController
|
||||
*/
|
||||
public function newAction(?Center $center, Request $request)
|
||||
{
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
throw new AccessDeniedHttpException('not a regular user. Maybe an administrator ?');
|
||||
}
|
||||
|
||||
if (null === $center) {
|
||||
$center_id = $request->query->get('center_id');
|
||||
$center = $this->getDoctrine()->getRepository(Center::class)->find($center_id);
|
||||
@@ -240,6 +220,7 @@ class EventController extends AbstractController
|
||||
|
||||
$entity = new Event();
|
||||
$entity->setCenter($center);
|
||||
$entity->setLocation($user->getCurrentLocation());
|
||||
|
||||
$form = $this->createCreateForm($entity);
|
||||
$form->handleRequest($request);
|
||||
@@ -282,7 +263,7 @@ class EventController extends AbstractController
|
||||
}
|
||||
|
||||
$form = $this->formFactoryInterface
|
||||
->createNamedBuilder(null, FormType::class, null, [
|
||||
->createNamedBuilder('', FormType::class, null, [
|
||||
'csrf_protection' => false,
|
||||
])
|
||||
->setMethod('GET')
|
||||
@@ -323,7 +304,7 @@ class EventController extends AbstractController
|
||||
}
|
||||
|
||||
$this->denyAccessUnlessGranted(
|
||||
'CHILL_EVENT_SEE_DETAILS',
|
||||
EventVoter::SEE_DETAILS,
|
||||
$event,
|
||||
'You are not allowed to see details on this event'
|
||||
);
|
||||
@@ -367,7 +348,7 @@ class EventController extends AbstractController
|
||||
$this->addFlash('success', $this->translator
|
||||
->trans('The event was updated'));
|
||||
|
||||
return $this->redirectToRoute('chill_event__event_edit', ['event_id' => $event_id]);
|
||||
return $this->redirectToRoute('chill_event__event_show', ['event_id' => $event_id]);
|
||||
}
|
||||
|
||||
return $this->render('@ChillEvent/Event/edit.html.twig', [
|
||||
@@ -385,7 +366,7 @@ class EventController extends AbstractController
|
||||
{
|
||||
/** @var \Symfony\Component\Form\FormBuilderInterface $builder */
|
||||
$builder = $this
|
||||
->get('form.factory')
|
||||
->formFactoryInterface
|
||||
->createNamedBuilder(
|
||||
null,
|
||||
FormType::class,
|
||||
@@ -430,11 +411,9 @@ class EventController extends AbstractController
|
||||
*/
|
||||
protected function createAddParticipationByPersonForm(Event $event)
|
||||
{
|
||||
/** @var \Symfony\Component\Form\FormBuilderInterface $builder */
|
||||
$builder = $this
|
||||
->get('form.factory')
|
||||
$builder = $this->formFactoryInterface
|
||||
->createNamedBuilder(
|
||||
null,
|
||||
'',
|
||||
FormType::class,
|
||||
null,
|
||||
[
|
||||
@@ -444,22 +423,17 @@ class EventController extends AbstractController
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add('person_id', PickPersonType::class, [
|
||||
'role' => 'CHILL_EVENT_CREATE',
|
||||
'centers' => $event->getCenter(),
|
||||
$builder->add('person_id', PickPersonDynamicType::class, [
|
||||
'as_id' => true,
|
||||
'multiple' => false,
|
||||
'submit_on_adding_new_entity' => true,
|
||||
'label' => 'Add a participation',
|
||||
]);
|
||||
|
||||
$builder->add('event_id', HiddenType::class, [
|
||||
'data' => $event->getId(),
|
||||
]);
|
||||
|
||||
$builder->add(
|
||||
'submit',
|
||||
SubmitType::class,
|
||||
[
|
||||
'label' => 'Add a participation',
|
||||
]
|
||||
);
|
||||
dump($event->getId());
|
||||
|
||||
return $builder->getForm();
|
||||
}
|
||||
@@ -469,7 +443,7 @@ class EventController extends AbstractController
|
||||
*/
|
||||
protected function createExportByFormatForm()
|
||||
{
|
||||
$builder = $this->createFormBuilder()
|
||||
$builder = $this->createFormBuilder(['format' => 'xlsx'])
|
||||
->add('format', ChoiceType::class, [
|
||||
'choices' => [
|
||||
'xlsx' => 'xlsx',
|
||||
|
118
src/Bundle/ChillEventBundle/Controller/EventListController.php
Normal file
118
src/Bundle/ChillEventBundle/Controller/EventListController.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?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\EventBundle\Controller;
|
||||
|
||||
use Chill\EventBundle\Entity\Event;
|
||||
use Chill\EventBundle\Entity\EventType;
|
||||
use Chill\EventBundle\Repository\EventACLAwareRepositoryInterface;
|
||||
use Chill\EventBundle\Repository\EventTypeRepository;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
|
||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactory;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Chill\PersonBundle\Form\Type\PickPersonDynamicType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
final readonly class EventListController
|
||||
{
|
||||
public function __construct(
|
||||
private Environment $environment,
|
||||
private EventACLAwareRepositoryInterface $eventACLAwareRepository,
|
||||
private EventTypeRepository $eventTypeRepository,
|
||||
private FilterOrderHelperFactory $filterOrderHelperFactory,
|
||||
private FormFactoryInterface $formFactory,
|
||||
private PaginatorFactoryInterface $paginatorFactory,
|
||||
private TranslatableStringHelperInterface $translatableStringHelper,
|
||||
private UrlGeneratorInterface $urlGenerator,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("{_locale}/event/event/list", name="chill_event_event_list")
|
||||
*/
|
||||
public function __invoke(): Response
|
||||
{
|
||||
$filter = $this->buildFilterOrder();
|
||||
$filterData = [
|
||||
'q' => (string) $filter->getQueryString(),
|
||||
'dates' => $filter->getDateRangeData('dates'),
|
||||
'event_types' => $filter->getEntityChoiceData('event_types'),
|
||||
];
|
||||
$total = $this->eventACLAwareRepository->countAllViewable($filterData);
|
||||
$pagination = $this->paginatorFactory->create($total);
|
||||
$events = $this->eventACLAwareRepository->findAllViewable($filterData, $pagination->getCurrentPageFirstItemNumber(), $pagination->getItemsPerPage());
|
||||
$eventForms = [];
|
||||
foreach ($events as $event) {
|
||||
$eventForms[$event->getId()] = $this->createAddParticipationByPersonForm($event)->createView();
|
||||
}
|
||||
|
||||
return new Response($this->environment->render(
|
||||
'@ChillEvent/Event/page_list.html.twig',
|
||||
[
|
||||
'events' => $events,
|
||||
'pagination' => $pagination,
|
||||
'eventForms' => $eventForms,
|
||||
'filter' => $filter,
|
||||
]
|
||||
));
|
||||
}
|
||||
|
||||
private function buildFilterOrder(): FilterOrderHelper
|
||||
{
|
||||
$types = $this->eventTypeRepository->findAllActive();
|
||||
|
||||
$builder = $this->filterOrderHelperFactory->create(__METHOD__);
|
||||
$builder
|
||||
->addDateRange('dates', 'event.filter.event_dates')
|
||||
->addSearchBox(['name'])
|
||||
->addEntityChoice('event_types', 'event.filter.event_types', EventType::class, $types, [
|
||||
'choice_label' => fn (EventType $e) => $this->translatableStringHelper->localize($e->getName()),
|
||||
]);
|
||||
|
||||
return $builder->build();
|
||||
}
|
||||
|
||||
private function createAddParticipationByPersonForm(Event $event): FormInterface
|
||||
{
|
||||
$builder = $this->formFactory
|
||||
->createNamedBuilder(
|
||||
'',
|
||||
FormType::class,
|
||||
null,
|
||||
[
|
||||
'method' => 'GET',
|
||||
'action' => $this->urlGenerator->generate('chill_event_participation_new'),
|
||||
'csrf_protection' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add('person_id', PickPersonDynamicType::class, [
|
||||
'as_id' => true,
|
||||
'multiple' => false,
|
||||
'submit_on_adding_new_entity' => true,
|
||||
'label' => 'Add a participation',
|
||||
]);
|
||||
|
||||
$builder->add('event_id', HiddenType::class, [
|
||||
'data' => $event->getId(),
|
||||
]);
|
||||
|
||||
return $builder->getForm();
|
||||
}
|
||||
}
|
@@ -14,7 +14,10 @@ namespace Chill\EventBundle\Controller;
|
||||
use Chill\EventBundle\Entity\Event;
|
||||
use Chill\EventBundle\Entity\Participation;
|
||||
use Chill\EventBundle\Form\ParticipationType;
|
||||
use Chill\EventBundle\Repository\EventRepository;
|
||||
use Chill\EventBundle\Security\Authorization\ParticipationVoter;
|
||||
use Chill\PersonBundle\Repository\PersonRepository;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
@@ -28,13 +31,17 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
/**
|
||||
* Class ParticipationController.
|
||||
*/
|
||||
class ParticipationController extends AbstractController
|
||||
final class ParticipationController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* ParticipationController constructor.
|
||||
*/
|
||||
public function __construct(private readonly LoggerInterface $logger, private readonly TranslatorInterface $translator)
|
||||
{
|
||||
public function __construct(
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly TranslatorInterface $translator,
|
||||
private readonly EventRepository $eventRepository,
|
||||
private readonly PersonRepository $personRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,6 +237,7 @@ class ParticipationController extends AbstractController
|
||||
return $this->render('@ChillEvent/Participation/new.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'participation' => $participation,
|
||||
'ignored_participations' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -539,7 +547,7 @@ class ParticipationController extends AbstractController
|
||||
* If the request is multiple, the $participation object is cloned.
|
||||
* Limitations: the $participation should not be persisted.
|
||||
*
|
||||
* @return Participation|Participation[] return one single participation if $multiple == false
|
||||
* @return Participation|list<Participation> return one single participation if $multiple == false
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException if the event/person is not found
|
||||
* @throws \Symfony\Component\Security\Core\Exception\AccessDeniedException if the user does not have access to event/person
|
||||
@@ -556,30 +564,25 @@ class ParticipationController extends AbstractController
|
||||
}
|
||||
|
||||
$event_id = $request->query->getInt('event_id', 0); // sf4 check:
|
||||
// prevent error: `Argument 2 passed to ::getInt() must be of the type int, null given`
|
||||
|
||||
if (null !== $event_id) {
|
||||
$event = $em->getRepository(Event::class)
|
||||
->find($event_id);
|
||||
$event = $this->eventRepository->find($event_id);
|
||||
|
||||
if (null === $event) {
|
||||
throw $this->createNotFoundException('The event with id '.$event_id.' is not found');
|
||||
}
|
||||
|
||||
$this->denyAccessUnlessGranted(
|
||||
'CHILL_EVENT_SEE',
|
||||
$event,
|
||||
'The user is not allowed to see the event'
|
||||
);
|
||||
|
||||
$participation->setEvent($event);
|
||||
if (null === $event) {
|
||||
throw $this->createNotFoundException('The event with id '.$event_id.' is not found');
|
||||
}
|
||||
|
||||
$this->denyAccessUnlessGranted(
|
||||
'CHILL_EVENT_SEE',
|
||||
$event,
|
||||
'The user is not allowed to see the event'
|
||||
);
|
||||
|
||||
$participation->setEvent($event);
|
||||
|
||||
// this script should be able to handle multiple, so we translate
|
||||
// single person_id in an array
|
||||
$persons_ids = $request->query->has('person_id') ?
|
||||
[$request->query->getInt('person_id', 0)] // sf4 check:
|
||||
// prevent error: `Argument 2 passed to ::getInt() must be of the type int, null given`
|
||||
[$request->query->get('person_id', 0)]
|
||||
: explode(',', (string) $request->query->get('persons_ids'));
|
||||
$participations = [];
|
||||
|
||||
@@ -588,15 +591,14 @@ class ParticipationController extends AbstractController
|
||||
$participation = \count($persons_ids) > 1 ? clone $participation : $participation;
|
||||
|
||||
if (null !== $person_id) {
|
||||
$person = $em->getRepository(\Chill\PersonBundle\Entity\Person::class)
|
||||
->find($person_id);
|
||||
$person = $this->personRepository->find($person_id);
|
||||
|
||||
if (null === $person) {
|
||||
throw $this->createNotFoundException('The person with id '.$person_id.' is not found');
|
||||
}
|
||||
|
||||
$this->denyAccessUnlessGranted(
|
||||
'CHILL_PERSON_SEE',
|
||||
PersonVoter::SEE,
|
||||
$person,
|
||||
'The user is not allowed to see the person'
|
||||
);
|
||||
|
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\EventBundle\DependencyInjection;
|
||||
|
||||
use Chill\EventBundle\Security\Authorization\EventVoter;
|
||||
use Chill\EventBundle\Security\Authorization\ParticipationVoter;
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
|
||||
@@ -33,10 +34,8 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
|
||||
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
|
||||
$loader->load('services.yaml');
|
||||
$loader->load('services/authorization.yaml');
|
||||
$loader->load('services/controller.yaml');
|
||||
$loader->load('services/fixtures.yaml');
|
||||
$loader->load('services/forms.yaml');
|
||||
$loader->load('services/menu.yaml');
|
||||
$loader->load('services/repositories.yaml');
|
||||
$loader->load('services/search.yaml');
|
||||
$loader->load('services/timeline.yaml');
|
||||
@@ -61,6 +60,8 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
|
||||
EventVoter::SEE_DETAILS => [EventVoter::SEE],
|
||||
EventVoter::UPDATE => [EventVoter::SEE_DETAILS],
|
||||
EventVoter::CREATE => [EventVoter::SEE_DETAILS],
|
||||
ParticipationVoter::SEE_DETAILS => [ParticipationVoter::SEE],
|
||||
ParticipationVoter::UPDATE => [ParticipationVoter::SEE_DETAILS],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
@@ -11,15 +11,23 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\EventBundle\Entity;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
|
||||
use Chill\MainBundle\Entity\HasCenterInterface;
|
||||
use Chill\MainBundle\Entity\HasScopeInterface;
|
||||
use Chill\MainBundle\Entity\Location;
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use DateTime;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* Class Event.
|
||||
@@ -30,10 +38,15 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
*
|
||||
* @ORM\HasLifecycleCallbacks
|
||||
*/
|
||||
class Event implements HasCenterInterface, HasScopeInterface
|
||||
class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInterface, TrackUpdateInterface
|
||||
{
|
||||
use TrackCreationTrait;
|
||||
use TrackUpdateTrait;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Center")
|
||||
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Center")A
|
||||
*
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private ?Center $center = null;
|
||||
|
||||
@@ -63,6 +76,8 @@ class Event implements HasCenterInterface, HasScopeInterface
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=150)
|
||||
*
|
||||
* @Assert\NotBlank()
|
||||
*/
|
||||
private ?string $name = null;
|
||||
|
||||
@@ -77,15 +92,45 @@ class Event implements HasCenterInterface, HasScopeInterface
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Chill\EventBundle\Entity\EventType")
|
||||
*
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private ?EventType $type = null;
|
||||
|
||||
/**
|
||||
* @ORM\Embedded(class=CommentEmbeddable::class, columnPrefix="comment_")
|
||||
*/
|
||||
private CommentEmbeddable $comment;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=Location::class)
|
||||
*
|
||||
* @ORM\JoinColumn(nullable=true)
|
||||
*/
|
||||
private ?Location $location = null;
|
||||
|
||||
/**
|
||||
* @var Collection<StoredObject>
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity=StoredObject::class, cascade={"persist","refresh"})
|
||||
*
|
||||
* @ORM\JoinTable("chill_event_event_documents")
|
||||
*/
|
||||
private Collection $documents;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="decimal", precision=10, scale=4, nullable=true, options={"default": null})
|
||||
*/
|
||||
private string $organizationCost = '0.0';
|
||||
|
||||
/**
|
||||
* Event constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->participations = new ArrayCollection();
|
||||
$this->documents = new ArrayCollection();
|
||||
$this->comment = new CommentEmbeddable();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,6 +145,22 @@ class Event implements HasCenterInterface, HasScopeInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addDocument(StoredObject $storedObject): self
|
||||
{
|
||||
if ($this->documents->contains($storedObject)) {
|
||||
$this->documents[] = $storedObject;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeDocument(StoredObject $storedObject): self
|
||||
{
|
||||
$this->documents->removeElement($storedObject);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Center
|
||||
*/
|
||||
@@ -136,7 +197,7 @@ class Event implements HasCenterInterface, HasScopeInterface
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getModerator(): User|null
|
||||
public function getModerator(): ?User
|
||||
{
|
||||
return $this->moderator;
|
||||
}
|
||||
@@ -259,4 +320,44 @@ class Event implements HasCenterInterface, HasScopeInterface
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getComment(): CommentEmbeddable
|
||||
{
|
||||
return $this->comment;
|
||||
}
|
||||
|
||||
public function setComment(CommentEmbeddable $comment): void
|
||||
{
|
||||
$this->comment = $comment;
|
||||
}
|
||||
|
||||
public function getLocation(): ?Location
|
||||
{
|
||||
return $this->location;
|
||||
}
|
||||
|
||||
public function setLocation(?Location $location): void
|
||||
{
|
||||
$this->location = $location;
|
||||
}
|
||||
|
||||
public function getDocuments(): Collection
|
||||
{
|
||||
return $this->documents;
|
||||
}
|
||||
|
||||
public function setDocuments(Collection $documents): void
|
||||
{
|
||||
$this->documents = $documents;
|
||||
}
|
||||
|
||||
public function getOrganizationCost(): string
|
||||
{
|
||||
return $this->organizationCost;
|
||||
}
|
||||
|
||||
public function setOrganizationCost(string $organizationCost): void
|
||||
{
|
||||
$this->organizationCost = $organizationCost;
|
||||
}
|
||||
}
|
||||
|
@@ -11,13 +11,17 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\EventBundle\Entity;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
||||
use Chill\MainBundle\Entity\HasCenterInterface;
|
||||
use Chill\MainBundle\Entity\HasScopeInterface;
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use DateTime;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
|
||||
/**
|
||||
@@ -26,12 +30,20 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
* @ORM\Entity(
|
||||
* repositoryClass="Chill\EventBundle\Repository\ParticipationRepository")
|
||||
*
|
||||
* @ORM\Table(name="chill_event_participation")
|
||||
* @ORM\Table(name="chill_event_participation", uniqueConstraints={
|
||||
*
|
||||
* @ORM\UniqueConstraint(name="chill_event_participation_event_person_unique_idx", columns={"event_id", "person_id"})
|
||||
* })
|
||||
*
|
||||
* @ORM\HasLifecycleCallbacks
|
||||
*
|
||||
* @UniqueEntity({"event", "person"}, message="event.validation.person_already_participate_to_event")
|
||||
*/
|
||||
class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterface
|
||||
class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterface, TrackUpdateInterface, TrackCreationInterface
|
||||
{
|
||||
use TrackCreationTrait;
|
||||
use TrackUpdateTrait;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(
|
||||
* targetEntity="Chill\EventBundle\Entity\Event",
|
||||
@@ -48,13 +60,10 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
||||
*/
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime")
|
||||
*/
|
||||
private ?\DateTime $lastUpdate = null;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Chill\PersonBundle\Entity\Person")
|
||||
*
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private ?Person $person = null;
|
||||
|
||||
@@ -65,12 +74,11 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Chill\EventBundle\Entity\Status")
|
||||
*
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private ?Status $status = null;
|
||||
|
||||
/**
|
||||
* @return Center
|
||||
*/
|
||||
public function getCenter()
|
||||
{
|
||||
if (null === $this->getEvent()) {
|
||||
@@ -83,17 +91,15 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
||||
/**
|
||||
* Get event.
|
||||
*/
|
||||
public function getEvent(): Event|null
|
||||
public function getEvent(): ?Event
|
||||
{
|
||||
return $this->event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
@@ -101,11 +107,11 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
||||
/**
|
||||
* Get lastUpdate.
|
||||
*
|
||||
* @return \DateTime
|
||||
* @return \DateTimeInterface|null
|
||||
*/
|
||||
public function getLastUpdate()
|
||||
{
|
||||
return $this->lastUpdate;
|
||||
return $this->getUpdatedAt();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,7 +127,7 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
||||
/**
|
||||
* Get role.
|
||||
*/
|
||||
public function getRole(): Role|null
|
||||
public function getRole(): ?Role
|
||||
{
|
||||
return $this->role;
|
||||
}
|
||||
@@ -141,7 +147,7 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
||||
/**
|
||||
* Get status.
|
||||
*/
|
||||
public function getStatus(): Status|null
|
||||
public function getStatus(): ?Status
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
@@ -235,10 +241,6 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
||||
*/
|
||||
public function setEvent(?Event $event = null)
|
||||
{
|
||||
if ($this->event !== $event) {
|
||||
$this->update();
|
||||
}
|
||||
|
||||
$this->event = $event;
|
||||
|
||||
return $this;
|
||||
@@ -251,10 +253,6 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
||||
*/
|
||||
public function setPerson(?Person $person = null)
|
||||
{
|
||||
if ($person !== $this->person) {
|
||||
$this->update();
|
||||
}
|
||||
|
||||
$this->person = $person;
|
||||
|
||||
return $this;
|
||||
@@ -267,9 +265,6 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
||||
*/
|
||||
public function setRole(?Role $role = null)
|
||||
{
|
||||
if ($role !== $this->role) {
|
||||
$this->update();
|
||||
}
|
||||
$this->role = $role;
|
||||
|
||||
return $this;
|
||||
@@ -282,10 +277,6 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
||||
*/
|
||||
public function setStatus(?Status $status = null)
|
||||
{
|
||||
if ($this->status !== $status) {
|
||||
$this->update();
|
||||
}
|
||||
|
||||
$this->status = $status;
|
||||
|
||||
return $this;
|
||||
@@ -295,11 +286,11 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
||||
* Set lastUpdate.
|
||||
*
|
||||
* @return Participation
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
protected function update()
|
||||
{
|
||||
$this->lastUpdate = new \DateTime('now');
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@@ -11,12 +11,18 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\EventBundle\Form;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Form\StoredObjectType;
|
||||
use Chill\EventBundle\Form\Type\PickEventTypeType;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Form\Type\ChillCollectionType;
|
||||
use Chill\MainBundle\Form\Type\ChillDateTimeType;
|
||||
use Chill\MainBundle\Form\Type\CommentType;
|
||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||
use Chill\MainBundle\Form\Type\PickUserLocationType;
|
||||
use Chill\MainBundle\Form\Type\ScopePickerType;
|
||||
use Chill\MainBundle\Form\Type\UserPickerType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
@@ -39,14 +45,30 @@ class EventType extends AbstractType
|
||||
'class' => '',
|
||||
],
|
||||
])
|
||||
->add('moderator', UserPickerType::class, [
|
||||
'center' => $options['center'],
|
||||
'role' => $options['role'],
|
||||
'placeholder' => 'Pick a moderator',
|
||||
'attr' => [
|
||||
'class' => '',
|
||||
],
|
||||
->add('moderator', PickUserDynamicType::class, [
|
||||
'label' => 'Pick a moderator',
|
||||
])
|
||||
->add('location', PickUserLocationType::class, [
|
||||
'label' => 'event.fields.location',
|
||||
])
|
||||
->add('comment', CommentType::class, [
|
||||
'label' => 'Comment',
|
||||
'required' => false,
|
||||
])
|
||||
->add('documents', ChillCollectionType::class, [
|
||||
'entry_type' => StoredObjectType::class,
|
||||
'entry_options' => [
|
||||
'has_title' => true,
|
||||
],
|
||||
'allow_add' => true,
|
||||
'allow_delete' => true,
|
||||
'delete_empty' => fn (StoredObject $storedObject): bool => '' === $storedObject->getFilename(),
|
||||
'button_remove_label' => 'event.form.remove_document',
|
||||
'button_add_label' => 'event.form.add_document',
|
||||
])
|
||||
->add('organizationCost', MoneyType::class, [
|
||||
'label' => 'event.fields.organizationCost',
|
||||
'help' => 'event.form.organisationCost_help',
|
||||
]);
|
||||
}
|
||||
|
||||
|
@@ -114,7 +114,7 @@ final class PickEventType extends AbstractType
|
||||
} else {
|
||||
$centers = $this->authorizationHelper->getReachableCenters(
|
||||
$user,
|
||||
(string) $options['role']->getRole()
|
||||
$options['role']
|
||||
);
|
||||
}
|
||||
|
||||
|
46
src/Bundle/ChillEventBundle/Menu/SectionMenuBuilder.php
Normal file
46
src/Bundle/ChillEventBundle/Menu/SectionMenuBuilder.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?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\EventBundle\Menu;
|
||||
|
||||
use Chill\EventBundle\Security\Authorization\EventVoter;
|
||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||
use Knp\Menu\MenuItem;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final readonly class SectionMenuBuilder implements LocalMenuBuilderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private Security $security,
|
||||
private TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
public function buildMenu($menuId, MenuItem $menu, array $parameters)
|
||||
{
|
||||
if ($this->security->isGranted(EventVoter::SEE)) {
|
||||
$menu->addChild(
|
||||
$this->translator->trans('Events'),
|
||||
[
|
||||
'route' => 'chill_event_event_list',
|
||||
]
|
||||
)->setExtras([
|
||||
'order' => 250,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getMenuIds(): array
|
||||
{
|
||||
return ['section'];
|
||||
}
|
||||
}
|
@@ -0,0 +1,142 @@
|
||||
<?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\EventBundle\Repository;
|
||||
|
||||
use Chill\EventBundle\Entity\Event;
|
||||
use Chill\EventBundle\Entity\Participation;
|
||||
use Chill\EventBundle\Security\Authorization\EventVoter;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
use Doctrine\ORM\NoResultException;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
final readonly class EventACLAwareRepository implements EventACLAwareRepositoryInterface
|
||||
{
|
||||
public function __construct(
|
||||
private AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private Security $security,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NonUniqueResultException
|
||||
* @throws NoResultException
|
||||
*/
|
||||
public function countAllViewable(array $filters): int
|
||||
{
|
||||
if (!$this->security->getUser() instanceof User) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$qb = $this->buildQueryByAllViewable($filters);
|
||||
$this->addFilters($filters, $qb);
|
||||
|
||||
$qb->select('COUNT(event.id)');
|
||||
|
||||
return $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function findAllViewable(array $filters, int $offset = 0, int $limit = 50): array
|
||||
{
|
||||
if (!$this->security->getUser() instanceof User) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$qb = $this->buildQueryByAllViewable($filters)->select('event');
|
||||
$this->addFilters($filters, $qb);
|
||||
|
||||
$qb->setFirstResult($offset)->setMaxResults($limit);
|
||||
|
||||
$qb->addOrderBy('event.date', 'DESC');
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
private function addFilters(array $filters, QueryBuilder $qb): void
|
||||
{
|
||||
if (($filters['q'] ?? '') !== '') {
|
||||
$qb->andWhere('event.name LIKE :content');
|
||||
$qb->setParameter('content', '%'.$filters['q'].'%');
|
||||
}
|
||||
|
||||
if (array_key_exists('dates', $filters)) {
|
||||
$dates = $filters['dates'];
|
||||
if (null !== ($dates['from'] ?? null)) {
|
||||
$qb->andWhere('event.date >= :date_from');
|
||||
$qb->setParameter('date_from', $dates['from']);
|
||||
}
|
||||
if (null !== ($dates['to'] ?? null)) {
|
||||
$qb->andWhere('event.date <= :date_to');
|
||||
$qb->setParameter('date_to', $dates['to']);
|
||||
}
|
||||
}
|
||||
|
||||
if (0 < count($filters['event_types'] ?? [])) {
|
||||
$qb->andWhere('event.type IN (:event_types)');
|
||||
$qb->setParameter('event_types', $filters['event_types']);
|
||||
}
|
||||
}
|
||||
|
||||
public function buildQueryByAllViewable(array $filters): QueryBuilder
|
||||
{
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
$qb->from(Event::class, 'event');
|
||||
|
||||
$aclConditions = $qb->expr()->orX();
|
||||
|
||||
$i = 0;
|
||||
foreach ($this->authorizationHelperForCurrentUser->getReachableCenters(EventVoter::SEE) as $center) {
|
||||
foreach ($this->authorizationHelperForCurrentUser->getReachableScopes(EventVoter::SEE, $center) as $scopes) {
|
||||
$aclConditions->add(
|
||||
$qb->expr()->andX(
|
||||
'event.circle IN (:scopes_'.$i.')',
|
||||
$qb->expr()->orX(
|
||||
'event.center = :center_'.$i,
|
||||
$qb->expr()->exists(
|
||||
'SELECT 1 FROM '.Participation::class.' participation_'.$i.' JOIN participation_'.$i.'.event event_'.$i.
|
||||
' JOIN '.Person\PersonCenterHistory::class.' person_center_history_'.$i.
|
||||
' WITH IDENTITY(person_center_history_'.$i.'.person) = IDENTITY(participation_'.$i.'.person) '.
|
||||
' AND event_'.$i.'.date <= person_center_history_'.$i.'.startDate AND (person_center_history_'.$i.'.endDate IS NULL OR person_center_history_'.$i.'.endDate > event_'.$i.'.date) '.
|
||||
' WHERE participation_'.$i.'.event = event'
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
$qb->setParameter('scopes_'.$i, $scopes);
|
||||
$qb->setParameter('center_'.$i, $center);
|
||||
|
||||
++$i;
|
||||
}
|
||||
}
|
||||
|
||||
if (0 === $i) {
|
||||
$aclConditions->add('FALSE = TRUE');
|
||||
}
|
||||
|
||||
$qb
|
||||
->andWhere(
|
||||
$qb->expr()->orX(
|
||||
'event.createdBy = :user',
|
||||
$aclConditions
|
||||
)
|
||||
);
|
||||
|
||||
$qb->setParameter('user', $this->security->getUser());
|
||||
|
||||
return $qb;
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
<?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\EventBundle\Repository;
|
||||
|
||||
use Chill\EventBundle\Entity\Event;
|
||||
use Chill\EventBundle\Entity\EventType;
|
||||
|
||||
interface EventACLAwareRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* @param array{q?: string, dates?: array{from?: \DateTimeImmutable|null, to?: \DateTimeImmutable|null}, event_types?: list<EventType>} $filters
|
||||
*/
|
||||
public function countAllViewable(array $filters): int;
|
||||
|
||||
/**
|
||||
* @param array{q?: string, dates?: array{from?: \DateTimeImmutable|null, to?: \DateTimeImmutable|null}, event_types?: list<EventType>} $filters
|
||||
*
|
||||
* @return list<Event>
|
||||
*/
|
||||
public function findAllViewable(array $filters, int $offset = 0, int $limit = 50): array;
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
<?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\EventBundle\Repository;
|
||||
|
||||
use Chill\EventBundle\Entity\EventType;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<EventType>
|
||||
*/
|
||||
final class EventTypeRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(
|
||||
ManagerRegistry $registry,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly TranslatorInterface $translator
|
||||
) {
|
||||
parent::__construct($registry, EventType::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<EventType>
|
||||
*/
|
||||
public function findAllActive(): array
|
||||
{
|
||||
$dql = 'SELECT et FROM '.EventType::class.' et WHERE et.active = TRUE ORDER BY JSON_EXTRACT(et.name, :lang)';
|
||||
|
||||
return $this->entityManager->createQuery($dql)
|
||||
->setParameter('lang', $this->translator->getLocale())
|
||||
->getResult();
|
||||
}
|
||||
}
|
@@ -1,10 +1,14 @@
|
||||
{% extends '@ChillEvent/layout.html.twig' %}
|
||||
{% extends '@ChillEvent/layout.html.twig' %} {% block js %}
|
||||
{{ encore_entry_script_tags("mod_async_upload") }}
|
||||
{{ encore_entry_script_tags("mod_pickentity_type") }}
|
||||
|
||||
{% block title 'Event edit'|trans %}
|
||||
{% endblock %} {% block css %}
|
||||
{{ encore_entry_link_tags("mod_async_upload") }}
|
||||
{{ encore_entry_link_tags("mod_pickentity_type") }}
|
||||
|
||||
{% block event_content -%}
|
||||
{% endblock %} {% block title 'Event edit'|trans %} {% block event_content -%}
|
||||
<div class="col-10">
|
||||
<h1>{{ 'Event edit'|trans }}</h1>
|
||||
<h1>{{ "Event edit" | trans }}</h1>
|
||||
|
||||
{{ form_start(edit_form) }}
|
||||
{{ form_errors(edit_form) }}
|
||||
@@ -12,25 +16,32 @@
|
||||
{{ form_row(edit_form.name) }}
|
||||
{{ form_row(edit_form.date) }}
|
||||
|
||||
{{ form_row(edit_form.type, { 'label': 'Event type' }) }}
|
||||
{{ form_row(edit_form.type, { label: "Event type" }) }}
|
||||
{{ form_row(edit_form.moderator) }}
|
||||
{{ form_row(edit_form.location) }}
|
||||
{{ form_row(edit_form.organizationCost) }}
|
||||
|
||||
<ul class="record_actions">
|
||||
{{ form_row(edit_form.comment) }}
|
||||
{{ form_row(edit_form.documents) }}
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
|
||||
{% set returnPath = app.request.get('return_path') %}
|
||||
{% set returnLabel = app.request.get('return_label') %}
|
||||
|
||||
<a href="{{ returnPath |default( path('chill_event_list_most_recent') ) }}" class="btn btn-cancel">
|
||||
{{ returnLabel |default('Back to the most recent events'|trans) }}
|
||||
<a
|
||||
href="{{ chill_return_path_or('chill_event_event_list') }}"
|
||||
class="btn btn-cancel"
|
||||
>
|
||||
{{ "List of events" | trans | chill_return_path_label }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
{{ form_widget(edit_form.submit, { 'attr' : { 'class' : 'btn btn-update' } }) }}
|
||||
{{
|
||||
form_widget(edit_form.submit, {
|
||||
attr: { class: "btn btn-update" }
|
||||
})
|
||||
}}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{ form_end(edit_form) }}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@@ -24,85 +24,89 @@
|
||||
{% block content %}
|
||||
<h2>{{ 'Events participation' |trans }}</h2>
|
||||
|
||||
<table class="table table-striped table-bordered border-dark align-middle mt-3 events">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="chill-green">{{ 'Date'|trans }}</th>
|
||||
<th class="chill-red">{{ 'Name'|trans }}</th>
|
||||
<th class="chill-orange">{{ 'Event type'|trans }}</th>
|
||||
<th class="chill-red">{{ 'Role'|trans }}</th>
|
||||
<th class="chill-green">{{ 'Status'|trans }}</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for participation in participations %}
|
||||
<tr>
|
||||
<td>{{ participation.event.date|format_date('short') }}</td>
|
||||
<td>{{ participation.event.name }}</td>
|
||||
<td>{{ participation.event.type.name|localize_translatable_string }}</td>
|
||||
<td>{{ participation.role.name|localize_translatable_string }}</td>
|
||||
<td>{{ participation.status.name|localize_translatable_string }}</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group" aria-label="Button group actions">
|
||||
{% if participations|length == 0 %}
|
||||
<p class="chill-no-data-statement">{{ 'Any participation for this person'|trans }}</p>
|
||||
{% else %}
|
||||
<table class="table table-striped table-bordered border-dark align-middle mt-3 events">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="chill-green">{{ 'Date'|trans }}</th>
|
||||
<th class="chill-red">{{ 'Name'|trans }}</th>
|
||||
<th class="chill-orange">{{ 'Event type'|trans }}</th>
|
||||
<th class="chill-red">{{ 'Role'|trans }}</th>
|
||||
<th class="chill-green">{{ 'Status'|trans }}</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for participation in participations %}
|
||||
<tr>
|
||||
<td>{{ participation.event.date|format_date('short') }}</td>
|
||||
<td>{{ participation.event.name }}</td>
|
||||
<td>{{ participation.event.type.name|localize_translatable_string }}</td>
|
||||
<td>{{ participation.role.name|localize_translatable_string }}</td>
|
||||
<td>{{ participation.status.name|localize_translatable_string }}</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group" aria-label="Button group actions">
|
||||
|
||||
{% set currentPath = path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) %}
|
||||
{% set returnLabel = 'Back to %person% events'|trans({ '%person%' : currentPerson } ) %}
|
||||
{% set currentPath = path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) %}
|
||||
{% set returnLabel = 'Back to %person% events'|trans({ '%person%' : currentPerson } ) %}
|
||||
|
||||
{% if is_granted('CHILL_EVENT_SEE_DETAILS', participation.event) %}
|
||||
<a href="{{ path('chill_event__event_show', { 'event_id' : participation.event.id, 'return_path' : currentPath, 'return_label' : returnLabel } ) }}"
|
||||
class="btn btn-primary btn-sm" title="{{ 'See details of the event'|trans }}">
|
||||
<i class="fa fa-fw fa-eye"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_EVENT_SEE_DETAILS', participation.event) %}
|
||||
<a href="{{ path('chill_event__event_show', { 'event_id' : participation.event.id, 'return_path' : currentPath, 'return_label' : returnLabel } ) }}"
|
||||
class="btn btn-primary btn-sm" title="{{ 'See details of the event'|trans }}">
|
||||
<i class="fa fa-fw fa-eye"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if is_granted('CHILL_EVENT_UPDATE', participation.event)
|
||||
and is_granted('CHILL_EVENT_PARTICIPATION_UPDATE', participation) %}
|
||||
{% if is_granted('CHILL_EVENT_UPDATE', participation.event)
|
||||
and is_granted('CHILL_EVENT_PARTICIPATION_UPDATE', participation) %}
|
||||
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-sm btn-warning dropdown-toggle" type="button" id="dropdownEdit" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="dropdownEdit">
|
||||
<li>
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-sm btn-warning dropdown-toggle" type="button" id="dropdownEdit" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="dropdownEdit">
|
||||
<li>
|
||||
<a href="{{ path('chill_event__event_edit', { 'event_id' : participation.event.id, 'return_path' : currentPath, 'return_label' : returnLabel }) }}"
|
||||
class="dropdown-item">
|
||||
{{ 'Edit the event'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ path('chill_event_participation_edit', { 'participation_id' : participation.id, 'return_path' : currentPath, 'return_label' : returnLabel }) }}"
|
||||
class="dropdown-item">
|
||||
{{ 'Edit the participation'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
{% if is_granted('CHILL_EVENT_UPDATE', participation.event) %}
|
||||
<a href="{{ path('chill_event__event_edit', { 'event_id' : participation.event.id, 'return_path' : currentPath, 'return_label' : returnLabel }) }}"
|
||||
class="dropdown-item">
|
||||
class="btn btn-warning btn-sm">
|
||||
{{ 'Edit the event'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_EVENT_PARTICIPATION_UPDATE', participation) %}
|
||||
<a href="{{ path('chill_event_participation_edit', { 'participation_id' : participation.id, 'return_path' : currentPath, 'return_label' : returnLabel }) }}"
|
||||
class="dropdown-item">
|
||||
class="btn btn-warning btn-sm">
|
||||
{{ 'Edit the participation'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
{% endif %}
|
||||
|
||||
{% if is_granted('CHILL_EVENT_UPDATE', participation.event) %}
|
||||
<a href="{{ path('chill_event__event_edit', { 'event_id' : participation.event.id, 'return_path' : currentPath, 'return_label' : returnLabel }) }}"
|
||||
class="btn btn-warning btn-sm">
|
||||
{{ 'Edit the event'|trans }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_EVENT_PARTICIPATION_UPDATE', participation) %}
|
||||
<a href="{{ path('chill_event_participation_edit', { 'participation_id' : participation.id, 'return_path' : currentPath, 'return_label' : returnLabel }) }}"
|
||||
class="btn btn-warning btn-sm">
|
||||
{{ 'Edit the participation'|trans }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% if participations|length < paginator.getTotalItems %}
|
||||
{{ chill_pagination(paginator) }}
|
||||
|
@@ -1,28 +1,41 @@
|
||||
{% extends '@ChillEvent/layout.html.twig' %}
|
||||
{% extends '@ChillEvent/layout.html.twig' %} {% block js %}
|
||||
{{ encore_entry_script_tags("mod_async_upload") }}
|
||||
{{ encore_entry_script_tags("mod_pickentity_type") }}
|
||||
|
||||
{% block title 'Event creation'|trans %}
|
||||
{% endblock %} {% block css %}
|
||||
{{ encore_entry_link_tags("mod_async_upload") }}
|
||||
{{ encore_entry_link_tags("mod_pickentity_type") }}
|
||||
|
||||
{% block event_content -%}
|
||||
{% endblock %} {% block title 'Event creation'|trans %} {% block event_content
|
||||
-%}
|
||||
<div class="col-10">
|
||||
<h1>{{ 'Event creation'|trans }}</h1>
|
||||
<h1>{{ "Event creation" | trans }}</h1>
|
||||
|
||||
{{ form_start(form) }}
|
||||
{{ form_errors(form) }}
|
||||
{{ form_row(form.circle) }}
|
||||
{{ form_row(form.name) }}
|
||||
{{ form_row(form.date) }}
|
||||
|
||||
{{ form_row(form.type, { 'label': 'Event type' }) }}
|
||||
{{ form_row(form.type, { label: "Event type" }) }}
|
||||
{{ form_row(form.moderator) }}
|
||||
{{ form_row(form.location) }}
|
||||
{{ form_row(form.organizationCost) }}
|
||||
{{ form_row(form.comment) }}
|
||||
{{ form_row(form.documents) }}
|
||||
|
||||
<ul class="record_actions">
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
<a href="{{ path('chill_event_list_most_recent') }}" class="btn btn-cancel">
|
||||
{{ 'Back to the most recent events'|trans }}
|
||||
<a
|
||||
href="{{ path('chill_event_list_most_recent') }}"
|
||||
class="btn btn-cancel"
|
||||
>
|
||||
{{ "Back to the most recent events" | trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
{{ form_widget(form.submit, { 'attr' : { 'class' : 'btn btn-create' } }) }}
|
||||
{{
|
||||
form_widget(form.submit, { attr: { class: "btn btn-create" } })
|
||||
}}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
@@ -0,0 +1,126 @@
|
||||
{% extends '@ChillEvent/layout.html.twig' %} {% block title 'Events'|trans %} {%
|
||||
block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags("mod_pickentity_type") }}
|
||||
{% endblock %} {% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags("mod_pickentity_type") }}
|
||||
{% endblock %} {% block content %}
|
||||
<div class="col-10">
|
||||
<h1>{{ block("title") }}</h1>
|
||||
|
||||
{{ filter | chill_render_filter_order_helper }}
|
||||
|
||||
{# {% if is_granted('CHILL_EVENT_CREATE') %} #}
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<a
|
||||
class="btn btn-create"
|
||||
href="{{
|
||||
chill_path_add_return_path(
|
||||
'chill_event__event_new_pickcenter'
|
||||
)
|
||||
}}"
|
||||
>{{ "Add an event" | trans }}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
{# {% endif %} #} {% if events|length > 0 %}
|
||||
<div class="flex-table">
|
||||
{% for e in events %}
|
||||
<div class="item-bloc">
|
||||
<div class="item-row">
|
||||
<div class="item-col">
|
||||
<div class="denomination h2">
|
||||
{{ e.name }}
|
||||
</div>
|
||||
<p>{{ e.type.name | localize_translatable_string }}</p>
|
||||
{% if e.moderator is not null %}
|
||||
<p>
|
||||
{{ "Moderator" | trans }}:
|
||||
{{ e.moderator | chill_entity_render_box }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="item-col">
|
||||
<div class="container" style="text-align: right">
|
||||
<p>{{ e.date|format_datetime('medium', 'medium') }}</p>
|
||||
<p>
|
||||
{{ 'count participations to this event'|trans({'count': e.participations|length}) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if e.participations|length > 0 %}
|
||||
<div class="item-row separator">
|
||||
<strong>{{ "Participations" | trans }} : </strong>
|
||||
{% for part in e.participations|slice(0, 20) %} {% include
|
||||
'@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
targetEntity: { name: 'person', id: part.person.id }, action:
|
||||
'show', displayBadge: true, buttonText:
|
||||
part.person|chill_entity_render_string, isDead:
|
||||
part.person.deathdate is not null } %} {% endfor %} {% if
|
||||
e.participations|length > 20 %}
|
||||
{{ 'events.and_other_count_participants'|trans({'count': e.participations|length - 20}) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="item-row">
|
||||
<div class="item-col">
|
||||
{{ form_start(eventForms[e.id]) }}
|
||||
{{ form_widget(eventForms[e.id].person_id) }}
|
||||
{{ form_end(eventForms[e.id]) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-row separator">
|
||||
<div class="item-col item-meta"></div>
|
||||
<div class="item-col">
|
||||
<ul class="record_actions">
|
||||
{% if is_granted('CHILL_EVENT_UPDATE', e) %}
|
||||
<li>
|
||||
<a
|
||||
href="{{
|
||||
chill_path_add_return_path(
|
||||
'chill_event__event_delete',
|
||||
{ event_id: e.id }
|
||||
)
|
||||
}}"
|
||||
class="btn btn-delete"
|
||||
></a>
|
||||
</li>
|
||||
{% endif %} {% if is_granted('CHILL_EVENT_UPDATE', e) %}
|
||||
<li>
|
||||
<a
|
||||
href="{{
|
||||
chill_path_add_return_path(
|
||||
'chill_event__event_edit',
|
||||
{ event_id: e.id }
|
||||
)
|
||||
}}"
|
||||
class="btn btn-edit"
|
||||
></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a
|
||||
href="{{
|
||||
chill_path_add_return_path(
|
||||
'chill_event__event_show',
|
||||
{ event_id: e.id }
|
||||
)
|
||||
}}"
|
||||
class="btn btn-show"
|
||||
></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{{ chill_pagination(pagination) }}
|
||||
|
||||
{% endblock %}
|
@@ -4,12 +4,28 @@
|
||||
|
||||
{% import '@ChillPerson/Person/macro.html.twig' as person_macro %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
||||
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_pickentity_type') }}
|
||||
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block event_content -%}
|
||||
<div class="col-10">
|
||||
<h1>{{ 'Details of an event'|trans }}</h1>
|
||||
|
||||
<table class="table table-bordered border-dark align-middle">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{{ 'Circle'|trans }}</th>
|
||||
<td>{{ event.circle.name|localize_translatable_string }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'Name'|trans }}</th>
|
||||
<td>{{ event.name }}</td>
|
||||
@@ -22,42 +38,62 @@
|
||||
<th>{{ 'Event type'|trans }}</th>
|
||||
<td>{{ event.type.name|localize_translatable_string }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'Circle'|trans }}</th>
|
||||
<td>{{ event.circle.name|localize_translatable_string }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'Moderator'|trans }}</th>
|
||||
<td>{{ event.moderator|trans|default('-') }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>{{ 'event.fields.organizationCost'|trans }}</th>
|
||||
<td>{{ event.organizationCost|format_currency('EUR') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'event.fields.location'|trans }}</th>
|
||||
<td>
|
||||
{% if event.location is not null %}
|
||||
{{ event.location.name }}
|
||||
{% if event.location.address is not same as(null) %}{{ event.location.address|chill_entity_render_box({'multiline': false, 'with_picto': (event.location.name is empty), 'details_button': true}) }}{% endif %}
|
||||
{% else %}
|
||||
<span class="chill-no-data-statement">{{ 'Any location for this event'|trans }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if event.documents|length > 0 %}
|
||||
<div>
|
||||
<p><strong>{{ 'event.fields.documents'|trans }}</strong></p>
|
||||
<ul>
|
||||
{% for d in event.documents %}
|
||||
<li class="document-list-item">{{ d.title|chill_print_or_message('document.Any title') }} {{ d|chill_document_button_group(d.title, is_granted('CHILL_EVENT_SEE_DETAILS', event), {small: false}) }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not event.comment.empty %}
|
||||
<div>
|
||||
{{ event.comment|chill_entity_render_box({
|
||||
'disable_markdown': false,
|
||||
'metadata': true,
|
||||
}) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<ul class="record_actions">
|
||||
|
||||
{% set returnPath = app.request.get('return_path') %}
|
||||
{% set returnLabel = app.request.get('return_label') %}
|
||||
|
||||
{% if returnPath and returnLabel %}
|
||||
<li class="cancel">
|
||||
<a href="{{ returnPath }}" class="btn btn-cancel">{{ returnLabel }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ path('chill_event__event_edit', {
|
||||
'event_id': event.id,
|
||||
'return_path': app.request.getRequestUri,
|
||||
'return_label': 'Back to details of the event'|trans
|
||||
}) }}" class="btn btn-edit">{{ 'Edit'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<a href="{{ path('chill_event__event_edit', {'event_id': event.id }) }}" class="btn btn-edit">
|
||||
{{ 'Edit'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="cancel">
|
||||
<a href="{{ chill_return_path_or('chill_event_event_list') }}" class="btn btn-cancel">{{ 'Back to the list'|trans|chill_return_path_label }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_event__event_edit', {'event_id': event.id }, false, 'See'|trans) }}" class="btn btn-edit">
|
||||
{{ 'Edit'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ path('chill_event__event_delete', {'event_id' : event.id } ) }}"
|
||||
class="btn btn-delete">{{ 'Delete event'|trans }}</a>
|
||||
@@ -83,7 +119,15 @@
|
||||
<tbody>
|
||||
{% for participation in event.participations %}
|
||||
<tr>
|
||||
<td>{{ person_macro.render(participation.person) }}</td>
|
||||
<td>
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
targetEntity: { name: 'person', id: participation.person.id },
|
||||
action: 'show',
|
||||
displayBadge: true,
|
||||
buttonText: participation.person|chill_entity_render_string,
|
||||
isDead: participation.person.deathdate is not null
|
||||
} %}
|
||||
</td>
|
||||
<td>{{ participation.role.name|localize_translatable_string }}</td>
|
||||
<td>{{ participation.status.name|localize_translatable_string }}</td>
|
||||
<td>{{ participation.lastUpdate|ago }} {# sf4 check: filter 'time_diff' is abandoned,
|
||||
@@ -94,7 +138,7 @@
|
||||
<ul class="record_actions">
|
||||
{% if is_granted('CHILL_EVENT_PARTICIPATION_UPDATE', participation) %}
|
||||
<li>
|
||||
<a href="{{ path('chill_event_participation_edit', { 'participation_id' : participation.id } ) }}"
|
||||
<a href="{{ chill_path_add_return_path('chill_event_participation_edit', { 'participation_id' : participation.id }, false, 'See'|trans ) }}"
|
||||
class="btn btn-edit" title="{{ 'Edit'|trans }}"></a>
|
||||
</li>
|
||||
<li>
|
||||
@@ -126,11 +170,8 @@
|
||||
'class' : 'custom-select',
|
||||
'style': 'min-width: 15em; max-width: 18em; display: inline-block;'
|
||||
}} ) }}
|
||||
<div class="input-group-append">
|
||||
{{ form_widget(form_add_participation_by_person.submit, { 'attr' : { 'class' : 'btn btn-create' } } ) }}
|
||||
</div>
|
||||
</div>
|
||||
{{ form_rest(form_add_participation_by_person) }}
|
||||
<input type="hidden" name="returnPath" value="{{ app.request.requestUri }}" />
|
||||
{{ form_end(form_add_participation_by_person) }}
|
||||
</div>
|
||||
|
||||
|
@@ -32,7 +32,7 @@
|
||||
|
||||
|
||||
|
||||
<ul class="record_actions">
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
<a href="{{ path('chill_event__event_show', { 'event_id' : participation.event.id } ) }}" class="btn btn-cancel">
|
||||
{{ 'Back to the event'|trans }}
|
||||
|
@@ -0,0 +1,44 @@
|
||||
<?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\EventBundle\Tests\Controller;
|
||||
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Test\PrepareClientTrait;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class EventListControllerTest extends WebTestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
use PrepareClientTrait;
|
||||
|
||||
private readonly PaginatorFactory $paginatorFactory;
|
||||
private readonly Environment $environment;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function testList(): void
|
||||
{
|
||||
$client = $this->getClientAuthenticated();
|
||||
|
||||
$client->request('GET', '/fr/event/event/list');
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
}
|
@@ -11,6 +11,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\EventBundle\Tests\Controller;
|
||||
|
||||
use Chill\EventBundle\Entity\Event;
|
||||
use Chill\EventBundle\Repository\EventRepository;
|
||||
use Chill\MainBundle\Test\PrepareClientTrait;
|
||||
use Chill\PersonBundle\DataFixtures\Helper\PersonRandomHelper;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use function count;
|
||||
|
||||
@@ -23,15 +28,12 @@ use function count;
|
||||
*/
|
||||
final class ParticipationControllerTest extends WebTestCase
|
||||
{
|
||||
/**
|
||||
* @var \Symfony\Component\BrowserKit\AbstractBrowser
|
||||
*/
|
||||
protected $client;
|
||||
use PersonRandomHelper;
|
||||
use PrepareClientTrait;
|
||||
|
||||
/**
|
||||
* @var \Doctrine\ORM\EntityManagerInterface
|
||||
*/
|
||||
protected $em;
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private EventRepository $eventRepository;
|
||||
|
||||
/**
|
||||
* Keep a cache for each person id given by the function getRandomPerson.
|
||||
@@ -44,23 +46,21 @@ final class ParticipationControllerTest extends WebTestCase
|
||||
*/
|
||||
private array $personsIdsCache = [];
|
||||
|
||||
protected function setUp(): void
|
||||
protected function prepareDI(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$this->client = self::createClient([], [
|
||||
'PHP_AUTH_USER' => 'center a_social',
|
||||
'PHP_AUTH_PW' => 'password',
|
||||
'HTTP_ACCEPT_LANGUAGE' => 'fr_FR',
|
||||
]);
|
||||
|
||||
$container = self::$kernel->getContainer();
|
||||
|
||||
$this->em = $container->get('doctrine.orm.entity_manager');
|
||||
$this->em = self::$container->get(EntityManagerInterface::class);
|
||||
$this->eventRepository = self::$container->get(EventRepository::class);
|
||||
|
||||
$this->personsIdsCache = [];
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
parent::tearDown();
|
||||
|
||||
self::ensureKernelShutdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method test participation creation with wrong parameters.
|
||||
*
|
||||
@@ -68,11 +68,13 @@ final class ParticipationControllerTest extends WebTestCase
|
||||
*/
|
||||
public function testCreateActionWrongParameters()
|
||||
{
|
||||
$client = $this->getClientAuthenticated();
|
||||
$this->prepareDI();
|
||||
$event = $this->getRandomEvent();
|
||||
$person = $this->getRandomPerson();
|
||||
$person = $this->getRandomPerson($this->em);
|
||||
|
||||
// missing person_id or persons_ids
|
||||
$this->client->request(
|
||||
$client->request(
|
||||
'GET',
|
||||
'/fr/event/participation/create',
|
||||
[
|
||||
@@ -81,33 +83,33 @@ final class ParticipationControllerTest extends WebTestCase
|
||||
);
|
||||
$this->assertEquals(
|
||||
400,
|
||||
$this->client->getResponse()->getStatusCode(),
|
||||
$client->getResponse()->getStatusCode(),
|
||||
'Test that /fr/event/participation/create fail if '
|
||||
.'both person_id and persons_ids are missing'
|
||||
);
|
||||
|
||||
// having both person_id and persons_ids
|
||||
$this->client->request(
|
||||
$client->request(
|
||||
'GET',
|
||||
'/fr/event/participation/create',
|
||||
[
|
||||
'event_id' => $event->getId(),
|
||||
'persons_ids' => implode(',', [
|
||||
$this->getRandomPerson()->getId(),
|
||||
$this->getRandomPerson()->getId(),
|
||||
$this->getRandomPerson($this->em)->getId(),
|
||||
$this->getRandomPerson($this->em)->getId(),
|
||||
]),
|
||||
'person_id' => $person->getId(),
|
||||
]
|
||||
);
|
||||
$this->assertEquals(
|
||||
400,
|
||||
$this->client->getResponse()->getStatusCode(),
|
||||
$client->getResponse()->getStatusCode(),
|
||||
'test that /fr/event/participation/create fail if both person_id and '
|
||||
.'persons_ids are set'
|
||||
);
|
||||
|
||||
// missing event_id
|
||||
$this->client->request(
|
||||
$client->request(
|
||||
'GET',
|
||||
'/fr/event/participation/create',
|
||||
[
|
||||
@@ -116,12 +118,12 @@ final class ParticipationControllerTest extends WebTestCase
|
||||
);
|
||||
$this->assertEquals(
|
||||
400,
|
||||
$this->client->getResponse()->getStatusCode(),
|
||||
$client->getResponse()->getStatusCode(),
|
||||
'Test that /fr/event/participation/create fails if event_id is missing'
|
||||
);
|
||||
|
||||
// persons_ids with wrong content
|
||||
$this->client->request(
|
||||
$client->request(
|
||||
'GET',
|
||||
'/fr/event/participation/create',
|
||||
[
|
||||
@@ -131,42 +133,47 @@ final class ParticipationControllerTest extends WebTestCase
|
||||
);
|
||||
$this->assertEquals(
|
||||
400,
|
||||
$this->client->getResponse()->getStatusCode(),
|
||||
$client->getResponse()->getStatusCode(),
|
||||
'Test that /fr/event/participation/create fails if persons_ids has wrong content'
|
||||
);
|
||||
}
|
||||
|
||||
public function testEditMultipleAction()
|
||||
{
|
||||
/** @var \Chill\EventBundle\Entity\Event $event */
|
||||
$client = $this->getClientAuthenticated();
|
||||
$this->prepareDI();
|
||||
|
||||
/** @var Event $event */
|
||||
$event = $this->getRandomEventWithMultipleParticipations();
|
||||
|
||||
$crawler = $this->client->request('GET', '/fr/event/participation/'.$event->getId().
|
||||
$crawler = $client->request('GET', '/fr/event/participation/'.$event->getId().
|
||||
'/edit_multiple');
|
||||
|
||||
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
|
||||
$this->assertEquals(200, $client->getResponse()->getStatusCode());
|
||||
|
||||
$button = $crawler->selectButton('Mettre à jour');
|
||||
$this->assertEquals(1, $button->count(), "test the form with button 'mettre à jour' exists ");
|
||||
|
||||
$this->client->submit($button->form(), [
|
||||
$client->submit($button->form(), [
|
||||
'form[participations][0][role]' => $event->getType()->getRoles()->first()->getId(),
|
||||
'form[participations][0][status]' => $event->getType()->getStatuses()->first()->getId(),
|
||||
'form[participations][1][role]' => $event->getType()->getRoles()->last()->getId(),
|
||||
'form[participations][1][status]' => $event->getType()->getStatuses()->last()->getId(),
|
||||
]);
|
||||
|
||||
$this->assertTrue($this->client->getResponse()
|
||||
$this->assertTrue($client->getResponse()
|
||||
->isRedirect('/fr/event/event/'.$event->getId().'/show'));
|
||||
}
|
||||
|
||||
public function testNewActionWrongParameters()
|
||||
{
|
||||
$client = $this->getClientAuthenticated();
|
||||
$this->prepareDI();
|
||||
$event = $this->getRandomEvent();
|
||||
$person = $this->getRandomPerson();
|
||||
$person = $this->getRandomPerson($this->em);
|
||||
|
||||
// missing person_id or persons_ids
|
||||
$this->client->request(
|
||||
$client->request(
|
||||
'GET',
|
||||
'/fr/event/participation/new',
|
||||
[
|
||||
@@ -175,33 +182,33 @@ final class ParticipationControllerTest extends WebTestCase
|
||||
);
|
||||
$this->assertEquals(
|
||||
400,
|
||||
$this->client->getResponse()->getStatusCode(),
|
||||
$client->getResponse()->getStatusCode(),
|
||||
'Test that /fr/event/participation/new fail if '
|
||||
.'both person_id and persons_ids are missing'
|
||||
);
|
||||
|
||||
// having both person_id and persons_ids
|
||||
$this->client->request(
|
||||
$client->request(
|
||||
'GET',
|
||||
'/fr/event/participation/new',
|
||||
[
|
||||
'event_id' => $event->getId(),
|
||||
'persons_ids' => implode(',', [
|
||||
$this->getRandomPerson()->getId(),
|
||||
$this->getRandomPerson()->getId(),
|
||||
$this->getRandomPerson($this->em)->getId(),
|
||||
$this->getRandomPerson($this->em)->getId(),
|
||||
]),
|
||||
'person_id' => $person->getId(),
|
||||
]
|
||||
);
|
||||
$this->assertEquals(
|
||||
400,
|
||||
$this->client->getResponse()->getStatusCode(),
|
||||
$client->getResponse()->getStatusCode(),
|
||||
'test that /fr/event/participation/new fail if both person_id and '
|
||||
.'persons_ids are set'
|
||||
);
|
||||
|
||||
// missing event_id
|
||||
$this->client->request(
|
||||
$client->request(
|
||||
'GET',
|
||||
'/fr/event/participation/new',
|
||||
[
|
||||
@@ -210,12 +217,12 @@ final class ParticipationControllerTest extends WebTestCase
|
||||
);
|
||||
$this->assertEquals(
|
||||
400,
|
||||
$this->client->getResponse()->getStatusCode(),
|
||||
$client->getResponse()->getStatusCode(),
|
||||
'Test that /fr/event/participation/new fails if event_id is missing'
|
||||
);
|
||||
|
||||
// persons_ids with wrong content
|
||||
$this->client->request(
|
||||
$client->request(
|
||||
'GET',
|
||||
'/fr/event/participation/new',
|
||||
[
|
||||
@@ -225,13 +232,15 @@ final class ParticipationControllerTest extends WebTestCase
|
||||
);
|
||||
$this->assertEquals(
|
||||
400,
|
||||
$this->client->getResponse()->getStatusCode(),
|
||||
$client->getResponse()->getStatusCode(),
|
||||
'Test that /fr/event/participation/new fails if persons_ids has wrong content'
|
||||
);
|
||||
}
|
||||
|
||||
public function testNewMultipleAction()
|
||||
{
|
||||
$client = $this->getClientAuthenticated();
|
||||
$this->prepareDI();
|
||||
$event = $this->getRandomEvent();
|
||||
// record the number of participation for the event (used later in this test)
|
||||
$nbParticipations = $event->getParticipations()->count();
|
||||
@@ -244,10 +253,10 @@ final class ParticipationControllerTest extends WebTestCase
|
||||
->toArray()
|
||||
);
|
||||
// get some random people
|
||||
$person1 = $this->getRandomPerson();
|
||||
$person2 = $this->getRandomPerson();
|
||||
$person1 = $this->getRandomPerson($this->em);
|
||||
$person2 = $this->getRandomPerson($this->em);
|
||||
|
||||
$crawler = $this->client->request(
|
||||
$crawler = $client->request(
|
||||
'GET',
|
||||
'/fr/event/participation/new',
|
||||
[
|
||||
@@ -258,7 +267,7 @@ final class ParticipationControllerTest extends WebTestCase
|
||||
|
||||
$this->assertEquals(
|
||||
200,
|
||||
$this->client->getResponse()->getStatusCode(),
|
||||
$client->getResponse()->getStatusCode(),
|
||||
'test that /fr/event/participation/new is successful'
|
||||
);
|
||||
|
||||
@@ -266,7 +275,7 @@ final class ParticipationControllerTest extends WebTestCase
|
||||
|
||||
$this->assertNotNull($button, "test the form with button 'Créer' exists");
|
||||
|
||||
$this->client->submit($button->form(), [
|
||||
$client->submit($button->form(), [
|
||||
'form' => [
|
||||
'participations' => [
|
||||
0 => [
|
||||
@@ -281,8 +290,8 @@ final class ParticipationControllerTest extends WebTestCase
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertTrue($this->client->getResponse()->isRedirect());
|
||||
$crawler = $this->client->followRedirect();
|
||||
$this->assertTrue($client->getResponse()->isRedirect());
|
||||
$crawler = $client->followRedirect();
|
||||
|
||||
$span1 = $crawler->filter('table td span.entity-person a:contains("'
|
||||
.$person1->getFirstName().'"):contains("'.$person1->getLastname().'")');
|
||||
@@ -292,7 +301,7 @@ final class ParticipationControllerTest extends WebTestCase
|
||||
$this->assertGreaterThan(0, \count($span2));
|
||||
|
||||
// as the container has reloaded, reload the event
|
||||
$event = $this->em->getRepository(\Chill\EventBundle\Entity\Event::class)->find($event->getId());
|
||||
$event = $this->em->getRepository(Event::class)->find($event->getId());
|
||||
$this->em->refresh($event);
|
||||
|
||||
$this->assertEquals($nbParticipations + 2, $event->getParticipations()->count());
|
||||
@@ -300,13 +309,15 @@ final class ParticipationControllerTest extends WebTestCase
|
||||
|
||||
public function testNewMultipleWithAllPeopleParticipating()
|
||||
{
|
||||
$client = $this->getClientAuthenticated();
|
||||
$this->prepareDI();
|
||||
$event = $this->getRandomEventWithMultipleParticipations();
|
||||
|
||||
$persons_id = implode(',', $event->getParticipations()->map(
|
||||
static fn ($p) => $p->getPerson()->getId()
|
||||
)->toArray());
|
||||
|
||||
$crawler = $this->client->request(
|
||||
$crawler = $client->request(
|
||||
'GET',
|
||||
'/fr/event/participation/new',
|
||||
[
|
||||
@@ -317,13 +328,15 @@ final class ParticipationControllerTest extends WebTestCase
|
||||
|
||||
$this->assertEquals(
|
||||
302,
|
||||
$this->client->getResponse()->getStatusCode(),
|
||||
$client->getResponse()->getStatusCode(),
|
||||
'test that /fr/event/participation/new is redirecting'
|
||||
);
|
||||
}
|
||||
|
||||
public function testNewMultipleWithSomePeopleParticipating()
|
||||
{
|
||||
$client = $this->getClientAuthenticated();
|
||||
$this->prepareDI();
|
||||
$event = $this->getRandomEventWithMultipleParticipations();
|
||||
// record the number of participation for the event (used later in this test)
|
||||
$nbParticipations = $event->getParticipations()->count();
|
||||
@@ -335,12 +348,12 @@ final class ParticipationControllerTest extends WebTestCase
|
||||
$this->personsIdsCache = array_merge($this->personsIdsCache, $persons_id);
|
||||
|
||||
// get a random person
|
||||
$newPerson = $this->getRandomPerson();
|
||||
$newPerson = $this->getRandomPerson($this->em);
|
||||
|
||||
// build the `persons_ids` parameter
|
||||
$persons_ids_string = implode(',', [...$persons_id, $newPerson->getId()]);
|
||||
|
||||
$crawler = $this->client->request(
|
||||
$crawler = $client->request(
|
||||
'GET',
|
||||
'/fr/event/participation/new',
|
||||
[
|
||||
@@ -351,7 +364,7 @@ final class ParticipationControllerTest extends WebTestCase
|
||||
|
||||
$this->assertEquals(
|
||||
200,
|
||||
$this->client->getResponse()->getStatusCode(),
|
||||
$client->getResponse()->getStatusCode(),
|
||||
'test that /fr/event/participation/new is successful'
|
||||
);
|
||||
|
||||
@@ -377,15 +390,15 @@ final class ParticipationControllerTest extends WebTestCase
|
||||
$this->assertNotNull($button, "test the form with button 'Créer' exists");
|
||||
|
||||
// submit the form
|
||||
$this->client->submit($button->form(), [
|
||||
$client->submit($button->form(), [
|
||||
'participation[role]' => $event->getType()->getRoles()->first()->getId(),
|
||||
'participation[status]' => $event->getType()->getStatuses()->first()->getId(),
|
||||
]);
|
||||
|
||||
$this->assertTrue($this->client->getResponse()->isRedirect());
|
||||
$this->assertTrue($client->getResponse()->isRedirect());
|
||||
|
||||
// reload the event and test there is a new participation
|
||||
$event = $this->em->getRepository(\Chill\EventBundle\Entity\Event::class)
|
||||
$event = $this->em->getRepository(Event::class)
|
||||
->find($event->getId());
|
||||
$this->em->refresh($event);
|
||||
|
||||
@@ -398,12 +411,14 @@ final class ParticipationControllerTest extends WebTestCase
|
||||
|
||||
public function testNewSingleAction()
|
||||
{
|
||||
$client = $this->getClientAuthenticated();
|
||||
$this->prepareDI();
|
||||
$event = $this->getRandomEvent();
|
||||
// record the number of participation for the event
|
||||
$nbParticipations = $event->getParticipations()->count();
|
||||
$person = $this->getRandomPerson();
|
||||
$person = $this->getRandomPerson($this->em);
|
||||
|
||||
$crawler = $this->client->request(
|
||||
$crawler = $client->request(
|
||||
'GET',
|
||||
'/fr/event/participation/new',
|
||||
[
|
||||
@@ -414,7 +429,7 @@ final class ParticipationControllerTest extends WebTestCase
|
||||
|
||||
$this->assertEquals(
|
||||
200,
|
||||
$this->client->getResponse()->getStatusCode(),
|
||||
$client->getResponse()->getStatusCode(),
|
||||
'test that /fr/event/participation/new is successful'
|
||||
);
|
||||
|
||||
@@ -422,13 +437,13 @@ final class ParticipationControllerTest extends WebTestCase
|
||||
|
||||
$this->assertNotNull($button, "test the form with button 'Créer' exists");
|
||||
|
||||
$this->client->submit($button->form(), [
|
||||
$client->submit($button->form(), [
|
||||
'participation[role]' => $event->getType()->getRoles()->first()->getId(),
|
||||
'participation[status]' => $event->getType()->getStatuses()->first()->getId(),
|
||||
]);
|
||||
|
||||
$this->assertTrue($this->client->getResponse()->isRedirect());
|
||||
$crawler = $this->client->followRedirect();
|
||||
$this->assertTrue($client->getResponse()->isRedirect());
|
||||
$crawler = $client->followRedirect();
|
||||
|
||||
$span = $crawler->filter('table td span.entity-person a:contains("'
|
||||
.$person->getFirstName().'"):contains("'.$person->getLastname().'")');
|
||||
@@ -436,29 +451,23 @@ final class ParticipationControllerTest extends WebTestCase
|
||||
$this->assertGreaterThan(0, \count($span));
|
||||
|
||||
// as the container has reloaded, reload the event
|
||||
$event = $this->em->getRepository(\Chill\EventBundle\Entity\Event::class)->find($event->getId());
|
||||
$event = $this->em->getRepository(Event::class)->find($event->getId());
|
||||
$this->em->refresh($event);
|
||||
|
||||
$this->assertEquals($nbParticipations + 1, $event->getParticipations()->count());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Chill\EventBundle\Entity\Event
|
||||
*/
|
||||
protected function getRandomEvent(mixed $centerName = 'Center A', mixed $circleName = 'social')
|
||||
private function getRandomEvent(string $centerName = 'Center A', string $circleName = 'social'): Event
|
||||
{
|
||||
$center = $this->em->getRepository(\Chill\MainBundle\Entity\Center::class)
|
||||
->findByName($centerName);
|
||||
$dql = 'FROM '.Event::class.' e JOIN e.center center JOIN e.circle scope WHERE center.name LIKE :cname AND JSON_EXTRACT(scope.name, \'fr\') LIKE :sname';
|
||||
|
||||
$circles = $this->em->getRepository(\Chill\MainBundle\Entity\Scope::class)
|
||||
->findAll();
|
||||
array_filter($circles, static fn ($circle) => \in_array($circleName, $circle->getName(), true));
|
||||
$circle = $circles[0];
|
||||
$ids = $this->em->createQuery(
|
||||
'SELECT DISTINCT e.id '.$dql
|
||||
)
|
||||
->setParameters(['cname' => $centerName, 'sname' => $circleName])
|
||||
->getResult();
|
||||
|
||||
$events = $this->em->getRepository(\Chill\EventBundle\Entity\Event::class)
|
||||
->findBy(['center' => $center, 'circle' => $circle]);
|
||||
|
||||
return $events[array_rand($events)];
|
||||
return $this->eventRepository->find($ids[array_rand($ids)]['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -467,7 +476,7 @@ final class ParticipationControllerTest extends WebTestCase
|
||||
* @param string $centerName
|
||||
* @param type $circleName
|
||||
*
|
||||
* @return \Chill\EventBundle\Entity\Event
|
||||
* @return Event
|
||||
*/
|
||||
protected function getRandomEventWithMultipleParticipations(
|
||||
$centerName = 'Center A',
|
||||
@@ -479,35 +488,4 @@ final class ParticipationControllerTest extends WebTestCase
|
||||
$event :
|
||||
$this->getRandomEventWithMultipleParticipations($centerName, $circleName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a person randomly.
|
||||
*
|
||||
* This function does not give the same person twice
|
||||
* for each test.
|
||||
*
|
||||
* You may ask to ignore some people by adding their id to the property
|
||||
* `$this->personsIdsCache`
|
||||
*
|
||||
* @param string $centerName
|
||||
*
|
||||
* @return \Chill\PersonBundle\Entity\Person
|
||||
*/
|
||||
protected function getRandomPerson($centerName = 'Center A')
|
||||
{
|
||||
$center = $this->em->getRepository(\Chill\MainBundle\Entity\Center::class)
|
||||
->findByName($centerName);
|
||||
|
||||
$persons = $this->em->getRepository(\Chill\PersonBundle\Entity\Person::class)
|
||||
->findBy(['center' => $center]);
|
||||
|
||||
$person = $persons[array_rand($persons)];
|
||||
|
||||
if (\in_array($person->getId(), $this->personsIdsCache, true)) {
|
||||
return $this->getRandomPerson($centerName); // we try another time
|
||||
}
|
||||
$this->personsIdsCache[] = $person->getId();
|
||||
|
||||
return $person;
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,97 @@
|
||||
<?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\EventBundle\Tests\Repository;
|
||||
|
||||
use Chill\EventBundle\Repository\EventACLAwareRepository;
|
||||
use Chill\EventBundle\Security\Authorization\EventVoter;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class EventACLAwareRepositoryTest extends KernelTestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider generateFilters
|
||||
*
|
||||
* @throws \Doctrine\ORM\NoResultException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function testCountAllViewable(array $filters): void
|
||||
{
|
||||
$repository = $this->buildEventACLAwareRepository();
|
||||
|
||||
$this->assertGreaterThanOrEqual(0, $repository->countAllViewable($filters));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider generateFilters
|
||||
*/
|
||||
public function testFindAllViewable(array $filters): void
|
||||
{
|
||||
$repository = $this->buildEventACLAwareRepository();
|
||||
|
||||
$this->assertIsArray($repository->findAllViewable($filters));
|
||||
}
|
||||
|
||||
public function generateFilters(): iterable
|
||||
{
|
||||
yield [[]];
|
||||
}
|
||||
|
||||
public function buildEventACLAwareRepository(): EventACLAwareRepository
|
||||
{
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
$user = $em->createQuery('SELECT u FROM '.User::class.' u')
|
||||
->setMaxResults(1)
|
||||
->getSingleResult()
|
||||
;
|
||||
|
||||
$scopes = $em->createQuery('SELECT s FROM '.Scope::class.' s')
|
||||
->setMaxResults(3)
|
||||
->getResult();
|
||||
|
||||
$centers = $em->createQuery('SELECT c FROM '.Center::class.' c')
|
||||
->setMaxResults(3)
|
||||
->getResult();
|
||||
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->getUser()->willReturn($user);
|
||||
|
||||
$authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class);
|
||||
$authorizationHelper->getReachableCenters(EventVoter::SEE)->willReturn($centers);
|
||||
$authorizationHelper->getReachableScopes(EventVoter::SEE, Argument::type(Center::class))->willReturn($scopes);
|
||||
|
||||
return new EventACLAwareRepository(
|
||||
$authorizationHelper->reveal(),
|
||||
$em,
|
||||
$security->reveal()
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
services:
|
||||
|
||||
Chill\EventBundle\Controller\EventController:
|
||||
arguments:
|
||||
$eventDispatcher: '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface'
|
||||
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
|
||||
$formFactoryInterface: '@Symfony\Component\Form\FormFactoryInterface'
|
||||
$translator: '@Symfony\Contracts\Translation\TranslatorInterface'
|
||||
$paginator: '@chill_main.paginator_factory'
|
||||
public: true
|
||||
tags: ['controller.service_arguments']
|
||||
|
||||
Chill\EventBundle\Controller\ParticipationController:
|
||||
arguments:
|
||||
$logger: '@Psr\Log\LoggerInterface'
|
||||
tags: ['controller.service_arguments']
|
@@ -1,7 +0,0 @@
|
||||
services:
|
||||
Chill\EventBundle\Menu\PersonMenuBuilder:
|
||||
arguments:
|
||||
$authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'
|
||||
$translator: '@Symfony\Contracts\Translation\TranslatorInterface'
|
||||
tags:
|
||||
- { name: 'chill.menu_builder' }
|
@@ -1,26 +0,0 @@
|
||||
Chill\EventBundle\Entity\Participation:
|
||||
properties:
|
||||
event:
|
||||
- NotNull: ~
|
||||
status:
|
||||
- NotNull: ~
|
||||
person:
|
||||
- NotNull: ~
|
||||
constraints:
|
||||
- Callback: isConsistent
|
||||
|
||||
|
||||
Chill\EventBundle\Entity\Event:
|
||||
properties:
|
||||
name:
|
||||
- Length:
|
||||
min: 3
|
||||
max: 75
|
||||
minMessage: The event name must have at least {{ limit }} characters.
|
||||
maxMessage: The event name must have maximum {{ limit }} characters.
|
||||
type:
|
||||
- NotNull: ~
|
||||
circle:
|
||||
- NotNull: ~
|
||||
center:
|
||||
- NotNull: ~
|
@@ -19,11 +19,13 @@ use Doctrine\Migrations\AbstractMigration;
|
||||
*/
|
||||
class Version20160318111334 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'initialize the bundle chill event';
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
|
||||
|
||||
$this->addSql('ALTER TABLE chill_event_role DROP CONSTRAINT FK_AA714E54C54C8C93');
|
||||
$this->addSql('ALTER TABLE chill_event_status DROP CONSTRAINT FK_A6CC85D0C54C8C93');
|
||||
$this->addSql('ALTER TABLE chill_event_participation DROP CONSTRAINT FK_4E7768ACD60322AC');
|
||||
@@ -50,9 +52,6 @@ class Version20160318111334 extends AbstractMigration
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
|
||||
|
||||
$this->addSql('CREATE SEQUENCE chill_event_event_type_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||
$this->addSql('CREATE SEQUENCE chill_event_role_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||
$this->addSql('CREATE SEQUENCE chill_event_status_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||
@@ -123,11 +122,26 @@ class Version20160318111334 extends AbstractMigration
|
||||
.'FOREIGN KEY (event_id) '
|
||||
.'REFERENCES chill_event_event (id) '
|
||||
.'NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_event_participation '
|
||||
|
||||
// before adding fk constraint to person, check what is the table name
|
||||
$results = $this->connection->executeQuery('SELECT EXISTS (SELECT 1 FROM pg_tables WHERE tablename = \'chill_person_person\')');
|
||||
/** @var bool $isChillPersonPersonTable */
|
||||
$isChillPersonPersonTable = $results->fetchFirstColumn()[0];
|
||||
|
||||
if ($isChillPersonPersonTable) {
|
||||
$this->addSql('ALTER TABLE chill_event_participation '
|
||||
.'ADD CONSTRAINT FK_4E7768AC217BBB47 '
|
||||
.'FOREIGN KEY (person_id) '
|
||||
.'REFERENCES chill_person_person (id) '
|
||||
.'NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
} else {
|
||||
$this->addSql('ALTER TABLE chill_event_participation '
|
||||
.'ADD CONSTRAINT FK_4E7768AC217BBB47 '
|
||||
.'FOREIGN KEY (person_id) '
|
||||
.'REFERENCES Person (id) '
|
||||
.'NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
}
|
||||
|
||||
$this->addSql('ALTER TABLE chill_event_participation '
|
||||
.'ADD CONSTRAINT FK_4E7768ACD60322AC '
|
||||
.'FOREIGN KEY (role_id) '
|
||||
|
@@ -19,18 +19,19 @@ use Doctrine\Migrations\AbstractMigration;
|
||||
*/
|
||||
final class Version20190110140538 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'switch event date to datetime';
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
|
||||
|
||||
$this->addSql('ALTER TABLE chill_event_event ALTER date TYPE DATE');
|
||||
$this->addSql('ALTER TABLE chill_event_event ALTER date DROP DEFAULT');
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
|
||||
|
||||
$this->addSql('ALTER TABLE chill_event_event ALTER date TYPE TIMESTAMP(0) WITHOUT TIME ZONE');
|
||||
$this->addSql('ALTER TABLE chill_event_event ALTER date DROP DEFAULT');
|
||||
}
|
||||
|
@@ -19,11 +19,13 @@ use Doctrine\Migrations\AbstractMigration;
|
||||
*/
|
||||
final class Version20190115140042 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'add a moderator field to events';
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
|
||||
|
||||
$this->addSql('ALTER TABLE chill_event_event DROP CONSTRAINT FK_FA320FC8D0AFA354');
|
||||
$this->addSql('DROP INDEX IDX_FA320FC8D0AFA354');
|
||||
$this->addSql('ALTER TABLE chill_event_event DROP moderator_id');
|
||||
@@ -31,9 +33,6 @@ final class Version20190115140042 extends AbstractMigration
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
|
||||
|
||||
$this->addSql('ALTER TABLE chill_event_event ADD moderator_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_event_event ADD CONSTRAINT FK_FA320FC8D0AFA354 FOREIGN KEY (moderator_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('CREATE INDEX IDX_FA320FC8D0AFA354 ON chill_event_event (moderator_id)');
|
||||
|
@@ -19,20 +19,19 @@ use Doctrine\Migrations\AbstractMigration;
|
||||
*/
|
||||
final class Version20190201143121 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'fix moderator: relation with user (not person)';
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
|
||||
|
||||
$this->addSql('ALTER TABLE chill_event_event DROP CONSTRAINT fk_fa320fc8d0afa354');
|
||||
$this->addSql('ALTER TABLE chill_event_event ADD CONSTRAINT fk_fa320fc8d0afa354 FOREIGN KEY (moderator_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
|
||||
|
||||
$this->addSql('ALTER TABLE chill_event_event DROP CONSTRAINT FK_FA320FC8D0AFA354');
|
||||
$this->addSql('ALTER TABLE chill_event_event ADD CONSTRAINT FK_FA320FC8D0AFA354 FOREIGN KEY (moderator_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
}
|
||||
|
@@ -0,0 +1,59 @@
|
||||
<?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\Migrations\Event;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20231127134244 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'add creation - update information on event and event participation';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_event_event ADD createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_event_event ADD updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_event_event ADD createdBy_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_event_event ADD updatedBy_id INT DEFAULT NULL');
|
||||
$this->addSql('COMMENT ON COLUMN chill_event_event.createdAt IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('COMMENT ON COLUMN chill_event_event.updatedAt IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('ALTER TABLE chill_event_event ADD CONSTRAINT FK_FA320FC83174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_event_event ADD CONSTRAINT FK_FA320FC865FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('CREATE INDEX IDX_FA320FC83174800F ON chill_event_event (createdBy_id)');
|
||||
$this->addSql('CREATE INDEX IDX_FA320FC865FF1AEC ON chill_event_event (updatedBy_id)');
|
||||
$this->addSql('ALTER TABLE chill_event_participation ADD createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_event_participation ADD updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_event_participation ADD createdBy_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_event_participation ADD updatedBy_id INT DEFAULT NULL');
|
||||
$this->addSql('COMMENT ON COLUMN chill_event_participation.createdAt IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('COMMENT ON COLUMN chill_event_participation.updatedAt IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('ALTER TABLE chill_event_participation ADD CONSTRAINT FK_4E7768AC3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_event_participation ADD CONSTRAINT FK_4E7768AC65FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('CREATE INDEX IDX_4E7768AC3174800F ON chill_event_participation (createdBy_id)');
|
||||
$this->addSql('CREATE INDEX IDX_4E7768AC65FF1AEC ON chill_event_participation (updatedBy_id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_event_event DROP createdAt');
|
||||
$this->addSql('ALTER TABLE chill_event_event DROP updatedAt');
|
||||
$this->addSql('ALTER TABLE chill_event_event DROP createdBy_id');
|
||||
$this->addSql('ALTER TABLE chill_event_event DROP updatedBy_id');
|
||||
$this->addSql('ALTER TABLE chill_event_participation DROP createdAt');
|
||||
$this->addSql('ALTER TABLE chill_event_participation DROP updatedAt');
|
||||
$this->addSql('ALTER TABLE chill_event_participation DROP createdBy_id');
|
||||
$this->addSql('ALTER TABLE chill_event_participation DROP updatedBy_id');
|
||||
}
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
<?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\Migrations\Event;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20231128114959 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add unique index on participation and drop column participation::lastUpdate';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('UPDATE chill_event_participation SET updatedAt=lastupdate WHERE updatedat IS NULL');
|
||||
$this->addSql('ALTER TABLE chill_event_participation DROP lastupdate');
|
||||
$this->addSql('WITH ordering AS (SELECT id, event_id, person_id, rank() OVER (PARTITION BY event_id, person_id ORDER BY id DESC) as ranked FROM chill_event_participation),
|
||||
not_last AS (SELECT * FROM ordering where ranked > 1)
|
||||
DELETE FROM chill_event_participation WHERE id IN (select id FROM not_last)');
|
||||
$this->addSql('CREATE UNIQUE INDEX chill_event_participation_event_person_unique_idx ON chill_event_participation (event_id, person_id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX chill_event_participation_event_person_unique_idx');
|
||||
$this->addSql('ALTER TABLE chill_event_participation ADD lastupdate TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL');
|
||||
$this->addSql('UPDATE chill_event_participation set lastupdate = updatedat');
|
||||
}
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
<?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\Migrations\Event;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20231128122635 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Append more fields on event: location, documents, and comment';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE chill_event_event_documents (event_id INT NOT NULL, storedobject_id INT NOT NULL, PRIMARY KEY(event_id, storedobject_id))');
|
||||
$this->addSql('CREATE INDEX IDX_5C1B638671F7E88B ON chill_event_event_documents (event_id)');
|
||||
$this->addSql('CREATE INDEX IDX_5C1B6386EE684399 ON chill_event_event_documents (storedobject_id)');
|
||||
$this->addSql('ALTER TABLE chill_event_event_documents ADD CONSTRAINT FK_5C1B638671F7E88B FOREIGN KEY (event_id) REFERENCES chill_event_event (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_event_event_documents ADD CONSTRAINT FK_5C1B6386EE684399 FOREIGN KEY (storedobject_id) REFERENCES chill_doc.stored_object (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_event_event ADD location_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_event_event ADD organizationCost NUMERIC(10, 4) DEFAULT 0.0');
|
||||
$this->addSql('ALTER TABLE chill_event_event ADD comment_comment TEXT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_event_event ADD comment_date TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_event_event ADD comment_userId INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_event_event ADD CONSTRAINT FK_FA320FC864D218E FOREIGN KEY (location_id) REFERENCES chill_main_location (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('CREATE INDEX IDX_FA320FC864D218E ON chill_event_event (location_id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_event_event_documents DROP CONSTRAINT FK_5C1B638671F7E88B');
|
||||
$this->addSql('ALTER TABLE chill_event_event_documents DROP CONSTRAINT FK_5C1B6386EE684399');
|
||||
$this->addSql('DROP TABLE chill_event_event_documents');
|
||||
$this->addSql('ALTER TABLE chill_event_event DROP location_id');
|
||||
$this->addSql('ALTER TABLE chill_event_event DROP organizationCost');
|
||||
$this->addSql('ALTER TABLE chill_event_event DROP comment_comment');
|
||||
$this->addSql('ALTER TABLE chill_event_event DROP comment_date');
|
||||
$this->addSql('ALTER TABLE chill_event_event DROP comment_userId');
|
||||
}
|
||||
}
|
@@ -11,3 +11,11 @@ count participations to this event: >-
|
||||
one {Un participant à l'événement}
|
||||
other {# participants à l'événement}
|
||||
}
|
||||
|
||||
events:
|
||||
and_other_count_participants: >-
|
||||
{ count, plural,
|
||||
=0 {Aucun autre participant}
|
||||
one {et un autre participant}
|
||||
other {et # autres participants}
|
||||
}
|
@@ -26,6 +26,8 @@ Event edit: Modifier un événement
|
||||
Edit the event: Modifier l'événement
|
||||
The event was updated: L'événement a été modifié
|
||||
The event was created: L'événement a été créé
|
||||
List of events: Liste des événements
|
||||
Any location for this event: Aucune localisation pour cet événement
|
||||
|
||||
#crud participation
|
||||
Edit all the participations: Modifier toutes les participations
|
||||
@@ -50,6 +52,7 @@ Remove participation: Supprimer la participation
|
||||
Delete event: Supprimer l'événement
|
||||
Are you sure you want to remove that participation ?: Êtes-vous certain de vouloir supprimer cette participation ?
|
||||
Are you sure you want to remove that event ?: Êtes-vous certain de vouloir supprimer cet événement, ainsi que toutes les participations associées ?
|
||||
Any participation for this person: Cet usager ne participe à aucun évenements
|
||||
|
||||
#search
|
||||
Event search: Recherche d'événements
|
||||
@@ -107,3 +110,17 @@ csv: csv
|
||||
Create a new role: Créer un nouveau rôle
|
||||
Create a new type: Créer un nouveau type
|
||||
Create a new status: Créer un nouveau statut
|
||||
|
||||
event:
|
||||
fields:
|
||||
organizationCost: Coût d'organisation
|
||||
location: Localisation
|
||||
documents: Documents
|
||||
form:
|
||||
organisationCost_help: Coût d'organisation pour la structure. Utile pour les statistiques.
|
||||
add_document: Ajouter un document
|
||||
remove_document: Supprimer le document
|
||||
filter:
|
||||
event_types: Par types d'événement
|
||||
event_dates: Par date d'événement
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user