mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-19 13:15:00 +00:00
Compare commits
68 Commits
testing-20
...
2.18.0
Author | SHA1 | Date | |
---|---|---|---|
f3002631ea
|
|||
9e667d4de4 | |||
fc88a5f40d | |||
9ff7aef3fc | |||
4f08019618
|
|||
2a58330832 | |||
a2cea3df02
|
|||
9ac43ecf5b | |||
f78f5e8419
|
|||
ccf3324bc2
|
|||
dfe780f0f5 | |||
dd056efa0d | |||
18c0b6a47f | |||
df0afcd228
|
|||
d66933c8b5 | |||
0ff51b0a5c | |||
d7f4895248
|
|||
7aee722957 | |||
5880858191 | |||
96105b101f | |||
d29415317b | |||
2ad3bbe96f | |||
1d636f5e9e
|
|||
f0dbb17172 | |||
f1dbc17dad | |||
09578a775c
|
|||
c888b5b84f | |||
27d76d9579 | |||
5b714f17be | |||
bbb167bb85 | |||
d713087dcb | |||
569aeeef87 | |||
97f2c75de8 | |||
4a2078dc65 | |||
00444e1e56 | |||
f02c5bca13
|
|||
0d56828ebd
|
|||
8b28667fe5
|
|||
72f73ec8e7
|
|||
b3d1320c94 | |||
2ed42e1a2c
|
|||
d0e5ba16fe | |||
8e65ad9476
|
|||
cf7338b690
|
|||
63dd71037a
|
|||
cc281762b3
|
|||
aa0cadfa84
|
|||
6e2cce9531
|
|||
1fbbf2b2ad
|
|||
e586b8ee5e
|
|||
6d04e477f8
|
|||
6b7b2ae522
|
|||
9b9c2774ad
|
|||
e902b6d409
|
|||
d8bf6a195f
|
|||
7c3152f277
|
|||
cef218fed5
|
|||
930a76cc66
|
|||
f11f7498d7
|
|||
1a9af6b0b1
|
|||
d347f6ae60
|
|||
3bb911b4d0
|
|||
f00b39980c
|
|||
09882bb4be
|
|||
1d21499eab
|
|||
8ef001e67e | |||
458df45fa5 | |||
2b968b9a5b |
@@ -1,6 +0,0 @@
|
|||||||
kind: Feature
|
|
||||||
body: Create new filter for persons having a participation in an accompanying period
|
|
||||||
during a certain time span
|
|
||||||
time: 2023-12-18T15:31:51.489901829+01:00
|
|
||||||
custom:
|
|
||||||
Issue: "231"
|
|
@@ -1,6 +0,0 @@
|
|||||||
kind: Feature
|
|
||||||
body: '[Export][List of accompanyign period] Add two columns: the list of persons
|
|
||||||
participating to the period, and their ids'
|
|
||||||
time: 2024-01-22T12:48:49.824833412+01:00
|
|
||||||
custom:
|
|
||||||
Issue: "241"
|
|
@@ -1,5 +0,0 @@
|
|||||||
kind: Feature
|
|
||||||
body: 'Add capability to generate export about change of steps of accompanying period, and generate exports for this'
|
|
||||||
time: 2024-01-29T13:33:19.190365565+01:00
|
|
||||||
custom:
|
|
||||||
Issue: "244"
|
|
@@ -1,5 +0,0 @@
|
|||||||
kind: Feature
|
|
||||||
body: 'Export: group accompanying period by person participating'
|
|
||||||
time: 2024-02-07T10:39:51.97331052+01:00
|
|
||||||
custom:
|
|
||||||
Issue: "253"
|
|
@@ -1,5 +0,0 @@
|
|||||||
kind: Feature
|
|
||||||
body: 'Export: add filter for courses not linked to a reference address'
|
|
||||||
time: 2024-02-07T11:46:29.491027007+01:00
|
|
||||||
custom:
|
|
||||||
Issue: "243"
|
|
@@ -1,5 +0,0 @@
|
|||||||
kind: Feature
|
|
||||||
body: Allow to group activities linked with accompanying period by reason
|
|
||||||
time: 2024-02-07T16:40:38.408575109+01:00
|
|
||||||
custom:
|
|
||||||
Issue: "229"
|
|
@@ -1,6 +0,0 @@
|
|||||||
kind: Fixed
|
|
||||||
body: Fix error in logs about wrong typing of eventArgs in onEditNotificationComment
|
|
||||||
method
|
|
||||||
time: 2023-11-29T11:31:38.933538592+01:00
|
|
||||||
custom:
|
|
||||||
Issue: "220"
|
|
@@ -1,6 +0,0 @@
|
|||||||
kind: Fixed
|
|
||||||
body: Fix the conditions upon which social actions should be optional or required
|
|
||||||
in relation to social issues within the activity creation form
|
|
||||||
time: 2024-01-30T14:03:01.942955636+01:00
|
|
||||||
custom:
|
|
||||||
Issue: "256"
|
|
15
.changes/v2.16.0.md
Normal file
15
.changes/v2.16.0.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
## v2.16.0 - 2024-02-08
|
||||||
|
### Feature
|
||||||
|
* ([#231](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/231)) Create new filter for persons having a participation in an accompanying period during a certain time span
|
||||||
|
* ([#241](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/241)) [Export][List of accompanyign period] Add two columns: the list of persons participating to the period, and their ids
|
||||||
|
* ([#244](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/244)) Add capability to generate export about change of steps of accompanying period, and generate exports for this
|
||||||
|
* ([#253](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/253)) Export: group accompanying period by person participating
|
||||||
|
* ([#243](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/243)) Export: add filter for courses not linked to a reference address
|
||||||
|
* ([#229](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/229)) Allow to group activities linked with accompanying period by reason
|
||||||
|
* ([#115](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/115)) Prevent social work to be saved when another user edited conccurently the social work
|
||||||
|
* Modernize the event bundle, with some new fields and multiple improvements
|
||||||
|
### Fixed
|
||||||
|
* ([#220](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/220)) Fix error in logs about wrong typing of eventArgs in onEditNotificationComment method
|
||||||
|
* ([#256](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/256)) Fix the conditions upon which social actions should be optional or required in relation to social issues within the activity creation form
|
||||||
|
### UX
|
||||||
|
* ([#260](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/260)) Order list of centers alphabetically in dropdown 'user' section admin.
|
3
.changes/v2.16.1.md
Normal file
3
.changes/v2.16.1.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## v2.16.1 - 2024-02-09
|
||||||
|
### Fixed
|
||||||
|
* Force bootstrap version to avoid error in builds with newer version
|
3
.changes/v2.16.2.md
Normal file
3
.changes/v2.16.2.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## v2.16.2 - 2024-02-21
|
||||||
|
### Fixed
|
||||||
|
* Check for null values in closing motive of parcours d'accompagnement for correct rendering of template
|
5
.changes/v2.16.3.md
Normal file
5
.changes/v2.16.3.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
## v2.16.3 - 2024-02-26
|
||||||
|
### Fixed
|
||||||
|
* ([#236](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/236)) Fix translation of user job -> 'service' must be 'métier'
|
||||||
|
### UX
|
||||||
|
* ([#232](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/232)) Order user jobs and services alphabetically in export filters
|
9
.changes/v2.17.0.md
Normal file
9
.changes/v2.17.0.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
## v2.17.0 - 2024-03-19
|
||||||
|
### Feature
|
||||||
|
* ([#237](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/237)) New export filter for social actions with an evaluation created between two dates
|
||||||
|
* ([#258](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/258)) In the list of accompangying period, add the list of person's centers and the duration of the course
|
||||||
|
* ([#238](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/238)) Allow to customize list person with new fields
|
||||||
|
* ([#159](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/159)) Admin can publish news on the homepage
|
||||||
|
### Fixed
|
||||||
|
* ([#264](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/264)) Fix languages: load the languages in all availables languages configured for Chill
|
||||||
|
* ([#259](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/259)) Keep a consistent behaviour between the filtering of activities within the document generation (model "accompanying period with activities"), and the same filter in the list of activities for an accompanying period
|
5
.changes/v2.18.0.md
Normal file
5
.changes/v2.18.0.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
## v2.18.0 - 2024-03-26
|
||||||
|
### Feature
|
||||||
|
* ([#268](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/268)) Improve admin UX to configure document templates for document generation
|
||||||
|
### Fixed
|
||||||
|
* ([#267](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/267)) Fix the join between job and user in the user list (admin): show only the current user job
|
@@ -23,3 +23,7 @@ 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=0
|
SYMFONY_DEPRECATIONS_HELPER: max[total]=99999999&max[self]=0&max[direct]=0&verbose=1
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- Composer install
|
- Composer install
|
||||||
|
46
CHANGELOG.md
46
CHANGELOG.md
@@ -6,6 +6,52 @@ 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
|
||||||
|
@@ -242,3 +242,129 @@ 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
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
"@symfony/webpack-encore": "^4.1.0",
|
"@symfony/webpack-encore": "^4.1.0",
|
||||||
"@tsconfig/node14": "^1.0.1",
|
"@tsconfig/node14": "^1.0.1",
|
||||||
"bindings": "^1.5.0",
|
"bindings": "^1.5.0",
|
||||||
"bootstrap": "^5.0.1",
|
"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",
|
||||||
|
@@ -80,7 +80,7 @@ final readonly class CreatorJobFilter implements FilterInterface
|
|||||||
{
|
{
|
||||||
$builder
|
$builder
|
||||||
->add('jobs', EntityType::class, [
|
->add('jobs', EntityType::class, [
|
||||||
'choices' => $this->userJobRepository->findAllOrderedByName(),
|
'choices' => $this->userJobRepository->findAllActive(),
|
||||||
'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,6 +15,7 @@ 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;
|
||||||
@@ -26,7 +27,8 @@ 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,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +78,7 @@ 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,6 +16,7 @@ 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;
|
||||||
@@ -27,7 +28,8 @@ 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
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +71,7 @@ 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 \Chill\PersonBundle\Entity\AccompanyingPeriod|null $accompanyingPeriod */
|
/** @var AccompanyingPeriod|null $accompanyingPeriod */
|
||||||
$accompanyingPeriod = null;
|
$accompanyingPeriod = null;
|
||||||
|
|
||||||
if ($options['accompanyingPeriod'] instanceof AccompanyingPeriod) {
|
if ($options['accompanyingPeriod'] instanceof AccompanyingPeriod) {
|
||||||
|
@@ -243,7 +243,8 @@ 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
|
||||||
@@ -283,6 +284,7 @@ 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,6 +11,7 @@ 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;
|
||||||
@@ -112,7 +113,7 @@ class ListActivitiesByAccompanyingPeriodContext implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return list
|
* @return list<Activity>
|
||||||
*/
|
*/
|
||||||
private function filterActivitiesByUser(array $activities, User $user): array
|
private function filterActivitiesByUser(array $activities, User $user): array
|
||||||
{
|
{
|
||||||
@@ -120,6 +121,12 @@ 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);
|
||||||
@@ -129,7 +136,7 @@ class ListActivitiesByAccompanyingPeriodContext implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return list
|
* @return list<AccompanyingPeriod\AccompanyingPeriodWork>
|
||||||
*/
|
*/
|
||||||
private function filterWorksByUser(array $works, User $user): array
|
private function filterWorksByUser(array $works, User $user): array
|
||||||
{
|
{
|
||||||
@@ -216,6 +223,15 @@ 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,6 +91,29 @@ 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
|
||||||
*/
|
*/
|
||||||
@@ -301,7 +324,10 @@ class ActivityACLAwareRepositoryTest extends KernelTestCase
|
|||||||
->getQuery()
|
->getQuery()
|
||||||
->getResult()
|
->getResult()
|
||||||
) {
|
) {
|
||||||
throw new \RuntimeException('no jobs found');
|
$job = new UserJob();
|
||||||
|
$job->setLabel(['fr' => 'test']);
|
||||||
|
$this->entityManager->persist($job);
|
||||||
|
$this->entityManager->flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null === $user = $this->entityManager
|
if (null === $user = $this->entityManager
|
||||||
|
@@ -0,0 +1,139 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\ActivityBundle\Tests\Service\DocGenerator;
|
||||||
|
|
||||||
|
use Chill\ActivityBundle\Entity\Activity;
|
||||||
|
use Chill\ActivityBundle\Service\DocGenerator\ListActivitiesByAccompanyingPeriodContext;
|
||||||
|
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||||
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
|
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class ListActivitiesByAccompanyingPeriodContextTest extends KernelTestCase
|
||||||
|
{
|
||||||
|
private ListActivitiesByAccompanyingPeriodContext $listActivitiesByAccompanyingPeriodContext;
|
||||||
|
private AccompanyingPeriodRepository $accompanyingPeriodRepository;
|
||||||
|
private UserRepositoryInterface $userRepository;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
$this->listActivitiesByAccompanyingPeriodContext = self::$container->get(ListActivitiesByAccompanyingPeriodContext::class);
|
||||||
|
$this->accompanyingPeriodRepository = self::$container->get(AccompanyingPeriodRepository::class);
|
||||||
|
$this->userRepository = self::$container->get(UserRepositoryInterface::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideAccompanyingPeriod
|
||||||
|
*/
|
||||||
|
public function testGetDataWithoutFilteringActivityNorWorks(int $accompanyingPeriodId, int $userId): void
|
||||||
|
{
|
||||||
|
$context = $this->getContext();
|
||||||
|
$template = new DocGeneratorTemplate();
|
||||||
|
$template->setOptions([
|
||||||
|
'mainPerson' => false,
|
||||||
|
'person1' => false,
|
||||||
|
'person2' => false,
|
||||||
|
'thirdParty' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = $context->getData(
|
||||||
|
$template,
|
||||||
|
$this->accompanyingPeriodRepository->find($accompanyingPeriodId),
|
||||||
|
['myActivitiesOnly' => false, 'myWorksOnly' => false]
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertIsArray($data);
|
||||||
|
self::assertArrayHasKey('activities', $data);
|
||||||
|
self::assertIsArray($data['activities']);
|
||||||
|
self::assertGreaterThan(0, count($data['activities']));
|
||||||
|
self::assertIsArray($data['activities'][0]);
|
||||||
|
self::assertArrayHasKey('user', $data['activities'][0]);
|
||||||
|
self::assertIsArray($data['activities'][0]['user']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideAccompanyingPeriod
|
||||||
|
*/
|
||||||
|
public function testGetDataWithoutFilteringActivityByUser(int $accompanyingPeriodId, int $userId): void
|
||||||
|
{
|
||||||
|
$context = $this->getContext();
|
||||||
|
$template = new DocGeneratorTemplate();
|
||||||
|
$template->setOptions([
|
||||||
|
'mainPerson' => false,
|
||||||
|
'person1' => false,
|
||||||
|
'person2' => false,
|
||||||
|
'thirdParty' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = $context->getData(
|
||||||
|
$template,
|
||||||
|
$this->accompanyingPeriodRepository->find($accompanyingPeriodId),
|
||||||
|
['myActivitiesOnly' => true, 'myWorksOnly' => false, 'creator' => $this->userRepository->find($userId)]
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertIsArray($data);
|
||||||
|
self::assertArrayHasKey('activities', $data);
|
||||||
|
self::assertIsArray($data['activities']);
|
||||||
|
self::assertGreaterThan(0, count($data['activities']));
|
||||||
|
self::assertIsArray($data['activities'][0]);
|
||||||
|
self::assertArrayHasKey('user', $data['activities'][0]);
|
||||||
|
self::assertIsArray($data['activities'][0]['user']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function provideAccompanyingPeriod(): array
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
$em = self::$container->get(EntityManagerInterface::class);
|
||||||
|
|
||||||
|
if (null === $period = $em->createQuery('SELECT a FROM '.AccompanyingPeriod::class.' a')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->getSingleResult()) {
|
||||||
|
throw new \RuntimeException('no period found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $user = $em->createQuery('SELECT u FROM '.User::class.' u')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->getSingleResult()
|
||||||
|
) {
|
||||||
|
throw new \RuntimeException('no user found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$activity = new Activity();
|
||||||
|
$activity
|
||||||
|
->setAccompanyingPeriod($period)
|
||||||
|
->setUser($user)
|
||||||
|
->setDate(new \DateTime());
|
||||||
|
|
||||||
|
$em->persist($activity);
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
self::ensureKernelShutdown();
|
||||||
|
|
||||||
|
return [
|
||||||
|
[$period->getId(), $user->getId()],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getContext(): ListActivitiesByAccompanyingPeriodContext
|
||||||
|
{
|
||||||
|
return $this->listActivitiesByAccompanyingPeriodContext;
|
||||||
|
}
|
||||||
|
}
|
@@ -396,7 +396,7 @@ export:
|
|||||||
by_creator_job:
|
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 service du créateur de l'échange: uniquement %jobs%"
|
'Filtered activity by user job: only %jobs%': "Filtré par métier du créateur de l'échange: uniquement %jobs%"
|
||||||
by_persons:
|
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,6 +16,7 @@ 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;
|
||||||
@@ -27,7 +28,8 @@ 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
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +71,7 @@ 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,
|
||||||
|
@@ -15,6 +15,7 @@ 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;
|
||||||
@@ -26,7 +27,8 @@ 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
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +76,7 @@ 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,6 +15,7 @@ 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;
|
||||||
@@ -28,7 +29,8 @@ 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
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +78,7 @@ 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|null
|
public function isUserAbsent(User $user): ?bool
|
||||||
{
|
{
|
||||||
$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|null;
|
public function isUserAbsent(User $user): ?bool;
|
||||||
}
|
}
|
||||||
|
@@ -16,29 +16,42 @@ use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface;
|
|||||||
use Chill\DocGeneratorBundle\Context\Exception\ContextNotFoundException;
|
use Chill\DocGeneratorBundle\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\FileType;
|
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||||
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(private readonly ContextManager $contextManager, private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository, private readonly GeneratorInterface $generator, private readonly MessageBusInterface $messageBus, private readonly PaginatorFactory $paginatorFactory, private readonly EntityManagerInterface $entityManager)
|
public function __construct(
|
||||||
{
|
private readonly ContextManager $contextManager,
|
||||||
|
private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
|
||||||
|
private readonly MessageBusInterface $messageBus,
|
||||||
|
private readonly PaginatorFactory $paginatorFactory,
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly ClockInterface $clock,
|
||||||
|
private readonly Security $security,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -163,9 +176,7 @@ final class DocGeneratorTemplateController extends AbstractController
|
|||||||
throw new NotFoundHttpException(sprintf('Entity with classname %s and id %s is not found', $context->getEntityClass(), $entityId));
|
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
|
||||||
@@ -175,25 +186,39 @@ final class DocGeneratorTemplateController extends AbstractController
|
|||||||
$builder = $this->createFormBuilder(
|
$builder = $this->createFormBuilder(
|
||||||
array_merge(
|
array_merge(
|
||||||
$context->getFormData($template, $entity),
|
$context->getFormData($template, $entity),
|
||||||
$isTest ? ['test_file' => null, 'show_data' => false] : []
|
$isTest ? ['creator' => null, 'dump_only' => false, 'send_result_to' => ''] : []
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
$context->buildPublicForm($builder, $template, $entity);
|
$context->buildPublicForm($builder, $template, $entity);
|
||||||
} else {
|
} else {
|
||||||
$builder = $this->createFormBuilder(
|
$builder = $this->createFormBuilder(
|
||||||
['test_file' => null, 'show_data' => false]
|
['creator' => null, 'show_data' => false, 'send_result_to' => '']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($isTest) {
|
if ($isTest) {
|
||||||
$builder->add('test_file', FileType::class, [
|
$builder->add('dump_only', CheckboxType::class, [
|
||||||
'label' => 'Template file',
|
'label' => 'docgen.Show data instead of generating',
|
||||||
'required' => false,
|
'required' => false,
|
||||||
]);
|
]);
|
||||||
$builder->add('show_data', CheckboxType::class, [
|
$builder->add('send_result_to', EmailType::class, [
|
||||||
'label' => 'Show data instead of generating',
|
'label' => 'docgen.Send report to',
|
||||||
'required' => false,
|
'help' => 'docgen.Send report errors to this email address',
|
||||||
|
'empty_data' => '',
|
||||||
|
'required' => true,
|
||||||
|
'constraints' => [
|
||||||
|
new NotBlank(),
|
||||||
|
new NotNull(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$builder->add('creator', PickUserDynamicType::class, [
|
||||||
|
'label' => 'docgen.Generate as creator',
|
||||||
|
'help' => 'docgen.The document will be generated as the given creator',
|
||||||
|
'multiple' => false,
|
||||||
|
'constraints' => [
|
||||||
|
new NotNull(),
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,8 +229,10 @@ final class DocGeneratorTemplateController extends AbstractController
|
|||||||
} elseif (!$form->isSubmitted() || ($form->isSubmitted() && !$form->isValid())) {
|
} 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, 'form' => $form->createView(),
|
'entity' => $entity,
|
||||||
'template' => $template, 'context' => $context,
|
'form' => $form->createView(),
|
||||||
|
'template' => $template,
|
||||||
|
'context' => $context,
|
||||||
];
|
];
|
||||||
|
|
||||||
return $this->render($templatePath, $templateOptions);
|
return $this->render($templatePath, $templateOptions);
|
||||||
@@ -218,60 +245,57 @@ 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
|
// we store the generated document (associate with the original entity, etc.)
|
||||||
$context
|
// but only if this is not a test
|
||||||
->storeGenerated(
|
if (!$isTest) {
|
||||||
$template,
|
$context
|
||||||
$storedObject,
|
->storeGenerated(
|
||||||
$entity,
|
$template,
|
||||||
$contextGenerationData
|
$storedObject,
|
||||||
);
|
$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(
|
||||||
$this->getUser(),
|
$creator,
|
||||||
$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;
|
private ?int $id = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\Column(type="json")
|
* @ORM\Column(type="json")
|
||||||
|
@@ -14,10 +14,9 @@ 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 ObjectRepository
|
final class DocGeneratorTemplateRepository implements DocGeneratorTemplateRepositoryInterface
|
||||||
{
|
{
|
||||||
private readonly EntityRepository $repository;
|
private readonly EntityRepository $repository;
|
||||||
|
|
||||||
|
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\DocGeneratorBundle\Repository;
|
||||||
|
|
||||||
|
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||||
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ObjectRepository<DocGeneratorTemplate>
|
||||||
|
*/
|
||||||
|
interface DocGeneratorTemplateRepositoryInterface extends ObjectRepository
|
||||||
|
{
|
||||||
|
public function countByEntity(string $entity): int;
|
||||||
|
}
|
@@ -1,5 +1,16 @@
|
|||||||
{% 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 %}
|
||||||
@@ -11,6 +22,47 @@
|
|||||||
{% 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>
|
||||||
@@ -18,7 +70,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', '/')|e('html_attr') }}" />
|
<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="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" />
|
||||||
@@ -27,7 +79,14 @@
|
|||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ chill_path_add_return_path('chill_crud_docgen_template_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'Edit'|trans }}"></a>
|
<ul class="record_actions">
|
||||||
|
<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,18 +6,20 @@
|
|||||||
<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">
|
<div class="container overflow-hidden">
|
||||||
{% for key, context in contexts %}
|
{% for key, context in contexts %}
|
||||||
<div class="row">
|
<div class="row g-3" style="margin-top: 1rem;">
|
||||||
<div class="col-md-4">
|
<div class="col-4 offset-1 text-center">
|
||||||
<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-md-8">
|
<div class="col">
|
||||||
{{ context.description|trans|nl2br }}
|
<div>
|
||||||
|
{{ context.description|trans|nl2br }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{{ creator.label }},
|
{% if creator is not same as null %}{{ creator.label }},{% endif %}
|
||||||
|
|
||||||
{{ 'docgen.failure_email.The generation of the document {template_name} failed'|trans({'{template_name}': template.name|localize_translatable_string}) }}
|
{{ 'docgen.failure_email.The generation of the document %template_name% failed'|trans({'%template_name%': template.name|localize_translatable_string}) }}
|
||||||
|
|
||||||
{{ 'docgen.failure_email.Forward this email to your administrator for solving'|trans }}
|
{{ 'docgen.failure_email.Forward this email to your administrator for solving'|trans }}
|
||||||
|
|
||||||
|
@@ -0,0 +1,7 @@
|
|||||||
|
{{ 'docgen.data_dump_email.Dear'|trans }}
|
||||||
|
|
||||||
|
{{ 'docgen.data_dump_email.data_dump_ready_and_link'|trans }}
|
||||||
|
|
||||||
|
{{ link }}
|
||||||
|
|
||||||
|
{{ 'docgen.data_dump_email.link_valid_until'|trans({validity: validity}) }}
|
@@ -17,54 +17,88 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
|||||||
use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface;
|
use Chill\DocGeneratorBundle\GeneratorDriver\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\ORM\EntityManagerInterface;
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\HttpFoundation\File\File;
|
use Symfony\Component\Yaml\Yaml;
|
||||||
|
|
||||||
class Generator implements GeneratorInterface
|
class Generator implements GeneratorInterface
|
||||||
{
|
{
|
||||||
private const LOG_PREFIX = '[docgen generator] ';
|
private const LOG_PREFIX = '[docgen generator] ';
|
||||||
|
|
||||||
public function __construct(private readonly ContextManagerInterface $contextManager, private readonly DriverInterface $driver, private readonly EntityManagerInterface $entityManager, private readonly LoggerInterface $logger, private readonly StoredObjectManagerInterface $storedObjectManager)
|
public function __construct(
|
||||||
{
|
private readonly ContextManagerInterface $contextManager,
|
||||||
|
private readonly DriverInterface $driver,
|
||||||
|
private readonly ManagerRegistry $objectManagerRegistry,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
private readonly StoredObjectManagerInterface $storedObjectManager
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateDataDump(
|
||||||
|
DocGeneratorTemplate $template,
|
||||||
|
int $entityId,
|
||||||
|
array $contextGenerationDataNormalized,
|
||||||
|
StoredObject $destinationStoredObject,
|
||||||
|
User $creator,
|
||||||
|
bool $clearEntityManagerDuringProcess = true,
|
||||||
|
): StoredObject {
|
||||||
|
return $this->generateFromTemplate(
|
||||||
|
$template,
|
||||||
|
$entityId,
|
||||||
|
$contextGenerationDataNormalized,
|
||||||
|
$destinationStoredObject,
|
||||||
|
$creator,
|
||||||
|
$clearEntityManagerDuringProcess,
|
||||||
|
true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @template T of File|null
|
|
||||||
* @template B of bool
|
|
||||||
*
|
|
||||||
* @param B $isTest
|
|
||||||
* @param (B is true ? T : null) $testFile
|
|
||||||
*
|
|
||||||
* @psalm-return (B is true ? string : null)
|
|
||||||
*
|
|
||||||
* @throws \Symfony\Component\Serializer\Exception\ExceptionInterface|\Throwable
|
|
||||||
*/
|
|
||||||
public function generateDocFromTemplate(
|
public function generateDocFromTemplate(
|
||||||
DocGeneratorTemplate $template,
|
DocGeneratorTemplate $template,
|
||||||
int $entityId,
|
int $entityId,
|
||||||
array $contextGenerationDataNormalized,
|
array $contextGenerationDataNormalized,
|
||||||
?StoredObject $destinationStoredObject = null,
|
StoredObject $destinationStoredObject,
|
||||||
bool $isTest = false,
|
User $creator,
|
||||||
?File $testFile = null,
|
bool $clearEntityManagerDuringProcess = true,
|
||||||
?User $creator = null
|
): StoredObject {
|
||||||
): ?string {
|
return $this->generateFromTemplate(
|
||||||
if ($destinationStoredObject instanceof StoredObject && StoredObject::STATUS_PENDING !== $destinationStoredObject->getStatus()) {
|
$template,
|
||||||
|
$entityId,
|
||||||
|
$contextGenerationDataNormalized,
|
||||||
|
$destinationStoredObject,
|
||||||
|
$creator,
|
||||||
|
$clearEntityManagerDuringProcess,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateFromTemplate(
|
||||||
|
DocGeneratorTemplate $template,
|
||||||
|
int $entityId,
|
||||||
|
array $contextGenerationDataNormalized,
|
||||||
|
StoredObject $destinationStoredObject,
|
||||||
|
User $creator,
|
||||||
|
bool $clearEntityManagerDuringProcess = true,
|
||||||
|
bool $generateDumpOnly = false,
|
||||||
|
): StoredObject {
|
||||||
|
if (StoredObject::STATUS_PENDING !== $destinationStoredObject->getStatus()) {
|
||||||
$this->logger->info(self::LOG_PREFIX.'Aborting generation of an already generated document');
|
$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' => null === $destinationStoredObject ? null : $destinationStoredObject->getId(),
|
'destination_stored_object' => $destinationStoredObject->getId(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$context = $this->contextManager->getContextByDocGeneratorTemplate($template);
|
$context = $this->contextManager->getContextByDocGeneratorTemplate($template);
|
||||||
|
|
||||||
$entity = $this
|
$entity = $this
|
||||||
->entityManager
|
->objectManagerRegistry
|
||||||
|
->getManagerForClass($context->getEntityClass())
|
||||||
->find($context->getEntityClass(), $entityId)
|
->find($context->getEntityClass(), $entityId)
|
||||||
;
|
;
|
||||||
|
|
||||||
@@ -82,17 +116,47 @@ class Generator implements GeneratorInterface
|
|||||||
|
|
||||||
$data = $context->getData($template, $entity, $contextGenerationDataNormalized);
|
$data = $context->getData($template, $entity, $contextGenerationDataNormalized);
|
||||||
|
|
||||||
$destinationStoredObjectId = $destinationStoredObject instanceof StoredObject ? $destinationStoredObject->getId() : null;
|
$destinationStoredObjectId = $destinationStoredObject->getId();
|
||||||
$this->entityManager->clear();
|
|
||||||
gc_collect_cycles();
|
if ($clearEntityManagerDuringProcess) {
|
||||||
if (null !== $destinationStoredObjectId) {
|
// we clean the entity manager
|
||||||
$destinationStoredObject = $this->entityManager->find(StoredObject::class, $destinationStoredObjectId);
|
$this->objectManagerRegistry->getManagerForClass($context->getEntityClass())?->clear();
|
||||||
|
|
||||||
|
// this will force php to clean the memory
|
||||||
|
gc_collect_cycles();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($isTest && ($testFile instanceof File)) {
|
// as we potentially deleted the storedObject from memory, we have to restore it
|
||||||
$templateDecrypted = file_get_contents($testFile->getPathname());
|
$destinationStoredObject = $this->objectManagerRegistry
|
||||||
} else {
|
->getManagerForClass(StoredObject::class)
|
||||||
|
->find(StoredObject::class, $destinationStoredObjectId);
|
||||||
|
|
||||||
|
if ($generateDumpOnly) {
|
||||||
|
$content = Yaml::dump($data, 6);
|
||||||
|
/* @var StoredObject $destinationStoredObject */
|
||||||
|
$destinationStoredObject
|
||||||
|
->setType('application/yaml')
|
||||||
|
->setFilename(sprintf('%s_yaml', uniqid('doc_', true)))
|
||||||
|
->setStatus(StoredObject::STATUS_READY)
|
||||||
|
;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->storedObjectManager->write($destinationStoredObject, $content);
|
||||||
|
} catch (StoredObjectManagerException $e) {
|
||||||
|
$destinationStoredObject->addGenerationErrors($e->getMessage());
|
||||||
|
|
||||||
|
throw new GeneratorException([$e->getMessage()], $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $destinationStoredObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
$templateDecrypted = $this->storedObjectManager->read($template->getFile());
|
$templateDecrypted = $this->storedObjectManager->read($template->getFile());
|
||||||
|
} catch (StoredObjectManagerException $e) {
|
||||||
|
$destinationStoredObject->addGenerationErrors($e->getMessage());
|
||||||
|
|
||||||
|
throw new GeneratorException([$e->getMessage()], $e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -105,19 +169,10 @@ 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())
|
||||||
@@ -125,15 +180,19 @@ class Generator implements GeneratorInterface
|
|||||||
->setStatus(StoredObject::STATUS_READY)
|
->setStatus(StoredObject::STATUS_READY)
|
||||||
;
|
;
|
||||||
|
|
||||||
$this->storedObjectManager->write($destinationStoredObject, $generatedResource);
|
try {
|
||||||
|
$this->storedObjectManager->write($destinationStoredObject, $generatedResource);
|
||||||
|
} catch (StoredObjectManagerException $e) {
|
||||||
|
$destinationStoredObject->addGenerationErrors($e->getMessage());
|
||||||
|
|
||||||
$this->entityManager->flush();
|
throw new GeneratorException([$e->getMessage()], $e);
|
||||||
|
}
|
||||||
|
|
||||||
$this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [
|
$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 null;
|
return $destinationStoredObject;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -13,29 +13,48 @@ 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
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @template T of File|null
|
* Generate a document and store the document on disk.
|
||||||
* @template B of bool
|
|
||||||
*
|
*
|
||||||
* @param B $isTest
|
* The given $destinationStoredObject will be updated with filename, status, and eventually errors will be stored
|
||||||
* @param (B is true ? T : null) $testFile
|
* into the object. The number of generation trial will also be incremented.
|
||||||
*
|
*
|
||||||
* @psalm-return (B is true ? string : null)
|
* This process requires a huge amount of data. For this reason, the entity manager will be cleaned during the process,
|
||||||
|
* unless the paarameter `$clearEntityManagerDuringProcess` is set on false.
|
||||||
*
|
*
|
||||||
* @throws \Symfony\Component\Serializer\Exception\ExceptionInterface|\Throwable
|
* As the entity manager might be cleaned, the new instance of the stored object will be returned by this method.
|
||||||
|
*
|
||||||
|
* Ensure to store change in the database after each generation trial (call `EntityManagerInterface::flush`).
|
||||||
|
*
|
||||||
|
* @phpstan-impure
|
||||||
|
*
|
||||||
|
* @param StoredObject $destinationStoredObject will be update with filename, status and incremented of generation trials
|
||||||
|
*
|
||||||
|
* @throws StoredObjectManagerException if unable to decrypt the template or store the document
|
||||||
*/
|
*/
|
||||||
public function generateDocFromTemplate(
|
public function generateDocFromTemplate(
|
||||||
DocGeneratorTemplate $template,
|
DocGeneratorTemplate $template,
|
||||||
int $entityId,
|
int $entityId,
|
||||||
array $contextGenerationDataNormalized,
|
array $contextGenerationDataNormalized,
|
||||||
?StoredObject $destinationStoredObject = null,
|
StoredObject $destinationStoredObject,
|
||||||
bool $isTest = false,
|
User $creator,
|
||||||
?File $testFile = null,
|
bool $clearEntityManagerDuringProcess = true,
|
||||||
?User $creator = null
|
): StoredObject;
|
||||||
): ?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;
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\DocGeneratorBundle\Service\Messenger;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
||||||
|
use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The OnAfterMessageHandledClearStoredObjectCache class is an event subscriber that clears the stored object cache
|
||||||
|
* after a specific message is handled or fails.
|
||||||
|
*/
|
||||||
|
final readonly class OnAfterMessageHandledClearStoredObjectCache implements EventSubscriberInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private StoredObjectManagerInterface $storedObjectManager,
|
||||||
|
private LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getSubscribedEvents()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
WorkerMessageHandledEvent::class => [
|
||||||
|
['afterHandling', 0],
|
||||||
|
],
|
||||||
|
WorkerMessageFailedEvent::class => [
|
||||||
|
['afterFails', 0],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function afterHandling(WorkerMessageHandledEvent $event): void
|
||||||
|
{
|
||||||
|
if ($event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) {
|
||||||
|
$this->clearStoredObjectCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function afterFails(WorkerMessageFailedEvent $event): void
|
||||||
|
{
|
||||||
|
if ($event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) {
|
||||||
|
$this->clearStoredObjectCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function clearStoredObjectCache(): void
|
||||||
|
{
|
||||||
|
$this->logger->debug('clear the cache after generation of a document');
|
||||||
|
|
||||||
|
$this->storedObjectManager->clearCache();
|
||||||
|
}
|
||||||
|
}
|
@@ -11,10 +11,11 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\DocGeneratorBundle\Service\Messenger;
|
namespace Chill\DocGeneratorBundle\Service\Messenger;
|
||||||
|
|
||||||
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
|
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface;
|
||||||
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\StoredObjectRepository;
|
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
|
||||||
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;
|
||||||
@@ -24,12 +25,22 @@ 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(private DocGeneratorTemplateRepository $docGeneratorTemplateRepository, private EntityManagerInterface $entityManager, private LoggerInterface $logger, private MailerInterface $mailer, private StoredObjectRepository $storedObjectRepository, private TranslatorInterface $translator, private UserRepositoryInterface $userRepository)
|
public function __construct(
|
||||||
{
|
private DocGeneratorTemplateRepositoryInterface $docGeneratorTemplateRepository,
|
||||||
|
private EntityManagerInterface $entityManager,
|
||||||
|
private LoggerInterface $logger,
|
||||||
|
private MailerInterface $mailer,
|
||||||
|
private StoredObjectRepositoryInterface $storedObjectRepository,
|
||||||
|
private TranslatorInterface $translator,
|
||||||
|
private UserRepositoryInterface $userRepository
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getSubscribedEvents()
|
public static function getSubscribedEvents()
|
||||||
@@ -45,13 +56,12 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) {
|
$message = $event->getEnvelope()->getMessage();
|
||||||
|
|
||||||
|
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(),
|
||||||
@@ -79,16 +89,8 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
|
|||||||
|
|
||||||
private function warnCreator(RequestGenerationMessage $message, WorkerMessageFailedEvent $event): void
|
private function warnCreator(RequestGenerationMessage $message, WorkerMessageFailedEvent $event): void
|
||||||
{
|
{
|
||||||
$creatorId = $message->getCreatorId();
|
if (null === $message->getSendResultToEmail() || '' === $message->getSendResultToEmail()) {
|
||||||
|
$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;
|
||||||
}
|
}
|
||||||
@@ -96,7 +98,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->getPrevious() ?? $throwable;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($throwable instanceof GeneratorException) {
|
if ($throwable instanceof GeneratorException) {
|
||||||
@@ -111,8 +113,14 @@ 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($creator->getEmail())
|
->to($message->getSendResultToEmail())
|
||||||
->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,15 +11,21 @@ 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.
|
||||||
@@ -30,8 +36,17 @@ class RequestGenerationHandler implements MessageHandlerInterface
|
|||||||
|
|
||||||
private const LOG_PREFIX = '[docgen message handler] ';
|
private const LOG_PREFIX = '[docgen message handler] ';
|
||||||
|
|
||||||
public function __construct(private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository, private readonly EntityManagerInterface $entityManager, private readonly Generator $generator, private readonly LoggerInterface $logger, private readonly StoredObjectRepository $storedObjectRepository, private readonly UserRepositoryInterface $userRepository)
|
public function __construct(
|
||||||
{
|
private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly Generator $generator,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
private readonly StoredObjectRepository $storedObjectRepository,
|
||||||
|
private readonly UserRepositoryInterface $userRepository,
|
||||||
|
private readonly MailerInterface $mailer,
|
||||||
|
private readonly TempUrlGeneratorInterface $tempUrlGenerator,
|
||||||
|
private readonly TranslatorInterface $translator,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __invoke(RequestGenerationMessage $message)
|
public function __invoke(RequestGenerationMessage $message)
|
||||||
@@ -45,25 +60,59 @@ 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();
|
||||||
|
|
||||||
$this->generator->generateDocFromTemplate(
|
try {
|
||||||
$template,
|
if ($message->isDumpOnly()) {
|
||||||
$message->getEntityId(),
|
$destinationStoredObject = $this->generator->generateDataDump(
|
||||||
$message->getContextGenerationData(),
|
$template,
|
||||||
$destinationStoredObject,
|
$message->getEntityId(),
|
||||||
false,
|
$message->getContextGenerationData(),
|
||||||
null,
|
$destinationStoredObject,
|
||||||
$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(),
|
||||||
@@ -71,4 +120,23 @@ 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,27 +15,33 @@ 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;
|
||||||
|
|
||||||
class RequestGenerationMessage
|
final readonly class RequestGenerationMessage
|
||||||
{
|
{
|
||||||
private readonly int $creatorId;
|
private int $creatorId;
|
||||||
|
|
||||||
private readonly int $templateId;
|
private int $templateId;
|
||||||
|
|
||||||
private readonly int $destinationStoredObjectId;
|
private int $destinationStoredObjectId;
|
||||||
|
|
||||||
private readonly \DateTimeImmutable $createdAt;
|
private \DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
|
private ?string $sendResultToEmail;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
User $creator,
|
User $creator,
|
||||||
DocGeneratorTemplate $template,
|
DocGeneratorTemplate $template,
|
||||||
private readonly int $entityId,
|
private int $entityId,
|
||||||
StoredObject $destinationStoredObject,
|
StoredObject $destinationStoredObject,
|
||||||
private readonly array $contextGenerationData
|
private array $contextGenerationData,
|
||||||
|
private bool $isTest = false,
|
||||||
|
?string $sendResultToEmail = null,
|
||||||
|
private bool $dumpOnly = false,
|
||||||
) {
|
) {
|
||||||
$this->creatorId = $creator->getId();
|
$this->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
|
||||||
@@ -67,4 +73,19 @@ 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,7 +20,9 @@ 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;
|
||||||
@@ -66,7 +68,11 @@ 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()->shouldBeCalled();
|
$entityManager->flush()->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$managerRegistry = $this->prophesize(ManagerRegistry::class);
|
||||||
|
$managerRegistry->getManagerForClass('DummyClass')->willReturn($entityManager->reveal());
|
||||||
|
$managerRegistry->getManagerForClass(StoredObject::class)->willReturn($entityManager->reveal());
|
||||||
|
|
||||||
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
|
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
|
||||||
$storedObjectManager->read($templateStoredObject)->willReturn('template');
|
$storedObjectManager->read($templateStoredObject)->willReturn('template');
|
||||||
@@ -75,7 +81,7 @@ class GeneratorTest extends TestCase
|
|||||||
$generator = new Generator(
|
$generator = new Generator(
|
||||||
$contextManagerInterface->reveal(),
|
$contextManagerInterface->reveal(),
|
||||||
$driver->reveal(),
|
$driver->reveal(),
|
||||||
$entityManager->reveal(),
|
$managerRegistry->reveal(),
|
||||||
new NullLogger(),
|
new NullLogger(),
|
||||||
$storedObjectManager->reveal()
|
$storedObjectManager->reveal()
|
||||||
);
|
);
|
||||||
@@ -84,7 +90,8 @@ class GeneratorTest extends TestCase
|
|||||||
$template,
|
$template,
|
||||||
1,
|
1,
|
||||||
[],
|
[],
|
||||||
$destinationStoredObject
|
$destinationStoredObject,
|
||||||
|
new User()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +102,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(EntityManagerInterface::class)->reveal(),
|
$this->prophesize(ManagerRegistry::class)->reveal(),
|
||||||
new NullLogger(),
|
new NullLogger(),
|
||||||
$this->prophesize(StoredObjectManagerInterface::class)->reveal()
|
$this->prophesize(StoredObjectManagerInterface::class)->reveal()
|
||||||
);
|
);
|
||||||
@@ -108,7 +115,8 @@ class GeneratorTest extends TestCase
|
|||||||
$template,
|
$template,
|
||||||
1,
|
1,
|
||||||
[],
|
[],
|
||||||
$destinationStoredObject
|
$destinationStoredObject,
|
||||||
|
new User()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,10 +144,14 @@ 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(),
|
||||||
$entityManager->reveal(),
|
$managerRegistry->reveal(),
|
||||||
new NullLogger(),
|
new NullLogger(),
|
||||||
$this->prophesize(StoredObjectManagerInterface::class)->reveal()
|
$this->prophesize(StoredObjectManagerInterface::class)->reveal()
|
||||||
);
|
);
|
||||||
@@ -148,7 +160,8 @@ class GeneratorTest extends TestCase
|
|||||||
$template,
|
$template,
|
||||||
1,
|
1,
|
||||||
[],
|
[],
|
||||||
$destinationStoredObject
|
$destinationStoredObject,
|
||||||
|
new User()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\DocGeneratorBundle\tests\Service\Messenger;
|
||||||
|
|
||||||
|
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||||
|
use Chill\DocGeneratorBundle\Service\Messenger\OnAfterMessageHandledClearStoredObjectCache;
|
||||||
|
use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage;
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
use Symfony\Component\Messenger\Envelope;
|
||||||
|
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
||||||
|
use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class OnAfterMessageHandledClearStoredObjectCacheTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
public function testThatNotGenerationMessageDoesNotCallAClearCache(): void
|
||||||
|
{
|
||||||
|
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
|
||||||
|
$storedObjectManager->clearCache()->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$eventSubscriber = $this->buildEventSubscriber($storedObjectManager->reveal());
|
||||||
|
|
||||||
|
$eventSubscriber->afterHandling($this->buildEventSuccess(new \stdClass()));
|
||||||
|
$eventSubscriber->afterFails($this->buildEventFailed(new \stdClass()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThatConcernedEventCallAClearCache(): void
|
||||||
|
{
|
||||||
|
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
|
||||||
|
$storedObjectManager->clearCache()->shouldBeCalledTimes(2);
|
||||||
|
|
||||||
|
$eventSubscriber = $this->buildEventSubscriber($storedObjectManager->reveal());
|
||||||
|
|
||||||
|
$eventSubscriber->afterHandling($this->buildEventSuccess($this->buildRequestGenerationMessage()));
|
||||||
|
$eventSubscriber->afterFails($this->buildEventFailed($this->buildRequestGenerationMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildRequestGenerationMessage(
|
||||||
|
): RequestGenerationMessage {
|
||||||
|
$creator = new User();
|
||||||
|
$creator->setEmail('fake@example.com');
|
||||||
|
|
||||||
|
$class = new \ReflectionClass($creator);
|
||||||
|
$property = $class->getProperty('id');
|
||||||
|
$property->setAccessible(true);
|
||||||
|
$property->setValue($creator, 1);
|
||||||
|
|
||||||
|
$template ??= new DocGeneratorTemplate();
|
||||||
|
$class = new \ReflectionClass($template);
|
||||||
|
$property = $class->getProperty('id');
|
||||||
|
$property->setAccessible(true);
|
||||||
|
$property->setValue($template, 2);
|
||||||
|
|
||||||
|
$destinationStoredObject = new StoredObject();
|
||||||
|
$class = new \ReflectionClass($destinationStoredObject);
|
||||||
|
$property = $class->getProperty('id');
|
||||||
|
$property->setAccessible(true);
|
||||||
|
$property->setValue($destinationStoredObject, 3);
|
||||||
|
|
||||||
|
return new RequestGenerationMessage(
|
||||||
|
$creator,
|
||||||
|
$template,
|
||||||
|
1,
|
||||||
|
$destinationStoredObject,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildEventSubscriber(StoredObjectManagerInterface $storedObjectManager): OnAfterMessageHandledClearStoredObjectCache
|
||||||
|
{
|
||||||
|
return new OnAfterMessageHandledClearStoredObjectCache($storedObjectManager, new NullLogger());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildEventFailed(object $message): WorkerMessageFailedEvent
|
||||||
|
{
|
||||||
|
$envelope = new Envelope($message);
|
||||||
|
|
||||||
|
return new WorkerMessageFailedEvent($envelope, 'testing', new \RuntimeException());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildEventSuccess(object $message): WorkerMessageHandledEvent
|
||||||
|
{
|
||||||
|
$envelope = new Envelope($message);
|
||||||
|
|
||||||
|
return new WorkerMessageHandledEvent($envelope, 'test_receiver');
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,226 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\DocGeneratorBundle\tests\Service\Messenger;
|
||||||
|
|
||||||
|
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||||
|
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface;
|
||||||
|
use Chill\DocGeneratorBundle\Service\Messenger\OnGenerationFails;
|
||||||
|
use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage;
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
|
use Symfony\Component\Messenger\Envelope;
|
||||||
|
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
use Symfony\Component\Mime\RawMessage;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class OnGenerationFailsTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
public function testNotConcernedMessageAreNotHandled(): void
|
||||||
|
{
|
||||||
|
$entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
$entityManager->flush()->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$mailer = $this->prophesize(MailerInterface::class);
|
||||||
|
$mailer->send()->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$eventSubscriber = $this->buildOnGenerationFailsEventSubscriber(
|
||||||
|
entityManager: $entityManager->reveal(),
|
||||||
|
mailer: $mailer->reveal()
|
||||||
|
);
|
||||||
|
|
||||||
|
$event = $this->buildEvent(new \stdClass());
|
||||||
|
|
||||||
|
$eventSubscriber->onMessageFailed($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMessageThatWillBeRetriedAreNotHandled(): void
|
||||||
|
{
|
||||||
|
$storedObject = new StoredObject();
|
||||||
|
|
||||||
|
$entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
$entityManager->flush()->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$mailer = $this->prophesize(MailerInterface::class);
|
||||||
|
$mailer->send()->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$eventSubscriber = $this->buildOnGenerationFailsEventSubscriber(
|
||||||
|
entityManager: $entityManager->reveal(),
|
||||||
|
mailer: $mailer->reveal()
|
||||||
|
);
|
||||||
|
|
||||||
|
$event = $this->buildEvent($this->buildRequestGenerationMessage($storedObject));
|
||||||
|
$event->setForRetry();
|
||||||
|
|
||||||
|
$eventSubscriber->onMessageFailed($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThatANotRetriyableEventWillMarkObjectAsFailed(): void
|
||||||
|
{
|
||||||
|
$storedObject = new StoredObject();
|
||||||
|
|
||||||
|
$entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
$entityManager->flush()->shouldBeCalled();
|
||||||
|
|
||||||
|
$mailer = $this->prophesize(MailerInterface::class);
|
||||||
|
$mailer->send(Argument::type(RawMessage::class), Argument::any())->shouldBeCalled();
|
||||||
|
|
||||||
|
$eventSubscriber = $this->buildOnGenerationFailsEventSubscriber(
|
||||||
|
entityManager: $entityManager->reveal(),
|
||||||
|
mailer: $mailer->reveal(),
|
||||||
|
storedObject: $storedObject
|
||||||
|
);
|
||||||
|
|
||||||
|
$event = $this->buildEvent($this->buildRequestGenerationMessage($storedObject));
|
||||||
|
|
||||||
|
$eventSubscriber->onMessageFailed($event);
|
||||||
|
|
||||||
|
self::assertEquals(StoredObject::STATUS_FAILURE, $storedObject->getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThatANonRetryableEventSendAnEmail(): void
|
||||||
|
{
|
||||||
|
$storedObject = new StoredObject();
|
||||||
|
|
||||||
|
$entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
$entityManager->flush()->shouldBeCalled();
|
||||||
|
|
||||||
|
$mailer = $this->prophesize(MailerInterface::class);
|
||||||
|
$mailer->send(
|
||||||
|
Argument::that(function ($arg): bool {
|
||||||
|
if (!$arg instanceof Email) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($arg->getTo() as $to) {
|
||||||
|
if ('test@test.com' === $to->getAddress()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}),
|
||||||
|
Argument::any()
|
||||||
|
)
|
||||||
|
->shouldBeCalled();
|
||||||
|
|
||||||
|
$eventSubscriber = $this->buildOnGenerationFailsEventSubscriber(
|
||||||
|
entityManager: $entityManager->reveal(),
|
||||||
|
mailer: $mailer->reveal(),
|
||||||
|
storedObject: $storedObject
|
||||||
|
);
|
||||||
|
|
||||||
|
$event = $this->buildEvent($this->buildRequestGenerationMessage($storedObject, sendResultToEmail: 'test@test.com'));
|
||||||
|
|
||||||
|
$eventSubscriber->onMessageFailed($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildRequestGenerationMessage(
|
||||||
|
StoredObject $destinationStoredObject,
|
||||||
|
?User $creator = null,
|
||||||
|
?DocGeneratorTemplate $template = null,
|
||||||
|
array $contextGenerationData = [],
|
||||||
|
bool $isTest = false,
|
||||||
|
?string $sendResultToEmail = null,
|
||||||
|
): RequestGenerationMessage {
|
||||||
|
if (null === $creator) {
|
||||||
|
$creator = new User();
|
||||||
|
$creator->setEmail('fake@example.com');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $creator->getId()) {
|
||||||
|
$class = new \ReflectionClass($creator);
|
||||||
|
$property = $class->getProperty('id');
|
||||||
|
$property->setAccessible(true);
|
||||||
|
$property->setValue($creator, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$template ??= new DocGeneratorTemplate();
|
||||||
|
$class = new \ReflectionClass($template);
|
||||||
|
$property = $class->getProperty('id');
|
||||||
|
$property->setAccessible(true);
|
||||||
|
$property->setValue($template, 2);
|
||||||
|
|
||||||
|
$class = new \ReflectionClass($destinationStoredObject);
|
||||||
|
$property = $class->getProperty('id');
|
||||||
|
$property->setAccessible(true);
|
||||||
|
$property->setValue($destinationStoredObject, 3);
|
||||||
|
|
||||||
|
return new RequestGenerationMessage(
|
||||||
|
$creator,
|
||||||
|
$template,
|
||||||
|
1,
|
||||||
|
$destinationStoredObject,
|
||||||
|
$contextGenerationData,
|
||||||
|
$isTest,
|
||||||
|
$sendResultToEmail
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildOnGenerationFailsEventSubscriber(
|
||||||
|
?StoredObject $storedObject = null,
|
||||||
|
?EntityManagerInterface $entityManager = null,
|
||||||
|
?MailerInterface $mailer = null,
|
||||||
|
): OnGenerationFails {
|
||||||
|
$storedObjectRepository = $this->prophesize(StoredObjectRepositoryInterface::class);
|
||||||
|
$storedObjectRepository->find(Argument::type('int'))->willReturn($storedObject ?? new StoredObject());
|
||||||
|
|
||||||
|
if (null === $entityManager) {
|
||||||
|
$entityManagerProphecy = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $mailer) {
|
||||||
|
$mailerProphecy = $this->prophesize(MailerInterface::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
$translator = $this->prophesize(TranslatorInterface::class);
|
||||||
|
$translator->trans(Argument::type('string'))->will(fn ($args) => $args[0]);
|
||||||
|
|
||||||
|
$userRepository = $this->prophesize(UserRepositoryInterface::class);
|
||||||
|
$userRepository->find(Argument::type('int'))->willReturn(new User());
|
||||||
|
|
||||||
|
$docGeneratorTemplateRepository = $this->prophesize(DocGeneratorTemplateRepositoryInterface::class);
|
||||||
|
$docGeneratorTemplateRepository->find(Argument::type('int'))->willReturn(new DocGeneratorTemplate());
|
||||||
|
|
||||||
|
return new OnGenerationFails(
|
||||||
|
$docGeneratorTemplateRepository->reveal(),
|
||||||
|
$entityManager ?? $entityManagerProphecy->reveal(),
|
||||||
|
new NullLogger(),
|
||||||
|
$mailer ?? $mailerProphecy->reveal(),
|
||||||
|
$storedObjectRepository->reveal(),
|
||||||
|
$translator->reveal(),
|
||||||
|
$userRepository->reveal()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildEvent(object $message): WorkerMessageFailedEvent
|
||||||
|
{
|
||||||
|
$envelope = new Envelope($message);
|
||||||
|
|
||||||
|
return new WorkerMessageFailedEvent($envelope, 'testing', new \RuntimeException());
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,4 @@
|
|||||||
|
docgen:
|
||||||
|
data_dump_email:
|
||||||
|
link_valid_until: >-
|
||||||
|
Ce lien est valide jusqu'au {validity, date, full}, {validity, time, medium}
|
@@ -14,13 +14,31 @@ docgen:
|
|||||||
Doc generation is pending: La génération de ce document est en cours
|
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:
|
||||||
@@ -28,5 +46,4 @@ 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
|
||||||
|
@@ -25,6 +25,11 @@ 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")
|
||||||
@@ -117,6 +122,16 @@ 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
|
||||||
*/
|
*/
|
||||||
@@ -144,6 +159,11 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,4 +323,37 @@ 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,11 +14,10 @@ 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 class StoredObjectRepository implements ObjectRepository
|
final readonly class StoredObjectRepository implements StoredObjectRepositoryInterface
|
||||||
{
|
{
|
||||||
private readonly EntityRepository $repository;
|
private EntityRepository $repository;
|
||||||
|
|
||||||
public function __construct(EntityManagerInterface $entityManager)
|
public function __construct(EntityManagerInterface $entityManager)
|
||||||
{
|
{
|
||||||
|
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\DocStoreBundle\Repository;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ObjectRepository<StoredObject>
|
||||||
|
*/
|
||||||
|
interface StoredObjectRepositoryInterface extends ObjectRepository
|
||||||
|
{
|
||||||
|
}
|
@@ -104,6 +104,12 @@ 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
|
||||||
@@ -118,6 +124,7 @@ final class StoredObjectManager implements StoredObjectManagerInterface
|
|||||||
->url,
|
->url,
|
||||||
[
|
[
|
||||||
'body' => $encryptedContent,
|
'body' => $encryptedContent,
|
||||||
|
'headers' => $headers,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
} catch (TransportExceptionInterface $exception) {
|
} catch (TransportExceptionInterface $exception) {
|
||||||
@@ -129,6 +136,11 @@ 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] ?? '');
|
||||||
|
@@ -12,6 +12,7 @@ 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
|
||||||
{
|
{
|
||||||
@@ -23,6 +24,8 @@ interface StoredObjectManagerInterface
|
|||||||
* @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;
|
||||||
|
|
||||||
@@ -31,6 +34,10 @@ 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;
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
namespace Chill\DocStoreBundle\Tests\Service;
|
||||||
|
|
||||||
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
|
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
@@ -117,6 +117,41 @@ 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) {
|
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\Migrations\DocStore;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20240322100107 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'StoredObject: add deleteAt and generationErrors columns';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object ADD deleteAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object ADD generationErrors TEXT DEFAULT \'\' NOT NULL');
|
||||||
|
$this->addSql('COMMENT ON COLUMN chill_doc.stored_object.deleteAt IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object DROP deleteAt');
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object DROP generationErrors');
|
||||||
|
}
|
||||||
|
}
|
@@ -17,10 +17,11 @@ 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\AuthorizationHelper;
|
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
use Chill\PersonBundle\Form\Type\PickPersonType;
|
use Chill\PersonBundle\Form\Type\PickPersonDynamicType;
|
||||||
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;
|
||||||
@@ -37,53 +38,26 @@ 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.
|
||||||
*/
|
*/
|
||||||
class EventController extends AbstractController
|
final class EventController extends AbstractController
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @var AuthorizationHelper
|
|
||||||
*/
|
|
||||||
protected $authorizationHelper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var EventDispatcherInterface
|
|
||||||
*/
|
|
||||||
protected $eventDispatcher;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var FormFactoryInterface
|
|
||||||
*/
|
|
||||||
protected $formFactoryInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var PaginatorFactory
|
|
||||||
*/
|
|
||||||
protected $paginator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var TranslatorInterface
|
|
||||||
*/
|
|
||||||
protected $translator;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EventController constructor.
|
* EventController constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
EventDispatcherInterface $eventDispatcher,
|
private readonly EventDispatcherInterface $eventDispatcher,
|
||||||
AuthorizationHelper $authorizationHelper,
|
private readonly AuthorizationHelperInterface $authorizationHelper,
|
||||||
FormFactoryInterface $formFactoryInterface,
|
private readonly FormFactoryInterface $formFactoryInterface,
|
||||||
TranslatorInterface $translator,
|
private readonly TranslatorInterface $translator,
|
||||||
PaginatorFactory $paginator
|
private readonly PaginatorFactory $paginator,
|
||||||
|
private readonly Security $security,
|
||||||
) {
|
) {
|
||||||
$this->eventDispatcher = $eventDispatcher;
|
|
||||||
$this->authorizationHelper = $authorizationHelper;
|
|
||||||
$this->formFactoryInterface = $formFactoryInterface;
|
|
||||||
$this->translator = $translator;
|
|
||||||
$this->paginator = $paginator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -181,7 +155,7 @@ class EventController extends AbstractController
|
|||||||
|
|
||||||
$this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person);
|
$this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person);
|
||||||
|
|
||||||
$reachablesCircles = $this->authorizationHelper->getReachableCircles(
|
$reachablesCircles = $this->authorizationHelper->getReachableScopes(
|
||||||
$this->getUser(),
|
$this->getUser(),
|
||||||
EventVoter::SEE,
|
EventVoter::SEE,
|
||||||
$person->getCenter()
|
$person->getCenter()
|
||||||
@@ -233,6 +207,12 @@ 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);
|
||||||
@@ -240,6 +220,7 @@ 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);
|
||||||
@@ -282,7 +263,7 @@ class EventController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$form = $this->formFactoryInterface
|
$form = $this->formFactoryInterface
|
||||||
->createNamedBuilder(null, FormType::class, null, [
|
->createNamedBuilder('', FormType::class, null, [
|
||||||
'csrf_protection' => false,
|
'csrf_protection' => false,
|
||||||
])
|
])
|
||||||
->setMethod('GET')
|
->setMethod('GET')
|
||||||
@@ -323,7 +304,7 @@ class EventController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->denyAccessUnlessGranted(
|
$this->denyAccessUnlessGranted(
|
||||||
'CHILL_EVENT_SEE_DETAILS',
|
EventVoter::SEE_DETAILS,
|
||||||
$event,
|
$event,
|
||||||
'You are not allowed to see details on this event'
|
'You are not allowed to see details on this event'
|
||||||
);
|
);
|
||||||
@@ -367,7 +348,7 @@ 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_edit', ['event_id' => $event_id]);
|
return $this->redirectToRoute('chill_event__event_show', ['event_id' => $event_id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->render('@ChillEvent/Event/edit.html.twig', [
|
return $this->render('@ChillEvent/Event/edit.html.twig', [
|
||||||
@@ -385,7 +366,7 @@ class EventController extends AbstractController
|
|||||||
{
|
{
|
||||||
/** @var \Symfony\Component\Form\FormBuilderInterface $builder */
|
/** @var \Symfony\Component\Form\FormBuilderInterface $builder */
|
||||||
$builder = $this
|
$builder = $this
|
||||||
->get('form.factory')
|
->formFactoryInterface
|
||||||
->createNamedBuilder(
|
->createNamedBuilder(
|
||||||
null,
|
null,
|
||||||
FormType::class,
|
FormType::class,
|
||||||
@@ -430,11 +411,9 @@ class EventController extends AbstractController
|
|||||||
*/
|
*/
|
||||||
protected function createAddParticipationByPersonForm(Event $event)
|
protected function createAddParticipationByPersonForm(Event $event)
|
||||||
{
|
{
|
||||||
/** @var \Symfony\Component\Form\FormBuilderInterface $builder */
|
$builder = $this->formFactoryInterface
|
||||||
$builder = $this
|
|
||||||
->get('form.factory')
|
|
||||||
->createNamedBuilder(
|
->createNamedBuilder(
|
||||||
null,
|
'',
|
||||||
FormType::class,
|
FormType::class,
|
||||||
null,
|
null,
|
||||||
[
|
[
|
||||||
@@ -444,22 +423,17 @@ class EventController extends AbstractController
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
$builder->add('person_id', PickPersonType::class, [
|
$builder->add('person_id', PickPersonDynamicType::class, [
|
||||||
'role' => 'CHILL_EVENT_CREATE',
|
'as_id' => true,
|
||||||
'centers' => $event->getCenter(),
|
'multiple' => false,
|
||||||
|
'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();
|
||||||
}
|
}
|
||||||
@@ -469,7 +443,7 @@ class EventController extends AbstractController
|
|||||||
*/
|
*/
|
||||||
protected function createExportByFormatForm()
|
protected function createExportByFormatForm()
|
||||||
{
|
{
|
||||||
$builder = $this->createFormBuilder()
|
$builder = $this->createFormBuilder(['format' => 'xlsx'])
|
||||||
->add('format', ChoiceType::class, [
|
->add('format', ChoiceType::class, [
|
||||||
'choices' => [
|
'choices' => [
|
||||||
'xlsx' => 'xlsx',
|
'xlsx' => 'xlsx',
|
||||||
|
118
src/Bundle/ChillEventBundle/Controller/EventListController.php
Normal file
118
src/Bundle/ChillEventBundle/Controller/EventListController.php
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\EventBundle\Controller;
|
||||||
|
|
||||||
|
use Chill\EventBundle\Entity\Event;
|
||||||
|
use Chill\EventBundle\Entity\EventType;
|
||||||
|
use Chill\EventBundle\Repository\EventACLAwareRepositoryInterface;
|
||||||
|
use Chill\EventBundle\Repository\EventTypeRepository;
|
||||||
|
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
|
||||||
|
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||||
|
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactory;
|
||||||
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
|
use Chill\PersonBundle\Form\Type\PickPersonDynamicType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||||
|
use Symfony\Component\Form\FormFactoryInterface;
|
||||||
|
use Symfony\Component\Form\FormInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
final readonly class EventListController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private Environment $environment,
|
||||||
|
private EventACLAwareRepositoryInterface $eventACLAwareRepository,
|
||||||
|
private EventTypeRepository $eventTypeRepository,
|
||||||
|
private FilterOrderHelperFactory $filterOrderHelperFactory,
|
||||||
|
private FormFactoryInterface $formFactory,
|
||||||
|
private PaginatorFactoryInterface $paginatorFactory,
|
||||||
|
private TranslatableStringHelperInterface $translatableStringHelper,
|
||||||
|
private UrlGeneratorInterface $urlGenerator,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("{_locale}/event/event/list", name="chill_event_event_list")
|
||||||
|
*/
|
||||||
|
public function __invoke(): Response
|
||||||
|
{
|
||||||
|
$filter = $this->buildFilterOrder();
|
||||||
|
$filterData = [
|
||||||
|
'q' => (string) $filter->getQueryString(),
|
||||||
|
'dates' => $filter->getDateRangeData('dates'),
|
||||||
|
'event_types' => $filter->getEntityChoiceData('event_types'),
|
||||||
|
];
|
||||||
|
$total = $this->eventACLAwareRepository->countAllViewable($filterData);
|
||||||
|
$pagination = $this->paginatorFactory->create($total);
|
||||||
|
$events = $this->eventACLAwareRepository->findAllViewable($filterData, $pagination->getCurrentPageFirstItemNumber(), $pagination->getItemsPerPage());
|
||||||
|
$eventForms = [];
|
||||||
|
foreach ($events as $event) {
|
||||||
|
$eventForms[$event->getId()] = $this->createAddParticipationByPersonForm($event)->createView();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response($this->environment->render(
|
||||||
|
'@ChillEvent/Event/page_list.html.twig',
|
||||||
|
[
|
||||||
|
'events' => $events,
|
||||||
|
'pagination' => $pagination,
|
||||||
|
'eventForms' => $eventForms,
|
||||||
|
'filter' => $filter,
|
||||||
|
]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildFilterOrder(): FilterOrderHelper
|
||||||
|
{
|
||||||
|
$types = $this->eventTypeRepository->findAllActive();
|
||||||
|
|
||||||
|
$builder = $this->filterOrderHelperFactory->create(__METHOD__);
|
||||||
|
$builder
|
||||||
|
->addDateRange('dates', 'event.filter.event_dates')
|
||||||
|
->addSearchBox(['name'])
|
||||||
|
->addEntityChoice('event_types', 'event.filter.event_types', EventType::class, $types, [
|
||||||
|
'choice_label' => fn (EventType $e) => $this->translatableStringHelper->localize($e->getName()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $builder->build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createAddParticipationByPersonForm(Event $event): FormInterface
|
||||||
|
{
|
||||||
|
$builder = $this->formFactory
|
||||||
|
->createNamedBuilder(
|
||||||
|
'',
|
||||||
|
FormType::class,
|
||||||
|
null,
|
||||||
|
[
|
||||||
|
'method' => 'GET',
|
||||||
|
'action' => $this->urlGenerator->generate('chill_event_participation_new'),
|
||||||
|
'csrf_protection' => false,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$builder->add('person_id', PickPersonDynamicType::class, [
|
||||||
|
'as_id' => true,
|
||||||
|
'multiple' => false,
|
||||||
|
'submit_on_adding_new_entity' => true,
|
||||||
|
'label' => 'Add a participation',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$builder->add('event_id', HiddenType::class, [
|
||||||
|
'data' => $event->getId(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $builder->getForm();
|
||||||
|
}
|
||||||
|
}
|
@@ -14,7 +14,10 @@ namespace Chill\EventBundle\Controller;
|
|||||||
use Chill\EventBundle\Entity\Event;
|
use Chill\EventBundle\Entity\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;
|
||||||
@@ -28,13 +31,17 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
|||||||
/**
|
/**
|
||||||
* Class ParticipationController.
|
* Class ParticipationController.
|
||||||
*/
|
*/
|
||||||
class ParticipationController extends AbstractController
|
final class ParticipationController extends AbstractController
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* ParticipationController constructor.
|
* ParticipationController constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct(private readonly LoggerInterface $logger, private readonly TranslatorInterface $translator)
|
public function __construct(
|
||||||
{
|
private readonly LoggerInterface $logger,
|
||||||
|
private readonly TranslatorInterface $translator,
|
||||||
|
private readonly EventRepository $eventRepository,
|
||||||
|
private readonly PersonRepository $personRepository,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -230,6 +237,7 @@ class ParticipationController extends AbstractController
|
|||||||
return $this->render('@ChillEvent/Participation/new.html.twig', [
|
return $this->render('@ChillEvent/Participation/new.html.twig', [
|
||||||
'form' => $form->createView(),
|
'form' => $form->createView(),
|
||||||
'participation' => $participation,
|
'participation' => $participation,
|
||||||
|
'ignored_participations' => [],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,7 +547,7 @@ 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|Participation[] return one single participation if $multiple == false
|
* @return Participation|list<Participation> return one single participation if $multiple == false
|
||||||
*
|
*
|
||||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException if the event/person is not found
|
* @throws \Symfony\Component\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
|
||||||
@@ -556,30 +564,25 @@ 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`
|
|
||||||
|
|
||||||
if (null !== $event_id) {
|
$event = $this->eventRepository->find($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->getInt('person_id', 0)] // sf4 check:
|
[$request->query->get('person_id', 0)]
|
||||||
// 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 = [];
|
||||||
|
|
||||||
@@ -588,15 +591,14 @@ 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 = $em->getRepository(\Chill\PersonBundle\Entity\Person::class)
|
$person = $this->personRepository->find($person_id);
|
||||||
->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(
|
||||||
'CHILL_PERSON_SEE',
|
PersonVoter::SEE,
|
||||||
$person,
|
$person,
|
||||||
'The user is not allowed to see the person'
|
'The user is not allowed to see the person'
|
||||||
);
|
);
|
||||||
|
@@ -12,6 +12,7 @@ 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;
|
||||||
@@ -33,10 +34,8 @@ 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');
|
||||||
@@ -61,6 +60,8 @@ 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,15 +11,23 @@ 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.
|
||||||
@@ -30,10 +38,15 @@ use Doctrine\ORM\Mapping as ORM;
|
|||||||
*
|
*
|
||||||
* @ORM\HasLifecycleCallbacks
|
* @ORM\HasLifecycleCallbacks
|
||||||
*/
|
*/
|
||||||
class Event implements HasCenterInterface, HasScopeInterface
|
class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInterface, TrackUpdateInterface
|
||||||
{
|
{
|
||||||
|
use TrackCreationTrait;
|
||||||
|
use TrackUpdateTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Center")
|
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Center")A
|
||||||
|
*
|
||||||
|
* @Assert\NotNull()
|
||||||
*/
|
*/
|
||||||
private ?Center $center = null;
|
private ?Center $center = null;
|
||||||
|
|
||||||
@@ -63,6 +76,8 @@ class Event implements HasCenterInterface, HasScopeInterface
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\Column(type="string", length=150)
|
* @ORM\Column(type="string", length=150)
|
||||||
|
*
|
||||||
|
* @Assert\NotBlank()
|
||||||
*/
|
*/
|
||||||
private ?string $name = null;
|
private ?string $name = null;
|
||||||
|
|
||||||
@@ -77,15 +92,45 @@ class Event implements HasCenterInterface, HasScopeInterface
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,6 +145,22 @@ class Event implements HasCenterInterface, HasScopeInterface
|
|||||||
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
|
||||||
*/
|
*/
|
||||||
@@ -136,7 +197,7 @@ class Event implements HasCenterInterface, HasScopeInterface
|
|||||||
return $this->id;
|
return $this->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getModerator(): User|null
|
public function getModerator(): ?User
|
||||||
{
|
{
|
||||||
return $this->moderator;
|
return $this->moderator;
|
||||||
}
|
}
|
||||||
@@ -259,4 +320,44 @@ class Event implements HasCenterInterface, HasScopeInterface
|
|||||||
|
|
||||||
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,13 +11,17 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\EventBundle\Entity;
|
namespace Chill\EventBundle\Entity;
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\Center;
|
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||||
|
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||||
|
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||||
|
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
||||||
use Chill\MainBundle\Entity\HasCenterInterface;
|
use Chill\MainBundle\Entity\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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,12 +30,20 @@ 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")
|
* @ORM\Table(name="chill_event_participation", uniqueConstraints={
|
||||||
|
*
|
||||||
|
* @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
|
class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterface, TrackUpdateInterface, TrackCreationInterface
|
||||||
{
|
{
|
||||||
|
use TrackCreationTrait;
|
||||||
|
use TrackUpdateTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\ManyToOne(
|
* @ORM\ManyToOne(
|
||||||
* targetEntity="Chill\EventBundle\Entity\Event",
|
* targetEntity="Chill\EventBundle\Entity\Event",
|
||||||
@@ -48,13 +60,10 @@ 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;
|
||||||
|
|
||||||
@@ -65,12 +74,11 @@ 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()) {
|
||||||
@@ -83,17 +91,15 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
|||||||
/**
|
/**
|
||||||
* Get event.
|
* Get event.
|
||||||
*/
|
*/
|
||||||
public function getEvent(): Event|null
|
public function getEvent(): ?Event
|
||||||
{
|
{
|
||||||
return $this->event;
|
return $this->event;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get id.
|
* Get id.
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
*/
|
||||||
public function getId()
|
public function getId(): int
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
}
|
}
|
||||||
@@ -101,11 +107,11 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
|||||||
/**
|
/**
|
||||||
* Get lastUpdate.
|
* Get lastUpdate.
|
||||||
*
|
*
|
||||||
* @return \DateTime
|
* @return \DateTimeInterface|null
|
||||||
*/
|
*/
|
||||||
public function getLastUpdate()
|
public function getLastUpdate()
|
||||||
{
|
{
|
||||||
return $this->lastUpdate;
|
return $this->getUpdatedAt();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -121,7 +127,7 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
|||||||
/**
|
/**
|
||||||
* Get role.
|
* Get role.
|
||||||
*/
|
*/
|
||||||
public function getRole(): Role|null
|
public function getRole(): ?Role
|
||||||
{
|
{
|
||||||
return $this->role;
|
return $this->role;
|
||||||
}
|
}
|
||||||
@@ -141,7 +147,7 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
|
|||||||
/**
|
/**
|
||||||
* Get status.
|
* Get status.
|
||||||
*/
|
*/
|
||||||
public function getStatus(): Status|null
|
public function getStatus(): ?Status
|
||||||
{
|
{
|
||||||
return $this->status;
|
return $this->status;
|
||||||
}
|
}
|
||||||
@@ -235,10 +241,6 @@ 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;
|
||||||
@@ -251,10 +253,6 @@ 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;
|
||||||
@@ -267,9 +265,6 @@ 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;
|
||||||
@@ -282,10 +277,6 @@ 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;
|
||||||
@@ -295,11 +286,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,12 +11,18 @@ 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;
|
||||||
|
|
||||||
@@ -47,6 +53,28 @@ 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,
|
||||||
(string) $options['role']->getRole()
|
$options['role']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
46
src/Bundle/ChillEventBundle/Menu/SectionMenuBuilder.php
Normal file
46
src/Bundle/ChillEventBundle/Menu/SectionMenuBuilder.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\EventBundle\Menu;
|
||||||
|
|
||||||
|
use Chill\EventBundle\Security\Authorization\EventVoter;
|
||||||
|
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||||
|
use Knp\Menu\MenuItem;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
final readonly class SectionMenuBuilder implements LocalMenuBuilderInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private Security $security,
|
||||||
|
private TranslatorInterface $translator,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildMenu($menuId, MenuItem $menu, array $parameters)
|
||||||
|
{
|
||||||
|
if ($this->security->isGranted(EventVoter::SEE)) {
|
||||||
|
$menu->addChild(
|
||||||
|
$this->translator->trans('Events'),
|
||||||
|
[
|
||||||
|
'route' => 'chill_event_event_list',
|
||||||
|
]
|
||||||
|
)->setExtras([
|
||||||
|
'order' => 250,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getMenuIds(): array
|
||||||
|
{
|
||||||
|
return ['section'];
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,142 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\EventBundle\Repository;
|
||||||
|
|
||||||
|
use Chill\EventBundle\Entity\Event;
|
||||||
|
use Chill\EventBundle\Entity\Participation;
|
||||||
|
use Chill\EventBundle\Security\Authorization\EventVoter;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\NonUniqueResultException;
|
||||||
|
use Doctrine\ORM\NoResultException;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
|
final readonly class EventACLAwareRepository implements EventACLAwareRepositoryInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser,
|
||||||
|
private EntityManagerInterface $entityManager,
|
||||||
|
private Security $security,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws NonUniqueResultException
|
||||||
|
* @throws NoResultException
|
||||||
|
*/
|
||||||
|
public function countAllViewable(array $filters): int
|
||||||
|
{
|
||||||
|
if (!$this->security->getUser() instanceof User) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$qb = $this->buildQueryByAllViewable($filters);
|
||||||
|
$this->addFilters($filters, $qb);
|
||||||
|
|
||||||
|
$qb->select('COUNT(event.id)');
|
||||||
|
|
||||||
|
return $qb->getQuery()->getSingleScalarResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findAllViewable(array $filters, int $offset = 0, int $limit = 50): array
|
||||||
|
{
|
||||||
|
if (!$this->security->getUser() instanceof User) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$qb = $this->buildQueryByAllViewable($filters)->select('event');
|
||||||
|
$this->addFilters($filters, $qb);
|
||||||
|
|
||||||
|
$qb->setFirstResult($offset)->setMaxResults($limit);
|
||||||
|
|
||||||
|
$qb->addOrderBy('event.date', 'DESC');
|
||||||
|
|
||||||
|
return $qb->getQuery()->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function addFilters(array $filters, QueryBuilder $qb): void
|
||||||
|
{
|
||||||
|
if (($filters['q'] ?? '') !== '') {
|
||||||
|
$qb->andWhere('event.name LIKE :content');
|
||||||
|
$qb->setParameter('content', '%'.$filters['q'].'%');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('dates', $filters)) {
|
||||||
|
$dates = $filters['dates'];
|
||||||
|
if (null !== ($dates['from'] ?? null)) {
|
||||||
|
$qb->andWhere('event.date >= :date_from');
|
||||||
|
$qb->setParameter('date_from', $dates['from']);
|
||||||
|
}
|
||||||
|
if (null !== ($dates['to'] ?? null)) {
|
||||||
|
$qb->andWhere('event.date <= :date_to');
|
||||||
|
$qb->setParameter('date_to', $dates['to']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 < count($filters['event_types'] ?? [])) {
|
||||||
|
$qb->andWhere('event.type IN (:event_types)');
|
||||||
|
$qb->setParameter('event_types', $filters['event_types']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildQueryByAllViewable(array $filters): QueryBuilder
|
||||||
|
{
|
||||||
|
$qb = $this->entityManager->createQueryBuilder();
|
||||||
|
$qb->from(Event::class, 'event');
|
||||||
|
|
||||||
|
$aclConditions = $qb->expr()->orX();
|
||||||
|
|
||||||
|
$i = 0;
|
||||||
|
foreach ($this->authorizationHelperForCurrentUser->getReachableCenters(EventVoter::SEE) as $center) {
|
||||||
|
foreach ($this->authorizationHelperForCurrentUser->getReachableScopes(EventVoter::SEE, $center) as $scopes) {
|
||||||
|
$aclConditions->add(
|
||||||
|
$qb->expr()->andX(
|
||||||
|
'event.circle IN (:scopes_'.$i.')',
|
||||||
|
$qb->expr()->orX(
|
||||||
|
'event.center = :center_'.$i,
|
||||||
|
$qb->expr()->exists(
|
||||||
|
'SELECT 1 FROM '.Participation::class.' participation_'.$i.' JOIN participation_'.$i.'.event event_'.$i.
|
||||||
|
' JOIN '.Person\PersonCenterHistory::class.' person_center_history_'.$i.
|
||||||
|
' WITH IDENTITY(person_center_history_'.$i.'.person) = IDENTITY(participation_'.$i.'.person) '.
|
||||||
|
' AND event_'.$i.'.date <= person_center_history_'.$i.'.startDate AND (person_center_history_'.$i.'.endDate IS NULL OR person_center_history_'.$i.'.endDate > event_'.$i.'.date) '.
|
||||||
|
' WHERE participation_'.$i.'.event = event'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$qb->setParameter('scopes_'.$i, $scopes);
|
||||||
|
$qb->setParameter('center_'.$i, $center);
|
||||||
|
|
||||||
|
++$i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 === $i) {
|
||||||
|
$aclConditions->add('FALSE = TRUE');
|
||||||
|
}
|
||||||
|
|
||||||
|
$qb
|
||||||
|
->andWhere(
|
||||||
|
$qb->expr()->orX(
|
||||||
|
'event.createdBy = :user',
|
||||||
|
$aclConditions
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$qb->setParameter('user', $this->security->getUser());
|
||||||
|
|
||||||
|
return $qb;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\EventBundle\Repository;
|
||||||
|
|
||||||
|
use Chill\EventBundle\Entity\Event;
|
||||||
|
use Chill\EventBundle\Entity\EventType;
|
||||||
|
|
||||||
|
interface EventACLAwareRepositoryInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array{q?: string, dates?: array{from?: \DateTimeImmutable|null, to?: \DateTimeImmutable|null}, event_types?: list<EventType>} $filters
|
||||||
|
*/
|
||||||
|
public function countAllViewable(array $filters): int;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{q?: string, dates?: array{from?: \DateTimeImmutable|null, to?: \DateTimeImmutable|null}, event_types?: list<EventType>} $filters
|
||||||
|
*
|
||||||
|
* @return list<Event>
|
||||||
|
*/
|
||||||
|
public function findAllViewable(array $filters, int $offset = 0, int $limit = 50): array;
|
||||||
|
}
|
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\EventBundle\Repository;
|
||||||
|
|
||||||
|
use Chill\EventBundle\Entity\EventType;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<EventType>
|
||||||
|
*/
|
||||||
|
final class EventTypeRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
ManagerRegistry $registry,
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly TranslatorInterface $translator
|
||||||
|
) {
|
||||||
|
parent::__construct($registry, EventType::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<EventType>
|
||||||
|
*/
|
||||||
|
public function findAllActive(): array
|
||||||
|
{
|
||||||
|
$dql = 'SELECT et FROM '.EventType::class.' et WHERE et.active = TRUE ORDER BY JSON_EXTRACT(et.name, :lang)';
|
||||||
|
|
||||||
|
return $this->entityManager->createQuery($dql)
|
||||||
|
->setParameter('lang', $this->translator->getLocale())
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
}
|
@@ -14,15 +14,16 @@
|
|||||||
|
|
||||||
{{ 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) }}
|
||||||
|
|
||||||
<ul class="record_actions">
|
{{ form_row(edit_form.comment) }}
|
||||||
|
{{ 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">
|
||||||
{% set returnPath = app.request.get('return_path') %}
|
{{ 'List of events'|trans|chill_return_path_label }}
|
||||||
{% 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,85 +24,89 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>{{ 'Events participation' |trans }}</h2>
|
<h2>{{ 'Events participation' |trans }}</h2>
|
||||||
|
|
||||||
<table class="table table-striped table-bordered border-dark align-middle mt-3 events">
|
{% if participations|length == 0 %}
|
||||||
<thead>
|
<p class="chill-no-data-statement">{{ 'Any participation for this person'|trans }}</p>
|
||||||
<tr>
|
{% else %}
|
||||||
<th class="chill-green">{{ 'Date'|trans }}</th>
|
<table class="table table-striped table-bordered border-dark align-middle mt-3 events">
|
||||||
<th class="chill-red">{{ 'Name'|trans }}</th>
|
<thead>
|
||||||
<th class="chill-orange">{{ 'Event type'|trans }}</th>
|
<tr>
|
||||||
<th class="chill-red">{{ 'Role'|trans }}</th>
|
<th class="chill-green">{{ 'Date'|trans }}</th>
|
||||||
<th class="chill-green">{{ 'Status'|trans }}</th>
|
<th class="chill-red">{{ 'Name'|trans }}</th>
|
||||||
<th> </th>
|
<th class="chill-orange">{{ 'Event type'|trans }}</th>
|
||||||
</tr>
|
<th class="chill-red">{{ 'Role'|trans }}</th>
|
||||||
</thead>
|
<th class="chill-green">{{ 'Status'|trans }}</th>
|
||||||
<tbody>
|
<th> </th>
|
||||||
{% for participation in participations %}
|
</tr>
|
||||||
<tr>
|
</thead>
|
||||||
<td>{{ participation.event.date|format_date('short') }}</td>
|
<tbody>
|
||||||
<td>{{ participation.event.name }}</td>
|
{% for participation in participations %}
|
||||||
<td>{{ participation.event.type.name|localize_translatable_string }}</td>
|
<tr>
|
||||||
<td>{{ participation.role.name|localize_translatable_string }}</td>
|
<td>{{ participation.event.date|format_date('short') }}</td>
|
||||||
<td>{{ participation.status.name|localize_translatable_string }}</td>
|
<td>{{ participation.event.name }}</td>
|
||||||
<td>
|
<td>{{ participation.event.type.name|localize_translatable_string }}</td>
|
||||||
<div class="btn-group" role="group" aria-label="Button group actions">
|
<td>{{ participation.role.name|localize_translatable_string }}</td>
|
||||||
|
<td>{{ participation.status.name|localize_translatable_string }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group" role="group" aria-label="Button group actions">
|
||||||
|
|
||||||
{% set currentPath = path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) %}
|
{% set 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="dropdown-item">
|
class="btn btn-warning btn-sm">
|
||||||
{{ 'Edit the event'|trans }}
|
{{ 'Edit the event'|trans }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
{% endif %}
|
||||||
<li>
|
{% 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 }) }}"
|
<a href="{{ path('chill_event_participation_edit', { 'participation_id' : participation.id, 'return_path' : currentPath, 'return_label' : returnLabel }) }}"
|
||||||
class="dropdown-item">
|
class="btn btn-warning btn-sm">
|
||||||
{{ 'Edit the participation'|trans }}
|
{{ 'Edit the participation'|trans }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
{% endif %}
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
{% if is_granted('CHILL_EVENT_UPDATE', participation.event) %}
|
{% endif %}
|
||||||
<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) }}
|
||||||
|
@@ -1,5 +1,13 @@
|
|||||||
{% extends '@ChillEvent/layout.html.twig' %}
|
{% extends '@ChillEvent/layout.html.twig' %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
{{ encore_entry_script_tags('mod_async_upload') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{{ encore_entry_link_tags('mod_async_upload') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block title 'Event creation'|trans %}
|
{% block title 'Event creation'|trans %}
|
||||||
|
|
||||||
{% block event_content -%}
|
{% block event_content -%}
|
||||||
@@ -14,8 +22,13 @@
|
|||||||
|
|
||||||
{{ form_row(form.type, { 'label': 'Event type' }) }}
|
{{ form_row(form.type, { 'label': 'Event type' }) }}
|
||||||
{{ form_row(form.moderator) }}
|
{{ form_row(form.moderator) }}
|
||||||
|
{{ form_row(form.location) }}
|
||||||
|
{{ form_row(form.organizationCost) }}
|
||||||
|
|
||||||
<ul class="record_actions">
|
{{ form_row(form.comment) }}
|
||||||
|
{{ form_row(form.documents) }}
|
||||||
|
|
||||||
|
<ul class="record_actions sticky-form-buttons">
|
||||||
<li class="cancel">
|
<li class="cancel">
|
||||||
<a href="{{ path('chill_event_list_most_recent') }}" class="btn btn-cancel">
|
<a href="{{ path('chill_event_list_most_recent') }}" class="btn btn-cancel">
|
||||||
{{ 'Back to the most recent events'|trans }}
|
{{ 'Back to the most recent events'|trans }}
|
||||||
@@ -25,7 +38,7 @@
|
|||||||
{{ form_widget(form.submit, { 'attr' : { 'class' : 'btn btn-create' } }) }}
|
{{ form_widget(form.submit, { 'attr' : { 'class' : 'btn btn-create' } }) }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{{ form_end(form) }}
|
{{ form_end(form) }}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -0,0 +1,92 @@
|
|||||||
|
{% extends '@ChillEvent/layout.html.twig' %}
|
||||||
|
|
||||||
|
{% block title 'Events'|trans %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
{{ parent() }}
|
||||||
|
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{{ parent() }}
|
||||||
|
{{ encore_entry_link_tags('mod_pickentity_type') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ block('title') }}</h1>
|
||||||
|
|
||||||
|
{{ filter|chill_render_filter_order_helper }}
|
||||||
|
|
||||||
|
{# {% if is_granted('CHILL_EVENT_CREATE') %} #}
|
||||||
|
<ul class="record_actions">
|
||||||
|
<li><a class="btn btn-create" href="{{ chill_path_add_return_path('chill_event__event_new_pickcenter') }}">{{ 'Add an event'|trans }}</a></li>
|
||||||
|
</ul>
|
||||||
|
{# {% endif %} #}
|
||||||
|
{% if events|length > 0 %}
|
||||||
|
<div class="flex-table">
|
||||||
|
{% for e in events %}
|
||||||
|
<div class="item-bloc">
|
||||||
|
<div class="item-row">
|
||||||
|
<div class="item-col">
|
||||||
|
<div class="denomination h2">
|
||||||
|
{{ e.name }}
|
||||||
|
</div>
|
||||||
|
<p>{{ e.type.name|localize_translatable_string }}</p>
|
||||||
|
{% if e.moderator is not null %}
|
||||||
|
<p>{{ 'Moderator'|trans }}: {{ e.moderator|chill_entity_render_box }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="item-col">
|
||||||
|
<div class="container" style="text-align: right;">
|
||||||
|
<p>{{ e.date|format_datetime('medium', 'medium') }}</p>
|
||||||
|
<p>{{ 'count participations to this event'|trans({'count': e.participations|length}) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if e.participations|length > 0 %}
|
||||||
|
<div class="item-row separator">
|
||||||
|
<strong>{{ 'Participations'|trans }} : </strong>
|
||||||
|
{% for part in e.participations|slice(0, 20) %}
|
||||||
|
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||||
|
targetEntity: { name: 'person', id: part.person.id },
|
||||||
|
action: 'show',
|
||||||
|
displayBadge: true,
|
||||||
|
buttonText: part.person|chill_entity_render_string,
|
||||||
|
isDead: part.person.deathdate is not null
|
||||||
|
} %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if e.participations|length > 20 %}
|
||||||
|
{{ 'events.and_other_count_participants'|trans({'count': e.participations|length - 20}) }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="item-row">
|
||||||
|
<div class="item-col">
|
||||||
|
{{ form_start(eventForms[e.id]) }}
|
||||||
|
{{ form_widget(eventForms[e.id].person_id) }}
|
||||||
|
{{ form_end(eventForms[e.id]) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-row separator">
|
||||||
|
<div class="item-col item-meta">
|
||||||
|
</div>
|
||||||
|
<div class="item-col">
|
||||||
|
<ul class="record_actions">
|
||||||
|
{% if is_granted('CHILL_EVENT_UPDATE', e) %}
|
||||||
|
<li><a href="{{ chill_path_add_return_path('chill_event__event_delete', {'event_id': e.id}) }}" class="btn btn-delete"></a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if is_granted('CHILL_EVENT_UPDATE', e) %}
|
||||||
|
<li><a href="{{ chill_path_add_return_path('chill_event__event_edit', {'event_id': e.id}) }}" class="btn btn-edit"></a></li>
|
||||||
|
{% endif %}
|
||||||
|
<li><a href="{{ chill_path_add_return_path('chill_event__event_show', {'event_id': e.id}) }}" class="btn btn-show"></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{{ chill_pagination(pagination) }}
|
||||||
|
|
||||||
|
{% endblock %}
|
@@ -4,12 +4,28 @@
|
|||||||
|
|
||||||
{% import '@ChillPerson/Person/macro.html.twig' as person_macro %}
|
{% import '@ChillPerson/Person/macro.html.twig' as person_macro %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
{{ parent() }}
|
||||||
|
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
||||||
|
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{{ parent() }}
|
||||||
|
{{ encore_entry_link_tags('mod_pickentity_type') }}
|
||||||
|
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block event_content -%}
|
{% block event_content -%}
|
||||||
<div class="col-10">
|
<div class="col-10">
|
||||||
<h1>{{ 'Details of an event'|trans }}</h1>
|
<h1>{{ 'Details of an event'|trans }}</h1>
|
||||||
|
|
||||||
<table class="table table-bordered border-dark align-middle">
|
<table class="table table-bordered border-dark align-middle">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>{{ 'Circle'|trans }}</th>
|
||||||
|
<td>{{ event.circle.name|localize_translatable_string }}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ 'Name'|trans }}</th>
|
<th>{{ 'Name'|trans }}</th>
|
||||||
<td>{{ event.name }}</td>
|
<td>{{ event.name }}</td>
|
||||||
@@ -22,42 +38,62 @@
|
|||||||
<th>{{ 'Event type'|trans }}</th>
|
<th>{{ 'Event type'|trans }}</th>
|
||||||
<td>{{ event.type.name|localize_translatable_string }}</td>
|
<td>{{ event.type.name|localize_translatable_string }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<th>{{ 'Circle'|trans }}</th>
|
|
||||||
<td>{{ event.circle.name|localize_translatable_string }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ 'Moderator'|trans }}</th>
|
<th>{{ 'Moderator'|trans }}</th>
|
||||||
<td>{{ event.moderator|trans|default('-') }}</td>
|
<td>{{ event.moderator|trans|default('-') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{{ 'event.fields.organizationCost'|trans }}</th>
|
||||||
|
<td>{{ event.organizationCost|format_currency('EUR') }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{{ 'event.fields.location'|trans }}</th>
|
||||||
|
<td>
|
||||||
|
{% if event.location is not null %}
|
||||||
|
{{ event.location.name }}
|
||||||
|
{% if event.location.address is not same as(null) %}{{ event.location.address|chill_entity_render_box({'multiline': false, 'with_picto': (event.location.name is empty), 'details_button': true}) }}{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span class="chill-no-data-statement">{{ 'Any location for this event'|trans }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
{% if event.documents|length > 0 %}
|
||||||
|
<div>
|
||||||
|
<p><strong>{{ 'event.fields.documents'|trans }}</strong></p>
|
||||||
|
<ul>
|
||||||
|
{% for d in event.documents %}
|
||||||
|
<li class="document-list-item">{{ d.title|chill_print_or_message('document.Any title') }} {{ d|chill_document_button_group(d.title, is_granted('CHILL_EVENT_SEE_DETAILS', event), {small: false}) }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not event.comment.empty %}
|
||||||
|
<div>
|
||||||
|
{{ event.comment|chill_entity_render_box({
|
||||||
|
'disable_markdown': false,
|
||||||
|
'metadata': true,
|
||||||
|
}) }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<ul class="record_actions">
|
<ul class="record_actions">
|
||||||
|
|
||||||
{% set returnPath = app.request.get('return_path') %}
|
{% set returnPath = app.request.get('return_path') %}
|
||||||
{% set returnLabel = app.request.get('return_label') %}
|
{% set returnLabel = app.request.get('return_label') %}
|
||||||
|
|
||||||
{% if returnPath and returnLabel %}
|
<li class="cancel">
|
||||||
<li class="cancel">
|
<a href="{{ chill_return_path_or('chill_event_event_list') }}" class="btn btn-cancel">{{ 'Back to the list'|trans|chill_return_path_label }}</a>
|
||||||
<a href="{{ returnPath }}" class="btn btn-cancel">{{ returnLabel }}</a>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<a href="{{ chill_path_add_return_path('chill_event__event_edit', {'event_id': event.id }, false, 'See'|trans) }}" class="btn btn-edit">
|
||||||
<a href="{{ path('chill_event__event_edit', {
|
{{ 'Edit'|trans }}
|
||||||
'event_id': event.id,
|
</a>
|
||||||
'return_path': app.request.getRequestUri,
|
</li>
|
||||||
'return_label': 'Back to details of the event'|trans
|
|
||||||
}) }}" class="btn btn-edit">{{ 'Edit'|trans }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% else %}
|
|
||||||
<li>
|
|
||||||
<a href="{{ path('chill_event__event_edit', {'event_id': event.id }) }}" class="btn btn-edit">
|
|
||||||
{{ 'Edit'|trans }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ path('chill_event__event_delete', {'event_id' : event.id } ) }}"
|
<a href="{{ path('chill_event__event_delete', {'event_id' : event.id } ) }}"
|
||||||
class="btn btn-delete">{{ 'Delete event'|trans }}</a>
|
class="btn btn-delete">{{ 'Delete event'|trans }}</a>
|
||||||
@@ -83,7 +119,15 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for participation in event.participations %}
|
{% for participation in event.participations %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ person_macro.render(participation.person) }}</td>
|
<td>
|
||||||
|
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||||
|
targetEntity: { name: 'person', id: participation.person.id },
|
||||||
|
action: 'show',
|
||||||
|
displayBadge: true,
|
||||||
|
buttonText: participation.person|chill_entity_render_string,
|
||||||
|
isDead: participation.person.deathdate is not null
|
||||||
|
} %}
|
||||||
|
</td>
|
||||||
<td>{{ participation.role.name|localize_translatable_string }}</td>
|
<td>{{ participation.role.name|localize_translatable_string }}</td>
|
||||||
<td>{{ participation.status.name|localize_translatable_string }}</td>
|
<td>{{ participation.status.name|localize_translatable_string }}</td>
|
||||||
<td>{{ participation.lastUpdate|ago }} {# sf4 check: filter 'time_diff' is abandoned,
|
<td>{{ participation.lastUpdate|ago }} {# sf4 check: filter 'time_diff' is abandoned,
|
||||||
@@ -94,7 +138,7 @@
|
|||||||
<ul class="record_actions">
|
<ul class="record_actions">
|
||||||
{% if is_granted('CHILL_EVENT_PARTICIPATION_UPDATE', participation) %}
|
{% if is_granted('CHILL_EVENT_PARTICIPATION_UPDATE', participation) %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ path('chill_event_participation_edit', { 'participation_id' : participation.id } ) }}"
|
<a href="{{ chill_path_add_return_path('chill_event_participation_edit', { 'participation_id' : participation.id }, false, 'See'|trans ) }}"
|
||||||
class="btn btn-edit" title="{{ 'Edit'|trans }}"></a>
|
class="btn btn-edit" title="{{ 'Edit'|trans }}"></a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@@ -126,11 +170,8 @@
|
|||||||
'class' : 'custom-select',
|
'class' : 'custom-select',
|
||||||
'style': 'min-width: 15em; max-width: 18em; display: inline-block;'
|
'style': 'min-width: 15em; max-width: 18em; display: inline-block;'
|
||||||
}} ) }}
|
}} ) }}
|
||||||
<div class="input-group-append">
|
|
||||||
{{ form_widget(form_add_participation_by_person.submit, { 'attr' : { 'class' : 'btn btn-create' } } ) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{{ form_rest(form_add_participation_by_person) }}
|
<input type="hidden" name="returnPath" value="{{ app.request.requestUri }}" />
|
||||||
{{ form_end(form_add_participation_by_person) }}
|
{{ form_end(form_add_participation_by_person) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -32,7 +32,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<ul class="record_actions">
|
<ul class="record_actions sticky-form-buttons">
|
||||||
<li class="cancel">
|
<li class="cancel">
|
||||||
<a href="{{ path('chill_event__event_show', { 'event_id' : participation.event.id } ) }}" class="btn btn-cancel">
|
<a href="{{ path('chill_event__event_show', { 'event_id' : participation.event.id } ) }}" class="btn btn-cancel">
|
||||||
{{ 'Back to the event'|trans }}
|
{{ 'Back to the event'|trans }}
|
||||||
|
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\EventBundle\Tests\Controller;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||||
|
use Chill\MainBundle\Test\PrepareClientTrait;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class EventListControllerTest extends WebTestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
use PrepareClientTrait;
|
||||||
|
|
||||||
|
private readonly PaginatorFactory $paginatorFactory;
|
||||||
|
private readonly Environment $environment;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testList(): void
|
||||||
|
{
|
||||||
|
$client = $this->getClientAuthenticated();
|
||||||
|
|
||||||
|
$client->request('GET', '/fr/event/event/list');
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
}
|
||||||
|
}
|
@@ -11,6 +11,11 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\EventBundle\Tests\Controller;
|
namespace Chill\EventBundle\Tests\Controller;
|
||||||
|
|
||||||
|
use Chill\EventBundle\Entity\Event;
|
||||||
|
use Chill\EventBundle\Repository\EventRepository;
|
||||||
|
use Chill\MainBundle\Test\PrepareClientTrait;
|
||||||
|
use Chill\PersonBundle\DataFixtures\Helper\PersonRandomHelper;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
use function count;
|
use function count;
|
||||||
|
|
||||||
@@ -23,15 +28,12 @@ use function count;
|
|||||||
*/
|
*/
|
||||||
final class ParticipationControllerTest extends WebTestCase
|
final class ParticipationControllerTest extends WebTestCase
|
||||||
{
|
{
|
||||||
/**
|
use PersonRandomHelper;
|
||||||
* @var \Symfony\Component\BrowserKit\AbstractBrowser
|
use PrepareClientTrait;
|
||||||
*/
|
|
||||||
protected $client;
|
|
||||||
|
|
||||||
/**
|
private EntityManagerInterface $em;
|
||||||
* @var \Doctrine\ORM\EntityManagerInterface
|
|
||||||
*/
|
private EventRepository $eventRepository;
|
||||||
protected $em;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keep a cache for each person id given by the function getRandomPerson.
|
* Keep a cache for each person id given by the function getRandomPerson.
|
||||||
@@ -44,23 +46,21 @@ final class ParticipationControllerTest extends WebTestCase
|
|||||||
*/
|
*/
|
||||||
private array $personsIdsCache = [];
|
private array $personsIdsCache = [];
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function prepareDI(): void
|
||||||
{
|
{
|
||||||
self::bootKernel();
|
$this->em = self::$container->get(EntityManagerInterface::class);
|
||||||
|
$this->eventRepository = self::$container->get(EventRepository::class);
|
||||||
$this->client = self::createClient([], [
|
|
||||||
'PHP_AUTH_USER' => 'center a_social',
|
|
||||||
'PHP_AUTH_PW' => 'password',
|
|
||||||
'HTTP_ACCEPT_LANGUAGE' => 'fr_FR',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$container = self::$kernel->getContainer();
|
|
||||||
|
|
||||||
$this->em = $container->get('doctrine.orm.entity_manager');
|
|
||||||
|
|
||||||
$this->personsIdsCache = [];
|
$this->personsIdsCache = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
parent::tearDown();
|
||||||
|
|
||||||
|
self::ensureKernelShutdown();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method test participation creation with wrong parameters.
|
* This method test participation creation with wrong parameters.
|
||||||
*
|
*
|
||||||
@@ -68,11 +68,13 @@ final class ParticipationControllerTest extends WebTestCase
|
|||||||
*/
|
*/
|
||||||
public function testCreateActionWrongParameters()
|
public function testCreateActionWrongParameters()
|
||||||
{
|
{
|
||||||
|
$client = $this->getClientAuthenticated();
|
||||||
|
$this->prepareDI();
|
||||||
$event = $this->getRandomEvent();
|
$event = $this->getRandomEvent();
|
||||||
$person = $this->getRandomPerson();
|
$person = $this->getRandomPerson($this->em);
|
||||||
|
|
||||||
// missing person_id or persons_ids
|
// missing person_id or persons_ids
|
||||||
$this->client->request(
|
$client->request(
|
||||||
'GET',
|
'GET',
|
||||||
'/fr/event/participation/create',
|
'/fr/event/participation/create',
|
||||||
[
|
[
|
||||||
@@ -81,33 +83,33 @@ final class ParticipationControllerTest extends WebTestCase
|
|||||||
);
|
);
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
400,
|
400,
|
||||||
$this->client->getResponse()->getStatusCode(),
|
$client->getResponse()->getStatusCode(),
|
||||||
'Test that /fr/event/participation/create fail if '
|
'Test that /fr/event/participation/create fail if '
|
||||||
.'both person_id and persons_ids are missing'
|
.'both person_id and persons_ids are missing'
|
||||||
);
|
);
|
||||||
|
|
||||||
// having both person_id and persons_ids
|
// having both person_id and persons_ids
|
||||||
$this->client->request(
|
$client->request(
|
||||||
'GET',
|
'GET',
|
||||||
'/fr/event/participation/create',
|
'/fr/event/participation/create',
|
||||||
[
|
[
|
||||||
'event_id' => $event->getId(),
|
'event_id' => $event->getId(),
|
||||||
'persons_ids' => implode(',', [
|
'persons_ids' => implode(',', [
|
||||||
$this->getRandomPerson()->getId(),
|
$this->getRandomPerson($this->em)->getId(),
|
||||||
$this->getRandomPerson()->getId(),
|
$this->getRandomPerson($this->em)->getId(),
|
||||||
]),
|
]),
|
||||||
'person_id' => $person->getId(),
|
'person_id' => $person->getId(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
400,
|
400,
|
||||||
$this->client->getResponse()->getStatusCode(),
|
$client->getResponse()->getStatusCode(),
|
||||||
'test that /fr/event/participation/create fail if both person_id and '
|
'test that /fr/event/participation/create fail if both person_id and '
|
||||||
.'persons_ids are set'
|
.'persons_ids are set'
|
||||||
);
|
);
|
||||||
|
|
||||||
// missing event_id
|
// missing event_id
|
||||||
$this->client->request(
|
$client->request(
|
||||||
'GET',
|
'GET',
|
||||||
'/fr/event/participation/create',
|
'/fr/event/participation/create',
|
||||||
[
|
[
|
||||||
@@ -116,12 +118,12 @@ final class ParticipationControllerTest extends WebTestCase
|
|||||||
);
|
);
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
400,
|
400,
|
||||||
$this->client->getResponse()->getStatusCode(),
|
$client->getResponse()->getStatusCode(),
|
||||||
'Test that /fr/event/participation/create fails if event_id is missing'
|
'Test that /fr/event/participation/create fails if event_id is missing'
|
||||||
);
|
);
|
||||||
|
|
||||||
// persons_ids with wrong content
|
// persons_ids with wrong content
|
||||||
$this->client->request(
|
$client->request(
|
||||||
'GET',
|
'GET',
|
||||||
'/fr/event/participation/create',
|
'/fr/event/participation/create',
|
||||||
[
|
[
|
||||||
@@ -131,42 +133,47 @@ final class ParticipationControllerTest extends WebTestCase
|
|||||||
);
|
);
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
400,
|
400,
|
||||||
$this->client->getResponse()->getStatusCode(),
|
$client->getResponse()->getStatusCode(),
|
||||||
'Test that /fr/event/participation/create fails if persons_ids has wrong content'
|
'Test that /fr/event/participation/create fails if persons_ids has wrong content'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testEditMultipleAction()
|
public function testEditMultipleAction()
|
||||||
{
|
{
|
||||||
/** @var \Chill\EventBundle\Entity\Event $event */
|
$client = $this->getClientAuthenticated();
|
||||||
|
$this->prepareDI();
|
||||||
|
|
||||||
|
/** @var Event $event */
|
||||||
$event = $this->getRandomEventWithMultipleParticipations();
|
$event = $this->getRandomEventWithMultipleParticipations();
|
||||||
|
|
||||||
$crawler = $this->client->request('GET', '/fr/event/participation/'.$event->getId().
|
$crawler = $client->request('GET', '/fr/event/participation/'.$event->getId().
|
||||||
'/edit_multiple');
|
'/edit_multiple');
|
||||||
|
|
||||||
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
|
$this->assertEquals(200, $client->getResponse()->getStatusCode());
|
||||||
|
|
||||||
$button = $crawler->selectButton('Mettre à jour');
|
$button = $crawler->selectButton('Mettre à jour');
|
||||||
$this->assertEquals(1, $button->count(), "test the form with button 'mettre à jour' exists ");
|
$this->assertEquals(1, $button->count(), "test the form with button 'mettre à jour' exists ");
|
||||||
|
|
||||||
$this->client->submit($button->form(), [
|
$client->submit($button->form(), [
|
||||||
'form[participations][0][role]' => $event->getType()->getRoles()->first()->getId(),
|
'form[participations][0][role]' => $event->getType()->getRoles()->first()->getId(),
|
||||||
'form[participations][0][status]' => $event->getType()->getStatuses()->first()->getId(),
|
'form[participations][0][status]' => $event->getType()->getStatuses()->first()->getId(),
|
||||||
'form[participations][1][role]' => $event->getType()->getRoles()->last()->getId(),
|
'form[participations][1][role]' => $event->getType()->getRoles()->last()->getId(),
|
||||||
'form[participations][1][status]' => $event->getType()->getStatuses()->last()->getId(),
|
'form[participations][1][status]' => $event->getType()->getStatuses()->last()->getId(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertTrue($this->client->getResponse()
|
$this->assertTrue($client->getResponse()
|
||||||
->isRedirect('/fr/event/event/'.$event->getId().'/show'));
|
->isRedirect('/fr/event/event/'.$event->getId().'/show'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testNewActionWrongParameters()
|
public function testNewActionWrongParameters()
|
||||||
{
|
{
|
||||||
|
$client = $this->getClientAuthenticated();
|
||||||
|
$this->prepareDI();
|
||||||
$event = $this->getRandomEvent();
|
$event = $this->getRandomEvent();
|
||||||
$person = $this->getRandomPerson();
|
$person = $this->getRandomPerson($this->em);
|
||||||
|
|
||||||
// missing person_id or persons_ids
|
// missing person_id or persons_ids
|
||||||
$this->client->request(
|
$client->request(
|
||||||
'GET',
|
'GET',
|
||||||
'/fr/event/participation/new',
|
'/fr/event/participation/new',
|
||||||
[
|
[
|
||||||
@@ -175,33 +182,33 @@ final class ParticipationControllerTest extends WebTestCase
|
|||||||
);
|
);
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
400,
|
400,
|
||||||
$this->client->getResponse()->getStatusCode(),
|
$client->getResponse()->getStatusCode(),
|
||||||
'Test that /fr/event/participation/new fail if '
|
'Test that /fr/event/participation/new fail if '
|
||||||
.'both person_id and persons_ids are missing'
|
.'both person_id and persons_ids are missing'
|
||||||
);
|
);
|
||||||
|
|
||||||
// having both person_id and persons_ids
|
// having both person_id and persons_ids
|
||||||
$this->client->request(
|
$client->request(
|
||||||
'GET',
|
'GET',
|
||||||
'/fr/event/participation/new',
|
'/fr/event/participation/new',
|
||||||
[
|
[
|
||||||
'event_id' => $event->getId(),
|
'event_id' => $event->getId(),
|
||||||
'persons_ids' => implode(',', [
|
'persons_ids' => implode(',', [
|
||||||
$this->getRandomPerson()->getId(),
|
$this->getRandomPerson($this->em)->getId(),
|
||||||
$this->getRandomPerson()->getId(),
|
$this->getRandomPerson($this->em)->getId(),
|
||||||
]),
|
]),
|
||||||
'person_id' => $person->getId(),
|
'person_id' => $person->getId(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
400,
|
400,
|
||||||
$this->client->getResponse()->getStatusCode(),
|
$client->getResponse()->getStatusCode(),
|
||||||
'test that /fr/event/participation/new fail if both person_id and '
|
'test that /fr/event/participation/new fail if both person_id and '
|
||||||
.'persons_ids are set'
|
.'persons_ids are set'
|
||||||
);
|
);
|
||||||
|
|
||||||
// missing event_id
|
// missing event_id
|
||||||
$this->client->request(
|
$client->request(
|
||||||
'GET',
|
'GET',
|
||||||
'/fr/event/participation/new',
|
'/fr/event/participation/new',
|
||||||
[
|
[
|
||||||
@@ -210,12 +217,12 @@ final class ParticipationControllerTest extends WebTestCase
|
|||||||
);
|
);
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
400,
|
400,
|
||||||
$this->client->getResponse()->getStatusCode(),
|
$client->getResponse()->getStatusCode(),
|
||||||
'Test that /fr/event/participation/new fails if event_id is missing'
|
'Test that /fr/event/participation/new fails if event_id is missing'
|
||||||
);
|
);
|
||||||
|
|
||||||
// persons_ids with wrong content
|
// persons_ids with wrong content
|
||||||
$this->client->request(
|
$client->request(
|
||||||
'GET',
|
'GET',
|
||||||
'/fr/event/participation/new',
|
'/fr/event/participation/new',
|
||||||
[
|
[
|
||||||
@@ -225,13 +232,15 @@ final class ParticipationControllerTest extends WebTestCase
|
|||||||
);
|
);
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
400,
|
400,
|
||||||
$this->client->getResponse()->getStatusCode(),
|
$client->getResponse()->getStatusCode(),
|
||||||
'Test that /fr/event/participation/new fails if persons_ids has wrong content'
|
'Test that /fr/event/participation/new fails if persons_ids has wrong content'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testNewMultipleAction()
|
public function testNewMultipleAction()
|
||||||
{
|
{
|
||||||
|
$client = $this->getClientAuthenticated();
|
||||||
|
$this->prepareDI();
|
||||||
$event = $this->getRandomEvent();
|
$event = $this->getRandomEvent();
|
||||||
// record the number of participation for the event (used later in this test)
|
// record the number of participation for the event (used later in this test)
|
||||||
$nbParticipations = $event->getParticipations()->count();
|
$nbParticipations = $event->getParticipations()->count();
|
||||||
@@ -244,10 +253,10 @@ final class ParticipationControllerTest extends WebTestCase
|
|||||||
->toArray()
|
->toArray()
|
||||||
);
|
);
|
||||||
// get some random people
|
// get some random people
|
||||||
$person1 = $this->getRandomPerson();
|
$person1 = $this->getRandomPerson($this->em);
|
||||||
$person2 = $this->getRandomPerson();
|
$person2 = $this->getRandomPerson($this->em);
|
||||||
|
|
||||||
$crawler = $this->client->request(
|
$crawler = $client->request(
|
||||||
'GET',
|
'GET',
|
||||||
'/fr/event/participation/new',
|
'/fr/event/participation/new',
|
||||||
[
|
[
|
||||||
@@ -258,7 +267,7 @@ final class ParticipationControllerTest extends WebTestCase
|
|||||||
|
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
200,
|
200,
|
||||||
$this->client->getResponse()->getStatusCode(),
|
$client->getResponse()->getStatusCode(),
|
||||||
'test that /fr/event/participation/new is successful'
|
'test that /fr/event/participation/new is successful'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -266,7 +275,7 @@ final class ParticipationControllerTest extends WebTestCase
|
|||||||
|
|
||||||
$this->assertNotNull($button, "test the form with button 'Créer' exists");
|
$this->assertNotNull($button, "test the form with button 'Créer' exists");
|
||||||
|
|
||||||
$this->client->submit($button->form(), [
|
$client->submit($button->form(), [
|
||||||
'form' => [
|
'form' => [
|
||||||
'participations' => [
|
'participations' => [
|
||||||
0 => [
|
0 => [
|
||||||
@@ -281,8 +290,8 @@ final class ParticipationControllerTest extends WebTestCase
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertTrue($this->client->getResponse()->isRedirect());
|
$this->assertTrue($client->getResponse()->isRedirect());
|
||||||
$crawler = $this->client->followRedirect();
|
$crawler = $client->followRedirect();
|
||||||
|
|
||||||
$span1 = $crawler->filter('table td span.entity-person a:contains("'
|
$span1 = $crawler->filter('table td span.entity-person a:contains("'
|
||||||
.$person1->getFirstName().'"):contains("'.$person1->getLastname().'")');
|
.$person1->getFirstName().'"):contains("'.$person1->getLastname().'")');
|
||||||
@@ -292,7 +301,7 @@ final class ParticipationControllerTest extends WebTestCase
|
|||||||
$this->assertGreaterThan(0, \count($span2));
|
$this->assertGreaterThan(0, \count($span2));
|
||||||
|
|
||||||
// as the container has reloaded, reload the event
|
// as the container has reloaded, reload the event
|
||||||
$event = $this->em->getRepository(\Chill\EventBundle\Entity\Event::class)->find($event->getId());
|
$event = $this->em->getRepository(Event::class)->find($event->getId());
|
||||||
$this->em->refresh($event);
|
$this->em->refresh($event);
|
||||||
|
|
||||||
$this->assertEquals($nbParticipations + 2, $event->getParticipations()->count());
|
$this->assertEquals($nbParticipations + 2, $event->getParticipations()->count());
|
||||||
@@ -300,13 +309,15 @@ final class ParticipationControllerTest extends WebTestCase
|
|||||||
|
|
||||||
public function testNewMultipleWithAllPeopleParticipating()
|
public function testNewMultipleWithAllPeopleParticipating()
|
||||||
{
|
{
|
||||||
|
$client = $this->getClientAuthenticated();
|
||||||
|
$this->prepareDI();
|
||||||
$event = $this->getRandomEventWithMultipleParticipations();
|
$event = $this->getRandomEventWithMultipleParticipations();
|
||||||
|
|
||||||
$persons_id = implode(',', $event->getParticipations()->map(
|
$persons_id = implode(',', $event->getParticipations()->map(
|
||||||
static fn ($p) => $p->getPerson()->getId()
|
static fn ($p) => $p->getPerson()->getId()
|
||||||
)->toArray());
|
)->toArray());
|
||||||
|
|
||||||
$crawler = $this->client->request(
|
$crawler = $client->request(
|
||||||
'GET',
|
'GET',
|
||||||
'/fr/event/participation/new',
|
'/fr/event/participation/new',
|
||||||
[
|
[
|
||||||
@@ -317,13 +328,15 @@ final class ParticipationControllerTest extends WebTestCase
|
|||||||
|
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
302,
|
302,
|
||||||
$this->client->getResponse()->getStatusCode(),
|
$client->getResponse()->getStatusCode(),
|
||||||
'test that /fr/event/participation/new is redirecting'
|
'test that /fr/event/participation/new is redirecting'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testNewMultipleWithSomePeopleParticipating()
|
public function testNewMultipleWithSomePeopleParticipating()
|
||||||
{
|
{
|
||||||
|
$client = $this->getClientAuthenticated();
|
||||||
|
$this->prepareDI();
|
||||||
$event = $this->getRandomEventWithMultipleParticipations();
|
$event = $this->getRandomEventWithMultipleParticipations();
|
||||||
// record the number of participation for the event (used later in this test)
|
// record the number of participation for the event (used later in this test)
|
||||||
$nbParticipations = $event->getParticipations()->count();
|
$nbParticipations = $event->getParticipations()->count();
|
||||||
@@ -335,12 +348,12 @@ final class ParticipationControllerTest extends WebTestCase
|
|||||||
$this->personsIdsCache = array_merge($this->personsIdsCache, $persons_id);
|
$this->personsIdsCache = array_merge($this->personsIdsCache, $persons_id);
|
||||||
|
|
||||||
// get a random person
|
// get a random person
|
||||||
$newPerson = $this->getRandomPerson();
|
$newPerson = $this->getRandomPerson($this->em);
|
||||||
|
|
||||||
// build the `persons_ids` parameter
|
// build the `persons_ids` parameter
|
||||||
$persons_ids_string = implode(',', [...$persons_id, $newPerson->getId()]);
|
$persons_ids_string = implode(',', [...$persons_id, $newPerson->getId()]);
|
||||||
|
|
||||||
$crawler = $this->client->request(
|
$crawler = $client->request(
|
||||||
'GET',
|
'GET',
|
||||||
'/fr/event/participation/new',
|
'/fr/event/participation/new',
|
||||||
[
|
[
|
||||||
@@ -351,7 +364,7 @@ final class ParticipationControllerTest extends WebTestCase
|
|||||||
|
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
200,
|
200,
|
||||||
$this->client->getResponse()->getStatusCode(),
|
$client->getResponse()->getStatusCode(),
|
||||||
'test that /fr/event/participation/new is successful'
|
'test that /fr/event/participation/new is successful'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -377,15 +390,15 @@ final class ParticipationControllerTest extends WebTestCase
|
|||||||
$this->assertNotNull($button, "test the form with button 'Créer' exists");
|
$this->assertNotNull($button, "test the form with button 'Créer' exists");
|
||||||
|
|
||||||
// submit the form
|
// submit the form
|
||||||
$this->client->submit($button->form(), [
|
$client->submit($button->form(), [
|
||||||
'participation[role]' => $event->getType()->getRoles()->first()->getId(),
|
'participation[role]' => $event->getType()->getRoles()->first()->getId(),
|
||||||
'participation[status]' => $event->getType()->getStatuses()->first()->getId(),
|
'participation[status]' => $event->getType()->getStatuses()->first()->getId(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertTrue($this->client->getResponse()->isRedirect());
|
$this->assertTrue($client->getResponse()->isRedirect());
|
||||||
|
|
||||||
// reload the event and test there is a new participation
|
// reload the event and test there is a new participation
|
||||||
$event = $this->em->getRepository(\Chill\EventBundle\Entity\Event::class)
|
$event = $this->em->getRepository(Event::class)
|
||||||
->find($event->getId());
|
->find($event->getId());
|
||||||
$this->em->refresh($event);
|
$this->em->refresh($event);
|
||||||
|
|
||||||
@@ -398,12 +411,14 @@ final class ParticipationControllerTest extends WebTestCase
|
|||||||
|
|
||||||
public function testNewSingleAction()
|
public function testNewSingleAction()
|
||||||
{
|
{
|
||||||
|
$client = $this->getClientAuthenticated();
|
||||||
|
$this->prepareDI();
|
||||||
$event = $this->getRandomEvent();
|
$event = $this->getRandomEvent();
|
||||||
// record the number of participation for the event
|
// record the number of participation for the event
|
||||||
$nbParticipations = $event->getParticipations()->count();
|
$nbParticipations = $event->getParticipations()->count();
|
||||||
$person = $this->getRandomPerson();
|
$person = $this->getRandomPerson($this->em);
|
||||||
|
|
||||||
$crawler = $this->client->request(
|
$crawler = $client->request(
|
||||||
'GET',
|
'GET',
|
||||||
'/fr/event/participation/new',
|
'/fr/event/participation/new',
|
||||||
[
|
[
|
||||||
@@ -414,7 +429,7 @@ final class ParticipationControllerTest extends WebTestCase
|
|||||||
|
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
200,
|
200,
|
||||||
$this->client->getResponse()->getStatusCode(),
|
$client->getResponse()->getStatusCode(),
|
||||||
'test that /fr/event/participation/new is successful'
|
'test that /fr/event/participation/new is successful'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -422,13 +437,13 @@ final class ParticipationControllerTest extends WebTestCase
|
|||||||
|
|
||||||
$this->assertNotNull($button, "test the form with button 'Créer' exists");
|
$this->assertNotNull($button, "test the form with button 'Créer' exists");
|
||||||
|
|
||||||
$this->client->submit($button->form(), [
|
$client->submit($button->form(), [
|
||||||
'participation[role]' => $event->getType()->getRoles()->first()->getId(),
|
'participation[role]' => $event->getType()->getRoles()->first()->getId(),
|
||||||
'participation[status]' => $event->getType()->getStatuses()->first()->getId(),
|
'participation[status]' => $event->getType()->getStatuses()->first()->getId(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertTrue($this->client->getResponse()->isRedirect());
|
$this->assertTrue($client->getResponse()->isRedirect());
|
||||||
$crawler = $this->client->followRedirect();
|
$crawler = $client->followRedirect();
|
||||||
|
|
||||||
$span = $crawler->filter('table td span.entity-person a:contains("'
|
$span = $crawler->filter('table td span.entity-person a:contains("'
|
||||||
.$person->getFirstName().'"):contains("'.$person->getLastname().'")');
|
.$person->getFirstName().'"):contains("'.$person->getLastname().'")');
|
||||||
@@ -436,29 +451,23 @@ final class ParticipationControllerTest extends WebTestCase
|
|||||||
$this->assertGreaterThan(0, \count($span));
|
$this->assertGreaterThan(0, \count($span));
|
||||||
|
|
||||||
// as the container has reloaded, reload the event
|
// as the container has reloaded, reload the event
|
||||||
$event = $this->em->getRepository(\Chill\EventBundle\Entity\Event::class)->find($event->getId());
|
$event = $this->em->getRepository(Event::class)->find($event->getId());
|
||||||
$this->em->refresh($event);
|
$this->em->refresh($event);
|
||||||
|
|
||||||
$this->assertEquals($nbParticipations + 1, $event->getParticipations()->count());
|
$this->assertEquals($nbParticipations + 1, $event->getParticipations()->count());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function getRandomEvent(string $centerName = 'Center A', string $circleName = 'social'): Event
|
||||||
* @return \Chill\EventBundle\Entity\Event
|
|
||||||
*/
|
|
||||||
protected function getRandomEvent(mixed $centerName = 'Center A', mixed $circleName = 'social')
|
|
||||||
{
|
{
|
||||||
$center = $this->em->getRepository(\Chill\MainBundle\Entity\Center::class)
|
$dql = 'FROM '.Event::class.' e JOIN e.center center JOIN e.circle scope WHERE center.name LIKE :cname AND JSON_EXTRACT(scope.name, \'fr\') LIKE :sname';
|
||||||
->findByName($centerName);
|
|
||||||
|
|
||||||
$circles = $this->em->getRepository(\Chill\MainBundle\Entity\Scope::class)
|
$ids = $this->em->createQuery(
|
||||||
->findAll();
|
'SELECT DISTINCT e.id '.$dql
|
||||||
array_filter($circles, static fn ($circle) => \in_array($circleName, $circle->getName(), true));
|
)
|
||||||
$circle = $circles[0];
|
->setParameters(['cname' => $centerName, 'sname' => $circleName])
|
||||||
|
->getResult();
|
||||||
|
|
||||||
$events = $this->em->getRepository(\Chill\EventBundle\Entity\Event::class)
|
return $this->eventRepository->find($ids[array_rand($ids)]['id']);
|
||||||
->findBy(['center' => $center, 'circle' => $circle]);
|
|
||||||
|
|
||||||
return $events[array_rand($events)];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -467,7 +476,7 @@ final class ParticipationControllerTest extends WebTestCase
|
|||||||
* @param string $centerName
|
* @param string $centerName
|
||||||
* @param type $circleName
|
* @param type $circleName
|
||||||
*
|
*
|
||||||
* @return \Chill\EventBundle\Entity\Event
|
* @return Event
|
||||||
*/
|
*/
|
||||||
protected function getRandomEventWithMultipleParticipations(
|
protected function getRandomEventWithMultipleParticipations(
|
||||||
$centerName = 'Center A',
|
$centerName = 'Center A',
|
||||||
@@ -479,35 +488,4 @@ final class ParticipationControllerTest extends WebTestCase
|
|||||||
$event :
|
$event :
|
||||||
$this->getRandomEventWithMultipleParticipations($centerName, $circleName);
|
$this->getRandomEventWithMultipleParticipations($centerName, $circleName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a person randomly.
|
|
||||||
*
|
|
||||||
* This function does not give the same person twice
|
|
||||||
* for each test.
|
|
||||||
*
|
|
||||||
* You may ask to ignore some people by adding their id to the property
|
|
||||||
* `$this->personsIdsCache`
|
|
||||||
*
|
|
||||||
* @param string $centerName
|
|
||||||
*
|
|
||||||
* @return \Chill\PersonBundle\Entity\Person
|
|
||||||
*/
|
|
||||||
protected function getRandomPerson($centerName = 'Center A')
|
|
||||||
{
|
|
||||||
$center = $this->em->getRepository(\Chill\MainBundle\Entity\Center::class)
|
|
||||||
->findByName($centerName);
|
|
||||||
|
|
||||||
$persons = $this->em->getRepository(\Chill\PersonBundle\Entity\Person::class)
|
|
||||||
->findBy(['center' => $center]);
|
|
||||||
|
|
||||||
$person = $persons[array_rand($persons)];
|
|
||||||
|
|
||||||
if (\in_array($person->getId(), $this->personsIdsCache, true)) {
|
|
||||||
return $this->getRandomPerson($centerName); // we try another time
|
|
||||||
}
|
|
||||||
$this->personsIdsCache[] = $person->getId();
|
|
||||||
|
|
||||||
return $person;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\EventBundle\Tests\Repository;
|
||||||
|
|
||||||
|
use Chill\EventBundle\Repository\EventACLAwareRepository;
|
||||||
|
use Chill\EventBundle\Security\Authorization\EventVoter;
|
||||||
|
use Chill\MainBundle\Entity\Center;
|
||||||
|
use Chill\MainBundle\Entity\Scope;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class EventACLAwareRepositoryTest extends KernelTestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider generateFilters
|
||||||
|
*
|
||||||
|
* @throws \Doctrine\ORM\NoResultException
|
||||||
|
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||||
|
*/
|
||||||
|
public function testCountAllViewable(array $filters): void
|
||||||
|
{
|
||||||
|
$repository = $this->buildEventACLAwareRepository();
|
||||||
|
|
||||||
|
$this->assertGreaterThanOrEqual(0, $repository->countAllViewable($filters));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider generateFilters
|
||||||
|
*/
|
||||||
|
public function testFindAllViewable(array $filters): void
|
||||||
|
{
|
||||||
|
$repository = $this->buildEventACLAwareRepository();
|
||||||
|
|
||||||
|
$this->assertIsArray($repository->findAllViewable($filters));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateFilters(): iterable
|
||||||
|
{
|
||||||
|
yield [[]];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildEventACLAwareRepository(): EventACLAwareRepository
|
||||||
|
{
|
||||||
|
$em = self::$container->get(EntityManagerInterface::class);
|
||||||
|
$user = $em->createQuery('SELECT u FROM '.User::class.' u')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->getSingleResult()
|
||||||
|
;
|
||||||
|
|
||||||
|
$scopes = $em->createQuery('SELECT s FROM '.Scope::class.' s')
|
||||||
|
->setMaxResults(3)
|
||||||
|
->getResult();
|
||||||
|
|
||||||
|
$centers = $em->createQuery('SELECT c FROM '.Center::class.' c')
|
||||||
|
->setMaxResults(3)
|
||||||
|
->getResult();
|
||||||
|
|
||||||
|
$security = $this->prophesize(Security::class);
|
||||||
|
$security->getUser()->willReturn($user);
|
||||||
|
|
||||||
|
$authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class);
|
||||||
|
$authorizationHelper->getReachableCenters(EventVoter::SEE)->willReturn($centers);
|
||||||
|
$authorizationHelper->getReachableScopes(EventVoter::SEE, Argument::type(Center::class))->willReturn($scopes);
|
||||||
|
|
||||||
|
return new EventACLAwareRepository(
|
||||||
|
$authorizationHelper->reveal(),
|
||||||
|
$em,
|
||||||
|
$security->reveal()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,16 +0,0 @@
|
|||||||
services:
|
|
||||||
|
|
||||||
Chill\EventBundle\Controller\EventController:
|
|
||||||
arguments:
|
|
||||||
$eventDispatcher: '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface'
|
|
||||||
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
|
|
||||||
$formFactoryInterface: '@Symfony\Component\Form\FormFactoryInterface'
|
|
||||||
$translator: '@Symfony\Contracts\Translation\TranslatorInterface'
|
|
||||||
$paginator: '@chill_main.paginator_factory'
|
|
||||||
public: true
|
|
||||||
tags: ['controller.service_arguments']
|
|
||||||
|
|
||||||
Chill\EventBundle\Controller\ParticipationController:
|
|
||||||
arguments:
|
|
||||||
$logger: '@Psr\Log\LoggerInterface'
|
|
||||||
tags: ['controller.service_arguments']
|
|
@@ -1,7 +0,0 @@
|
|||||||
services:
|
|
||||||
Chill\EventBundle\Menu\PersonMenuBuilder:
|
|
||||||
arguments:
|
|
||||||
$authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'
|
|
||||||
$translator: '@Symfony\Contracts\Translation\TranslatorInterface'
|
|
||||||
tags:
|
|
||||||
- { name: 'chill.menu_builder' }
|
|
@@ -1,26 +0,0 @@
|
|||||||
Chill\EventBundle\Entity\Participation:
|
|
||||||
properties:
|
|
||||||
event:
|
|
||||||
- NotNull: ~
|
|
||||||
status:
|
|
||||||
- NotNull: ~
|
|
||||||
person:
|
|
||||||
- NotNull: ~
|
|
||||||
constraints:
|
|
||||||
- Callback: isConsistent
|
|
||||||
|
|
||||||
|
|
||||||
Chill\EventBundle\Entity\Event:
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
- Length:
|
|
||||||
min: 3
|
|
||||||
max: 75
|
|
||||||
minMessage: The event name must have at least {{ limit }} characters.
|
|
||||||
maxMessage: The event name must have maximum {{ limit }} characters.
|
|
||||||
type:
|
|
||||||
- NotNull: ~
|
|
||||||
circle:
|
|
||||||
- NotNull: ~
|
|
||||||
center:
|
|
||||||
- NotNull: ~
|
|
@@ -19,11 +19,13 @@ use Doctrine\Migrations\AbstractMigration;
|
|||||||
*/
|
*/
|
||||||
class Version20160318111334 extends AbstractMigration
|
class Version20160318111334 extends AbstractMigration
|
||||||
{
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'initialize the bundle chill event';
|
||||||
|
}
|
||||||
|
|
||||||
public function down(Schema $schema): void
|
public function down(Schema $schema): void
|
||||||
{
|
{
|
||||||
// this down() migration is auto-generated, please modify it to your needs
|
|
||||||
$this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
|
|
||||||
|
|
||||||
$this->addSql('ALTER TABLE chill_event_role DROP CONSTRAINT FK_AA714E54C54C8C93');
|
$this->addSql('ALTER TABLE chill_event_role DROP CONSTRAINT FK_AA714E54C54C8C93');
|
||||||
$this->addSql('ALTER TABLE chill_event_status DROP CONSTRAINT FK_A6CC85D0C54C8C93');
|
$this->addSql('ALTER TABLE chill_event_status DROP CONSTRAINT FK_A6CC85D0C54C8C93');
|
||||||
$this->addSql('ALTER TABLE chill_event_participation DROP CONSTRAINT FK_4E7768ACD60322AC');
|
$this->addSql('ALTER TABLE chill_event_participation DROP CONSTRAINT FK_4E7768ACD60322AC');
|
||||||
@@ -50,9 +52,6 @@ class Version20160318111334 extends AbstractMigration
|
|||||||
|
|
||||||
public function up(Schema $schema): void
|
public function up(Schema $schema): void
|
||||||
{
|
{
|
||||||
// this up() migration is auto-generated, please modify it to your needs
|
|
||||||
$this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
|
|
||||||
|
|
||||||
$this->addSql('CREATE SEQUENCE chill_event_event_type_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
$this->addSql('CREATE SEQUENCE chill_event_event_type_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||||
$this->addSql('CREATE SEQUENCE chill_event_role_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
$this->addSql('CREATE SEQUENCE chill_event_role_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||||
$this->addSql('CREATE SEQUENCE chill_event_status_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
$this->addSql('CREATE SEQUENCE chill_event_status_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||||
@@ -123,11 +122,26 @@ class Version20160318111334 extends AbstractMigration
|
|||||||
.'FOREIGN KEY (event_id) '
|
.'FOREIGN KEY (event_id) '
|
||||||
.'REFERENCES chill_event_event (id) '
|
.'REFERENCES chill_event_event (id) '
|
||||||
.'NOT DEFERRABLE INITIALLY IMMEDIATE');
|
.'NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
$this->addSql('ALTER TABLE chill_event_participation '
|
|
||||||
|
// before adding fk constraint to person, check what is the table name
|
||||||
|
$results = $this->connection->executeQuery('SELECT EXISTS (SELECT 1 FROM pg_tables WHERE tablename = \'chill_person_person\')');
|
||||||
|
/** @var bool $isChillPersonPersonTable */
|
||||||
|
$isChillPersonPersonTable = $results->fetchFirstColumn()[0];
|
||||||
|
|
||||||
|
if ($isChillPersonPersonTable) {
|
||||||
|
$this->addSql('ALTER TABLE chill_event_participation '
|
||||||
|
.'ADD CONSTRAINT FK_4E7768AC217BBB47 '
|
||||||
|
.'FOREIGN KEY (person_id) '
|
||||||
|
.'REFERENCES chill_person_person (id) '
|
||||||
|
.'NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
} else {
|
||||||
|
$this->addSql('ALTER TABLE chill_event_participation '
|
||||||
.'ADD CONSTRAINT FK_4E7768AC217BBB47 '
|
.'ADD CONSTRAINT FK_4E7768AC217BBB47 '
|
||||||
.'FOREIGN KEY (person_id) '
|
.'FOREIGN KEY (person_id) '
|
||||||
.'REFERENCES Person (id) '
|
.'REFERENCES Person (id) '
|
||||||
.'NOT DEFERRABLE INITIALLY IMMEDIATE');
|
.'NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
}
|
||||||
|
|
||||||
$this->addSql('ALTER TABLE chill_event_participation '
|
$this->addSql('ALTER TABLE chill_event_participation '
|
||||||
.'ADD CONSTRAINT FK_4E7768ACD60322AC '
|
.'ADD CONSTRAINT FK_4E7768ACD60322AC '
|
||||||
.'FOREIGN KEY (role_id) '
|
.'FOREIGN KEY (role_id) '
|
||||||
|
@@ -19,18 +19,19 @@ use Doctrine\Migrations\AbstractMigration;
|
|||||||
*/
|
*/
|
||||||
final class Version20190110140538 extends AbstractMigration
|
final class Version20190110140538 extends AbstractMigration
|
||||||
{
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'switch event date to datetime';
|
||||||
|
}
|
||||||
|
|
||||||
public function down(Schema $schema): void
|
public function down(Schema $schema): void
|
||||||
{
|
{
|
||||||
$this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
|
|
||||||
|
|
||||||
$this->addSql('ALTER TABLE chill_event_event ALTER date TYPE DATE');
|
$this->addSql('ALTER TABLE chill_event_event ALTER date TYPE DATE');
|
||||||
$this->addSql('ALTER TABLE chill_event_event ALTER date DROP DEFAULT');
|
$this->addSql('ALTER TABLE chill_event_event ALTER date DROP DEFAULT');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function up(Schema $schema): void
|
public function up(Schema $schema): void
|
||||||
{
|
{
|
||||||
$this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
|
|
||||||
|
|
||||||
$this->addSql('ALTER TABLE chill_event_event ALTER date TYPE TIMESTAMP(0) WITHOUT TIME ZONE');
|
$this->addSql('ALTER TABLE chill_event_event ALTER date TYPE TIMESTAMP(0) WITHOUT TIME ZONE');
|
||||||
$this->addSql('ALTER TABLE chill_event_event ALTER date DROP DEFAULT');
|
$this->addSql('ALTER TABLE chill_event_event ALTER date DROP DEFAULT');
|
||||||
}
|
}
|
||||||
|
@@ -19,11 +19,13 @@ use Doctrine\Migrations\AbstractMigration;
|
|||||||
*/
|
*/
|
||||||
final class Version20190115140042 extends AbstractMigration
|
final class Version20190115140042 extends AbstractMigration
|
||||||
{
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'add a moderator field to events';
|
||||||
|
}
|
||||||
|
|
||||||
public function down(Schema $schema): void
|
public function down(Schema $schema): void
|
||||||
{
|
{
|
||||||
// this down() migration is auto-generated, please modify it to your needs
|
|
||||||
$this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
|
|
||||||
|
|
||||||
$this->addSql('ALTER TABLE chill_event_event DROP CONSTRAINT FK_FA320FC8D0AFA354');
|
$this->addSql('ALTER TABLE chill_event_event DROP CONSTRAINT FK_FA320FC8D0AFA354');
|
||||||
$this->addSql('DROP INDEX IDX_FA320FC8D0AFA354');
|
$this->addSql('DROP INDEX IDX_FA320FC8D0AFA354');
|
||||||
$this->addSql('ALTER TABLE chill_event_event DROP moderator_id');
|
$this->addSql('ALTER TABLE chill_event_event DROP moderator_id');
|
||||||
@@ -31,9 +33,6 @@ final class Version20190115140042 extends AbstractMigration
|
|||||||
|
|
||||||
public function up(Schema $schema): void
|
public function up(Schema $schema): void
|
||||||
{
|
{
|
||||||
// this up() migration is auto-generated, please modify it to your needs
|
|
||||||
$this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
|
|
||||||
|
|
||||||
$this->addSql('ALTER TABLE chill_event_event ADD moderator_id INT DEFAULT NULL');
|
$this->addSql('ALTER TABLE chill_event_event ADD moderator_id INT DEFAULT NULL');
|
||||||
$this->addSql('ALTER TABLE chill_event_event ADD CONSTRAINT FK_FA320FC8D0AFA354 FOREIGN KEY (moderator_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
$this->addSql('ALTER TABLE chill_event_event ADD CONSTRAINT FK_FA320FC8D0AFA354 FOREIGN KEY (moderator_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
$this->addSql('CREATE INDEX IDX_FA320FC8D0AFA354 ON chill_event_event (moderator_id)');
|
$this->addSql('CREATE INDEX IDX_FA320FC8D0AFA354 ON chill_event_event (moderator_id)');
|
||||||
|
@@ -19,20 +19,19 @@ use Doctrine\Migrations\AbstractMigration;
|
|||||||
*/
|
*/
|
||||||
final class Version20190201143121 extends AbstractMigration
|
final class Version20190201143121 extends AbstractMigration
|
||||||
{
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'fix moderator: relation with user (not person)';
|
||||||
|
}
|
||||||
|
|
||||||
public function down(Schema $schema): void
|
public function down(Schema $schema): void
|
||||||
{
|
{
|
||||||
// this down() migration is auto-generated, please modify it to your needs
|
|
||||||
$this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
|
|
||||||
|
|
||||||
$this->addSql('ALTER TABLE chill_event_event DROP CONSTRAINT fk_fa320fc8d0afa354');
|
$this->addSql('ALTER TABLE chill_event_event DROP CONSTRAINT fk_fa320fc8d0afa354');
|
||||||
$this->addSql('ALTER TABLE chill_event_event ADD CONSTRAINT fk_fa320fc8d0afa354 FOREIGN KEY (moderator_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
$this->addSql('ALTER TABLE chill_event_event ADD CONSTRAINT fk_fa320fc8d0afa354 FOREIGN KEY (moderator_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function up(Schema $schema): void
|
public function up(Schema $schema): void
|
||||||
{
|
{
|
||||||
// this up() migration is auto-generated, please modify it to your needs
|
|
||||||
$this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
|
|
||||||
|
|
||||||
$this->addSql('ALTER TABLE chill_event_event DROP CONSTRAINT FK_FA320FC8D0AFA354');
|
$this->addSql('ALTER TABLE chill_event_event DROP CONSTRAINT FK_FA320FC8D0AFA354');
|
||||||
$this->addSql('ALTER TABLE chill_event_event ADD CONSTRAINT FK_FA320FC8D0AFA354 FOREIGN KEY (moderator_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
$this->addSql('ALTER TABLE chill_event_event ADD CONSTRAINT FK_FA320FC8D0AFA354 FOREIGN KEY (moderator_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\Migrations\Event;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20231127134244 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'add creation - update information on event and event participation';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event ADD createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event ADD updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event ADD createdBy_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event ADD updatedBy_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('COMMENT ON COLUMN chill_event_event.createdAt IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('COMMENT ON COLUMN chill_event_event.updatedAt IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event ADD CONSTRAINT FK_FA320FC83174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event ADD CONSTRAINT FK_FA320FC865FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('CREATE INDEX IDX_FA320FC83174800F ON chill_event_event (createdBy_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_FA320FC865FF1AEC ON chill_event_event (updatedBy_id)');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_participation ADD createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_participation ADD updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_participation ADD createdBy_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_participation ADD updatedBy_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('COMMENT ON COLUMN chill_event_participation.createdAt IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('COMMENT ON COLUMN chill_event_participation.updatedAt IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_participation ADD CONSTRAINT FK_4E7768AC3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_participation ADD CONSTRAINT FK_4E7768AC65FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('CREATE INDEX IDX_4E7768AC3174800F ON chill_event_participation (createdBy_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_4E7768AC65FF1AEC ON chill_event_participation (updatedBy_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event DROP createdAt');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event DROP updatedAt');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event DROP createdBy_id');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event DROP updatedBy_id');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_participation DROP createdAt');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_participation DROP updatedAt');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_participation DROP createdBy_id');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_participation DROP updatedBy_id');
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\Migrations\Event;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20231128114959 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add unique index on participation and drop column participation::lastUpdate';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('UPDATE chill_event_participation SET updatedAt=lastupdate WHERE updatedat IS NULL');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_participation DROP lastupdate');
|
||||||
|
$this->addSql('WITH ordering AS (SELECT id, event_id, person_id, rank() OVER (PARTITION BY event_id, person_id ORDER BY id DESC) as ranked FROM chill_event_participation),
|
||||||
|
not_last AS (SELECT * FROM ordering where ranked > 1)
|
||||||
|
DELETE FROM chill_event_participation WHERE id IN (select id FROM not_last)');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX chill_event_participation_event_person_unique_idx ON chill_event_participation (event_id, person_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP INDEX chill_event_participation_event_person_unique_idx');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_participation ADD lastupdate TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL');
|
||||||
|
$this->addSql('UPDATE chill_event_participation set lastupdate = updatedat');
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\Migrations\Event;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20231128122635 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Append more fields on event: location, documents, and comment';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('CREATE TABLE chill_event_event_documents (event_id INT NOT NULL, storedobject_id INT NOT NULL, PRIMARY KEY(event_id, storedobject_id))');
|
||||||
|
$this->addSql('CREATE INDEX IDX_5C1B638671F7E88B ON chill_event_event_documents (event_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_5C1B6386EE684399 ON chill_event_event_documents (storedobject_id)');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event_documents ADD CONSTRAINT FK_5C1B638671F7E88B FOREIGN KEY (event_id) REFERENCES chill_event_event (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event_documents ADD CONSTRAINT FK_5C1B6386EE684399 FOREIGN KEY (storedobject_id) REFERENCES chill_doc.stored_object (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event ADD location_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event ADD organizationCost NUMERIC(10, 4) DEFAULT 0.0');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event ADD comment_comment TEXT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event ADD comment_date TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event ADD comment_userId INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event ADD CONSTRAINT FK_FA320FC864D218E FOREIGN KEY (location_id) REFERENCES chill_main_location (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('CREATE INDEX IDX_FA320FC864D218E ON chill_event_event (location_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event_documents DROP CONSTRAINT FK_5C1B638671F7E88B');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event_documents DROP CONSTRAINT FK_5C1B6386EE684399');
|
||||||
|
$this->addSql('DROP TABLE chill_event_event_documents');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event DROP location_id');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event DROP organizationCost');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event DROP comment_comment');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event DROP comment_date');
|
||||||
|
$this->addSql('ALTER TABLE chill_event_event DROP comment_userId');
|
||||||
|
}
|
||||||
|
}
|
@@ -11,3 +11,11 @@ count participations to this event: >-
|
|||||||
one {Un participant à l'événement}
|
one {Un participant à l'événement}
|
||||||
other {# participants à l'événement}
|
other {# participants à l'événement}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
events:
|
||||||
|
and_other_count_participants: >-
|
||||||
|
{ count, plural,
|
||||||
|
=0 {Aucun autre participant}
|
||||||
|
one {et un autre participant}
|
||||||
|
other {et # autres participants}
|
||||||
|
}
|
@@ -26,6 +26,8 @@ Event edit: Modifier un événement
|
|||||||
Edit the event: Modifier l'événement
|
Edit the event: Modifier l'événement
|
||||||
The event was updated: L'événement a été modifié
|
The event was updated: L'événement a été modifié
|
||||||
The event was created: L'événement a été créé
|
The event was created: L'événement a été créé
|
||||||
|
List of events: Liste des événements
|
||||||
|
Any location for this event: Aucune localisation pour cet événement
|
||||||
|
|
||||||
#crud participation
|
#crud participation
|
||||||
Edit all the participations: Modifier toutes les participations
|
Edit all the participations: Modifier toutes les participations
|
||||||
@@ -50,6 +52,7 @@ Remove participation: Supprimer la participation
|
|||||||
Delete event: Supprimer l'événement
|
Delete event: Supprimer l'événement
|
||||||
Are you sure you want to remove that participation ?: Êtes-vous certain de vouloir supprimer cette participation ?
|
Are you sure you want to remove that participation ?: Êtes-vous certain de vouloir supprimer cette participation ?
|
||||||
Are you sure you want to remove that event ?: Êtes-vous certain de vouloir supprimer cet événement, ainsi que toutes les participations associées ?
|
Are you sure you want to remove that event ?: Êtes-vous certain de vouloir supprimer cet événement, ainsi que toutes les participations associées ?
|
||||||
|
Any participation for this person: Cet usager ne participe à aucun évenements
|
||||||
|
|
||||||
#search
|
#search
|
||||||
Event search: Recherche d'événements
|
Event search: Recherche d'événements
|
||||||
@@ -107,3 +110,17 @@ csv: csv
|
|||||||
Create a new role: Créer un nouveau rôle
|
Create a new role: Créer un nouveau rôle
|
||||||
Create a new type: Créer un nouveau type
|
Create a new type: Créer un nouveau type
|
||||||
Create a new status: Créer un nouveau statut
|
Create a new status: Créer un nouveau statut
|
||||||
|
|
||||||
|
event:
|
||||||
|
fields:
|
||||||
|
organizationCost: Coût d'organisation
|
||||||
|
location: Localisation
|
||||||
|
documents: Documents
|
||||||
|
form:
|
||||||
|
organisationCost_help: Coût d'organisation pour la structure. Utile pour les statistiques.
|
||||||
|
add_document: Ajouter un document
|
||||||
|
remove_document: Supprimer le document
|
||||||
|
filter:
|
||||||
|
event_types: Par types d'événement
|
||||||
|
event_dates: Par date d'événement
|
||||||
|
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
event:
|
||||||
|
validation:
|
||||||
|
person_already_participate_to_event: L'usager est déjà inscrit à l'événement
|
@@ -15,10 +15,14 @@ use Chill\MainBundle\CRUD\Resolver\Resolver;
|
|||||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||||
use Chill\MainBundle\Pagination\PaginatorInterface;
|
use Chill\MainBundle\Pagination\PaginatorInterface;
|
||||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||||
|
use Doctrine\DBAL\LockMode;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\OptimisticLockException;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
||||||
use Symfony\Component\Serializer\SerializerInterface;
|
use Symfony\Component\Serializer\SerializerInterface;
|
||||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
@@ -173,6 +177,21 @@ abstract class AbstractCRUDController extends AbstractController
|
|||||||
if (null === $e) {
|
if (null === $e) {
|
||||||
throw $this->createNotFoundException(sprintf('The object %s for id %s is not found', $this->getEntityClass(), $id));
|
throw $this->createNotFoundException(sprintf('The object %s for id %s is not found', $this->getEntityClass(), $id));
|
||||||
}
|
}
|
||||||
|
if ($request->query->has('entity_version')) {
|
||||||
|
$expectedVersion = $request->query->getInt('entity_version');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$manager = $this->getDoctrine()->getManagerForClass($this->getEntityClass());
|
||||||
|
|
||||||
|
if ($manager instanceof EntityManagerInterface) {
|
||||||
|
$manager->lock($e, LockMode::OPTIMISTIC, $expectedVersion);
|
||||||
|
} else {
|
||||||
|
throw new \LogicException('This manager does not allow locking.');
|
||||||
|
}
|
||||||
|
} catch (OptimisticLockException $e) {
|
||||||
|
throw new ConflictHttpException('Sorry, but someone else has already changed this entity. Please refresh the page and apply the changes again', $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $e;
|
return $e;
|
||||||
}
|
}
|
||||||
|
@@ -135,7 +135,7 @@ class ApiController extends AbstractCRUDController
|
|||||||
try {
|
try {
|
||||||
$entity = $this->deserialize($action, $request, $_format, $entity);
|
$entity = $this->deserialize($action, $request, $_format, $entity);
|
||||||
} catch (NotEncodableValueException $e) {
|
} catch (NotEncodableValueException $e) {
|
||||||
throw new BadRequestHttpException('invalid json', 400, $e);
|
throw new BadRequestHttpException('invalid json', $e, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
$errors = $this->validate($action, $request, $_format, $entity);
|
$errors = $this->validate($action, $request, $_format, $entity);
|
||||||
@@ -153,7 +153,7 @@ class ApiController extends AbstractCRUDController
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->getDoctrine()->getManager()->flush();
|
$this->getDoctrine()->getManagerForClass($this->getEntityClass())->flush();
|
||||||
|
|
||||||
$response = $this->onAfterFlush($action, $request, $_format, $entity, $errors);
|
$response = $this->onAfterFlush($action, $request, $_format, $entity, $errors);
|
||||||
|
|
||||||
|
@@ -12,11 +12,12 @@ declare(strict_types=1);
|
|||||||
namespace Chill\MainBundle\Command;
|
namespace Chill\MainBundle\Command;
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\Language;
|
use Chill\MainBundle\Entity\Language;
|
||||||
use Doctrine\ORM\EntityManager;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||||
use Symfony\Component\Intl\Languages;
|
use Symfony\Component\Intl\Languages;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -39,7 +40,7 @@ class LoadAndUpdateLanguagesCommand extends Command
|
|||||||
/**
|
/**
|
||||||
* LoadCountriesCommand constructor.
|
* LoadCountriesCommand constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct(private readonly EntityManager $entityManager, private $availableLanguages)
|
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly ParameterBagInterface $parameterBag)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
@@ -79,7 +80,7 @@ class LoadAndUpdateLanguagesCommand extends Command
|
|||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
{
|
{
|
||||||
$em = $this->entityManager;
|
$em = $this->entityManager;
|
||||||
$chillAvailableLanguages = $this->availableLanguages;
|
$chillAvailableLanguages = $this->parameterBag->get('chill_main.available_languages');
|
||||||
$languages = [];
|
$languages = [];
|
||||||
|
|
||||||
foreach ($chillAvailableLanguages as $avLang) {
|
foreach ($chillAvailableLanguages as $avLang) {
|
||||||
@@ -113,7 +114,7 @@ class LoadAndUpdateLanguagesCommand extends Command
|
|||||||
$avLangNames = [];
|
$avLangNames = [];
|
||||||
|
|
||||||
foreach ($chillAvailableLanguages as $avLang) {
|
foreach ($chillAvailableLanguages as $avLang) {
|
||||||
$avLangNames[$avLang] = Languages::getName($code, $avLang);
|
$avLangNames[$avLang] = ucfirst(Languages::getName($code, $avLang));
|
||||||
}
|
}
|
||||||
|
|
||||||
$languageDB->setName($avLangNames);
|
$languageDB->setName($avLangNames);
|
||||||
|
@@ -47,4 +47,12 @@ class AdminController extends AbstractController
|
|||||||
{
|
{
|
||||||
return $this->render('@ChillMain/Admin/indexUser.html.twig');
|
return $this->render('@ChillMain/Admin/indexUser.html.twig');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/{_locale}/admin/dashboard", name="chill_main_dashboard_admin")
|
||||||
|
*/
|
||||||
|
public function indexDashboardAction()
|
||||||
|
{
|
||||||
|
return $this->render('@ChillMain/Admin/indexDashboard.html.twig');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Controller;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Repository\NewsItemRepository;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
final readonly class DashboardApiController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private NewsItemRepository $newsItemRepository,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user dashboard config (not yet based on user id and still hardcoded for now).
|
||||||
|
*
|
||||||
|
* @Route("/api/1.0/main/dashboard-config-item.json", methods={"get"})
|
||||||
|
*/
|
||||||
|
public function getDashboardConfiguration(): JsonResponse
|
||||||
|
{
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
if (0 < $this->newsItemRepository->countCurrentNews()) {
|
||||||
|
// show news only if we have news
|
||||||
|
// NOTE: maybe this should be done in the frontend...
|
||||||
|
$data[] =
|
||||||
|
[
|
||||||
|
'position' => 'top-left',
|
||||||
|
'id' => 1,
|
||||||
|
'type' => 'news',
|
||||||
|
'metadata' => [
|
||||||
|
// arbitrary data that will be store "some time"
|
||||||
|
'only_unread' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse($data, JsonResponse::HTTP_OK, []);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Controller;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||||
|
use Chill\MainBundle\Repository\NewsItemRepository;
|
||||||
|
use Chill\MainBundle\Serializer\Model\Collection;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||||
|
use Symfony\Component\Serializer\SerializerInterface;
|
||||||
|
|
||||||
|
class NewsItemApiController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly NewsItemRepository $newsItemRepository,
|
||||||
|
private readonly SerializerInterface $serializer,
|
||||||
|
private readonly PaginatorFactory $paginatorFactory
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of news items filtered on start and end date.
|
||||||
|
*
|
||||||
|
* @Route("/api/1.0/main/news/current.json", methods={"get"})
|
||||||
|
*/
|
||||||
|
public function listCurrentNewsItems(): JsonResponse
|
||||||
|
{
|
||||||
|
$total = $this->newsItemRepository->countCurrentNews();
|
||||||
|
$paginator = $this->paginatorFactory->create($total);
|
||||||
|
$newsItems = $this->newsItemRepository->findCurrentNews(
|
||||||
|
$paginator->getItemsPerPage(),
|
||||||
|
$paginator->getCurrentPage()->getFirstItemNumber()
|
||||||
|
);
|
||||||
|
|
||||||
|
return new JsonResponse($this->serializer->serialize(
|
||||||
|
new Collection(array_values($newsItems), $paginator),
|
||||||
|
'json',
|
||||||
|
[
|
||||||
|
AbstractNormalizer::GROUPS => ['read'],
|
||||||
|
]
|
||||||
|
), JsonResponse::HTTP_OK, [], true);
|
||||||
|
}
|
||||||
|
}
|
27
src/Bundle/ChillMainBundle/Controller/NewsItemController.php
Normal file
27
src/Bundle/ChillMainBundle/Controller/NewsItemController.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?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\MainBundle\Controller;
|
||||||
|
|
||||||
|
use Chill\MainBundle\CRUD\Controller\CRUDController;
|
||||||
|
use Chill\MainBundle\Pagination\PaginatorInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
|
class NewsItemController extends CRUDController
|
||||||
|
{
|
||||||
|
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
|
||||||
|
{
|
||||||
|
$query->addOrderBy('e.startDate', 'DESC');
|
||||||
|
$query->addOrderBy('e.id', 'DESC');
|
||||||
|
|
||||||
|
return parent::orderQuery($action, $query, $request, $paginator);
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user