mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-27 17:15:02 +00:00
Compare commits
119 Commits
2.18.0
...
testing-20
Author | SHA1 | Date | |
---|---|---|---|
24eb13f440
|
|||
2b14c132d5
|
|||
ae6355e1e7
|
|||
e96c246ef9
|
|||
d0af191a00
|
|||
95ee573dc5
|
|||
1004e98acd
|
|||
4ed50979bd
|
|||
85b91250fb | |||
8f2409fc06 | |||
ea47d9ff09 | |||
81e46f2b52 | |||
f4bbb1950b | |||
fd48d45872 | |||
92aa9af052 | |||
49aeda86d4
|
|||
cf1df462dc
|
|||
dd62581226
|
|||
b369d94bc3
|
|||
f5879cf275
|
|||
8cc5859a3b
|
|||
e86954143b
|
|||
a0328b9d68
|
|||
813a80d6f9
|
|||
ab95bb157e
|
|||
18fd1dbc4a
|
|||
a35f7656cb
|
|||
ff05f9f48a
|
|||
482c494034
|
|||
81eafde216
|
|||
146f5ac80f
|
|||
5f74682cba
|
|||
49dbd09167 | |||
726f71c8f1 | |||
f03ae2cabc | |||
3a080ebebe | |||
2402050f5f | |||
a97a22d464 | |||
de9251942c | |||
807ffb845a | |||
e0fc87ef58 | |||
e876b75d41 | |||
229cef8942 | |||
6676e06fb5 | |||
6e48f8f7ea
|
|||
e2efb267f5
|
|||
684f1a3015
|
|||
2af9ff7d00
|
|||
ae2265df21
|
|||
6da297d1d2
|
|||
6787612071
|
|||
53d18c7748
|
|||
8bbe094e70
|
|||
df16ca9a60
|
|||
f1df2d5165 | |||
4a58d7f300 | |||
d6b1216021 | |||
dadde29bc2 | |||
693bf65721 | |||
8f3256e46e | |||
f7de5fe1ed | |||
6dd463a7b0 | |||
ed271bed31 | |||
502894ecea | |||
c185c35c44 | |||
e8b8f30e3c | |||
caa2bc1f3c | |||
50a6cb5af6 | |||
13c33567fd | |||
af3d06e7d3 | |||
b74ab2fa0e | |||
001fb269b3
|
|||
262e76c993
|
|||
caf45af4e5 | |||
cea801e620 | |||
19b53e4a4c | |||
09f823ac08 | |||
5be516b14e | |||
eb8dc441b9 | |||
32a103d86a | |||
6d608ab35a | |||
334d357189 | |||
8363c5c3cf | |||
cd793d6842 | |||
3ae8e0c406 | |||
6c93c8b8fa | |||
efdc84930b | |||
6cd6cb1000 | |||
f4c08ee0d7 | |||
b5f7f578da | |||
b172ebdf76 | |||
312a43c093 | |||
e17b4da2a4 | |||
003ca30c74 | |||
88447bbbf8 | |||
1c49eb492a | |||
7bdb5bfce6 | |||
87615d179e | |||
ed2d41c225 | |||
d828a6b9e0 | |||
d6641f70c9 | |||
7b4969e89d | |||
fc22bf1194 | |||
997a6ea419 | |||
a55cd3b7e9 | |||
6b966285a6 | |||
5a400fd162 | |||
01a5c291e0 | |||
4646cd1cf0 | |||
2997dff237 | |||
2624e44e2f | |||
9ec1376d29 | |||
9591f1e49c | |||
ddb90c2e41 | |||
e97571059c | |||
3a6d5fc22a | |||
a542d319f7 | |||
4286a51bf4 | |||
6893c833e4
|
6
.changes/unreleased/Feature-20231218-153151.yaml
Normal file
6
.changes/unreleased/Feature-20231218-153151.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
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"
|
6
.changes/unreleased/Feature-20240122-124849.yaml
Normal file
6
.changes/unreleased/Feature-20240122-124849.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
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"
|
5
.changes/unreleased/Feature-20240129-133319.yaml
Normal file
5
.changes/unreleased/Feature-20240129-133319.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
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"
|
5
.changes/unreleased/Feature-20240207-103951.yaml
Normal file
5
.changes/unreleased/Feature-20240207-103951.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
kind: Feature
|
||||||
|
body: 'Export: group accompanying period by person participating'
|
||||||
|
time: 2024-02-07T10:39:51.97331052+01:00
|
||||||
|
custom:
|
||||||
|
Issue: "253"
|
5
.changes/unreleased/Feature-20240207-114629.yaml
Normal file
5
.changes/unreleased/Feature-20240207-114629.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
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"
|
5
.changes/unreleased/Feature-20240207-164038.yaml
Normal file
5
.changes/unreleased/Feature-20240207-164038.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
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"
|
6
.changes/unreleased/Fixed-20231129-113138.yaml
Normal file
6
.changes/unreleased/Fixed-20231129-113138.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
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"
|
6
.changes/unreleased/Fixed-20240130-140301.yaml
Normal file
6
.changes/unreleased/Fixed-20240130-140301.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
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"
|
@@ -1,15 +0,0 @@
|
|||||||
## 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.
|
|
@@ -1,3 +0,0 @@
|
|||||||
## v2.16.1 - 2024-02-09
|
|
||||||
### Fixed
|
|
||||||
* Force bootstrap version to avoid error in builds with newer version
|
|
@@ -1,3 +0,0 @@
|
|||||||
## v2.16.2 - 2024-02-21
|
|
||||||
### Fixed
|
|
||||||
* Check for null values in closing motive of parcours d'accompagnement for correct rendering of template
|
|
@@ -1,5 +0,0 @@
|
|||||||
## 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
|
|
@@ -1,9 +0,0 @@
|
|||||||
## 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
|
|
@@ -1,5 +0,0 @@
|
|||||||
## 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
|
|
@@ -23,7 +23,3 @@ max_line_length = 0
|
|||||||
indent_size = 2
|
indent_size = 2
|
||||||
indent_style = space
|
indent_style = space
|
||||||
|
|
||||||
[.rst]
|
|
||||||
ident_size = 3
|
|
||||||
ident_style = space
|
|
||||||
|
|
||||||
|
@@ -35,7 +35,7 @@ variables:
|
|||||||
# force a timezone
|
# force a timezone
|
||||||
TZ: Europe/Brussels
|
TZ: Europe/Brussels
|
||||||
# avoid direct deprecations (using symfony phpunit bridge: https://symfony.com/doc/4.x/components/phpunit_bridge.html#internal-deprecations
|
# avoid direct deprecations (using symfony phpunit bridge: https://symfony.com/doc/4.x/components/phpunit_bridge.html#internal-deprecations
|
||||||
SYMFONY_DEPRECATIONS_HELPER: max[total]=99999999&max[self]=0&max[direct]=0&verbose=1
|
SYMFONY_DEPRECATIONS_HELPER: max[total]=99999999&max[self]=0&max[direct]=0&verbose=0
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- Composer install
|
- Composer install
|
||||||
|
46
CHANGELOG.md
46
CHANGELOG.md
@@ -6,52 +6,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
|||||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||||
|
|
||||||
|
|
||||||
## 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
|
## v2.15.2 - 2024-01-11
|
||||||
### Fixed
|
### Fixed
|
||||||
* Fix the id_seq used when creating a new accompanying period participation during fusion of two person files
|
* Fix the id_seq used when creating a new accompanying period participation during fusion of two person files
|
||||||
|
@@ -9,6 +9,7 @@
|
|||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
|
"ext-dom": "*",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-openssl": "*",
|
"ext-openssl": "*",
|
||||||
"ext-redis": "*",
|
"ext-redis": "*",
|
||||||
|
@@ -242,129 +242,3 @@ This is an example of the *filter by birthdate*. This filter asks some informati
|
|||||||
Continue to explain the export framework
|
Continue to explain the export framework
|
||||||
|
|
||||||
.. _main bundle: https://git.framasoft.org/Chill-project/Chill-Main
|
.. _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
|
|
||||||
|
@@ -14,8 +14,8 @@
|
|||||||
"@ckeditor/ckeditor5-vue": "^4.0.1",
|
"@ckeditor/ckeditor5-vue": "^4.0.1",
|
||||||
"@symfony/webpack-encore": "^4.1.0",
|
"@symfony/webpack-encore": "^4.1.0",
|
||||||
"@tsconfig/node14": "^1.0.1",
|
"@tsconfig/node14": "^1.0.1",
|
||||||
|
"@types/dompurify": "^3.0.5",
|
||||||
"bindings": "^1.5.0",
|
"bindings": "^1.5.0",
|
||||||
"bootstrap": "5.2.3",
|
|
||||||
"chokidar": "^3.5.1",
|
"chokidar": "^3.5.1",
|
||||||
"fork-awesome": "^1.1.7",
|
"fork-awesome": "^1.1.7",
|
||||||
"jquery": "^3.6.0",
|
"jquery": "^3.6.0",
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
"webpack-cli": "^5.0.1"
|
"webpack-cli": "^5.0.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bootstrap": "~5.2.0",
|
||||||
"@fullcalendar/core": "^6.1.4",
|
"@fullcalendar/core": "^6.1.4",
|
||||||
"@fullcalendar/daygrid": "^6.1.4",
|
"@fullcalendar/daygrid": "^6.1.4",
|
||||||
"@fullcalendar/interaction": "^6.1.4",
|
"@fullcalendar/interaction": "^6.1.4",
|
||||||
@@ -42,9 +43,11 @@
|
|||||||
"@fullcalendar/vue3": "^6.1.4",
|
"@fullcalendar/vue3": "^6.1.4",
|
||||||
"@popperjs/core": "^2.9.2",
|
"@popperjs/core": "^2.9.2",
|
||||||
"@types/leaflet": "^1.9.3",
|
"@types/leaflet": "^1.9.3",
|
||||||
|
"dompurify": "^3.0.6",
|
||||||
"dropzone": "^5.7.6",
|
"dropzone": "^5.7.6",
|
||||||
"es6-promise": "^4.2.8",
|
"es6-promise": "^4.2.8",
|
||||||
"leaflet": "^1.7.1",
|
"leaflet": "^1.7.1",
|
||||||
|
"marked": "^9.1.5",
|
||||||
"masonry-layout": "^4.2.2",
|
"masonry-layout": "^4.2.2",
|
||||||
"mime": "^3.0.0",
|
"mime": "^3.0.0",
|
||||||
"swagger-ui": "^4.15.5",
|
"swagger-ui": "^4.15.5",
|
||||||
|
@@ -80,7 +80,7 @@ final readonly class CreatorJobFilter implements FilterInterface
|
|||||||
{
|
{
|
||||||
$builder
|
$builder
|
||||||
->add('jobs', EntityType::class, [
|
->add('jobs', EntityType::class, [
|
||||||
'choices' => $this->userJobRepository->findAllActive(),
|
'choices' => $this->userJobRepository->findAllOrderedByName(),
|
||||||
'class' => UserJob::class,
|
'class' => UserJob::class,
|
||||||
'choice_label' => fn (UserJob $s) => $this->translatableStringHelper->localize(
|
'choice_label' => fn (UserJob $s) => $this->translatableStringHelper->localize(
|
||||||
$s->getLabel()
|
$s->getLabel()
|
||||||
|
@@ -15,7 +15,6 @@ use Chill\ActivityBundle\Export\Declarations;
|
|||||||
use Chill\MainBundle\Entity\Scope;
|
use Chill\MainBundle\Entity\Scope;
|
||||||
use Chill\MainBundle\Entity\User\UserScopeHistory;
|
use Chill\MainBundle\Entity\User\UserScopeHistory;
|
||||||
use Chill\MainBundle\Export\FilterInterface;
|
use Chill\MainBundle\Export\FilterInterface;
|
||||||
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
|
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||||
use Doctrine\ORM\Query\Expr\Join;
|
use Doctrine\ORM\Query\Expr\Join;
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
@@ -27,8 +26,7 @@ class CreatorScopeFilter implements FilterInterface
|
|||||||
private const PREFIX = 'acp_act_filter_creator_scope';
|
private const PREFIX = 'acp_act_filter_creator_scope';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly TranslatableStringHelper $translatableStringHelper,
|
private readonly TranslatableStringHelper $translatableStringHelper
|
||||||
private readonly ScopeRepositoryInterface $scopeRepository,
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +76,6 @@ class CreatorScopeFilter implements FilterInterface
|
|||||||
$builder
|
$builder
|
||||||
->add('scopes', EntityType::class, [
|
->add('scopes', EntityType::class, [
|
||||||
'class' => Scope::class,
|
'class' => Scope::class,
|
||||||
'choices' => $this->scopeRepository->findAllActive(),
|
|
||||||
'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize(
|
'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize(
|
||||||
$s->getName()
|
$s->getName()
|
||||||
),
|
),
|
||||||
|
@@ -16,7 +16,6 @@ use Chill\ActivityBundle\Export\Declarations;
|
|||||||
use Chill\MainBundle\Entity\User\UserJobHistory;
|
use Chill\MainBundle\Entity\User\UserJobHistory;
|
||||||
use Chill\MainBundle\Entity\UserJob;
|
use Chill\MainBundle\Entity\UserJob;
|
||||||
use Chill\MainBundle\Export\FilterInterface;
|
use Chill\MainBundle\Export\FilterInterface;
|
||||||
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
|
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
@@ -28,8 +27,7 @@ class UsersJobFilter implements FilterInterface
|
|||||||
private const PREFIX = 'act_filter_user_job';
|
private const PREFIX = 'act_filter_user_job';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly TranslatableStringHelperInterface $translatableStringHelper,
|
private readonly TranslatableStringHelperInterface $translatableStringHelper
|
||||||
private readonly UserJobRepositoryInterface $userJobRepository
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +69,6 @@ class UsersJobFilter implements FilterInterface
|
|||||||
$builder
|
$builder
|
||||||
->add('jobs', EntityType::class, [
|
->add('jobs', EntityType::class, [
|
||||||
'class' => UserJob::class,
|
'class' => UserJob::class,
|
||||||
'choices' => $this->userJobRepository->findAllActive(),
|
|
||||||
'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()),
|
'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()),
|
||||||
'multiple' => true,
|
'multiple' => true,
|
||||||
'expanded' => true,
|
'expanded' => true,
|
||||||
|
@@ -95,7 +95,7 @@ class ActivityType extends AbstractType
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var AccompanyingPeriod|null $accompanyingPeriod */
|
/** @var \Chill\PersonBundle\Entity\AccompanyingPeriod|null $accompanyingPeriod */
|
||||||
$accompanyingPeriod = null;
|
$accompanyingPeriod = null;
|
||||||
|
|
||||||
if ($options['accompanyingPeriod'] instanceof AccompanyingPeriod) {
|
if ($options['accompanyingPeriod'] instanceof AccompanyingPeriod) {
|
||||||
|
@@ -243,8 +243,7 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos
|
|||||||
thirdparties.thirdpartyids,
|
thirdparties.thirdpartyids,
|
||||||
persons.personids,
|
persons.personids,
|
||||||
actions.socialactionids,
|
actions.socialactionids,
|
||||||
issues.socialissueids,
|
issues.socialissueids
|
||||||
a.user_id
|
|
||||||
|
|
||||||
FROM activity a
|
FROM activity a
|
||||||
LEFT JOIN chill_main_location location ON a.location_id = location.id
|
LEFT JOIN chill_main_location location ON a.location_id = location.id
|
||||||
@@ -284,7 +283,6 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos
|
|||||||
->addJoinedEntityResult(ActivityPresence::class, 'activityPresence', 'a', 'attendee')
|
->addJoinedEntityResult(ActivityPresence::class, 'activityPresence', 'a', 'attendee')
|
||||||
->addFieldResult('activityPresence', 'presence_id', 'id')
|
->addFieldResult('activityPresence', 'presence_id', 'id')
|
||||||
->addFieldResult('activityPresence', 'presence_name', 'name')
|
->addFieldResult('activityPresence', 'presence_name', 'name')
|
||||||
->addScalarResult('user_id', 'userId', Types::INTEGER)
|
|
||||||
|
|
||||||
// results which cannot be mapped into entity
|
// results which cannot be mapped into entity
|
||||||
->addScalarResult('comment_comment', 'comment', Types::TEXT)
|
->addScalarResult('comment_comment', 'comment', Types::TEXT)
|
||||||
|
@@ -11,7 +11,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\ActivityBundle\Service\DocGenerator;
|
namespace Chill\ActivityBundle\Service\DocGenerator;
|
||||||
|
|
||||||
use Chill\ActivityBundle\Entity\Activity;
|
|
||||||
use Chill\ActivityBundle\Entity\ActivityPresence;
|
use Chill\ActivityBundle\Entity\ActivityPresence;
|
||||||
use Chill\ActivityBundle\Entity\ActivityType;
|
use Chill\ActivityBundle\Entity\ActivityType;
|
||||||
use Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface;
|
use Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface;
|
||||||
@@ -113,7 +112,7 @@ class ListActivitiesByAccompanyingPeriodContext implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return list<Activity>
|
* @return list
|
||||||
*/
|
*/
|
||||||
private function filterActivitiesByUser(array $activities, User $user): array
|
private function filterActivitiesByUser(array $activities, User $user): array
|
||||||
{
|
{
|
||||||
@@ -121,12 +120,6 @@ class ListActivitiesByAccompanyingPeriodContext implements
|
|||||||
array_filter(
|
array_filter(
|
||||||
$activities,
|
$activities,
|
||||||
function ($activity) use ($user) {
|
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'] ?? []);
|
$activityUsernames = array_map(static fn ($user) => $user['username'], $activity['users'] ?? []);
|
||||||
|
|
||||||
return \in_array($user->getUsername(), $activityUsernames, true);
|
return \in_array($user->getUsername(), $activityUsernames, true);
|
||||||
@@ -136,7 +129,7 @@ class ListActivitiesByAccompanyingPeriodContext implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return list<AccompanyingPeriod\AccompanyingPeriodWork>
|
* @return list
|
||||||
*/
|
*/
|
||||||
private function filterWorksByUser(array $works, User $user): array
|
private function filterWorksByUser(array $works, User $user): array
|
||||||
{
|
{
|
||||||
@@ -223,15 +216,6 @@ class ListActivitiesByAccompanyingPeriodContext implements
|
|||||||
foreach ($activities as $row) {
|
foreach ($activities as $row) {
|
||||||
$activity = $row[0];
|
$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', [
|
$activity['date'] = $this->normalizer->normalize($activity['date'], 'docgen', [
|
||||||
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => \DateTime::class,
|
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => \DateTime::class,
|
||||||
]);
|
]);
|
||||||
|
@@ -91,29 +91,6 @@ class ActivityACLAwareRepositoryTest extends KernelTestCase
|
|||||||
self::assertIsArray($actual);
|
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
|
* @dataProvider provideDataFindByAccompanyingPeriod
|
||||||
*/
|
*/
|
||||||
@@ -324,10 +301,7 @@ class ActivityACLAwareRepositoryTest extends KernelTestCase
|
|||||||
->getQuery()
|
->getQuery()
|
||||||
->getResult()
|
->getResult()
|
||||||
) {
|
) {
|
||||||
$job = new UserJob();
|
throw new \RuntimeException('no jobs found');
|
||||||
$job->setLabel(['fr' => 'test']);
|
|
||||||
$this->entityManager->persist($job);
|
|
||||||
$this->entityManager->flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null === $user = $this->entityManager
|
if (null === $user = $this->entityManager
|
||||||
|
@@ -1,139 +0,0 @@
|
|||||||
<?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:
|
by_creator_job:
|
||||||
job_form_label: Métiers
|
job_form_label: Métiers
|
||||||
Filter activity by user job: Filtrer les échanges par métier du créateur de l'échange
|
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 métier du créateur de l'échange: uniquement %jobs%"
|
'Filtered activity by user job: only %jobs%': "Filtré par service du créateur de l'échange: uniquement %jobs%"
|
||||||
by_persons:
|
by_persons:
|
||||||
Filter activity by persons: Filtrer les échanges par usager participant
|
Filter activity by persons: Filtrer les échanges par usager participant
|
||||||
'Filtered activity by persons: only %persons%': 'Échanges filtrés par usagers participants: seulement %persons%'
|
'Filtered activity by persons: only %persons%': 'Échanges filtrés par usagers participants: seulement %persons%'
|
||||||
|
@@ -16,7 +16,6 @@ use Chill\AsideActivityBundle\Export\Declarations;
|
|||||||
use Chill\MainBundle\Entity\User\UserJobHistory;
|
use Chill\MainBundle\Entity\User\UserJobHistory;
|
||||||
use Chill\MainBundle\Entity\UserJob;
|
use Chill\MainBundle\Entity\UserJob;
|
||||||
use Chill\MainBundle\Export\FilterInterface;
|
use Chill\MainBundle\Export\FilterInterface;
|
||||||
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
|
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
@@ -28,8 +27,7 @@ class ByUserJobFilter implements FilterInterface
|
|||||||
private const PREFIX = 'aside_act_filter_user_job';
|
private const PREFIX = 'aside_act_filter_user_job';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly TranslatableStringHelperInterface $translatableStringHelper,
|
private readonly TranslatableStringHelperInterface $translatableStringHelper
|
||||||
private readonly UserJobRepositoryInterface $userJobRepository
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +69,6 @@ class ByUserJobFilter implements FilterInterface
|
|||||||
$builder
|
$builder
|
||||||
->add('jobs', EntityType::class, [
|
->add('jobs', EntityType::class, [
|
||||||
'class' => UserJob::class,
|
'class' => UserJob::class,
|
||||||
'choices' => $this->userJobRepository->findAllActive(),
|
|
||||||
'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()),
|
'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()),
|
||||||
'multiple' => true,
|
'multiple' => true,
|
||||||
'expanded' => true,
|
'expanded' => true,
|
||||||
|
@@ -0,0 +1,66 @@
|
|||||||
|
<?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\Tests\Security\Guard;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
|
use Chill\DocStoreBundle\Security\Guard\DavTokenAuthenticationEventSubscriber;
|
||||||
|
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTAuthenticatedEvent;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class DavTokenAuthenticationEventSubscriberTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testOnJWTAuthenticatedWithDavDataInPayload(): void
|
||||||
|
{
|
||||||
|
$eventSubscriber = new DavTokenAuthenticationEventSubscriber();
|
||||||
|
$token = new class () extends AbstractToken {
|
||||||
|
public function getCredentials()
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$event = new JWTAuthenticatedEvent([
|
||||||
|
'dav' => 1,
|
||||||
|
'so' => '1234',
|
||||||
|
'e' => 1,
|
||||||
|
], $token);
|
||||||
|
|
||||||
|
$eventSubscriber->onJWTAuthenticated($event);
|
||||||
|
|
||||||
|
self::assertTrue($token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT));
|
||||||
|
self::assertTrue($token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS));
|
||||||
|
self::assertEquals('1234', $token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT));
|
||||||
|
self::assertEquals(StoredObjectRoleEnum::EDIT, $token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOnJWTAuthenticatedWithDavNoDataInPayload(): void
|
||||||
|
{
|
||||||
|
$eventSubscriber = new DavTokenAuthenticationEventSubscriber();
|
||||||
|
$token = new class () extends AbstractToken {
|
||||||
|
public function getCredentials()
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$event = new JWTAuthenticatedEvent([], $token);
|
||||||
|
|
||||||
|
$eventSubscriber->onJWTAuthenticated($event);
|
||||||
|
|
||||||
|
self::assertFalse($token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT));
|
||||||
|
self::assertFalse($token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS));
|
||||||
|
}
|
||||||
|
}
|
@@ -15,7 +15,6 @@ use Chill\CalendarBundle\Export\Declarations;
|
|||||||
use Chill\MainBundle\Entity\User\UserJobHistory;
|
use Chill\MainBundle\Entity\User\UserJobHistory;
|
||||||
use Chill\MainBundle\Entity\UserJob;
|
use Chill\MainBundle\Entity\UserJob;
|
||||||
use Chill\MainBundle\Export\FilterInterface;
|
use Chill\MainBundle\Export\FilterInterface;
|
||||||
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
|
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||||
use Doctrine\ORM\Query\Expr\Join;
|
use Doctrine\ORM\Query\Expr\Join;
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
@@ -27,8 +26,7 @@ final readonly class JobFilter implements FilterInterface
|
|||||||
private const PREFIX = 'cal_filter_job';
|
private const PREFIX = 'cal_filter_job';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private TranslatableStringHelper $translatableStringHelper,
|
private TranslatableStringHelper $translatableStringHelper
|
||||||
private UserJobRepositoryInterface $userJobRepository
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +74,6 @@ final readonly class JobFilter implements FilterInterface
|
|||||||
$builder
|
$builder
|
||||||
->add('job', EntityType::class, [
|
->add('job', EntityType::class, [
|
||||||
'class' => UserJob::class,
|
'class' => UserJob::class,
|
||||||
'choices' => $this->userJobRepository->findAllActive(),
|
|
||||||
'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize(
|
'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize(
|
||||||
$j->getLabel()
|
$j->getLabel()
|
||||||
),
|
),
|
||||||
|
@@ -15,7 +15,6 @@ use Chill\CalendarBundle\Export\Declarations;
|
|||||||
use Chill\MainBundle\Entity\Scope;
|
use Chill\MainBundle\Entity\Scope;
|
||||||
use Chill\MainBundle\Entity\User\UserScopeHistory;
|
use Chill\MainBundle\Entity\User\UserScopeHistory;
|
||||||
use Chill\MainBundle\Export\FilterInterface;
|
use Chill\MainBundle\Export\FilterInterface;
|
||||||
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
|
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||||
use Doctrine\ORM\Query\Expr\Join;
|
use Doctrine\ORM\Query\Expr\Join;
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
@@ -29,8 +28,7 @@ class ScopeFilter implements FilterInterface
|
|||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected TranslatorInterface $translator,
|
protected TranslatorInterface $translator,
|
||||||
private readonly TranslatableStringHelper $translatableStringHelper,
|
private readonly TranslatableStringHelper $translatableStringHelper
|
||||||
private readonly ScopeRepositoryInterface $scopeRepository
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +76,6 @@ class ScopeFilter implements FilterInterface
|
|||||||
$builder
|
$builder
|
||||||
->add('scope', EntityType::class, [
|
->add('scope', EntityType::class, [
|
||||||
'class' => Scope::class,
|
'class' => Scope::class,
|
||||||
'choices' => $this->scopeRepository->findAllActive(),
|
|
||||||
'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize(
|
'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize(
|
||||||
$s->getName()
|
$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
|
* @throw UserAbsenceSyncException when the data cannot be reached or is not valid from microsoft
|
||||||
*/
|
*/
|
||||||
public function isUserAbsent(User $user): ?bool
|
public function isUserAbsent(User $user): bool|null
|
||||||
{
|
{
|
||||||
$id = $this->mapCalendarToUser->getUserId($user);
|
$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
|
* @throw UserAbsenceSyncException when the data cannot be reached or is not valid from microsoft
|
||||||
*/
|
*/
|
||||||
public function isUserAbsent(User $user): ?bool;
|
public function isUserAbsent(User $user): bool|null;
|
||||||
}
|
}
|
||||||
|
@@ -16,42 +16,29 @@ use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface;
|
|||||||
use Chill\DocGeneratorBundle\Context\Exception\ContextNotFoundException;
|
use Chill\DocGeneratorBundle\Context\Exception\ContextNotFoundException;
|
||||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||||
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
|
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
|
||||||
|
use Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface;
|
||||||
use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage;
|
use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Chill\MainBundle\Entity\User;
|
|
||||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
|
||||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||||
use Chill\MainBundle\Serializer\Model\Collection;
|
use Chill\MainBundle\Serializer\Model\Collection;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
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\CheckboxType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
// TODO à mettre dans services
|
// TODO à mettre dans services
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
|
||||||
use Symfony\Component\Validator\Constraints\NotNull;
|
|
||||||
|
|
||||||
final class DocGeneratorTemplateController extends AbstractController
|
final class DocGeneratorTemplateController extends AbstractController
|
||||||
{
|
{
|
||||||
public function __construct(
|
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)
|
||||||
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,
|
|
||||||
) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -176,7 +163,9 @@ final class DocGeneratorTemplateController extends AbstractController
|
|||||||
throw new NotFoundHttpException(sprintf('Entity with classname %s and id %s is not found', $context->getEntityClass(), $entityId));
|
throw new NotFoundHttpException(sprintf('Entity with classname %s and id %s is not found', $context->getEntityClass(), $entityId));
|
||||||
}
|
}
|
||||||
|
|
||||||
$contextGenerationData = [];
|
$contextGenerationData = [
|
||||||
|
'test_file' => null,
|
||||||
|
];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
$context instanceof DocGeneratorContextWithPublicFormInterface
|
$context instanceof DocGeneratorContextWithPublicFormInterface
|
||||||
@@ -186,39 +175,25 @@ final class DocGeneratorTemplateController extends AbstractController
|
|||||||
$builder = $this->createFormBuilder(
|
$builder = $this->createFormBuilder(
|
||||||
array_merge(
|
array_merge(
|
||||||
$context->getFormData($template, $entity),
|
$context->getFormData($template, $entity),
|
||||||
$isTest ? ['creator' => null, 'dump_only' => false, 'send_result_to' => ''] : []
|
$isTest ? ['test_file' => null, 'show_data' => false] : []
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
$context->buildPublicForm($builder, $template, $entity);
|
$context->buildPublicForm($builder, $template, $entity);
|
||||||
} else {
|
} else {
|
||||||
$builder = $this->createFormBuilder(
|
$builder = $this->createFormBuilder(
|
||||||
['creator' => null, 'show_data' => false, 'send_result_to' => '']
|
['test_file' => null, 'show_data' => false]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($isTest) {
|
if ($isTest) {
|
||||||
$builder->add('dump_only', CheckboxType::class, [
|
$builder->add('test_file', FileType::class, [
|
||||||
'label' => 'docgen.Show data instead of generating',
|
'label' => 'Template file',
|
||||||
'required' => false,
|
'required' => false,
|
||||||
]);
|
]);
|
||||||
$builder->add('send_result_to', EmailType::class, [
|
$builder->add('show_data', CheckboxType::class, [
|
||||||
'label' => 'docgen.Send report to',
|
'label' => 'Show data instead of generating',
|
||||||
'help' => 'docgen.Send report errors to this email address',
|
'required' => false,
|
||||||
'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(),
|
|
||||||
],
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,10 +204,8 @@ final class DocGeneratorTemplateController extends AbstractController
|
|||||||
} elseif (!$form->isSubmitted() || ($form->isSubmitted() && !$form->isValid())) {
|
} elseif (!$form->isSubmitted() || ($form->isSubmitted() && !$form->isValid())) {
|
||||||
$templatePath = '@ChillDocGenerator/Generator/basic_form.html.twig';
|
$templatePath = '@ChillDocGenerator/Generator/basic_form.html.twig';
|
||||||
$templateOptions = [
|
$templateOptions = [
|
||||||
'entity' => $entity,
|
'entity' => $entity, 'form' => $form->createView(),
|
||||||
'form' => $form->createView(),
|
'template' => $template, 'context' => $context,
|
||||||
'template' => $template,
|
|
||||||
'context' => $context,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return $this->render($templatePath, $templateOptions);
|
return $this->render($templatePath, $templateOptions);
|
||||||
@@ -245,57 +218,60 @@ final class DocGeneratorTemplateController extends AbstractController
|
|||||||
$context->contextGenerationDataNormalize($template, $entity, $contextGenerationData)
|
$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
|
// we prepare the object to store the document
|
||||||
$storedObject = (new StoredObject())
|
$storedObject = (new StoredObject())
|
||||||
->setStatus(StoredObject::STATUS_PENDING)
|
->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);
|
$this->entityManager->persist($storedObject);
|
||||||
|
|
||||||
// we store the generated document (associate with the original entity, etc.)
|
// we store the generated document
|
||||||
// but only if this is not a test
|
$context
|
||||||
if (!$isTest) {
|
->storeGenerated(
|
||||||
$context
|
$template,
|
||||||
->storeGenerated(
|
$storedObject,
|
||||||
$template,
|
$entity,
|
||||||
$storedObject,
|
$contextGenerationData
|
||||||
$entity,
|
);
|
||||||
$contextGenerationData
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->entityManager->flush();
|
$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(
|
$this->messageBus->dispatch(
|
||||||
new RequestGenerationMessage(
|
new RequestGenerationMessage(
|
||||||
$creator,
|
$this->getUser(),
|
||||||
$template,
|
$template,
|
||||||
$entityId,
|
$entityId,
|
||||||
$storedObject,
|
$storedObject,
|
||||||
$contextGenerationDataSanitized,
|
$contextGenerationDataSanitized,
|
||||||
$isTest,
|
|
||||||
$sendResultTo,
|
|
||||||
$dumpOnly,
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -69,7 +69,7 @@ class DocGeneratorTemplate
|
|||||||
*
|
*
|
||||||
* @Serializer\Groups({"read"})
|
* @Serializer\Groups({"read"})
|
||||||
*/
|
*/
|
||||||
private ?int $id = null;
|
private int $id;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\Column(type="json")
|
* @ORM\Column(type="json")
|
||||||
|
@@ -14,9 +14,10 @@ namespace Chill\DocGeneratorBundle\Repository;
|
|||||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
use Symfony\Component\HttpFoundation\RequestStack;
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
|
||||||
final class DocGeneratorTemplateRepository implements DocGeneratorTemplateRepositoryInterface
|
final class DocGeneratorTemplateRepository implements ObjectRepository
|
||||||
{
|
{
|
||||||
private readonly EntityRepository $repository;
|
private readonly EntityRepository $repository;
|
||||||
|
|
||||||
|
@@ -1,23 +0,0 @@
|
|||||||
<?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,16 +1,5 @@
|
|||||||
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
|
{% 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 %}
|
{% block admin_content %}
|
||||||
{% embed '@ChillMain/CRUD/_index.html.twig' %}
|
{% embed '@ChillMain/CRUD/_index.html.twig' %}
|
||||||
{% block table_entities_thead_tr %}
|
{% block table_entities_thead_tr %}
|
||||||
@@ -22,47 +11,6 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block table_entities_tbody %}
|
{% block table_entities_tbody %}
|
||||||
{% 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 %}
|
|
||||||
|
|
||||||
|
|
||||||
{% for entity in entities %}
|
{% for entity in entities %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ entity.id }}</td>
|
<td>{{ entity.id }}</td>
|
||||||
@@ -70,7 +18,7 @@
|
|||||||
<td>{{ contextManager.getContextByKey(entity.context).name|trans }}</td>
|
<td>{{ contextManager.getContextByKey(entity.context).name|trans }}</td>
|
||||||
<td>
|
<td>
|
||||||
<form method="get" action="{{ path('chill_docgenerator_test_generate_redirect') }}">
|
<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="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="template" value="{{ entity.id|e('html_attr') }}" />
|
||||||
<input type="hidden" name="entityClassName" value="{{ contextManager.getContextByKey(entity.context).entityClass|e('html_attr') }}" />
|
<input type="hidden" name="entityClassName" value="{{ contextManager.getContextByKey(entity.context).entityClass|e('html_attr') }}" />
|
||||||
<input type="text" name="entityId" />
|
<input type="text" name="entityId" />
|
||||||
@@ -79,14 +27,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<ul class="record_actions">
|
<a href="{{ chill_path_add_return_path('chill_crud_docgen_template_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'Edit'|trans }}"></a>
|
||||||
<li>
|
|
||||||
{{ entity.file|chill_document_button_group('Template file', true, {small: 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>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@@ -6,20 +6,18 @@
|
|||||||
<div class="col-md-10 col-xxl">
|
<div class="col-md-10 col-xxl">
|
||||||
|
|
||||||
<h1>{{ block('title') }}</h1>
|
<h1>{{ block('title') }}</h1>
|
||||||
<div class="container overflow-hidden">
|
<div class="container">
|
||||||
{% for key, context in contexts %}
|
{% for key, context in contexts %}
|
||||||
<div class="row g-3" style="margin-top: 1rem;">
|
<div class="row">
|
||||||
<div class="col-4 offset-1 text-center">
|
<div class="col-md-4">
|
||||||
<a
|
<a
|
||||||
href="{{ path('chill_crud_docgen_template_new', { 'context': key }) }}"
|
href="{{ path('chill_crud_docgen_template_new', { 'context': key }) }}"
|
||||||
class="btn btn-outline-chill-green-dark">
|
class="btn btn-outline-chill-green-dark">
|
||||||
{{ context.name|trans }}
|
{{ context.name|trans }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col-md-8">
|
||||||
<div>
|
{{ context.description|trans|nl2br }}
|
||||||
{{ context.description|trans|nl2br }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{% if creator is not same as null %}{{ creator.label }},{% endif %}
|
{{ creator.label }},
|
||||||
|
|
||||||
{{ '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 }}
|
{{ 'docgen.failure_email.Forward this email to your administrator for solving'|trans }}
|
||||||
|
|
||||||
|
@@ -1,7 +0,0 @@
|
|||||||
{{ '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,88 +17,54 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
|||||||
use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface;
|
use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface;
|
||||||
use Chill\DocGeneratorBundle\GeneratorDriver\Exception\TemplateException;
|
use Chill\DocGeneratorBundle\GeneratorDriver\Exception\TemplateException;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
|
||||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Doctrine\Persistence\ManagerRegistry;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\Yaml\Yaml;
|
use Symfony\Component\HttpFoundation\File\File;
|
||||||
|
|
||||||
class Generator implements GeneratorInterface
|
class Generator implements GeneratorInterface
|
||||||
{
|
{
|
||||||
private const LOG_PREFIX = '[docgen generator] ';
|
private const LOG_PREFIX = '[docgen generator] ';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(private readonly ContextManagerInterface $contextManager, private readonly DriverInterface $driver, private readonly EntityManagerInterface $entityManager, private readonly LoggerInterface $logger, private readonly StoredObjectManagerInterface $storedObjectManager)
|
||||||
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(
|
public function generateDocFromTemplate(
|
||||||
DocGeneratorTemplate $template,
|
DocGeneratorTemplate $template,
|
||||||
int $entityId,
|
int $entityId,
|
||||||
array $contextGenerationDataNormalized,
|
array $contextGenerationDataNormalized,
|
||||||
StoredObject $destinationStoredObject,
|
?StoredObject $destinationStoredObject = null,
|
||||||
User $creator,
|
bool $isTest = false,
|
||||||
bool $clearEntityManagerDuringProcess = true,
|
?File $testFile = null,
|
||||||
): StoredObject {
|
?User $creator = null
|
||||||
return $this->generateFromTemplate(
|
): ?string {
|
||||||
$template,
|
if ($destinationStoredObject instanceof StoredObject && StoredObject::STATUS_PENDING !== $destinationStoredObject->getStatus()) {
|
||||||
$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');
|
$this->logger->info(self::LOG_PREFIX.'Aborting generation of an already generated document');
|
||||||
throw new ObjectReadyException();
|
throw new ObjectReadyException();
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->logger->info(self::LOG_PREFIX.'Starting generation of a document', [
|
$this->logger->info(self::LOG_PREFIX.'Starting generation of a document', [
|
||||||
'entity_id' => $entityId,
|
'entity_id' => $entityId,
|
||||||
'destination_stored_object' => $destinationStoredObject->getId(),
|
'destination_stored_object' => null === $destinationStoredObject ? null : $destinationStoredObject->getId(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$context = $this->contextManager->getContextByDocGeneratorTemplate($template);
|
$context = $this->contextManager->getContextByDocGeneratorTemplate($template);
|
||||||
|
|
||||||
$entity = $this
|
$entity = $this
|
||||||
->objectManagerRegistry
|
->entityManager
|
||||||
->getManagerForClass($context->getEntityClass())
|
|
||||||
->find($context->getEntityClass(), $entityId)
|
->find($context->getEntityClass(), $entityId)
|
||||||
;
|
;
|
||||||
|
|
||||||
@@ -116,47 +82,17 @@ class Generator implements GeneratorInterface
|
|||||||
|
|
||||||
$data = $context->getData($template, $entity, $contextGenerationDataNormalized);
|
$data = $context->getData($template, $entity, $contextGenerationDataNormalized);
|
||||||
|
|
||||||
$destinationStoredObjectId = $destinationStoredObject->getId();
|
$destinationStoredObjectId = $destinationStoredObject instanceof StoredObject ? $destinationStoredObject->getId() : null;
|
||||||
|
$this->entityManager->clear();
|
||||||
if ($clearEntityManagerDuringProcess) {
|
gc_collect_cycles();
|
||||||
// we clean the entity manager
|
if (null !== $destinationStoredObjectId) {
|
||||||
$this->objectManagerRegistry->getManagerForClass($context->getEntityClass())?->clear();
|
$destinationStoredObject = $this->entityManager->find(StoredObject::class, $destinationStoredObjectId);
|
||||||
|
|
||||||
// this will force php to clean the memory
|
|
||||||
gc_collect_cycles();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// as we potentially deleted the storedObject from memory, we have to restore it
|
if ($isTest && ($testFile instanceof File)) {
|
||||||
$destinationStoredObject = $this->objectManagerRegistry
|
$templateDecrypted = file_get_contents($testFile->getPathname());
|
||||||
->getManagerForClass(StoredObject::class)
|
} else {
|
||||||
->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());
|
$templateDecrypted = $this->storedObjectManager->read($template->getFile());
|
||||||
} catch (StoredObjectManagerException $e) {
|
|
||||||
$destinationStoredObject->addGenerationErrors($e->getMessage());
|
|
||||||
|
|
||||||
throw new GeneratorException([$e->getMessage()], $e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -169,10 +105,19 @@ class Generator implements GeneratorInterface
|
|||||||
$template->getFile()->getFilename()
|
$template->getFile()->getFilename()
|
||||||
);
|
);
|
||||||
} catch (TemplateException $e) {
|
} catch (TemplateException $e) {
|
||||||
$destinationStoredObject->addGenerationErrors(implode("\n", $e->getErrors()));
|
|
||||||
throw new GeneratorException($e->getErrors(), $e);
|
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 */
|
/* @var StoredObject $destinationStoredObject */
|
||||||
$destinationStoredObject
|
$destinationStoredObject
|
||||||
->setType($template->getFile()->getType())
|
->setType($template->getFile()->getType())
|
||||||
@@ -180,19 +125,15 @@ class Generator implements GeneratorInterface
|
|||||||
->setStatus(StoredObject::STATUS_READY)
|
->setStatus(StoredObject::STATUS_READY)
|
||||||
;
|
;
|
||||||
|
|
||||||
try {
|
$this->storedObjectManager->write($destinationStoredObject, $generatedResource);
|
||||||
$this->storedObjectManager->write($destinationStoredObject, $generatedResource);
|
|
||||||
} catch (StoredObjectManagerException $e) {
|
|
||||||
$destinationStoredObject->addGenerationErrors($e->getMessage());
|
|
||||||
|
|
||||||
throw new GeneratorException([$e->getMessage()], $e);
|
$this->entityManager->flush();
|
||||||
}
|
|
||||||
|
|
||||||
$this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [
|
$this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [
|
||||||
'entity_id' => $entityId,
|
'entity_id' => $entityId,
|
||||||
'destination_stored_object' => $destinationStoredObject->getId(),
|
'destination_stored_object' => $destinationStoredObject->getId(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $destinationStoredObject;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -13,48 +13,29 @@ namespace Chill\DocGeneratorBundle\Service\Generator;
|
|||||||
|
|
||||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Symfony\Component\HttpFoundation\File\File;
|
||||||
|
|
||||||
interface GeneratorInterface
|
interface GeneratorInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Generate a document and store the document on disk.
|
* @template T of File|null
|
||||||
|
* @template B of bool
|
||||||
*
|
*
|
||||||
* The given $destinationStoredObject will be updated with filename, status, and eventually errors will be stored
|
* @param B $isTest
|
||||||
* into the object. The number of generation trial will also be incremented.
|
* @param (B is true ? T : null) $testFile
|
||||||
*
|
*
|
||||||
* This process requires a huge amount of data. For this reason, the entity manager will be cleaned during the process,
|
* @psalm-return (B is true ? string : null)
|
||||||
* unless the paarameter `$clearEntityManagerDuringProcess` is set on false.
|
|
||||||
*
|
*
|
||||||
* As the entity manager might be cleaned, the new instance of the stored object will be returned by this method.
|
* @throws \Symfony\Component\Serializer\Exception\ExceptionInterface|\Throwable
|
||||||
*
|
|
||||||
* 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(
|
public function generateDocFromTemplate(
|
||||||
DocGeneratorTemplate $template,
|
DocGeneratorTemplate $template,
|
||||||
int $entityId,
|
int $entityId,
|
||||||
array $contextGenerationDataNormalized,
|
array $contextGenerationDataNormalized,
|
||||||
StoredObject $destinationStoredObject,
|
?StoredObject $destinationStoredObject = null,
|
||||||
User $creator,
|
bool $isTest = false,
|
||||||
bool $clearEntityManagerDuringProcess = true,
|
?File $testFile = null,
|
||||||
): StoredObject;
|
?User $creator = null
|
||||||
|
): ?string;
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
}
|
||||||
|
@@ -1,64 +0,0 @@
|
|||||||
<?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,11 +11,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\DocGeneratorBundle\Service\Messenger;
|
namespace Chill\DocGeneratorBundle\Service\Messenger;
|
||||||
|
|
||||||
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface;
|
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
|
||||||
use Chill\DocGeneratorBundle\Service\Generator\GeneratorException;
|
use Chill\DocGeneratorBundle\Service\Generator\GeneratorException;
|
||||||
use Chill\DocGeneratorBundle\tests\Service\Messenger\OnGenerationFailsTest;
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
|
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
|
||||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
@@ -25,22 +24,12 @@ use Symfony\Component\Mailer\MailerInterface;
|
|||||||
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
/**
|
|
||||||
* @see OnGenerationFailsTest for test suite
|
|
||||||
*/
|
|
||||||
final readonly class OnGenerationFails implements EventSubscriberInterface
|
final readonly class OnGenerationFails implements EventSubscriberInterface
|
||||||
{
|
{
|
||||||
public const LOG_PREFIX = '[docgen failed] ';
|
public const LOG_PREFIX = '[docgen failed] ';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(private DocGeneratorTemplateRepository $docGeneratorTemplateRepository, private EntityManagerInterface $entityManager, private LoggerInterface $logger, private MailerInterface $mailer, private StoredObjectRepository $storedObjectRepository, private TranslatorInterface $translator, private UserRepositoryInterface $userRepository)
|
||||||
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()
|
public static function getSubscribedEvents()
|
||||||
@@ -56,12 +45,13 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$message = $event->getEnvelope()->getMessage();
|
if (!$event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) {
|
||||||
|
|
||||||
if (!$message instanceof RequestGenerationMessage) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @var RequestGenerationMessage $message */
|
||||||
|
$message = $event->getEnvelope()->getMessage();
|
||||||
|
|
||||||
$this->logger->error(self::LOG_PREFIX.'Docgen failed', [
|
$this->logger->error(self::LOG_PREFIX.'Docgen failed', [
|
||||||
'stored_object_id' => $message->getDestinationStoredObjectId(),
|
'stored_object_id' => $message->getDestinationStoredObjectId(),
|
||||||
'entity_id' => $message->getEntityId(),
|
'entity_id' => $message->getEntityId(),
|
||||||
@@ -89,8 +79,16 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
|
|||||||
|
|
||||||
private function warnCreator(RequestGenerationMessage $message, WorkerMessageFailedEvent $event): void
|
private function warnCreator(RequestGenerationMessage $message, WorkerMessageFailedEvent $event): void
|
||||||
{
|
{
|
||||||
if (null === $message->getSendResultToEmail() || '' === $message->getSendResultToEmail()) {
|
$creatorId = $message->getCreatorId();
|
||||||
$this->logger->info(self::LOG_PREFIX.'No email associated with this request generation');
|
|
||||||
|
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()]);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -98,7 +96,7 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
|
|||||||
// if the exception is not a GeneratorException, we try the previous one...
|
// if the exception is not a GeneratorException, we try the previous one...
|
||||||
$throwable = $event->getThrowable();
|
$throwable = $event->getThrowable();
|
||||||
if (!$throwable instanceof GeneratorException) {
|
if (!$throwable instanceof GeneratorException) {
|
||||||
$throwable = $throwable->getPrevious() ?? $throwable;
|
$throwable = $throwable->getPrevious();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($throwable instanceof GeneratorException) {
|
if ($throwable instanceof GeneratorException) {
|
||||||
@@ -113,14 +111,8 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null === $creator = $this->userRepository->find($message->getCreatorId())) {
|
|
||||||
$this->logger->error(self::LOG_PREFIX.'Creator not found');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$email = (new TemplatedEmail())
|
$email = (new TemplatedEmail())
|
||||||
->to($message->getSendResultToEmail())
|
->to($creator->getEmail())
|
||||||
->subject($this->translator->trans('docgen.failure_email.The generation of a document failed'))
|
->subject($this->translator->trans('docgen.failure_email.The generation of a document failed'))
|
||||||
->textTemplate('@ChillDocGenerator/Email/on_generation_failed_email.txt.twig')
|
->textTemplate('@ChillDocGenerator/Email/on_generation_failed_email.txt.twig')
|
||||||
->context([
|
->context([
|
||||||
|
@@ -11,21 +11,15 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\DocGeneratorBundle\Service\Messenger;
|
namespace Chill\DocGeneratorBundle\Service\Messenger;
|
||||||
|
|
||||||
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
|
|
||||||
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
|
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
|
||||||
use Chill\DocGeneratorBundle\Service\Generator\Generator;
|
use Chill\DocGeneratorBundle\Service\Generator\Generator;
|
||||||
use Chill\DocGeneratorBundle\Service\Generator\GeneratorException;
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
|
||||||
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
|
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
|
||||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
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\Exception\UnrecoverableMessageHandlingException;
|
||||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle the request of document generation.
|
* Handle the request of document generation.
|
||||||
@@ -36,17 +30,8 @@ class RequestGenerationHandler implements MessageHandlerInterface
|
|||||||
|
|
||||||
private const LOG_PREFIX = '[docgen message handler] ';
|
private const LOG_PREFIX = '[docgen message handler] ';
|
||||||
|
|
||||||
public function __construct(
|
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 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)
|
public function __invoke(RequestGenerationMessage $message)
|
||||||
@@ -60,59 +45,25 @@ class RequestGenerationHandler implements MessageHandlerInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($destinationStoredObject->getGenerationTrialsCounter() >= self::AUTHORIZED_TRIALS) {
|
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');
|
throw new UnrecoverableMessageHandlingException('maximum number of retry reached');
|
||||||
}
|
}
|
||||||
|
|
||||||
$creator = $this->userRepository->find($message->getCreatorId());
|
$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();
|
$destinationStoredObject->addGenerationTrial();
|
||||||
$this->entityManager->createQuery('UPDATE '.StoredObject::class.' s SET s.generationTrialsCounter = s.generationTrialsCounter + 1 WHERE s.id = :id')
|
$this->entityManager->createQuery('UPDATE '.StoredObject::class.' s SET s.generationTrialsCounter = s.generationTrialsCounter + 1 WHERE s.id = :id')
|
||||||
->setParameter('id', $destinationStoredObject->getId())
|
->setParameter('id', $destinationStoredObject->getId())
|
||||||
->execute();
|
->execute();
|
||||||
|
|
||||||
try {
|
$this->generator->generateDocFromTemplate(
|
||||||
if ($message->isDumpOnly()) {
|
$template,
|
||||||
$destinationStoredObject = $this->generator->generateDataDump(
|
$message->getEntityId(),
|
||||||
$template,
|
$message->getContextGenerationData(),
|
||||||
$message->getEntityId(),
|
$destinationStoredObject,
|
||||||
$message->getContextGenerationData(),
|
false,
|
||||||
$destinationStoredObject,
|
null,
|
||||||
$creator
|
$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', [
|
$this->logger->info(self::LOG_PREFIX.'Request generation finished', [
|
||||||
'template_id' => $message->getTemplateId(),
|
'template_id' => $message->getTemplateId(),
|
||||||
@@ -120,23 +71,4 @@ class RequestGenerationHandler implements MessageHandlerInterface
|
|||||||
'duration_int' => (new \DateTimeImmutable('now'))->getTimestamp() - $message->getCreatedAt()->getTimestamp(),
|
'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,33 +15,27 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
|||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
|
|
||||||
final readonly class RequestGenerationMessage
|
class RequestGenerationMessage
|
||||||
{
|
{
|
||||||
private int $creatorId;
|
private readonly int $creatorId;
|
||||||
|
|
||||||
private int $templateId;
|
private readonly int $templateId;
|
||||||
|
|
||||||
private int $destinationStoredObjectId;
|
private readonly int $destinationStoredObjectId;
|
||||||
|
|
||||||
private \DateTimeImmutable $createdAt;
|
private readonly \DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
private ?string $sendResultToEmail;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
User $creator,
|
User $creator,
|
||||||
DocGeneratorTemplate $template,
|
DocGeneratorTemplate $template,
|
||||||
private int $entityId,
|
private readonly int $entityId,
|
||||||
StoredObject $destinationStoredObject,
|
StoredObject $destinationStoredObject,
|
||||||
private array $contextGenerationData,
|
private readonly array $contextGenerationData
|
||||||
private bool $isTest = false,
|
|
||||||
?string $sendResultToEmail = null,
|
|
||||||
private bool $dumpOnly = false,
|
|
||||||
) {
|
) {
|
||||||
$this->creatorId = $creator->getId();
|
$this->creatorId = $creator->getId();
|
||||||
$this->templateId = $template->getId();
|
$this->templateId = $template->getId();
|
||||||
$this->destinationStoredObjectId = $destinationStoredObject->getId();
|
$this->destinationStoredObjectId = $destinationStoredObject->getId();
|
||||||
$this->createdAt = new \DateTimeImmutable('now');
|
$this->createdAt = new \DateTimeImmutable('now');
|
||||||
$this->sendResultToEmail = $sendResultToEmail ?? $creator->getEmail();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCreatorId(): int
|
public function getCreatorId(): int
|
||||||
@@ -73,19 +67,4 @@ final readonly class RequestGenerationMessage
|
|||||||
{
|
{
|
||||||
return $this->createdAt;
|
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,9 +20,7 @@ use Chill\DocGeneratorBundle\Service\Generator\ObjectReadyException;
|
|||||||
use Chill\DocGeneratorBundle\Service\Generator\RelatedEntityNotFoundException;
|
use Chill\DocGeneratorBundle\Service\Generator\RelatedEntityNotFoundException;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||||
use Chill\MainBundle\Entity\User;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\Persistence\ManagerRegistry;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
use Prophecy\PhpUnit\ProphecyTrait;
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
@@ -68,11 +66,7 @@ class GeneratorTest extends TestCase
|
|||||||
$entityManager->find('DummyClass', Argument::type('int'))
|
$entityManager->find('DummyClass', Argument::type('int'))
|
||||||
->willReturn($entity);
|
->willReturn($entity);
|
||||||
$entityManager->clear()->shouldBeCalled();
|
$entityManager->clear()->shouldBeCalled();
|
||||||
$entityManager->flush()->shouldNotBeCalled();
|
$entityManager->flush()->shouldBeCalled();
|
||||||
|
|
||||||
$managerRegistry = $this->prophesize(ManagerRegistry::class);
|
|
||||||
$managerRegistry->getManagerForClass('DummyClass')->willReturn($entityManager->reveal());
|
|
||||||
$managerRegistry->getManagerForClass(StoredObject::class)->willReturn($entityManager->reveal());
|
|
||||||
|
|
||||||
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
|
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
|
||||||
$storedObjectManager->read($templateStoredObject)->willReturn('template');
|
$storedObjectManager->read($templateStoredObject)->willReturn('template');
|
||||||
@@ -81,7 +75,7 @@ class GeneratorTest extends TestCase
|
|||||||
$generator = new Generator(
|
$generator = new Generator(
|
||||||
$contextManagerInterface->reveal(),
|
$contextManagerInterface->reveal(),
|
||||||
$driver->reveal(),
|
$driver->reveal(),
|
||||||
$managerRegistry->reveal(),
|
$entityManager->reveal(),
|
||||||
new NullLogger(),
|
new NullLogger(),
|
||||||
$storedObjectManager->reveal()
|
$storedObjectManager->reveal()
|
||||||
);
|
);
|
||||||
@@ -90,8 +84,7 @@ class GeneratorTest extends TestCase
|
|||||||
$template,
|
$template,
|
||||||
1,
|
1,
|
||||||
[],
|
[],
|
||||||
$destinationStoredObject,
|
$destinationStoredObject
|
||||||
new User()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +95,7 @@ class GeneratorTest extends TestCase
|
|||||||
$generator = new Generator(
|
$generator = new Generator(
|
||||||
$this->prophesize(ContextManagerInterface::class)->reveal(),
|
$this->prophesize(ContextManagerInterface::class)->reveal(),
|
||||||
$this->prophesize(DriverInterface::class)->reveal(),
|
$this->prophesize(DriverInterface::class)->reveal(),
|
||||||
$this->prophesize(ManagerRegistry::class)->reveal(),
|
$this->prophesize(EntityManagerInterface::class)->reveal(),
|
||||||
new NullLogger(),
|
new NullLogger(),
|
||||||
$this->prophesize(StoredObjectManagerInterface::class)->reveal()
|
$this->prophesize(StoredObjectManagerInterface::class)->reveal()
|
||||||
);
|
);
|
||||||
@@ -115,8 +108,7 @@ class GeneratorTest extends TestCase
|
|||||||
$template,
|
$template,
|
||||||
1,
|
1,
|
||||||
[],
|
[],
|
||||||
$destinationStoredObject,
|
$destinationStoredObject
|
||||||
new User()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,14 +136,10 @@ class GeneratorTest extends TestCase
|
|||||||
$entityManager->find(Argument::type('string'), Argument::type('int'))
|
$entityManager->find(Argument::type('string'), Argument::type('int'))
|
||||||
->willReturn(null);
|
->willReturn(null);
|
||||||
|
|
||||||
$managerRegistry = $this->prophesize(ManagerRegistry::class);
|
|
||||||
$managerRegistry->getManagerForClass('DummyClass')->willReturn($entityManager->reveal());
|
|
||||||
$managerRegistry->getManagerForClass(StoredObject::class)->willReturn($entityManager->reveal());
|
|
||||||
|
|
||||||
$generator = new Generator(
|
$generator = new Generator(
|
||||||
$contextManagerInterface->reveal(),
|
$contextManagerInterface->reveal(),
|
||||||
$this->prophesize(DriverInterface::class)->reveal(),
|
$this->prophesize(DriverInterface::class)->reveal(),
|
||||||
$managerRegistry->reveal(),
|
$entityManager->reveal(),
|
||||||
new NullLogger(),
|
new NullLogger(),
|
||||||
$this->prophesize(StoredObjectManagerInterface::class)->reveal()
|
$this->prophesize(StoredObjectManagerInterface::class)->reveal()
|
||||||
);
|
);
|
||||||
@@ -160,8 +148,7 @@ class GeneratorTest extends TestCase
|
|||||||
$template,
|
$template,
|
||||||
1,
|
1,
|
||||||
[],
|
[],
|
||||||
$destinationStoredObject,
|
$destinationStoredObject
|
||||||
new User()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,107 +0,0 @@
|
|||||||
<?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');
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,226 +0,0 @@
|
|||||||
<?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());
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,4 +0,0 @@
|
|||||||
docgen:
|
|
||||||
data_dump_email:
|
|
||||||
link_valid_until: >-
|
|
||||||
Ce lien est valide jusqu'au {validity, date, full}, {validity, time, medium}
|
|
@@ -14,31 +14,13 @@ docgen:
|
|||||||
Doc generation is pending: La génération de ce document est en cours
|
Doc generation is pending: La génération de ce document est en cours
|
||||||
Come back later: Revenir plus tard
|
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:
|
failure_email:
|
||||||
The generation of a document failed: La génération d'un document a échoué
|
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
|
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.
|
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
|
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:
|
crud:
|
||||||
docgen_template:
|
docgen_template:
|
||||||
index:
|
index:
|
||||||
@@ -46,4 +28,5 @@ crud:
|
|||||||
add_new: Créer
|
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
|
Template file: Fichier modèle
|
||||||
|
252
src/Bundle/ChillDocStoreBundle/Controller/WebdavController.php
Normal file
252
src/Bundle/ChillDocStoreBundle/Controller/WebdavController.php
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
<?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\Controller;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Dav\Request\PropfindRequestAnalyzer;
|
||||||
|
use Chill\DocStoreBundle\Dav\Response\DavResponse;
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
|
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide endpoint for editing a document on the desktop using dav.
|
||||||
|
*
|
||||||
|
* This controller implements the minimal required methods to edit a document on a desktop software (i.e. LibreOffice)
|
||||||
|
* and save the document online.
|
||||||
|
*
|
||||||
|
* To avoid to ask for a password, the endpoints are protected using a JWT access token, which is inside the
|
||||||
|
* URL. This avoid the DAV Client (LibreOffice) to keep an access token in query parameter or in some header (which
|
||||||
|
* they are not able to understand). The JWT Guard is adapted with a dedicated token extractor which is going to read
|
||||||
|
* the segments (separation of "/"): the first segment must be the string "dav", and the second one must be the JWT.
|
||||||
|
*/
|
||||||
|
final readonly class WebdavController
|
||||||
|
{
|
||||||
|
private PropfindRequestAnalyzer $requestAnalyzer;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private \Twig\Environment $engine,
|
||||||
|
private StoredObjectManagerInterface $storedObjectManager,
|
||||||
|
private Security $security,
|
||||||
|
) {
|
||||||
|
$this->requestAnalyzer = new PropfindRequestAnalyzer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/dav/{access_token}/get/{uuid}/", methods={"GET", "HEAD"}, name="chill_docstore_dav_directory_get")
|
||||||
|
*/
|
||||||
|
public function getDirectory(StoredObject $storedObject, string $access_token): Response
|
||||||
|
{
|
||||||
|
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
||||||
|
throw new AccessDeniedHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DavResponse(
|
||||||
|
$this->engine->render('@ChillDocStore/Webdav/directory.html.twig', [
|
||||||
|
'stored_object' => $storedObject,
|
||||||
|
'access_token' => $access_token,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/dav/{access_token}/get/{uuid}/", methods={"OPTIONS"})
|
||||||
|
*/
|
||||||
|
public function optionsDirectory(StoredObject $storedObject): Response
|
||||||
|
{
|
||||||
|
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
||||||
|
throw new AccessDeniedHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = (new DavResponse(''))
|
||||||
|
->setEtag($this->storedObjectManager->etag($storedObject))
|
||||||
|
;
|
||||||
|
|
||||||
|
// $response->headers->add(['Allow' => 'OPTIONS,GET,HEAD,DELETE,PROPFIND,PUT,PROPPATCH,COPY,MOVE,REPORT,PATCH,POST,TRACE']);
|
||||||
|
$response->headers->add(['Allow' => 'OPTIONS,GET,HEAD,DELETE,PROPFIND,PUT']);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/dav/{access_token}/get/{uuid}/", methods={"PROPFIND"})
|
||||||
|
*/
|
||||||
|
public function propfindDirectory(StoredObject $storedObject, string $access_token, Request $request): Response
|
||||||
|
{
|
||||||
|
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
||||||
|
throw new AccessDeniedHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$depth = $request->headers->get('depth');
|
||||||
|
|
||||||
|
if ('0' !== $depth && '1' !== $depth) {
|
||||||
|
throw new BadRequestHttpException('only 1 and 0 are accepted for Depth header');
|
||||||
|
}
|
||||||
|
|
||||||
|
[$properties, $lastModified, $etag, $length] = $this->parseDavRequest($request->getContent(), $storedObject);
|
||||||
|
|
||||||
|
$response = new DavResponse(
|
||||||
|
$this->engine->render('@ChillDocStore/Webdav/directory_propfind.xml.twig', [
|
||||||
|
'stored_object' => $storedObject,
|
||||||
|
'properties' => $properties,
|
||||||
|
'last_modified' => $lastModified,
|
||||||
|
'etag' => $etag,
|
||||||
|
'content_length' => $length,
|
||||||
|
'depth' => (int) $depth,
|
||||||
|
'access_token' => $access_token,
|
||||||
|
]),
|
||||||
|
207
|
||||||
|
);
|
||||||
|
|
||||||
|
$response->headers->add([
|
||||||
|
'Content-Type' => 'text/xml',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/dav/{access_token}/get/{uuid}/d", name="chill_docstore_dav_document_get", methods={"GET"})
|
||||||
|
*/
|
||||||
|
public function getDocument(StoredObject $storedObject): Response
|
||||||
|
{
|
||||||
|
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
||||||
|
throw new AccessDeniedHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (new DavResponse($this->storedObjectManager->read($storedObject)))
|
||||||
|
->setEtag($this->storedObjectManager->etag($storedObject));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/dav/{access_token}/get/{uuid}/d", methods={"HEAD"})
|
||||||
|
*/
|
||||||
|
public function headDocument(StoredObject $storedObject): Response
|
||||||
|
{
|
||||||
|
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
||||||
|
throw new AccessDeniedHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = new DavResponse('');
|
||||||
|
|
||||||
|
$response->headers->add(
|
||||||
|
[
|
||||||
|
'Content-Length' => $this->storedObjectManager->getContentLength($storedObject),
|
||||||
|
'Content-Type' => $storedObject->getType(),
|
||||||
|
'Etag' => $this->storedObjectManager->etag($storedObject),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/dav/{access_token}/get/{uuid}/d", methods={"OPTIONS"})
|
||||||
|
*/
|
||||||
|
public function optionsDocument(StoredObject $storedObject): Response
|
||||||
|
{
|
||||||
|
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
||||||
|
throw new AccessDeniedHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = (new DavResponse(''))
|
||||||
|
->setEtag($this->storedObjectManager->etag($storedObject))
|
||||||
|
;
|
||||||
|
|
||||||
|
$response->headers->add(['Allow' => 'OPTIONS,GET,HEAD,DELETE,PROPFIND,PUT']);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/dav/{access_token}/get/{uuid}/d", methods={"PROPFIND"})
|
||||||
|
*/
|
||||||
|
public function propfindDocument(StoredObject $storedObject, string $access_token, Request $request): Response
|
||||||
|
{
|
||||||
|
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
||||||
|
throw new AccessDeniedHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
[$properties, $lastModified, $etag, $length] = $this->parseDavRequest($request->getContent(), $storedObject);
|
||||||
|
|
||||||
|
$response = new DavResponse(
|
||||||
|
$this->engine->render(
|
||||||
|
'@ChillDocStore/Webdav/doc_props.xml.twig',
|
||||||
|
[
|
||||||
|
'stored_object' => $storedObject,
|
||||||
|
'properties' => $properties,
|
||||||
|
'etag' => $etag,
|
||||||
|
'last_modified' => $lastModified,
|
||||||
|
'content_length' => $length,
|
||||||
|
'access_token' => $access_token,
|
||||||
|
]
|
||||||
|
),
|
||||||
|
207
|
||||||
|
);
|
||||||
|
|
||||||
|
$response
|
||||||
|
->headers->add([
|
||||||
|
'Content-Type' => 'text/xml',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/dav/{access_token}/get/{uuid}/d", methods={"PUT"})
|
||||||
|
*/
|
||||||
|
public function putDocument(StoredObject $storedObject, Request $request): Response
|
||||||
|
{
|
||||||
|
if (!$this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject)) {
|
||||||
|
throw new AccessDeniedHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->storedObjectManager->write($storedObject, $request->getContent());
|
||||||
|
|
||||||
|
return new DavResponse('', Response::HTTP_NO_CONTENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{0: array, 1: \DateTimeInterface, 2: string, 3: int} properties, lastModified, etag, length
|
||||||
|
*/
|
||||||
|
private function parseDavRequest(string $content, StoredObject $storedObject): array
|
||||||
|
{
|
||||||
|
$xml = new \DOMDocument();
|
||||||
|
$xml->loadXML($content);
|
||||||
|
|
||||||
|
$properties = $this->requestAnalyzer->getRequestedProperties($xml);
|
||||||
|
$requested = array_keys(array_filter($properties, fn ($item) => true === $item));
|
||||||
|
|
||||||
|
if (
|
||||||
|
in_array('lastModified', $requested, true)
|
||||||
|
|| in_array('etag', $requested, true)
|
||||||
|
) {
|
||||||
|
$lastModified = $this->storedObjectManager->getLastModified($storedObject);
|
||||||
|
$etag = $this->storedObjectManager->etag($storedObject);
|
||||||
|
}
|
||||||
|
if (in_array('contentLength', $requested, true)) {
|
||||||
|
$length = $this->storedObjectManager->getContentLength($storedObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
$properties,
|
||||||
|
$lastModified ?? null,
|
||||||
|
$etag ?? null,
|
||||||
|
$length ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
<?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\Dav\Exception;
|
||||||
|
|
||||||
|
class ParseRequestException extends \UnexpectedValueException
|
||||||
|
{
|
||||||
|
}
|
@@ -0,0 +1,103 @@
|
|||||||
|
<?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\Dav\Request;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Dav\Exception\ParseRequestException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @phpstan-type davProperties array{resourceType: bool, contentType: bool, lastModified: bool, creationDate: bool, contentLength: bool, etag: bool, supportedLock: bool, unknowns: list<array{xmlns: string, prop: string}>}
|
||||||
|
*/
|
||||||
|
class PropfindRequestAnalyzer
|
||||||
|
{
|
||||||
|
private const KNOWN_PROPS = [
|
||||||
|
'resourceType',
|
||||||
|
'contentType',
|
||||||
|
'lastModified',
|
||||||
|
'creationDate',
|
||||||
|
'contentLength',
|
||||||
|
'etag',
|
||||||
|
'supportedLock',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return davProperties
|
||||||
|
*/
|
||||||
|
public function getRequestedProperties(\DOMDocument $request): array
|
||||||
|
{
|
||||||
|
$propfinds = $request->getElementsByTagNameNS('DAV:', 'propfind');
|
||||||
|
|
||||||
|
if (0 === $propfinds->count()) {
|
||||||
|
throw new ParseRequestException('any propfind element found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (1 < $propfinds->count()) {
|
||||||
|
throw new ParseRequestException('too much propfind element found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$propfind = $propfinds->item(0);
|
||||||
|
|
||||||
|
if (0 === $propfind->childNodes->count()) {
|
||||||
|
throw new ParseRequestException('no element under propfind');
|
||||||
|
}
|
||||||
|
|
||||||
|
$unknows = [];
|
||||||
|
$props = [];
|
||||||
|
|
||||||
|
foreach ($propfind->childNodes->getIterator() as $prop) {
|
||||||
|
/** @var \DOMNode $prop */
|
||||||
|
if (XML_ELEMENT_NODE !== $prop->nodeType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('propname' === $prop->nodeName) {
|
||||||
|
return $this->baseProps(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($prop->childNodes->getIterator() as $getProp) {
|
||||||
|
if (XML_ELEMENT_NODE !== $getProp->nodeType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('DAV:' !== $getProp->lookupNamespaceURI(null)) {
|
||||||
|
$unknows[] = ['xmlns' => $getProp->lookupNamespaceURI(null), 'prop' => $getProp->nodeName];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$props[] = match ($getProp->nodeName) {
|
||||||
|
'resourcetype' => 'resourceType',
|
||||||
|
'getcontenttype' => 'contentType',
|
||||||
|
'getlastmodified' => 'lastModified',
|
||||||
|
default => '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$props = array_filter(array_values($props), fn (string $item) => '' !== $item);
|
||||||
|
|
||||||
|
return [...$this->baseProps(false), ...array_combine($props, array_fill(0, count($props), true)), 'unknowns' => $unknows];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return davProperties
|
||||||
|
*/
|
||||||
|
private function baseProps(bool $default = false): array
|
||||||
|
{
|
||||||
|
return
|
||||||
|
[
|
||||||
|
...array_combine(
|
||||||
|
self::KNOWN_PROPS,
|
||||||
|
array_fill(0, count(self::KNOWN_PROPS), $default)
|
||||||
|
),
|
||||||
|
'unknowns' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
24
src/Bundle/ChillDocStoreBundle/Dav/Response/DavResponse.php
Normal file
24
src/Bundle/ChillDocStoreBundle/Dav/Response/DavResponse.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?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\Dav\Response;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class DavResponse extends Response
|
||||||
|
{
|
||||||
|
public function __construct($content = '', int $status = 200, array $headers = [])
|
||||||
|
{
|
||||||
|
parent::__construct($content, $status, $headers);
|
||||||
|
|
||||||
|
$this->headers->add(['DAV' => '1']);
|
||||||
|
}
|
||||||
|
}
|
@@ -25,11 +25,6 @@ use Symfony\Component\Serializer\Annotation as Serializer;
|
|||||||
/**
|
/**
|
||||||
* Represent a document stored in an object store.
|
* 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\Entity
|
||||||
*
|
*
|
||||||
* @ORM\Table("chill_doc.stored_object")
|
* @ORM\Table("chill_doc.stored_object")
|
||||||
@@ -122,16 +117,6 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa
|
|||||||
*/
|
*/
|
||||||
private int $generationTrialsCounter = 0;
|
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
|
* @param StoredObject::STATUS_* $status
|
||||||
*/
|
*/
|
||||||
@@ -159,11 +144,6 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa
|
|||||||
*/
|
*/
|
||||||
public function getCreationDate(): \DateTime
|
public function getCreationDate(): \DateTime
|
||||||
{
|
{
|
||||||
if (null === $this->createdAt) {
|
|
||||||
// this scenario will quite never happens
|
|
||||||
return new \DateTime('now');
|
|
||||||
}
|
|
||||||
|
|
||||||
return \DateTime::createFromImmutable($this->createdAt);
|
return \DateTime::createFromImmutable($this->createdAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,37 +303,4 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa
|
|||||||
{
|
{
|
||||||
return self::STATUS_FAILURE === $this->getStatus();
|
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,10 +14,11 @@ namespace Chill\DocStoreBundle\Repository;
|
|||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
|
||||||
final readonly class StoredObjectRepository implements StoredObjectRepositoryInterface
|
final class StoredObjectRepository implements ObjectRepository
|
||||||
{
|
{
|
||||||
private EntityRepository $repository;
|
private readonly EntityRepository $repository;
|
||||||
|
|
||||||
public function __construct(EntityManagerInterface $entityManager)
|
public function __construct(EntityManagerInterface $entityManager)
|
||||||
{
|
{
|
||||||
|
@@ -1,22 +0,0 @@
|
|||||||
<?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
|
|
||||||
{
|
|
||||||
}
|
|
@@ -17,18 +17,22 @@ window.addEventListener('DOMContentLoaded', function (e) {
|
|||||||
canEdit: string,
|
canEdit: string,
|
||||||
storedObject: string,
|
storedObject: string,
|
||||||
buttonSmall: string,
|
buttonSmall: string,
|
||||||
|
davLink: string,
|
||||||
|
davLinkExpiration: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
const
|
const
|
||||||
storedObject = JSON.parse(datasets.storedObject) as StoredObject,
|
storedObject = JSON.parse(datasets.storedObject) as StoredObject,
|
||||||
filename = datasets.filename,
|
filename = datasets.filename,
|
||||||
canEdit = datasets.canEdit === '1',
|
canEdit = datasets.canEdit === '1',
|
||||||
small = datasets.buttonSmall === '1'
|
small = datasets.buttonSmall === '1',
|
||||||
|
davLink = 'davLink' in datasets && datasets.davLink !== '' ? datasets.davLink : null,
|
||||||
|
davLinkExpiration = 'davLinkExpiration' in datasets ? Number.parseInt(datasets.davLinkExpiration) : null
|
||||||
;
|
;
|
||||||
|
|
||||||
return { storedObject, filename, canEdit, small };
|
return { storedObject, filename, canEdit, small, davLink, davLinkExpiration };
|
||||||
},
|
},
|
||||||
template: '<document-action-buttons-group :can-edit="canEdit" :filename="filename" :stored-object="storedObject" :small="small" @on-stored-object-status-change="onStoredObjectStatusChange"></document-action-buttons-group>',
|
template: '<document-action-buttons-group :can-edit="canEdit" :filename="filename" :stored-object="storedObject" :small="small" :dav-link="davLink" :dav-link-expiration="davLinkExpiration" @on-stored-object-status-change="onStoredObjectStatusChange"></document-action-buttons-group>',
|
||||||
methods: {
|
methods: {
|
||||||
onStoredObjectStatusChange: function(newStatus: StoredObjectStatusChange): void {
|
onStoredObjectStatusChange: function(newStatus: StoredObjectStatusChange): void {
|
||||||
this.$data.storedObject.status = newStatus.status;
|
this.$data.storedObject.status = newStatus.status;
|
||||||
|
@@ -7,6 +7,9 @@
|
|||||||
<li v-if="props.canEdit && is_extension_editable(props.storedObject.type)">
|
<li v-if="props.canEdit && is_extension_editable(props.storedObject.type)">
|
||||||
<wopi-edit-button :stored-object="props.storedObject" :classes="{'dropdown-item': true}" :execute-before-leave="props.executeBeforeLeave"></wopi-edit-button>
|
<wopi-edit-button :stored-object="props.storedObject" :classes="{'dropdown-item': true}" :execute-before-leave="props.executeBeforeLeave"></wopi-edit-button>
|
||||||
</li>
|
</li>
|
||||||
|
<li v-if="props.canEdit && is_extension_editable(props.storedObject.type) && props.davLink !== undefined && props.davLinkExpiration !== undefined">
|
||||||
|
<desktop-edit-button :classes="{'dropdown-item': true}" :edit-link="props.davLink" :expiration-link="props.davLinkExpiration"></desktop-edit-button>
|
||||||
|
</li>
|
||||||
<li v-if="props.storedObject.type != 'application/pdf' && is_extension_viewable(props.storedObject.type) && props.canConvertPdf">
|
<li v-if="props.storedObject.type != 'application/pdf' && is_extension_viewable(props.storedObject.type) && props.canConvertPdf">
|
||||||
<convert-button :stored-object="props.storedObject" :filename="filename" :classes="{'dropdown-item': true}"></convert-button>
|
<convert-button :stored-object="props.storedObject" :filename="filename" :classes="{'dropdown-item': true}"></convert-button>
|
||||||
</li>
|
</li>
|
||||||
@@ -36,6 +39,7 @@ import {
|
|||||||
StoredObjectStatusChange,
|
StoredObjectStatusChange,
|
||||||
WopiEditButtonExecutableBeforeLeaveFunction
|
WopiEditButtonExecutableBeforeLeaveFunction
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
import DesktopEditButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DesktopEditButton.vue";
|
||||||
|
|
||||||
interface DocumentActionButtonsGroupConfig {
|
interface DocumentActionButtonsGroupConfig {
|
||||||
storedObject: StoredObject,
|
storedObject: StoredObject,
|
||||||
@@ -57,6 +61,16 @@ interface DocumentActionButtonsGroupConfig {
|
|||||||
* If set, will execute this function before leaving to the editor
|
* If set, will execute this function before leaving to the editor
|
||||||
*/
|
*/
|
||||||
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction,
|
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a link to download and edit file using webdav
|
||||||
|
*/
|
||||||
|
davLink?: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the expiration date of the download, as a unix timestamp
|
||||||
|
*/
|
||||||
|
davLinkExpiration?: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -68,7 +82,7 @@ const props = withDefaults(defineProps<DocumentActionButtonsGroupConfig>(), {
|
|||||||
canEdit: true,
|
canEdit: true,
|
||||||
canDownload: true,
|
canDownload: true,
|
||||||
canConvertPdf: true,
|
canConvertPdf: true,
|
||||||
returnPath: window.location.pathname + window.location.search + window.location.hash,
|
returnPath: window.location.pathname + window.location.search + window.location.hash
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -0,0 +1,66 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
||||||
|
import {computed, reactive} from "vue";
|
||||||
|
|
||||||
|
export interface DesktopEditButtonConfig {
|
||||||
|
editLink: null,
|
||||||
|
classes: { [k: string]: boolean },
|
||||||
|
expirationLink: number|Date,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DesktopEditButtonState {
|
||||||
|
modalOpened: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
const state: DesktopEditButtonState = reactive({modalOpened: false});
|
||||||
|
|
||||||
|
const props = defineProps<DesktopEditButtonConfig>();
|
||||||
|
|
||||||
|
const buildCommand = computed<string>(() => 'vnd.libreoffice.command:ofe|u|' + props.editLink);
|
||||||
|
|
||||||
|
const editionUntilFormatted = computed<string>(() => {
|
||||||
|
let d;
|
||||||
|
|
||||||
|
if (props.expirationLink instanceof Date) {
|
||||||
|
d = props.expirationLink;
|
||||||
|
} else {
|
||||||
|
d = new Date(props.expirationLink * 1000);
|
||||||
|
}
|
||||||
|
console.log(props.expirationLink);
|
||||||
|
|
||||||
|
return (new Intl.DateTimeFormat(undefined, {'dateStyle': 'long', 'timeStyle': 'medium'})).format(d);
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<teleport to="body">
|
||||||
|
<modal v-if="state.modalOpened" @close="state.modalOpened=false">
|
||||||
|
<template v-slot:body>
|
||||||
|
<div class="desktop-edit">
|
||||||
|
<p class="center">Veuillez enregistrer vos modifications avant le</p>
|
||||||
|
<p><strong>{{ editionUntilFormatted }}</strong></p>
|
||||||
|
|
||||||
|
<p><a class="btn btn-primary" :href="buildCommand">Ouvrir le document pour édition</a></p>
|
||||||
|
|
||||||
|
<p><small>Le document peut être édité uniquement en utilisant Libre Office.</small></p>
|
||||||
|
|
||||||
|
<p><small>En cas d'échec lors de l'enregistrement, sauver le document sur le poste de travail avant de le déposer à nouveau ici.</small></p>
|
||||||
|
|
||||||
|
<p><small>Vous pouvez naviguez sur d'autres pages pendant l'édition.</small></p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</modal>
|
||||||
|
</teleport>
|
||||||
|
<a :class="props.classes" @click="state.modalOpened = true">
|
||||||
|
<i class="fa fa-desktop"></i>
|
||||||
|
Éditer sur le bureau
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.desktop-edit {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -3,5 +3,7 @@
|
|||||||
data-download-buttons
|
data-download-buttons
|
||||||
data-stored-object="{{ document_json|json_encode|escape('html_attr') }}"
|
data-stored-object="{{ document_json|json_encode|escape('html_attr') }}"
|
||||||
data-can-edit="{{ can_edit ? '1' : '0' }}"
|
data-can-edit="{{ can_edit ? '1' : '0' }}"
|
||||||
|
data-dav-link="{{ dav_link|escape('html_attr') }}"
|
||||||
|
data-dav-link-expiration="{{ dav_link_expiration|escape('html_attr') }}"
|
||||||
{% if options['small'] is defined %}data-button-small="{{ options['small'] ? '1' : '0' }}"{% endif %}
|
{% if options['small'] is defined %}data-button-small="{{ options['small'] ? '1' : '0' }}"{% endif %}
|
||||||
{% if title|default(document.title)|default(null) is not null %}data-filename="{{ title|default(document.title)|escape('html_attr') }}"{% endif %}></div>
|
{% if title|default(document.title)|default(null) is not null %}data-filename="{{ title|default(document.title)|escape('html_attr') }}"{% endif %}></div>
|
||||||
|
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Directory for {{ stored_object.uuid }}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<ul>
|
||||||
|
<li><a href="{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid, 'access_token': access_token })) }}">d</a></li>
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
@@ -0,0 +1,81 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<d:multistatus xmlns:d="DAV:">
|
||||||
|
<d:response>
|
||||||
|
<d:href>{{ path('chill_docstore_dav_directory_get', { 'uuid': stored_object.uuid, 'access_token': access_token } ) }}</d:href>
|
||||||
|
{% if properties.resourceType or properties.contentType %}
|
||||||
|
<d:propstat>
|
||||||
|
<d:prop>
|
||||||
|
{% if properties.resourceType %}
|
||||||
|
<d:resourcetype><d:collection/></d:resourcetype>
|
||||||
|
{% endif %}
|
||||||
|
{% if properties.contentType %}
|
||||||
|
<d:getcontenttype>httpd/unix-directory</d:getcontenttype>
|
||||||
|
{% endif %}
|
||||||
|
</d:prop>
|
||||||
|
<d:status>HTTP/1.1 200 OK</d:status>
|
||||||
|
</d:propstat>
|
||||||
|
{% endif %}
|
||||||
|
{% if properties.unknowns|length > 0 %}
|
||||||
|
<d:propstat>
|
||||||
|
{% for k,u in properties.unknowns %}
|
||||||
|
<d:prop {{ ('xmlns:ns' ~ k ~ '="' ~ u.xmlns|e('html_attr') ~ '"')|raw }}>
|
||||||
|
<{{ 'ns'~ k ~ ':' ~ u.prop }} />
|
||||||
|
</d:prop>
|
||||||
|
{% endfor %}
|
||||||
|
<d:status>HTTP/1.1 404 Not Found</d:status>
|
||||||
|
</d:propstat>
|
||||||
|
{% endif %}
|
||||||
|
</d:response>
|
||||||
|
{% if depth == 1 %}
|
||||||
|
<d:response>
|
||||||
|
<d:href>{{ path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid, 'access_token':access_token}) }}</d:href>
|
||||||
|
{% if properties.lastModified or properties.contentLength or properties.resourceType or properties.etag or properties.contentType or properties.creationDate %}
|
||||||
|
<d:propstat>
|
||||||
|
<d:prop>
|
||||||
|
{% if properties.resourceType %}
|
||||||
|
<d:resourcetype/>
|
||||||
|
{% endif %}
|
||||||
|
{% if properties.creationDate %}
|
||||||
|
<d:creationdate />
|
||||||
|
{% endif %}
|
||||||
|
{% if properties.lastModified %}
|
||||||
|
{% if last_modified is not same as null %}
|
||||||
|
<d:getlastmodified>{{ last_modified.format(constant('DATE_RSS')) }}</d:getlastmodified>
|
||||||
|
{% else %}
|
||||||
|
<d:getlastmodified />
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if properties.contentLength %}
|
||||||
|
{% if content_length is not same as null %}
|
||||||
|
<d:getcontentlength>{{ content_length }}</d:getcontentlength>
|
||||||
|
{% else %}
|
||||||
|
<d:getcontentlength />
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if properties.etag %}
|
||||||
|
{% if etag is not same as null %}
|
||||||
|
<d:getetag>"{{ etag }}"</d:getetag>
|
||||||
|
{% else %}
|
||||||
|
<d:getetag />
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if properties.contentType %}
|
||||||
|
<d:getcontenttype>{{ stored_object.type }}</d:getcontenttype>
|
||||||
|
{% endif %}
|
||||||
|
</d:prop>
|
||||||
|
<d:status>HTTP/1.1 200 OK</d:status>
|
||||||
|
</d:propstat>
|
||||||
|
{% endif %}
|
||||||
|
{% if properties.unknowns|length > 0 %}
|
||||||
|
<d:propstat>
|
||||||
|
{% for k,u in properties.unknowns %}
|
||||||
|
<d:prop {{ ('xmlns:ns' ~ k ~ '="' ~ u.xmlns|e('html_attr') ~ '"')|raw }}>
|
||||||
|
<{{ 'ns'~ k ~ ':' ~ u.prop }} />
|
||||||
|
</d:prop>
|
||||||
|
{% endfor %}
|
||||||
|
<d:status>HTTP/1.1 404 Not Found</d:status>
|
||||||
|
</d:propstat>
|
||||||
|
{% endif %}
|
||||||
|
</d:response>
|
||||||
|
{% endif %}
|
||||||
|
</d:multistatus>
|
@@ -0,0 +1,53 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<d:multistatus xmlns:d="DAV:">
|
||||||
|
<d:response>
|
||||||
|
<d:href>{{ path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid, 'access_token': access_token}) }}</d:href>
|
||||||
|
{% if properties.lastModified or properties.contentLength or properties.resourceType or properties.etag or properties.contentType or properties.creationDate %}
|
||||||
|
<d:propstat>
|
||||||
|
<d:prop>
|
||||||
|
{% if properties.resourceType %}
|
||||||
|
<d:resourcetype/>
|
||||||
|
{% endif %}
|
||||||
|
{% if properties.creationDate %}
|
||||||
|
<d:creationdate />
|
||||||
|
{% endif %}
|
||||||
|
{% if properties.lastModified %}
|
||||||
|
{% if last_modified is not same as null %}
|
||||||
|
<d:getlastmodified>{{ last_modified.format(constant('DATE_RSS')) }}</d:getlastmodified>
|
||||||
|
{% else %}
|
||||||
|
<d:getlastmodified />
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if properties.contentLength %}
|
||||||
|
{% if content_length is not same as null %}
|
||||||
|
<d:getcontentlength>{{ content_length }}</d:getcontentlength>
|
||||||
|
{% else %}
|
||||||
|
<d:getcontentlength />
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if properties.etag %}
|
||||||
|
{% if etag is not same as null %}
|
||||||
|
<d:getetag>"{{ etag }}"</d:getetag>
|
||||||
|
{% else %}
|
||||||
|
<d:getetag />
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if properties.contentType %}
|
||||||
|
<d:getcontenttype>{{ stored_object.type }}</d:getcontenttype>
|
||||||
|
{% endif %}
|
||||||
|
</d:prop>
|
||||||
|
<d:status>HTTP/1.1 200 OK</d:status>
|
||||||
|
</d:propstat>
|
||||||
|
{% endif %}
|
||||||
|
{% if properties.unknowns|length > 0 %}
|
||||||
|
<d:propstat>
|
||||||
|
{% for k,u in properties.unknowns %}
|
||||||
|
<d:prop {{ ('xmlns:ns' ~ k ~ '="' ~ u.xmlns|e('html_attr') ~ '"')|raw }}>
|
||||||
|
<{{ 'ns'~ k ~ ':' ~ u.prop }} />
|
||||||
|
</d:prop>
|
||||||
|
{% endfor %}
|
||||||
|
<d:status>HTTP/1.1 404 Not Found</d:status>
|
||||||
|
</d:propstat>
|
||||||
|
{% endif %}
|
||||||
|
</d:response>
|
||||||
|
</d:multistatus>
|
@@ -0,0 +1,7 @@
|
|||||||
|
{% extends '@ChillMain/layout.html.twig' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>document uuid: {{ stored_object.uuid }}</p>
|
||||||
|
<p>{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid, 'access_token': access_token })) }}</p>
|
||||||
|
<a href="vnd.libreoffice.command:ofe|u|{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid, 'access_token': access_token })) }}">Open document</a>
|
||||||
|
{% endblock %}
|
@@ -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\Security\Authorization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Role to edit or see the stored object content.
|
||||||
|
*/
|
||||||
|
enum StoredObjectRoleEnum: string
|
||||||
|
{
|
||||||
|
case SEE = 'SEE';
|
||||||
|
|
||||||
|
case EDIT = 'SEE_AND_EDIT';
|
||||||
|
}
|
@@ -0,0 +1,55 @@
|
|||||||
|
<?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\Security\Authorization;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Security\Guard\DavTokenAuthenticationEventSubscriber;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Voter for the content of a stored object.
|
||||||
|
*
|
||||||
|
* This is in use to allow or disallow the edition of the stored object's content.
|
||||||
|
*/
|
||||||
|
class StoredObjectVoter extends Voter
|
||||||
|
{
|
||||||
|
protected function supports($attribute, $subject): bool
|
||||||
|
{
|
||||||
|
return StoredObjectRoleEnum::tryFrom($attribute) instanceof StoredObjectRoleEnum
|
||||||
|
&& $subject instanceof StoredObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||||
|
{
|
||||||
|
/** @var StoredObject $subject */
|
||||||
|
if (
|
||||||
|
!$token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)
|
||||||
|
|| $subject->getUuid()->toString() !== $token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$askedRole = StoredObjectRoleEnum::from($attribute);
|
||||||
|
$tokenRoleAuthorization =
|
||||||
|
$token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS);
|
||||||
|
|
||||||
|
return match ($askedRole) {
|
||||||
|
StoredObjectRoleEnum::SEE => StoredObjectRoleEnum::EDIT === $tokenRoleAuthorization || StoredObjectRoleEnum::SEE === $tokenRoleAuthorization,
|
||||||
|
StoredObjectRoleEnum::EDIT => StoredObjectRoleEnum::EDIT === $tokenRoleAuthorization
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,58 @@
|
|||||||
|
<?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\Security\Guard;
|
||||||
|
|
||||||
|
use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the JWT Token from the segment of the dav endpoints.
|
||||||
|
*
|
||||||
|
* A segment is a separation inside the string, using the character "/".
|
||||||
|
*
|
||||||
|
* For recognizing the JWT, the first segment must be "dav", and the second one must be
|
||||||
|
* the JWT endpoint.
|
||||||
|
*/
|
||||||
|
final readonly class DavOnUrlTokenExtractor implements TokenExtractorInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function extract(Request $request): false|string
|
||||||
|
{
|
||||||
|
$uri = $request->getRequestUri();
|
||||||
|
|
||||||
|
$segments = array_values(
|
||||||
|
array_filter(
|
||||||
|
explode('/', $uri),
|
||||||
|
fn ($item) => '' !== trim($item)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (2 > count($segments)) {
|
||||||
|
$this->logger->info('not enough segment for parsing URL');
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('dav' !== $segments[0]) {
|
||||||
|
$this->logger->info('the first segment of the url must be DAV');
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $segments[1];
|
||||||
|
}
|
||||||
|
}
|
@@ -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\DocStoreBundle\Security\Guard;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
|
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTAuthenticatedEvent;
|
||||||
|
use Lexik\Bundle\JWTAuthenticationBundle\Events;
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store some data from the JWT's payload inside the token's attributes.
|
||||||
|
*/
|
||||||
|
class DavTokenAuthenticationEventSubscriber implements EventSubscriberInterface
|
||||||
|
{
|
||||||
|
final public const STORED_OBJECT = 'stored_object';
|
||||||
|
final public const ACTIONS = 'stored_objects_actions';
|
||||||
|
|
||||||
|
public static function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Events::JWT_AUTHENTICATED => ['onJWTAuthenticated', 0],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onJWTAuthenticated(JWTAuthenticatedEvent $event): void
|
||||||
|
{
|
||||||
|
$payload = $event->getPayload();
|
||||||
|
|
||||||
|
if (!(array_key_exists('dav', $payload) && 1 === $payload['dav'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $event->getToken();
|
||||||
|
$token->setAttribute(self::ACTIONS, match ($payload['e']) {
|
||||||
|
0 => StoredObjectRoleEnum::SEE,
|
||||||
|
1 => StoredObjectRoleEnum::EDIT,
|
||||||
|
default => throw new \UnexpectedValueException('unsupported value for e parameter')
|
||||||
|
});
|
||||||
|
|
||||||
|
$token->setAttribute(self::STORED_OBJECT, $payload['so']);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,48 @@
|
|||||||
|
<?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\Security\Guard;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
|
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide a JWT Token which will be valid for viewing or editing a document.
|
||||||
|
*/
|
||||||
|
final readonly class JWTDavTokenProvider implements JWTDavTokenProviderInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private JWTTokenManagerInterface $JWTTokenManager,
|
||||||
|
private Security $security,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createToken(StoredObject $storedObject, StoredObjectRoleEnum $roleEnum): string
|
||||||
|
{
|
||||||
|
return $this->JWTTokenManager->createFromPayload($this->security->getUser(), [
|
||||||
|
'dav' => 1,
|
||||||
|
'e' => match ($roleEnum) {
|
||||||
|
StoredObjectRoleEnum::SEE => 0,
|
||||||
|
StoredObjectRoleEnum::EDIT => 1,
|
||||||
|
},
|
||||||
|
'so' => $storedObject->getUuid(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTokenExpiration(string $tokenString): \DateTimeImmutable
|
||||||
|
{
|
||||||
|
$jwt = $this->JWTTokenManager->parse($tokenString);
|
||||||
|
|
||||||
|
return \DateTimeImmutable::createFromFormat('U', (string) $jwt['exp']);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,25 @@
|
|||||||
|
<?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\Security\Guard;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide a JWT Token which will be valid for viewing or editing a document.
|
||||||
|
*/
|
||||||
|
interface JWTDavTokenProviderInterface
|
||||||
|
{
|
||||||
|
public function createToken(StoredObject $storedObject, StoredObjectRoleEnum $roleEnum): string;
|
||||||
|
|
||||||
|
public function getTokenExpiration(string $tokenString): \DateTimeImmutable;
|
||||||
|
}
|
@@ -0,0 +1,41 @@
|
|||||||
|
<?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\Security\Guard;
|
||||||
|
|
||||||
|
use Lexik\Bundle\JWTAuthenticationBundle\Security\Guard\JWTTokenAuthenticator;
|
||||||
|
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
|
||||||
|
use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||||
|
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alter the base JWTTokenAuthenticator to add the special extractor for dav url endpoints.
|
||||||
|
*/
|
||||||
|
class JWTOnDavUrlAuthenticator extends JWTTokenAuthenticator
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
JWTTokenManagerInterface $jwtManager,
|
||||||
|
EventDispatcherInterface $dispatcher,
|
||||||
|
TokenExtractorInterface $tokenExtractor,
|
||||||
|
private readonly DavOnUrlTokenExtractor $davOnUrlTokenExtractor,
|
||||||
|
TokenStorageInterface $preAuthenticationTokenStorage,
|
||||||
|
TranslatorInterface $translator = null,
|
||||||
|
) {
|
||||||
|
parent::__construct($jwtManager, $dispatcher, $tokenExtractor, $preAuthenticationTokenStorage, $translator);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getTokenExtractor()
|
||||||
|
{
|
||||||
|
return $this->davOnUrlTokenExtractor;
|
||||||
|
}
|
||||||
|
}
|
@@ -57,6 +57,62 @@ final class StoredObjectManager implements StoredObjectManagerInterface
|
|||||||
return $this->extractLastModifiedFromResponse($response);
|
return $this->extractLastModifiedFromResponse($response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getContentLength(StoredObject $document): int
|
||||||
|
{
|
||||||
|
if ([] === $document->getKeyInfos()) {
|
||||||
|
if ($this->hasCache($document)) {
|
||||||
|
$response = $this->getResponseFromCache($document);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$response = $this
|
||||||
|
->client
|
||||||
|
->request(
|
||||||
|
Request::METHOD_HEAD,
|
||||||
|
$this
|
||||||
|
->tempUrlGenerator
|
||||||
|
->generate(
|
||||||
|
Request::METHOD_HEAD,
|
||||||
|
$document->getFilename()
|
||||||
|
)
|
||||||
|
->url
|
||||||
|
);
|
||||||
|
} catch (TransportExceptionInterface $exception) {
|
||||||
|
throw StoredObjectManagerException::errorDuringHttpRequest($exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->extractContentLengthFromResponse($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return strlen($this->read($document));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function etag(StoredObject $document): string
|
||||||
|
{
|
||||||
|
if ($this->hasCache($document)) {
|
||||||
|
$response = $this->getResponseFromCache($document);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$response = $this
|
||||||
|
->client
|
||||||
|
->request(
|
||||||
|
Request::METHOD_HEAD,
|
||||||
|
$this
|
||||||
|
->tempUrlGenerator
|
||||||
|
->generate(
|
||||||
|
Request::METHOD_HEAD,
|
||||||
|
$document->getFilename()
|
||||||
|
)
|
||||||
|
->url
|
||||||
|
);
|
||||||
|
} catch (TransportExceptionInterface $exception) {
|
||||||
|
throw StoredObjectManagerException::errorDuringHttpRequest($exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->extractEtagFromResponse($response, $document);
|
||||||
|
}
|
||||||
|
|
||||||
public function read(StoredObject $document): string
|
public function read(StoredObject $document): string
|
||||||
{
|
{
|
||||||
$response = $this->getResponseFromCache($document);
|
$response = $this->getResponseFromCache($document);
|
||||||
@@ -104,12 +160,6 @@ final class StoredObjectManager implements StoredObjectManagerInterface
|
|||||||
)
|
)
|
||||||
: $clearContent;
|
: $clearContent;
|
||||||
|
|
||||||
$headers = [];
|
|
||||||
|
|
||||||
if (null !== $document->getDeleteAt()) {
|
|
||||||
$headers['X-Delete-At'] = $document->getDeleteAt()->getTimestamp();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$response = $this
|
$response = $this
|
||||||
->client
|
->client
|
||||||
@@ -124,7 +174,6 @@ final class StoredObjectManager implements StoredObjectManagerInterface
|
|||||||
->url,
|
->url,
|
||||||
[
|
[
|
||||||
'body' => $encryptedContent,
|
'body' => $encryptedContent,
|
||||||
'headers' => $headers,
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
} catch (TransportExceptionInterface $exception) {
|
} catch (TransportExceptionInterface $exception) {
|
||||||
@@ -136,11 +185,6 @@ final class StoredObjectManager implements StoredObjectManagerInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function clearCache(): void
|
|
||||||
{
|
|
||||||
$this->inMemory = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function extractLastModifiedFromResponse(ResponseInterface $response): \DateTimeImmutable
|
private function extractLastModifiedFromResponse(ResponseInterface $response): \DateTimeImmutable
|
||||||
{
|
{
|
||||||
$lastModifiedString = (($response->getHeaders()['last-modified'] ?? [])[0] ?? '');
|
$lastModifiedString = (($response->getHeaders()['last-modified'] ?? [])[0] ?? '');
|
||||||
@@ -158,6 +202,22 @@ final class StoredObjectManager implements StoredObjectManagerInterface
|
|||||||
return $date;
|
return $date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function extractContentLengthFromResponse(ResponseInterface $response): int
|
||||||
|
{
|
||||||
|
return (int) ($response->getHeaders()['content-length'] ?? ['0'])[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractEtagFromResponse(ResponseInterface $response, StoredObject $storedObject): ?string
|
||||||
|
{
|
||||||
|
$etag = ($response->getHeaders()['etag'] ?? [''])[0];
|
||||||
|
|
||||||
|
if ('' === $etag) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $etag;
|
||||||
|
}
|
||||||
|
|
||||||
private function fillCache(StoredObject $document): void
|
private function fillCache(StoredObject $document): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
@@ -12,20 +12,19 @@ declare(strict_types=1);
|
|||||||
namespace Chill\DocStoreBundle\Service;
|
namespace Chill\DocStoreBundle\Service;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
|
||||||
|
|
||||||
interface StoredObjectManagerInterface
|
interface StoredObjectManagerInterface
|
||||||
{
|
{
|
||||||
public function getLastModified(StoredObject $document): \DateTimeInterface;
|
public function getLastModified(StoredObject $document): \DateTimeInterface;
|
||||||
|
|
||||||
|
public function getContentLength(StoredObject $document): int;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the content of a StoredObject.
|
* Get the content of a StoredObject.
|
||||||
*
|
*
|
||||||
* @param StoredObject $document the document
|
* @param StoredObject $document the document
|
||||||
*
|
*
|
||||||
* @return string the retrieved content in clear
|
* @return string the retrieved content in clear
|
||||||
*
|
|
||||||
* @throws StoredObjectManagerException if unable to read or decrypt the content
|
|
||||||
*/
|
*/
|
||||||
public function read(StoredObject $document): string;
|
public function read(StoredObject $document): string;
|
||||||
|
|
||||||
@@ -34,10 +33,8 @@ interface StoredObjectManagerInterface
|
|||||||
*
|
*
|
||||||
* @param StoredObject $document the document
|
* @param StoredObject $document the document
|
||||||
* @param $clearContent The content to store in clear
|
* @param $clearContent The content to store in clear
|
||||||
*
|
|
||||||
* @throws StoredObjectManagerException
|
|
||||||
*/
|
*/
|
||||||
public function write(StoredObject $document, string $clearContent): void;
|
public function write(StoredObject $document, string $clearContent): void;
|
||||||
|
|
||||||
public function clearCache(): void;
|
public function etag(StoredObject $document): string;
|
||||||
}
|
}
|
||||||
|
@@ -13,6 +13,9 @@ namespace Chill\DocStoreBundle\Templating;
|
|||||||
|
|
||||||
use ChampsLibres\WopiLib\Contract\Service\Discovery\DiscoveryInterface;
|
use ChampsLibres\WopiLib\Contract\Service\Discovery\DiscoveryInterface;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
|
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
|
||||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
@@ -120,8 +123,12 @@ final readonly class WopiEditTwigExtensionRuntime implements RuntimeExtensionInt
|
|||||||
|
|
||||||
private const TEMPLATE_BUTTON_GROUP = '@ChillDocStore/Button/button_group.html.twig';
|
private const TEMPLATE_BUTTON_GROUP = '@ChillDocStore/Button/button_group.html.twig';
|
||||||
|
|
||||||
public function __construct(private DiscoveryInterface $discovery, private NormalizerInterface $normalizer)
|
public function __construct(
|
||||||
{
|
private DiscoveryInterface $discovery,
|
||||||
|
private NormalizerInterface $normalizer,
|
||||||
|
private JWTDavTokenProviderInterface $davTokenProvider,
|
||||||
|
private UrlGeneratorInterface $urlGenerator,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -132,7 +139,7 @@ final readonly class WopiEditTwigExtensionRuntime implements RuntimeExtensionInt
|
|||||||
*/
|
*/
|
||||||
public function isEditable(StoredObject $document): bool
|
public function isEditable(StoredObject $document): bool
|
||||||
{
|
{
|
||||||
return \in_array($document->getType(), self::SUPPORTED_MIMES, true);
|
return in_array($document->getType(), self::SUPPORTED_MIMES, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,12 +151,26 @@ final readonly class WopiEditTwigExtensionRuntime implements RuntimeExtensionInt
|
|||||||
*/
|
*/
|
||||||
public function renderButtonGroup(Environment $environment, StoredObject $document, ?string $title = null, bool $canEdit = true, array $options = []): string
|
public function renderButtonGroup(Environment $environment, StoredObject $document, ?string $title = null, bool $canEdit = true, array $options = []): string
|
||||||
{
|
{
|
||||||
|
$accessToken = $this->davTokenProvider->createToken(
|
||||||
|
$document,
|
||||||
|
$canEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE
|
||||||
|
);
|
||||||
|
|
||||||
return $environment->render(self::TEMPLATE_BUTTON_GROUP, [
|
return $environment->render(self::TEMPLATE_BUTTON_GROUP, [
|
||||||
'document' => $document,
|
'document' => $document,
|
||||||
'document_json' => $this->normalizer->normalize($document, 'json', [AbstractNormalizer::GROUPS => ['read']]),
|
'document_json' => $this->normalizer->normalize($document, 'json', [AbstractNormalizer::GROUPS => ['read']]),
|
||||||
'title' => $title,
|
'title' => $title,
|
||||||
'can_edit' => $canEdit,
|
'can_edit' => $canEdit,
|
||||||
'options' => [...self::DEFAULT_OPTIONS_TEMPLATE_BUTTON_GROUP, ...$options],
|
'options' => [...self::DEFAULT_OPTIONS_TEMPLATE_BUTTON_GROUP, ...$options],
|
||||||
|
'dav_link' => $this->urlGenerator->generate(
|
||||||
|
'chill_docstore_dav_document_get',
|
||||||
|
[
|
||||||
|
'uuid' => $document->getUuid(),
|
||||||
|
'access_token' => $accessToken,
|
||||||
|
],
|
||||||
|
UrlGeneratorInterface::ABSOLUTE_URL,
|
||||||
|
),
|
||||||
|
'dav_link_expiration' => $this->davTokenProvider->getTokenExpiration($accessToken)->format('U'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,410 @@
|
|||||||
|
<?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\Tests\Controller;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Controller\WebdavController;
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Ramsey\Uuid\Uuid;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class WebdavControllerTest extends KernelTestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
private \Twig\Environment $engine;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
|
||||||
|
$this->engine = self::$container->get(\Twig\Environment::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildController(): WebdavController
|
||||||
|
{
|
||||||
|
$storedObjectManager = new MockedStoredObjectManager();
|
||||||
|
$security = $this->prophesize(Security::class);
|
||||||
|
$security->isGranted(Argument::in(['EDIT', 'SEE']), Argument::type(StoredObject::class))
|
||||||
|
->willReturn(true);
|
||||||
|
|
||||||
|
return new WebdavController($this->engine, $storedObjectManager, $security->reveal());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildDocument(): StoredObject
|
||||||
|
{
|
||||||
|
$object = (new StoredObject())
|
||||||
|
->setType('application/vnd.oasis.opendocument.text');
|
||||||
|
|
||||||
|
$reflectionObject = new \ReflectionClass($object);
|
||||||
|
$reflectionObjectUuid = $reflectionObject->getProperty('uuid');
|
||||||
|
|
||||||
|
$reflectionObjectUuid->setValue($object, Uuid::fromString('716e6688-4579-4938-acf3-c4ab5856803b'));
|
||||||
|
|
||||||
|
return $object;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGet(): void
|
||||||
|
{
|
||||||
|
$controller = $this->buildController();
|
||||||
|
|
||||||
|
$response = $controller->getDocument($this->buildDocument());
|
||||||
|
|
||||||
|
self::assertEquals(200, $response->getStatusCode());
|
||||||
|
self::assertEquals('abcde', $response->getContent());
|
||||||
|
self::assertContains('etag', $response->headers->keys());
|
||||||
|
self::assertStringContainsString('ab56b4', $response->headers->get('etag'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOptionsOnDocument(): void
|
||||||
|
{
|
||||||
|
$controller = $this->buildController();
|
||||||
|
|
||||||
|
$response = $controller->optionsDocument($this->buildDocument());
|
||||||
|
|
||||||
|
self::assertEquals(200, $response->getStatusCode());
|
||||||
|
self::assertContains('allow', $response->headers->keys());
|
||||||
|
|
||||||
|
foreach (explode(',', 'OPTIONS,GET,HEAD,PROPFIND') as $method) {
|
||||||
|
self::assertStringContainsString($method, $response->headers->get('allow'));
|
||||||
|
}
|
||||||
|
|
||||||
|
self::assertContains('dav', $response->headers->keys());
|
||||||
|
self::assertStringContainsString('1', $response->headers->get('dav'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOptionsOnDirectory(): void
|
||||||
|
{
|
||||||
|
$controller = $this->buildController();
|
||||||
|
|
||||||
|
$response = $controller->optionsDirectory($this->buildDocument());
|
||||||
|
|
||||||
|
self::assertEquals(200, $response->getStatusCode());
|
||||||
|
self::assertContains('allow', $response->headers->keys());
|
||||||
|
|
||||||
|
foreach (explode(',', 'OPTIONS,GET,HEAD,PROPFIND') as $method) {
|
||||||
|
self::assertStringContainsString($method, $response->headers->get('allow'));
|
||||||
|
}
|
||||||
|
|
||||||
|
self::assertContains('dav', $response->headers->keys());
|
||||||
|
self::assertStringContainsString('1', $response->headers->get('dav'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider generateDataPropfindDocument
|
||||||
|
*/
|
||||||
|
public function testPropfindDocument(string $requestContent, int $expectedStatusCode, string $expectedXmlResponse, string $message): void
|
||||||
|
{
|
||||||
|
$controller = $this->buildController();
|
||||||
|
|
||||||
|
$request = new Request([], [], [], [], [], [], $requestContent);
|
||||||
|
$request->setMethod('PROPFIND');
|
||||||
|
$response = $controller->propfindDocument($this->buildDocument(), '1234', $request);
|
||||||
|
|
||||||
|
self::assertEquals($expectedStatusCode, $response->getStatusCode());
|
||||||
|
self::assertContains('content-type', $response->headers->keys());
|
||||||
|
self::assertStringContainsString('text/xml', $response->headers->get('content-type'));
|
||||||
|
self::assertTrue((new \DOMDocument())->loadXML($response->getContent()), $message.' test that the xml response is a valid xml');
|
||||||
|
self::assertXmlStringEqualsXmlString($expectedXmlResponse, $response->getContent(), $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider generateDataPropfindDirectory
|
||||||
|
*/
|
||||||
|
public function testPropfindDirectory(string $requestContent, int $expectedStatusCode, string $expectedXmlResponse, string $message): void
|
||||||
|
{
|
||||||
|
$controller = $this->buildController();
|
||||||
|
|
||||||
|
$request = new Request([], [], [], [], [], [], $requestContent);
|
||||||
|
$request->setMethod('PROPFIND');
|
||||||
|
$request->headers->add(['Depth' => '0']);
|
||||||
|
$response = $controller->propfindDirectory($this->buildDocument(), '1234', $request);
|
||||||
|
|
||||||
|
self::assertEquals($expectedStatusCode, $response->getStatusCode());
|
||||||
|
self::assertContains('content-type', $response->headers->keys());
|
||||||
|
self::assertStringContainsString('text/xml', $response->headers->get('content-type'));
|
||||||
|
self::assertTrue((new \DOMDocument())->loadXML($response->getContent()), $message.' test that the xml response is a valid xml');
|
||||||
|
self::assertXmlStringEqualsXmlString($expectedXmlResponse, $response->getContent(), $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHeadDocument(): void
|
||||||
|
{
|
||||||
|
$controller = $this->buildController();
|
||||||
|
$response = $controller->headDocument($this->buildDocument());
|
||||||
|
|
||||||
|
self::assertEquals(200, $response->getStatusCode());
|
||||||
|
self::assertContains('content-length', $response->headers->keys());
|
||||||
|
self::assertContains('content-type', $response->headers->keys());
|
||||||
|
self::assertContains('etag', $response->headers->keys());
|
||||||
|
self::assertEquals('ab56b4d92b40713acc5af89985d4b786', $response->headers->get('etag'));
|
||||||
|
self::assertEquals('application/vnd.oasis.opendocument.text', $response->headers->get('content-type'));
|
||||||
|
self::assertEquals(5, $response->headers->get('content-length'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function generateDataPropfindDocument(): iterable
|
||||||
|
{
|
||||||
|
$content =
|
||||||
|
<<<'XML'
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<propfind xmlns="DAV:"><prop><resourcetype xmlns="DAV:"/><IsReadOnly xmlns="http://ucb.openoffice.org/dav/props/"/><getcontenttype xmlns="DAV:"/><supportedlock xmlns="DAV:"/></prop></propfind>
|
||||||
|
XML;
|
||||||
|
|
||||||
|
$response =
|
||||||
|
<<<'XML'
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<d:multistatus xmlns:d="DAV:" >
|
||||||
|
<d:response>
|
||||||
|
<d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
|
||||||
|
<d:propstat>
|
||||||
|
<d:prop>
|
||||||
|
<d:resourcetype/>
|
||||||
|
<d:getcontenttype>application/vnd.oasis.opendocument.text</d:getcontenttype>
|
||||||
|
</d:prop>
|
||||||
|
<d:status>HTTP/1.1 200 OK</d:status>
|
||||||
|
</d:propstat>
|
||||||
|
<d:propstat>
|
||||||
|
<d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/">
|
||||||
|
<ns0:IsReadOnly/>
|
||||||
|
</d:prop>
|
||||||
|
<d:status>HTTP/1.1 404 Not Found</d:status>
|
||||||
|
</d:propstat>
|
||||||
|
</d:response>
|
||||||
|
</d:multistatus>
|
||||||
|
XML;
|
||||||
|
|
||||||
|
yield [$content, 207, $response, 'get IsReadOnly and contenttype from server'];
|
||||||
|
|
||||||
|
$content =
|
||||||
|
<<<'XML'
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<propfind xmlns="DAV:">
|
||||||
|
<prop>
|
||||||
|
<IsReadOnly xmlns="http://ucb.openoffice.org/dav/props/"/>
|
||||||
|
</prop>
|
||||||
|
</propfind>
|
||||||
|
XML;
|
||||||
|
|
||||||
|
$response =
|
||||||
|
<<<'XML'
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<d:multistatus xmlns:d="DAV:">
|
||||||
|
<d:response>
|
||||||
|
<d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
|
||||||
|
<d:propstat>
|
||||||
|
<d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/">
|
||||||
|
<ns0:IsReadOnly/>
|
||||||
|
</d:prop>
|
||||||
|
<d:status>HTTP/1.1 404 Not Found</d:status>
|
||||||
|
</d:propstat>
|
||||||
|
</d:response>
|
||||||
|
</d:multistatus>
|
||||||
|
XML;
|
||||||
|
|
||||||
|
yield [$content, 207, $response, 'get property IsReadOnly'];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
<<<'XML'
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<propfind xmlns="DAV:">
|
||||||
|
<prop>
|
||||||
|
<BaseURI xmlns="http://ucb.openoffice.org/dav/props/"/>
|
||||||
|
</prop>
|
||||||
|
</propfind>
|
||||||
|
XML,
|
||||||
|
207,
|
||||||
|
<<<'XML'
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<d:multistatus xmlns:d="DAV:">
|
||||||
|
<d:response>
|
||||||
|
<d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
|
||||||
|
<d:propstat>
|
||||||
|
<d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/">
|
||||||
|
<ns0:BaseURI/>
|
||||||
|
</d:prop>
|
||||||
|
<d:status>HTTP/1.1 404 Not Found</d:status>
|
||||||
|
</d:propstat>
|
||||||
|
</d:response>
|
||||||
|
</d:multistatus>
|
||||||
|
XML,
|
||||||
|
'Test requesting an unknow property',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
<<<'XML'
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<propfind xmlns="DAV:">
|
||||||
|
<prop>
|
||||||
|
<getlastmodified xmlns="DAV:"/>
|
||||||
|
</prop>
|
||||||
|
</propfind>
|
||||||
|
XML,
|
||||||
|
207,
|
||||||
|
<<<'XML'
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<d:multistatus xmlns:d="DAV:">
|
||||||
|
<d:response>
|
||||||
|
<d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
|
||||||
|
<d:propstat>
|
||||||
|
<d:prop>
|
||||||
|
<!-- the date scraped from a webserver is >Sun, 10 Sep 2023 14:10:23 GMT -->
|
||||||
|
<d:getlastmodified>Wed, 13 Sep 2023 14:15:00 +0200</d:getlastmodified>
|
||||||
|
</d:prop>
|
||||||
|
<d:status>HTTP/1.1 200 OK</d:status>
|
||||||
|
</d:propstat>
|
||||||
|
</d:response>
|
||||||
|
</d:multistatus>
|
||||||
|
XML,
|
||||||
|
'test getting the last modified date',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
<<<'XML'
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<propfind xmlns="DAV:">
|
||||||
|
<propname/>
|
||||||
|
</propfind>
|
||||||
|
XML,
|
||||||
|
207,
|
||||||
|
<<<'XML'
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<d:multistatus xmlns:d="DAV:">
|
||||||
|
<d:response>
|
||||||
|
<d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
|
||||||
|
<d:propstat>
|
||||||
|
<d:prop>
|
||||||
|
<d:resourcetype/>
|
||||||
|
<d:creationdate/>
|
||||||
|
<d:getlastmodified>Wed, 13 Sep 2023 14:15:00 +0200</d:getlastmodified>
|
||||||
|
<!-- <d:getcontentlength/> -->
|
||||||
|
<d:getcontentlength>5</d:getcontentlength>
|
||||||
|
<!-- <d:getlastmodified/> -->
|
||||||
|
<d:getetag>"ab56b4d92b40713acc5af89985d4b786"</d:getetag>
|
||||||
|
<!--
|
||||||
|
<d:supportedlock/>
|
||||||
|
<d:lockdiscovery/>
|
||||||
|
-->
|
||||||
|
<!-- <d:getcontenttype/> -->
|
||||||
|
<d:getcontenttype>application/vnd.oasis.opendocument.text</d:getcontenttype>
|
||||||
|
</d:prop>
|
||||||
|
<d:status>HTTP/1.1 200 OK</d:status>
|
||||||
|
</d:propstat>
|
||||||
|
</d:response>
|
||||||
|
</d:multistatus>
|
||||||
|
XML,
|
||||||
|
'test finding all properties',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function generateDataPropfindDirectory(): iterable
|
||||||
|
{
|
||||||
|
yield [
|
||||||
|
<<<'XML'
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<propfind xmlns="DAV:"><prop><resourcetype xmlns="DAV:"/><IsReadOnly xmlns="http://ucb.openoffice.org/dav/props/"/><getcontenttype xmlns="DAV:"/><supportedlock xmlns="DAV:"/></prop></propfind>
|
||||||
|
XML,
|
||||||
|
207,
|
||||||
|
<<<'XML'
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<d:multistatus xmlns:d="DAV:">
|
||||||
|
<d:response>
|
||||||
|
<d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/</d:href>
|
||||||
|
<d:propstat>
|
||||||
|
<d:prop>
|
||||||
|
<d:resourcetype><d:collection/></d:resourcetype>
|
||||||
|
<d:getcontenttype>httpd/unix-directory</d:getcontenttype>
|
||||||
|
<!--
|
||||||
|
<d:supportedlock>
|
||||||
|
<d:lockentry>
|
||||||
|
<d:lockscope><d:exclusive/></d:lockscope>
|
||||||
|
<d:locktype><d:write/></d:locktype>
|
||||||
|
</d:lockentry>
|
||||||
|
<d:lockentry>
|
||||||
|
<d:lockscope><d:shared/></d:lockscope>
|
||||||
|
<d:locktype><d:write/></d:locktype>
|
||||||
|
</d:lockentry>
|
||||||
|
</d:supportedlock>
|
||||||
|
-->
|
||||||
|
</d:prop>
|
||||||
|
<d:status>HTTP/1.1 200 OK</d:status>
|
||||||
|
</d:propstat>
|
||||||
|
<d:propstat>
|
||||||
|
<d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/">
|
||||||
|
<ns0:IsReadOnly/>
|
||||||
|
</d:prop>
|
||||||
|
<d:status>HTTP/1.1 404 Not Found</d:status>
|
||||||
|
</d:propstat>
|
||||||
|
</d:response>
|
||||||
|
</d:multistatus>
|
||||||
|
XML,
|
||||||
|
'test resourceType and IsReadOnly ',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
<<<'XML'
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<propfind xmlns="DAV:"><prop><CreatableContentsInfo xmlns="http://ucb.openoffice.org/dav/props/"/></prop></propfind>
|
||||||
|
XML,
|
||||||
|
207,
|
||||||
|
<<<'XML'
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<d:multistatus xmlns:d="DAV:">
|
||||||
|
<d:response>
|
||||||
|
<d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/</d:href>
|
||||||
|
<d:propstat>
|
||||||
|
<d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/" >
|
||||||
|
<ns0:CreatableContentsInfo/>
|
||||||
|
</d:prop>
|
||||||
|
<d:status>HTTP/1.1 404 Not Found</d:status>
|
||||||
|
</d:propstat>
|
||||||
|
</d:response>
|
||||||
|
</d:multistatus>
|
||||||
|
XML,
|
||||||
|
'test creatableContentsInfo',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockedStoredObjectManager implements StoredObjectManagerInterface
|
||||||
|
{
|
||||||
|
public function getLastModified(StoredObject $document): \DateTimeInterface
|
||||||
|
{
|
||||||
|
return new \DateTimeImmutable('2023-09-13T14:15');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContentLength(StoredObject $document): int
|
||||||
|
{
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function read(StoredObject $document): string
|
||||||
|
{
|
||||||
|
return 'abcde';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function write(StoredObject $document, string $clearContent): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function etag(StoredObject $document): string
|
||||||
|
{
|
||||||
|
return 'ab56b4d92b40713acc5af89985d4b786';
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,134 @@
|
|||||||
|
<?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\Tests\Dav\Request;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Dav\Request\PropfindRequestAnalyzer;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class PropfindRequestAnalyzerTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @dataProvider provideRequestedProperties
|
||||||
|
*/
|
||||||
|
public function testGetRequestedProperties(string $xml, array $expected): void
|
||||||
|
{
|
||||||
|
$analyzer = new PropfindRequestAnalyzer();
|
||||||
|
|
||||||
|
$request = new \DOMDocument();
|
||||||
|
$request->loadXML($xml);
|
||||||
|
$actual = $analyzer->getRequestedProperties($request);
|
||||||
|
|
||||||
|
foreach ($expected as $key => $value) {
|
||||||
|
if ('unknowns' === $key) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::assertArrayHasKey($key, $actual, "Check that key {$key} does exists in list of expected values");
|
||||||
|
self::assertEquals($value, $actual[$key], "Does the value match expected for key {$key}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('unknowns', $expected)) {
|
||||||
|
self::assertEquals(count($expected['unknowns']), count($actual['unknowns']));
|
||||||
|
self::assertEqualsCanonicalizing($expected['unknowns'], $actual['unknowns']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideRequestedProperties(): iterable
|
||||||
|
{
|
||||||
|
yield [
|
||||||
|
<<<'XML'
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<propfind xmlns="DAV:">
|
||||||
|
<prop>
|
||||||
|
<BaseURI xmlns="http://ucb.openoffice.org/dav/props/"/>
|
||||||
|
</prop>
|
||||||
|
</propfind>
|
||||||
|
XML,
|
||||||
|
[
|
||||||
|
'resourceType' => false,
|
||||||
|
'contentType' => false,
|
||||||
|
'lastModified' => false,
|
||||||
|
'creationDate' => false,
|
||||||
|
'contentLength' => false,
|
||||||
|
'etag' => false,
|
||||||
|
'supportedLock' => false,
|
||||||
|
'unknowns' => [
|
||||||
|
['xmlns' => 'http://ucb.openoffice.org/dav/props/', 'prop' => 'BaseURI'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
<<<'XML'
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<propfind xmlns="DAV:">
|
||||||
|
<propname/>
|
||||||
|
</propfind>
|
||||||
|
XML,
|
||||||
|
[
|
||||||
|
'resourceType' => true,
|
||||||
|
'contentType' => true,
|
||||||
|
'lastModified' => true,
|
||||||
|
'creationDate' => true,
|
||||||
|
'contentLength' => true,
|
||||||
|
'etag' => true,
|
||||||
|
'supportedLock' => true,
|
||||||
|
'unknowns' => [],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
<<<'XML'
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<propfind xmlns="DAV:">
|
||||||
|
<prop>
|
||||||
|
<getlastmodified xmlns="DAV:"/>
|
||||||
|
</prop>
|
||||||
|
</propfind>
|
||||||
|
XML,
|
||||||
|
[
|
||||||
|
'resourceType' => false,
|
||||||
|
'contentType' => false,
|
||||||
|
'lastModified' => true,
|
||||||
|
'creationDate' => false,
|
||||||
|
'contentLength' => false,
|
||||||
|
'etag' => false,
|
||||||
|
'supportedLock' => false,
|
||||||
|
'unknowns' => [],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
<<<'XML'
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<propfind xmlns="DAV:"><prop><resourcetype xmlns="DAV:"/><IsReadOnly xmlns="http://ucb.openoffice.org/dav/props/"/><getcontenttype xmlns="DAV:"/><supportedlock xmlns="DAV:"/></prop></propfind>
|
||||||
|
XML,
|
||||||
|
[
|
||||||
|
'resourceType' => true,
|
||||||
|
'contentType' => true,
|
||||||
|
'lastModified' => false,
|
||||||
|
'creationDate' => false,
|
||||||
|
'contentLength' => false,
|
||||||
|
'etag' => false,
|
||||||
|
'supportedLock' => false,
|
||||||
|
'unknowns' => [
|
||||||
|
['xmlns' => 'http://ucb.openoffice.org/dav/props/', 'prop' => 'IsReadOnly'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,123 @@
|
|||||||
|
<?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\Tests\Security\Authorization;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter;
|
||||||
|
use Chill\DocStoreBundle\Security\Guard\DavTokenAuthenticationEventSubscriber;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class StoredObjectVoterTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideDataVote
|
||||||
|
*/
|
||||||
|
public function testVote(TokenInterface $token, null|object $subject, string $attribute, mixed $expected): void
|
||||||
|
{
|
||||||
|
$voter = new StoredObjectVoter();
|
||||||
|
|
||||||
|
self::assertEquals($expected, $voter->vote($token, $subject, [$attribute]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideDataVote(): iterable
|
||||||
|
{
|
||||||
|
yield [
|
||||||
|
$this->buildToken(StoredObjectRoleEnum::EDIT, new StoredObject()),
|
||||||
|
new \stdClass(),
|
||||||
|
'SOMETHING',
|
||||||
|
VoterInterface::ACCESS_ABSTAIN,
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
$this->buildToken(StoredObjectRoleEnum::EDIT, $so = new StoredObject()),
|
||||||
|
$so,
|
||||||
|
'SOMETHING',
|
||||||
|
VoterInterface::ACCESS_ABSTAIN,
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
$this->buildToken(StoredObjectRoleEnum::EDIT, $so = new StoredObject()),
|
||||||
|
$so,
|
||||||
|
StoredObjectRoleEnum::SEE->value,
|
||||||
|
VoterInterface::ACCESS_GRANTED,
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
$this->buildToken(StoredObjectRoleEnum::EDIT, $so = new StoredObject()),
|
||||||
|
$so,
|
||||||
|
StoredObjectRoleEnum::EDIT->value,
|
||||||
|
VoterInterface::ACCESS_GRANTED,
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
$this->buildToken(StoredObjectRoleEnum::SEE, $so = new StoredObject()),
|
||||||
|
$so,
|
||||||
|
StoredObjectRoleEnum::EDIT->value,
|
||||||
|
VoterInterface::ACCESS_DENIED,
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
$this->buildToken(StoredObjectRoleEnum::SEE, $so = new StoredObject()),
|
||||||
|
$so,
|
||||||
|
StoredObjectRoleEnum::SEE->value,
|
||||||
|
VoterInterface::ACCESS_GRANTED,
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
$this->buildToken(null, null),
|
||||||
|
new StoredObject(),
|
||||||
|
StoredObjectRoleEnum::SEE->value,
|
||||||
|
VoterInterface::ACCESS_DENIED,
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
$this->buildToken(null, null),
|
||||||
|
new StoredObject(),
|
||||||
|
StoredObjectRoleEnum::SEE->value,
|
||||||
|
VoterInterface::ACCESS_DENIED,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildToken(StoredObjectRoleEnum $storedObjectRoleEnum = null, StoredObject $storedObject = null): TokenInterface
|
||||||
|
{
|
||||||
|
$token = $this->prophesize(TokenInterface::class);
|
||||||
|
|
||||||
|
if (null !== $storedObjectRoleEnum) {
|
||||||
|
$token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willReturn(true);
|
||||||
|
$token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willReturn($storedObjectRoleEnum);
|
||||||
|
} else {
|
||||||
|
$token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willReturn(false);
|
||||||
|
$token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willThrow(new \InvalidArgumentException());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $storedObject) {
|
||||||
|
$token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willReturn(true);
|
||||||
|
$token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willReturn($storedObject->getUuid()->toString());
|
||||||
|
} else {
|
||||||
|
$token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willReturn(false);
|
||||||
|
$token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willThrow(new \InvalidArgumentException());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $token->reveal();
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,55 @@
|
|||||||
|
<?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\Tests\Security\Guard;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Security\Guard\DavOnUrlTokenExtractor;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class DavOnUrlTokenExtractorTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideDataUri
|
||||||
|
*/
|
||||||
|
public function testExtract(string $uri, false|string $expected): void
|
||||||
|
{
|
||||||
|
$request = $this->prophesize(Request::class);
|
||||||
|
$request->getRequestUri()->willReturn($uri);
|
||||||
|
|
||||||
|
$extractor = new DavOnUrlTokenExtractor(new NullLogger());
|
||||||
|
|
||||||
|
$actual = $extractor->extract($request->reveal());
|
||||||
|
|
||||||
|
self::assertEquals($expected, $actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @phpstan-pure
|
||||||
|
*/
|
||||||
|
public static function provideDataUri(): iterable
|
||||||
|
{
|
||||||
|
yield ['/dav/123456789/get/d07d2230-5326-11ee-8fd4-93696acf5ea1/d', '123456789'];
|
||||||
|
yield ['/dav/123456789', '123456789'];
|
||||||
|
yield ['/not-dav/123456978', false];
|
||||||
|
yield ['/dav', false];
|
||||||
|
yield ['/', false];
|
||||||
|
}
|
||||||
|
}
|
@@ -9,7 +9,7 @@ declare(strict_types=1);
|
|||||||
* the LICENSE file that was distributed with this source code.
|
* the LICENSE file that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace Chill\DocStoreBundle\Tests\Service;
|
namespace Chill\DocStoreBundle\Tests;
|
||||||
|
|
||||||
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
|
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
@@ -117,41 +117,6 @@ final class StoredObjectManagerTest extends TestCase
|
|||||||
self::assertEquals($clearContent, $storedObjectManager->read($storedObject));
|
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
|
private function getHttpClient(string $encodedContent): HttpClientInterface
|
||||||
{
|
{
|
||||||
$callback = static function ($method, $url, $options) use ($encodedContent) {
|
$callback = static function ($method, $url, $options) use ($encodedContent) {
|
@@ -34,6 +34,11 @@ services:
|
|||||||
autoconfigure: true
|
autoconfigure: true
|
||||||
autowire: true
|
autowire: true
|
||||||
|
|
||||||
|
Chill\DocStoreBundle\Security\:
|
||||||
|
resource: './../Security'
|
||||||
|
autoconfigure: true
|
||||||
|
autowire: true
|
||||||
|
|
||||||
Chill\DocStoreBundle\Serializer\Normalizer\:
|
Chill\DocStoreBundle\Serializer\Normalizer\:
|
||||||
autowire: true
|
autowire: true
|
||||||
resource: '../Serializer/Normalizer/'
|
resource: '../Serializer/Normalizer/'
|
||||||
|
@@ -1,36 +0,0 @@
|
|||||||
<?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');
|
|
||||||
}
|
|
||||||
}
|
|
@@ -17,11 +17,10 @@ use Chill\EventBundle\Form\EventType;
|
|||||||
use Chill\EventBundle\Form\Type\PickEventType;
|
use Chill\EventBundle\Form\Type\PickEventType;
|
||||||
use Chill\EventBundle\Security\Authorization\EventVoter;
|
use Chill\EventBundle\Security\Authorization\EventVoter;
|
||||||
use Chill\MainBundle\Entity\Center;
|
use Chill\MainBundle\Entity\Center;
|
||||||
use Chill\MainBundle\Entity\User;
|
|
||||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
use Chill\PersonBundle\Form\Type\PickPersonDynamicType;
|
use Chill\PersonBundle\Form\Type\PickPersonType;
|
||||||
use Chill\PersonBundle\Privacy\PrivacyEvent;
|
use Chill\PersonBundle\Privacy\PrivacyEvent;
|
||||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
use PhpOffice\PhpSpreadsheet\Writer\Csv;
|
use PhpOffice\PhpSpreadsheet\Writer\Csv;
|
||||||
@@ -38,26 +37,53 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
|||||||
use Symfony\Component\Form\FormFactoryInterface;
|
use Symfony\Component\Form\FormFactoryInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class EventController.
|
* Class EventController.
|
||||||
*/
|
*/
|
||||||
final class EventController extends AbstractController
|
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.
|
* EventController constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly EventDispatcherInterface $eventDispatcher,
|
EventDispatcherInterface $eventDispatcher,
|
||||||
private readonly AuthorizationHelperInterface $authorizationHelper,
|
AuthorizationHelper $authorizationHelper,
|
||||||
private readonly FormFactoryInterface $formFactoryInterface,
|
FormFactoryInterface $formFactoryInterface,
|
||||||
private readonly TranslatorInterface $translator,
|
TranslatorInterface $translator,
|
||||||
private readonly PaginatorFactory $paginator,
|
PaginatorFactory $paginator
|
||||||
private readonly Security $security,
|
|
||||||
) {
|
) {
|
||||||
|
$this->eventDispatcher = $eventDispatcher;
|
||||||
|
$this->authorizationHelper = $authorizationHelper;
|
||||||
|
$this->formFactoryInterface = $formFactoryInterface;
|
||||||
|
$this->translator = $translator;
|
||||||
|
$this->paginator = $paginator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -155,7 +181,7 @@ final class EventController extends AbstractController
|
|||||||
|
|
||||||
$this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person);
|
$this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person);
|
||||||
|
|
||||||
$reachablesCircles = $this->authorizationHelper->getReachableScopes(
|
$reachablesCircles = $this->authorizationHelper->getReachableCircles(
|
||||||
$this->getUser(),
|
$this->getUser(),
|
||||||
EventVoter::SEE,
|
EventVoter::SEE,
|
||||||
$person->getCenter()
|
$person->getCenter()
|
||||||
@@ -207,12 +233,6 @@ final class EventController extends AbstractController
|
|||||||
*/
|
*/
|
||||||
public function newAction(?Center $center, Request $request)
|
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) {
|
if (null === $center) {
|
||||||
$center_id = $request->query->get('center_id');
|
$center_id = $request->query->get('center_id');
|
||||||
$center = $this->getDoctrine()->getRepository(Center::class)->find($center_id);
|
$center = $this->getDoctrine()->getRepository(Center::class)->find($center_id);
|
||||||
@@ -220,7 +240,6 @@ final class EventController extends AbstractController
|
|||||||
|
|
||||||
$entity = new Event();
|
$entity = new Event();
|
||||||
$entity->setCenter($center);
|
$entity->setCenter($center);
|
||||||
$entity->setLocation($user->getCurrentLocation());
|
|
||||||
|
|
||||||
$form = $this->createCreateForm($entity);
|
$form = $this->createCreateForm($entity);
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
@@ -263,7 +282,7 @@ final class EventController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$form = $this->formFactoryInterface
|
$form = $this->formFactoryInterface
|
||||||
->createNamedBuilder('', FormType::class, null, [
|
->createNamedBuilder(null, FormType::class, null, [
|
||||||
'csrf_protection' => false,
|
'csrf_protection' => false,
|
||||||
])
|
])
|
||||||
->setMethod('GET')
|
->setMethod('GET')
|
||||||
@@ -304,7 +323,7 @@ final class EventController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->denyAccessUnlessGranted(
|
$this->denyAccessUnlessGranted(
|
||||||
EventVoter::SEE_DETAILS,
|
'CHILL_EVENT_SEE_DETAILS',
|
||||||
$event,
|
$event,
|
||||||
'You are not allowed to see details on this event'
|
'You are not allowed to see details on this event'
|
||||||
);
|
);
|
||||||
@@ -348,7 +367,7 @@ final class EventController extends AbstractController
|
|||||||
$this->addFlash('success', $this->translator
|
$this->addFlash('success', $this->translator
|
||||||
->trans('The event was updated'));
|
->trans('The event was updated'));
|
||||||
|
|
||||||
return $this->redirectToRoute('chill_event__event_show', ['event_id' => $event_id]);
|
return $this->redirectToRoute('chill_event__event_edit', ['event_id' => $event_id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->render('@ChillEvent/Event/edit.html.twig', [
|
return $this->render('@ChillEvent/Event/edit.html.twig', [
|
||||||
@@ -366,7 +385,7 @@ final class EventController extends AbstractController
|
|||||||
{
|
{
|
||||||
/** @var \Symfony\Component\Form\FormBuilderInterface $builder */
|
/** @var \Symfony\Component\Form\FormBuilderInterface $builder */
|
||||||
$builder = $this
|
$builder = $this
|
||||||
->formFactoryInterface
|
->get('form.factory')
|
||||||
->createNamedBuilder(
|
->createNamedBuilder(
|
||||||
null,
|
null,
|
||||||
FormType::class,
|
FormType::class,
|
||||||
@@ -411,9 +430,11 @@ final class EventController extends AbstractController
|
|||||||
*/
|
*/
|
||||||
protected function createAddParticipationByPersonForm(Event $event)
|
protected function createAddParticipationByPersonForm(Event $event)
|
||||||
{
|
{
|
||||||
$builder = $this->formFactoryInterface
|
/** @var \Symfony\Component\Form\FormBuilderInterface $builder */
|
||||||
|
$builder = $this
|
||||||
|
->get('form.factory')
|
||||||
->createNamedBuilder(
|
->createNamedBuilder(
|
||||||
'',
|
null,
|
||||||
FormType::class,
|
FormType::class,
|
||||||
null,
|
null,
|
||||||
[
|
[
|
||||||
@@ -423,17 +444,22 @@ final class EventController extends AbstractController
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
$builder->add('person_id', PickPersonDynamicType::class, [
|
$builder->add('person_id', PickPersonType::class, [
|
||||||
'as_id' => true,
|
'role' => 'CHILL_EVENT_CREATE',
|
||||||
'multiple' => false,
|
'centers' => $event->getCenter(),
|
||||||
'submit_on_adding_new_entity' => true,
|
|
||||||
'label' => 'Add a participation',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$builder->add('event_id', HiddenType::class, [
|
$builder->add('event_id', HiddenType::class, [
|
||||||
'data' => $event->getId(),
|
'data' => $event->getId(),
|
||||||
]);
|
]);
|
||||||
dump($event->getId());
|
|
||||||
|
$builder->add(
|
||||||
|
'submit',
|
||||||
|
SubmitType::class,
|
||||||
|
[
|
||||||
|
'label' => 'Add a participation',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
return $builder->getForm();
|
return $builder->getForm();
|
||||||
}
|
}
|
||||||
@@ -443,7 +469,7 @@ final class EventController extends AbstractController
|
|||||||
*/
|
*/
|
||||||
protected function createExportByFormatForm()
|
protected function createExportByFormatForm()
|
||||||
{
|
{
|
||||||
$builder = $this->createFormBuilder(['format' => 'xlsx'])
|
$builder = $this->createFormBuilder()
|
||||||
->add('format', ChoiceType::class, [
|
->add('format', ChoiceType::class, [
|
||||||
'choices' => [
|
'choices' => [
|
||||||
'xlsx' => 'xlsx',
|
'xlsx' => 'xlsx',
|
||||||
|
@@ -1,118 +0,0 @@
|
|||||||
<?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,10 +14,7 @@ namespace Chill\EventBundle\Controller;
|
|||||||
use Chill\EventBundle\Entity\Event;
|
use Chill\EventBundle\Entity\Event;
|
||||||
use Chill\EventBundle\Entity\Participation;
|
use Chill\EventBundle\Entity\Participation;
|
||||||
use Chill\EventBundle\Form\ParticipationType;
|
use Chill\EventBundle\Form\ParticipationType;
|
||||||
use Chill\EventBundle\Repository\EventRepository;
|
|
||||||
use Chill\EventBundle\Security\Authorization\ParticipationVoter;
|
use Chill\EventBundle\Security\Authorization\ParticipationVoter;
|
||||||
use Chill\PersonBundle\Repository\PersonRepository;
|
|
||||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
@@ -31,17 +28,13 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
|||||||
/**
|
/**
|
||||||
* Class ParticipationController.
|
* Class ParticipationController.
|
||||||
*/
|
*/
|
||||||
final class ParticipationController extends AbstractController
|
class ParticipationController extends AbstractController
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* ParticipationController constructor.
|
* ParticipationController constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(private readonly LoggerInterface $logger, private readonly TranslatorInterface $translator)
|
||||||
private readonly LoggerInterface $logger,
|
{
|
||||||
private readonly TranslatorInterface $translator,
|
|
||||||
private readonly EventRepository $eventRepository,
|
|
||||||
private readonly PersonRepository $personRepository,
|
|
||||||
) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -237,7 +230,6 @@ final class ParticipationController extends AbstractController
|
|||||||
return $this->render('@ChillEvent/Participation/new.html.twig', [
|
return $this->render('@ChillEvent/Participation/new.html.twig', [
|
||||||
'form' => $form->createView(),
|
'form' => $form->createView(),
|
||||||
'participation' => $participation,
|
'participation' => $participation,
|
||||||
'ignored_participations' => [],
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,7 +539,7 @@ final class ParticipationController extends AbstractController
|
|||||||
* If the request is multiple, the $participation object is cloned.
|
* If the request is multiple, the $participation object is cloned.
|
||||||
* Limitations: the $participation should not be persisted.
|
* Limitations: the $participation should not be persisted.
|
||||||
*
|
*
|
||||||
* @return Participation|list<Participation> return one single participation if $multiple == false
|
* @return Participation|Participation[] return one single participation if $multiple == false
|
||||||
*
|
*
|
||||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException if the event/person is not found
|
* @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
|
* @throws \Symfony\Component\Security\Core\Exception\AccessDeniedException if the user does not have access to event/person
|
||||||
@@ -564,25 +556,30 @@ final class ParticipationController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$event_id = $request->query->getInt('event_id', 0); // sf4 check:
|
$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`
|
||||||
|
|
||||||
$event = $this->eventRepository->find($event_id);
|
if (null !== $event_id) {
|
||||||
|
$event = $em->getRepository(Event::class)
|
||||||
|
->find($event_id);
|
||||||
|
|
||||||
if (null === $event) {
|
if (null === $event) {
|
||||||
throw $this->createNotFoundException('The event with id '.$event_id.' is not found');
|
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->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
|
// this script should be able to handle multiple, so we translate
|
||||||
// single person_id in an array
|
// single person_id in an array
|
||||||
$persons_ids = $request->query->has('person_id') ?
|
$persons_ids = $request->query->has('person_id') ?
|
||||||
[$request->query->get('person_id', 0)]
|
[$request->query->getInt('person_id', 0)] // sf4 check:
|
||||||
|
// prevent error: `Argument 2 passed to ::getInt() must be of the type int, null given`
|
||||||
: explode(',', (string) $request->query->get('persons_ids'));
|
: explode(',', (string) $request->query->get('persons_ids'));
|
||||||
$participations = [];
|
$participations = [];
|
||||||
|
|
||||||
@@ -591,14 +588,15 @@ final class ParticipationController extends AbstractController
|
|||||||
$participation = \count($persons_ids) > 1 ? clone $participation : $participation;
|
$participation = \count($persons_ids) > 1 ? clone $participation : $participation;
|
||||||
|
|
||||||
if (null !== $person_id) {
|
if (null !== $person_id) {
|
||||||
$person = $this->personRepository->find($person_id);
|
$person = $em->getRepository(\Chill\PersonBundle\Entity\Person::class)
|
||||||
|
->find($person_id);
|
||||||
|
|
||||||
if (null === $person) {
|
if (null === $person) {
|
||||||
throw $this->createNotFoundException('The person with id '.$person_id.' is not found');
|
throw $this->createNotFoundException('The person with id '.$person_id.' is not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->denyAccessUnlessGranted(
|
$this->denyAccessUnlessGranted(
|
||||||
PersonVoter::SEE,
|
'CHILL_PERSON_SEE',
|
||||||
$person,
|
$person,
|
||||||
'The user is not allowed to see the person'
|
'The user is not allowed to see the person'
|
||||||
);
|
);
|
||||||
|
@@ -12,7 +12,6 @@ declare(strict_types=1);
|
|||||||
namespace Chill\EventBundle\DependencyInjection;
|
namespace Chill\EventBundle\DependencyInjection;
|
||||||
|
|
||||||
use Chill\EventBundle\Security\Authorization\EventVoter;
|
use Chill\EventBundle\Security\Authorization\EventVoter;
|
||||||
use Chill\EventBundle\Security\Authorization\ParticipationVoter;
|
|
||||||
use Symfony\Component\Config\FileLocator;
|
use Symfony\Component\Config\FileLocator;
|
||||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||||
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
|
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
|
||||||
@@ -34,8 +33,10 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
|
|||||||
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
|
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
|
||||||
$loader->load('services.yaml');
|
$loader->load('services.yaml');
|
||||||
$loader->load('services/authorization.yaml');
|
$loader->load('services/authorization.yaml');
|
||||||
|
$loader->load('services/controller.yaml');
|
||||||
$loader->load('services/fixtures.yaml');
|
$loader->load('services/fixtures.yaml');
|
||||||
$loader->load('services/forms.yaml');
|
$loader->load('services/forms.yaml');
|
||||||
|
$loader->load('services/menu.yaml');
|
||||||
$loader->load('services/repositories.yaml');
|
$loader->load('services/repositories.yaml');
|
||||||
$loader->load('services/search.yaml');
|
$loader->load('services/search.yaml');
|
||||||
$loader->load('services/timeline.yaml');
|
$loader->load('services/timeline.yaml');
|
||||||
@@ -60,8 +61,6 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
|
|||||||
EventVoter::SEE_DETAILS => [EventVoter::SEE],
|
EventVoter::SEE_DETAILS => [EventVoter::SEE],
|
||||||
EventVoter::UPDATE => [EventVoter::SEE_DETAILS],
|
EventVoter::UPDATE => [EventVoter::SEE_DETAILS],
|
||||||
EventVoter::CREATE => [EventVoter::SEE_DETAILS],
|
EventVoter::CREATE => [EventVoter::SEE_DETAILS],
|
||||||
ParticipationVoter::SEE_DETAILS => [ParticipationVoter::SEE],
|
|
||||||
ParticipationVoter::UPDATE => [ParticipationVoter::SEE_DETAILS],
|
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@@ -11,23 +11,15 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\EventBundle\Entity;
|
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\Center;
|
||||||
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
|
|
||||||
use Chill\MainBundle\Entity\HasCenterInterface;
|
use Chill\MainBundle\Entity\HasCenterInterface;
|
||||||
use Chill\MainBundle\Entity\HasScopeInterface;
|
use Chill\MainBundle\Entity\HasScopeInterface;
|
||||||
use Chill\MainBundle\Entity\Location;
|
|
||||||
use Chill\MainBundle\Entity\Scope;
|
use Chill\MainBundle\Entity\Scope;
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Event.
|
* Class Event.
|
||||||
@@ -38,15 +30,10 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||||||
*
|
*
|
||||||
* @ORM\HasLifecycleCallbacks
|
* @ORM\HasLifecycleCallbacks
|
||||||
*/
|
*/
|
||||||
class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInterface, TrackUpdateInterface
|
class Event implements HasCenterInterface, HasScopeInterface
|
||||||
{
|
{
|
||||||
use TrackCreationTrait;
|
|
||||||
use TrackUpdateTrait;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Center")A
|
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Center")
|
||||||
*
|
|
||||||
* @Assert\NotNull()
|
|
||||||
*/
|
*/
|
||||||
private ?Center $center = null;
|
private ?Center $center = null;
|
||||||
|
|
||||||
@@ -76,8 +63,6 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\Column(type="string", length=150)
|
* @ORM\Column(type="string", length=150)
|
||||||
*
|
|
||||||
* @Assert\NotBlank()
|
|
||||||
*/
|
*/
|
||||||
private ?string $name = null;
|
private ?string $name = null;
|
||||||
|
|
||||||
@@ -92,45 +77,15 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\ManyToOne(targetEntity="Chill\EventBundle\Entity\EventType")
|
* @ORM\ManyToOne(targetEntity="Chill\EventBundle\Entity\EventType")
|
||||||
*
|
|
||||||
* @Assert\NotNull()
|
|
||||||
*/
|
*/
|
||||||
private ?EventType $type = null;
|
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.
|
* Event constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->participations = new ArrayCollection();
|
$this->participations = new ArrayCollection();
|
||||||
$this->documents = new ArrayCollection();
|
|
||||||
$this->comment = new CommentEmbeddable();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -145,22 +100,6 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
|
|||||||
return $this;
|
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
|
* @return Center
|
||||||
*/
|
*/
|
||||||
@@ -197,7 +136,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
|
|||||||
return $this->id;
|
return $this->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getModerator(): ?User
|
public function getModerator(): User|null
|
||||||
{
|
{
|
||||||
return $this->moderator;
|
return $this->moderator;
|
||||||
}
|
}
|
||||||
@@ -320,44 +259,4 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
|
|||||||
|
|
||||||
return $this;
|
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,17 +11,13 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\EventBundle\Entity;
|
namespace Chill\EventBundle\Entity;
|
||||||
|
|
||||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
use Chill\MainBundle\Entity\Center;
|
||||||
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\HasCenterInterface;
|
||||||
use Chill\MainBundle\Entity\HasScopeInterface;
|
use Chill\MainBundle\Entity\HasScopeInterface;
|
||||||
use Chill\MainBundle\Entity\Scope;
|
use Chill\MainBundle\Entity\Scope;
|
||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use DateTime;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
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;
|
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,20 +26,12 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
|||||||
* @ORM\Entity(
|
* @ORM\Entity(
|
||||||
* repositoryClass="Chill\EventBundle\Repository\ParticipationRepository")
|
* repositoryClass="Chill\EventBundle\Repository\ParticipationRepository")
|
||||||
*
|
*
|
||||||
* @ORM\Table(name="chill_event_participation", uniqueConstraints={
|
* @ORM\Table(name="chill_event_participation")
|
||||||
*
|
|
||||||
* @ORM\UniqueConstraint(name="chill_event_participation_event_person_unique_idx", columns={"event_id", "person_id"})
|
|
||||||
* })
|
|
||||||
*
|
*
|
||||||
* @ORM\HasLifecycleCallbacks
|
* @ORM\HasLifecycleCallbacks
|
||||||
*
|
|
||||||
* @UniqueEntity({"event", "person"}, message="event.validation.person_already_participate_to_event")
|
|
||||||
*/
|
*/
|
||||||
class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterface, TrackUpdateInterface, TrackCreationInterface
|
class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterface
|
||||||
{
|
{
|
||||||
use TrackCreationTrait;
|
|
||||||
use TrackUpdateTrait;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\ManyToOne(
|
* @ORM\ManyToOne(
|
||||||
* targetEntity="Chill\EventBundle\Entity\Event",
|
* targetEntity="Chill\EventBundle\Entity\Event",
|
||||||
@@ -60,10 +48,13 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
|||||||
*/
|
*/
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Column(type="datetime")
|
||||||
|
*/
|
||||||
|
private ?\DateTime $lastUpdate = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\ManyToOne(targetEntity="Chill\PersonBundle\Entity\Person")
|
* @ORM\ManyToOne(targetEntity="Chill\PersonBundle\Entity\Person")
|
||||||
*
|
|
||||||
* @Assert\NotNull()
|
|
||||||
*/
|
*/
|
||||||
private ?Person $person = null;
|
private ?Person $person = null;
|
||||||
|
|
||||||
@@ -74,11 +65,12 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\ManyToOne(targetEntity="Chill\EventBundle\Entity\Status")
|
* @ORM\ManyToOne(targetEntity="Chill\EventBundle\Entity\Status")
|
||||||
*
|
|
||||||
* @Assert\NotNull()
|
|
||||||
*/
|
*/
|
||||||
private ?Status $status = null;
|
private ?Status $status = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Center
|
||||||
|
*/
|
||||||
public function getCenter()
|
public function getCenter()
|
||||||
{
|
{
|
||||||
if (null === $this->getEvent()) {
|
if (null === $this->getEvent()) {
|
||||||
@@ -91,15 +83,17 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
|||||||
/**
|
/**
|
||||||
* Get event.
|
* Get event.
|
||||||
*/
|
*/
|
||||||
public function getEvent(): ?Event
|
public function getEvent(): Event|null
|
||||||
{
|
{
|
||||||
return $this->event;
|
return $this->event;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get id.
|
* Get id.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
*/
|
*/
|
||||||
public function getId(): int
|
public function getId()
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
}
|
}
|
||||||
@@ -107,11 +101,11 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
|||||||
/**
|
/**
|
||||||
* Get lastUpdate.
|
* Get lastUpdate.
|
||||||
*
|
*
|
||||||
* @return \DateTimeInterface|null
|
* @return \DateTime
|
||||||
*/
|
*/
|
||||||
public function getLastUpdate()
|
public function getLastUpdate()
|
||||||
{
|
{
|
||||||
return $this->getUpdatedAt();
|
return $this->lastUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -127,7 +121,7 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
|||||||
/**
|
/**
|
||||||
* Get role.
|
* Get role.
|
||||||
*/
|
*/
|
||||||
public function getRole(): ?Role
|
public function getRole(): Role|null
|
||||||
{
|
{
|
||||||
return $this->role;
|
return $this->role;
|
||||||
}
|
}
|
||||||
@@ -147,7 +141,7 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
|||||||
/**
|
/**
|
||||||
* Get status.
|
* Get status.
|
||||||
*/
|
*/
|
||||||
public function getStatus(): ?Status
|
public function getStatus(): Status|null
|
||||||
{
|
{
|
||||||
return $this->status;
|
return $this->status;
|
||||||
}
|
}
|
||||||
@@ -241,6 +235,10 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
|||||||
*/
|
*/
|
||||||
public function setEvent(?Event $event = null)
|
public function setEvent(?Event $event = null)
|
||||||
{
|
{
|
||||||
|
if ($this->event !== $event) {
|
||||||
|
$this->update();
|
||||||
|
}
|
||||||
|
|
||||||
$this->event = $event;
|
$this->event = $event;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
@@ -253,6 +251,10 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
|||||||
*/
|
*/
|
||||||
public function setPerson(?Person $person = null)
|
public function setPerson(?Person $person = null)
|
||||||
{
|
{
|
||||||
|
if ($person !== $this->person) {
|
||||||
|
$this->update();
|
||||||
|
}
|
||||||
|
|
||||||
$this->person = $person;
|
$this->person = $person;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
@@ -265,6 +267,9 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
|||||||
*/
|
*/
|
||||||
public function setRole(?Role $role = null)
|
public function setRole(?Role $role = null)
|
||||||
{
|
{
|
||||||
|
if ($role !== $this->role) {
|
||||||
|
$this->update();
|
||||||
|
}
|
||||||
$this->role = $role;
|
$this->role = $role;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
@@ -277,6 +282,10 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
|||||||
*/
|
*/
|
||||||
public function setStatus(?Status $status = null)
|
public function setStatus(?Status $status = null)
|
||||||
{
|
{
|
||||||
|
if ($this->status !== $status) {
|
||||||
|
$this->update();
|
||||||
|
}
|
||||||
|
|
||||||
$this->status = $status;
|
$this->status = $status;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
@@ -286,11 +295,11 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
|||||||
* Set lastUpdate.
|
* Set lastUpdate.
|
||||||
*
|
*
|
||||||
* @return Participation
|
* @return Participation
|
||||||
*
|
|
||||||
* @deprecated
|
|
||||||
*/
|
*/
|
||||||
protected function update()
|
protected function update()
|
||||||
{
|
{
|
||||||
|
$this->lastUpdate = new \DateTime('now');
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,18 +11,12 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\EventBundle\Form;
|
namespace Chill\EventBundle\Form;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
|
||||||
use Chill\DocStoreBundle\Form\StoredObjectType;
|
|
||||||
use Chill\EventBundle\Form\Type\PickEventTypeType;
|
use Chill\EventBundle\Form\Type\PickEventTypeType;
|
||||||
use Chill\MainBundle\Entity\Center;
|
use Chill\MainBundle\Entity\Center;
|
||||||
use Chill\MainBundle\Form\Type\ChillCollectionType;
|
|
||||||
use Chill\MainBundle\Form\Type\ChillDateTimeType;
|
use Chill\MainBundle\Form\Type\ChillDateTimeType;
|
||||||
use Chill\MainBundle\Form\Type\CommentType;
|
|
||||||
use Chill\MainBundle\Form\Type\PickUserLocationType;
|
|
||||||
use Chill\MainBundle\Form\Type\ScopePickerType;
|
use Chill\MainBundle\Form\Type\ScopePickerType;
|
||||||
use Chill\MainBundle\Form\Type\UserPickerType;
|
use Chill\MainBundle\Form\Type\UserPickerType;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
|
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
@@ -53,28 +47,6 @@ class EventType extends AbstractType
|
|||||||
'class' => '',
|
'class' => '',
|
||||||
],
|
],
|
||||||
'required' => false,
|
'required' => false,
|
||||||
])
|
|
||||||
->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 {
|
} else {
|
||||||
$centers = $this->authorizationHelper->getReachableCenters(
|
$centers = $this->authorizationHelper->getReachableCenters(
|
||||||
$user,
|
$user,
|
||||||
$options['role']
|
(string) $options['role']->getRole()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,46 +0,0 @@
|
|||||||
<?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'];
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,142 +0,0 @@
|
|||||||
<?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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,30 +0,0 @@
|
|||||||
<?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;
|
|
||||||
}
|
|
@@ -1,44 +0,0 @@
|
|||||||
<?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();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -14,16 +14,15 @@
|
|||||||
|
|
||||||
{{ 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.moderator) }}
|
||||||
{{ form_row(edit_form.location) }}
|
|
||||||
{{ form_row(edit_form.organizationCost) }}
|
|
||||||
|
|
||||||
{{ form_row(edit_form.comment) }}
|
<ul class="record_actions">
|
||||||
{{ form_row(edit_form.documents) }}
|
|
||||||
|
|
||||||
<ul class="record_actions sticky-form-buttons">
|
|
||||||
<li class="cancel">
|
<li class="cancel">
|
||||||
<a href="{{ chill_return_path_or('chill_event_event_list') }}" class="btn btn-cancel">
|
|
||||||
{{ 'List of events'|trans|chill_return_path_label }}
|
{% 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>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
@@ -24,89 +24,85 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>{{ 'Events participation' |trans }}</h2>
|
<h2>{{ 'Events participation' |trans }}</h2>
|
||||||
|
|
||||||
{% if participations|length == 0 %}
|
<table class="table table-striped table-bordered border-dark align-middle mt-3 events">
|
||||||
<p class="chill-no-data-statement">{{ 'Any participation for this person'|trans }}</p>
|
<thead>
|
||||||
{% else %}
|
<tr>
|
||||||
<table class="table table-striped table-bordered border-dark align-middle mt-3 events">
|
<th class="chill-green">{{ 'Date'|trans }}</th>
|
||||||
<thead>
|
<th class="chill-red">{{ 'Name'|trans }}</th>
|
||||||
<tr>
|
<th class="chill-orange">{{ 'Event type'|trans }}</th>
|
||||||
<th class="chill-green">{{ 'Date'|trans }}</th>
|
<th class="chill-red">{{ 'Role'|trans }}</th>
|
||||||
<th class="chill-red">{{ 'Name'|trans }}</th>
|
<th class="chill-green">{{ 'Status'|trans }}</th>
|
||||||
<th class="chill-orange">{{ 'Event type'|trans }}</th>
|
<th> </th>
|
||||||
<th class="chill-red">{{ 'Role'|trans }}</th>
|
</tr>
|
||||||
<th class="chill-green">{{ 'Status'|trans }}</th>
|
</thead>
|
||||||
<th> </th>
|
<tbody>
|
||||||
</tr>
|
{% for participation in participations %}
|
||||||
</thead>
|
<tr>
|
||||||
<tbody>
|
<td>{{ participation.event.date|format_date('short') }}</td>
|
||||||
{% for participation in participations %}
|
<td>{{ participation.event.name }}</td>
|
||||||
<tr>
|
<td>{{ participation.event.type.name|localize_translatable_string }}</td>
|
||||||
<td>{{ participation.event.date|format_date('short') }}</td>
|
<td>{{ participation.role.name|localize_translatable_string }}</td>
|
||||||
<td>{{ participation.event.name }}</td>
|
<td>{{ participation.status.name|localize_translatable_string }}</td>
|
||||||
<td>{{ participation.event.type.name|localize_translatable_string }}</td>
|
<td>
|
||||||
<td>{{ participation.role.name|localize_translatable_string }}</td>
|
<div class="btn-group" role="group" aria-label="Button group actions">
|
||||||
<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 currentPath = path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) %}
|
||||||
{% set returnLabel = 'Back to %person% events'|trans({ '%person%' : currentPerson } ) %}
|
{% set returnLabel = 'Back to %person% events'|trans({ '%person%' : currentPerson } ) %}
|
||||||
|
|
||||||
{% if is_granted('CHILL_EVENT_SEE_DETAILS', participation.event) %}
|
{% 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 } ) }}"
|
<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 }}">
|
class="btn btn-primary btn-sm" title="{{ 'See details of the event'|trans }}">
|
||||||
<i class="fa fa-fw fa-eye"></i>
|
<i class="fa fa-fw fa-eye"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if is_granted('CHILL_EVENT_UPDATE', participation.event)
|
{% if is_granted('CHILL_EVENT_UPDATE', participation.event)
|
||||||
and is_granted('CHILL_EVENT_PARTICIPATION_UPDATE', participation) %}
|
and is_granted('CHILL_EVENT_PARTICIPATION_UPDATE', participation) %}
|
||||||
|
|
||||||
<div class="btn-group" role="group">
|
<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">
|
<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>
|
<i class="fa fa-pencil"></i>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu" aria-labelledby="dropdownEdit">
|
<ul class="dropdown-menu" aria-labelledby="dropdownEdit">
|
||||||
<li>
|
<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 }) }}"
|
<a href="{{ path('chill_event__event_edit', { 'event_id' : participation.event.id, 'return_path' : currentPath, 'return_label' : returnLabel }) }}"
|
||||||
class="btn btn-warning btn-sm">
|
class="dropdown-item">
|
||||||
{{ 'Edit the event'|trans }}
|
{{ 'Edit the event'|trans }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
</li>
|
||||||
{% if is_granted('CHILL_EVENT_PARTICIPATION_UPDATE', participation) %}
|
<li>
|
||||||
<a href="{{ path('chill_event_participation_edit', { 'participation_id' : participation.id, 'return_path' : currentPath, 'return_label' : returnLabel }) }}"
|
<a href="{{ path('chill_event_participation_edit', { 'participation_id' : participation.id, 'return_path' : currentPath, 'return_label' : returnLabel }) }}"
|
||||||
class="btn btn-warning btn-sm">
|
class="dropdown-item">
|
||||||
{{ 'Edit the participation'|trans }}
|
{{ 'Edit the participation'|trans }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
</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 %}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if participations|length < paginator.getTotalItems %}
|
{% if participations|length < paginator.getTotalItems %}
|
||||||
{{ chill_pagination(paginator) }}
|
{{ chill_pagination(paginator) }}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user