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