mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-10 08:44:58 +00:00
Compare commits
132 Commits
2.10.5
...
async-uplo
Author | SHA1 | Date | |
---|---|---|---|
3fb04594eb
|
|||
1065706e60
|
|||
f131572344
|
|||
45e1ce034a
|
|||
82ca321715
|
|||
28d09a8206
|
|||
1195767eb3
|
|||
4fd5a37df3
|
|||
450e7c348b
|
|||
a70572266f
|
|||
d688022825
|
|||
264fff5c36
|
|||
91e6b035bd
|
|||
2adb6105eb | |||
33d187f329 | |||
8cc93a8b07
|
|||
f7184ca7bb
|
|||
dab80a84d8 | |||
68e00dc42f | |||
5fae49821f | |||
0e599a99a7 | |||
f0605c6b08
|
|||
54606403b4
|
|||
47d829d72d
|
|||
fa3b305ab9 | |||
b26b7a2706 | |||
fe54e51362
|
|||
179e3e92ed
|
|||
c00c6066a9 | |||
f424c5464f
|
|||
b17b2a8cfb
|
|||
00de657cae
|
|||
673518e0eb | |||
4700a0fef7 | |||
f4d258882f
|
|||
fffc4a9c33
|
|||
a344f36592 | |||
eef93b8a0d | |||
75b78d3c99 | |||
3d4c439be4 | |||
4727a57825
|
|||
5f441eb5ac
|
|||
2e4e5ee79a
|
|||
1467c708f2 | |||
170bb9586d
|
|||
c704ffa379 | |||
947b7b90e2
|
|||
992f7761bb
|
|||
35170e1f7c
|
|||
7132dfa3f6 | |||
7e09e0ea54 | |||
ccf8cc4d6e | |||
63124f8f92
|
|||
75d80ebd98 | |||
0ea6f36297 | |||
975ea417b7 | |||
af8e02f76b | |||
cbaeb3d7e8 | |||
f609ddb315 | |||
d0bceb59dc
|
|||
2883e085ed
|
|||
b05ed86d1e
|
|||
c855d0badc | |||
be57c96a2f | |||
eb01c7c203
|
|||
53b4747697
|
|||
89e19502d3
|
|||
ff344dbb0c
|
|||
8719b4dedd | |||
d8fa743bc9
|
|||
eaa40d6725
|
|||
1f47f157ea
|
|||
5ab0d3f8da
|
|||
98fd5cae78 | |||
2bb29242e4 | |||
e6cab938c8
|
|||
f5f4d8fcdd
|
|||
e9df26c2f7
|
|||
af6bee2497
|
|||
af585bada3
|
|||
f9763b866d | |||
7f18a2fb7d | |||
3cdad6caff | |||
200ab836c7 | |||
3e39c0ced7 | |||
83c3621c26 | |||
b790e2fcf1 | |||
fde6000d0b | |||
3892d1e877 | |||
6944773868 | |||
d5bc9d10d5 | |||
3743b336a6 | |||
3332413fe0 | |||
86e659edd4
|
|||
2e60ba3137
|
|||
1663c6f7c7
|
|||
d01b6db5dc
|
|||
847c8238e8
|
|||
2cc5fa06ae
|
|||
0d741ab886
|
|||
9cb794fef2 | |||
fd03855ee6 | |||
ac9897d9e7
|
|||
88469dbe8e
|
|||
cbc83c0e63
|
|||
27012d842d
|
|||
4112e59af4
|
|||
1b0a30baf8 | |||
9673b369ef
|
|||
43966c4d5a
|
|||
c9f438319c
|
|||
58b45c6f1b
|
|||
f756c0d71e | |||
bb1602934c
|
|||
83fe3ec3fc
|
|||
abebb79e8b
|
|||
3a269ba953 | |||
a4a4aa119e | |||
139f5a1ff9 | |||
1e43772b17 | |||
c29b356c02 | |||
050a2955ca | |||
a1132cf82f | |||
db70ce4a61 | |||
4d20a46717
|
|||
dd75cb695c
|
|||
3ce78d0c16 | |||
f43bd2bc58 | |||
ec85f05e0b | |||
bfe0d9a137 | |||
cac092c45a | |||
39bb06b991 |
5
.changes/unreleased/Feature-20231212-154841.yaml
Normal file
5
.changes/unreleased/Feature-20231212-154841.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
kind: Feature
|
||||
body: '[DX] move async-upload-bundle features into chill-bundles'
|
||||
time: 2023-12-12T15:48:41.954970271+01:00
|
||||
custom:
|
||||
Issue: "221"
|
4
.changes/v2.10.6.md
Normal file
4
.changes/v2.10.6.md
Normal file
@@ -0,0 +1,4 @@
|
||||
## v2.10.6 - 2023-11-07
|
||||
### Fixed
|
||||
* ([#182](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/182)) Fix merging of double person files. Adjustement relationship sql statement
|
||||
* ([#185](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/185)) Export: fix aggregator by geographical unit on person: avoid inconsistencies
|
6
.changes/v2.11.0.md
Normal file
6
.changes/v2.11.0.md
Normal file
@@ -0,0 +1,6 @@
|
||||
## v2.11.0 - 2023-11-07
|
||||
### Feature
|
||||
* ([#194](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/194)) Export: add a filter "filter activity by creator job"
|
||||
### Fixed
|
||||
* ([#185](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/185)) Export: fix "group accompanying period by geographical unit": take into account the accompanying periods when the period is not located within an unit
|
||||
* Fix "group activity by creator job" aggregator
|
26
.changes/v2.12.0.md
Normal file
26
.changes/v2.12.0.md
Normal file
@@ -0,0 +1,26 @@
|
||||
## v2.12.0 - 2023-11-15
|
||||
### Feature
|
||||
* ([#199](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/199)) Export: add an aggregator "group activities by presence"
|
||||
* ([#199](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/199)) Export: add a filter "filter activity by activity presence"
|
||||
* ([#199](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/199)) Export: add an aggregator "group activities by person" (only for the activities saved in a person context)
|
||||
* ([#199](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/199)) Export: add a new aggregator "group peoples by postal code"
|
||||
* ([#200](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/200)) Export: split export about person on accompanying period work: one with the people associated with the work, another one with the people associated with the accompanying period
|
||||
* ([#204](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/204)) Add 3 new filters and 3 new aggregators for work action creator (with jobs and scopes)
|
||||
|
||||
* ([#202](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/202)) Create export for the average duration of social work actions
|
||||
* ([#206](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/206)) Export: add a export which count persons on accompanying period work
|
||||
* ([#206](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/206)) Export: add an export which count persons on activity
|
||||
* ([#203](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/203)) Export: add clauses on the social work start date and end date within the filter "Filter accompanying period by accompanying period work"
|
||||
### Fixed
|
||||
* Export: fix typo in filter "filter accompanying period work on end date"
|
||||
* ([#189](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/189)) Export: Fix failure in export linked to household
|
||||
* ([#205](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/205)) Fix loading of accompanying period work referrers
|
||||
### Traduction francophone des principaux changements
|
||||
* export: ajout d'un regroupement "grouper les échanges par présence de l'usager";
|
||||
* export: ajout d'un filtre "filtre les échanges par présence de l'usager";
|
||||
* export: ajout d'un regroupement "regrouper les échanges par personne" (seulement pour les échanges enregistrés dans le contexte de l'usager);
|
||||
* export: ajout d'un regroupement "grouper les usagers par codes postaux"
|
||||
* export: séparation des exports sur les actions: dans l'un, les filtres des usagers portent sur les usagers concernés par l'action, dans l'autre, les filtres portent sur les usagers concernés par le parcours de l'action;
|
||||
* export: ajout de 3 nouveaux filtres et regroupements sur le créateur de l'action, son métier et son service;
|
||||
* export: correction de l'export sur les ménages liés aux parcours;
|
||||
* correction du chargement des actions d'accompagnement
|
3
.changes/v2.12.1.md
Normal file
3
.changes/v2.12.1.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## v2.12.1 - 2023-11-16
|
||||
### Fixed
|
||||
* ([#208](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/208)) Export: fix loading of form for "filter action by type, goal and result"
|
9
.changes/v2.13.0.md
Normal file
9
.changes/v2.13.0.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## v2.13.0 - 2023-11-21
|
||||
### Feature
|
||||
* ([#173](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/173)) Allow user to add a phonenumber to their profile which will be included in automatically generated documents
|
||||
### Fixed
|
||||
* ([#211](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/211)) Export: fix loading of "Group activity by type"
|
||||
* ([#190](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/190)) Export: fix loading of "group activity by reasons"
|
||||
* ([#213](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/213)) Export: fix usage of some Collection returned instead of array in export filters
|
||||
* ([#215](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/215)) Use only the string 'both' for gender (with a database migration)
|
||||
* ([#212](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/212)) Clean the database to make working the "Group people by gender" aggregator
|
8
.changes/v2.14.0.md
Normal file
8
.changes/v2.14.0.md
Normal file
@@ -0,0 +1,8 @@
|
||||
## v2.14.0 - 2023-11-24
|
||||
### Feature
|
||||
* ([#161](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/161)) Export: in filter "Filter accompanying period work (social action) by type, goal and result", order the items alphabetically or with the defined order
|
||||
### Fixed
|
||||
* ([#141](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/141)) Export: on filter "action by type goals, and results", restore the fields when editing a saved export
|
||||
* ([#219](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/219)) Export: fix the list of accompanying period work, when the "calc date" is null
|
||||
* ([#222](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/222)) Fix rendering of custom fields
|
||||
* Fix various errors in custom fields administration
|
5
.changes/v2.14.1.md
Normal file
5
.changes/v2.14.1.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## v2.14.1 - 2023-11-29
|
||||
### Fixed
|
||||
* Export: fix list person with custom fields
|
||||
* ([#100](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/100)) Add a paginator to budget elements (resource and charge types) in the admin
|
||||
* Fix error in ListEvaluation when "handling agents" are alone
|
11
.changes/v2.15.0.md
Normal file
11
.changes/v2.15.0.md
Normal file
@@ -0,0 +1,11 @@
|
||||
## v2.15.0 - 2023-12-11
|
||||
### Feature
|
||||
* ([#191](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/191)) Add export "number of household associate with an exchange"
|
||||
* ([#235](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/235)) Export: add dates on the filter "filter course by activity type"
|
||||
### Fixed
|
||||
* ([#214](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/214)) Fix error when posting an empty comment on an accompanying period.
|
||||
* ([#233](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/233)) Fix "filter evaluation by evaluation type" (and add select2 to the list of evaluation types to pick)
|
||||
* ([#234](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/234)) Fix "filter aside activity by date"
|
||||
|
||||
* ([#228](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/228)) Fix export of activity for people created before the introduction of the createdAt column on person (during v1)
|
||||
* ([#246](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/246)) Do not show activities, evaluations and social work when associated to a confidential accompanying period, except for the users which are allowed to see them
|
@@ -11,6 +11,10 @@ cache:
|
||||
services:
|
||||
- name: postgis/postgis:14-3.3-alpine
|
||||
alias: db
|
||||
command:
|
||||
- postgres
|
||||
- "-c"
|
||||
- max_connections=1000
|
||||
- name: redis
|
||||
alias: redis
|
||||
|
||||
@@ -108,8 +112,6 @@ rector_tests:
|
||||
# - tests/app/vendor/
|
||||
|
||||
unit_tests:
|
||||
# temporarily allow failure due to problem with timezone
|
||||
allow_failure: true
|
||||
stage: Tests
|
||||
image: gitea.champs-libres.be/chill-project/chill-skeleton-basic/base-image:php82
|
||||
script:
|
||||
|
85
CHANGELOG.md
85
CHANGELOG.md
@@ -6,6 +6,91 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
|
||||
|
||||
## v2.15.0 - 2023-12-11
|
||||
### Feature
|
||||
* ([#191](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/191)) Add export "number of household associate with an exchange"
|
||||
* ([#235](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/235)) Export: add dates on the filter "filter course by activity type"
|
||||
### Fixed
|
||||
* ([#214](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/214)) Fix error when posting an empty comment on an accompanying period.
|
||||
* ([#233](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/233)) Fix "filter evaluation by evaluation type" (and add select2 to the list of evaluation types to pick)
|
||||
* ([#234](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/234)) Fix "filter aside activity by date"
|
||||
|
||||
* ([#228](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/228)) Fix export of activity for people created before the introduction of the createdAt column on person (during v1)
|
||||
* ([#246](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/246)) Do not show activities, evaluations and social work when associated to a confidential accompanying period, except for the users which are allowed to see them
|
||||
|
||||
## v2.14.1 - 2023-11-29
|
||||
### Fixed
|
||||
* Export: fix list person with custom fields
|
||||
* ([#100](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/100)) Add a paginator to budget elements (resource and charge types) in the admin
|
||||
* Fix error in ListEvaluation when "handling agents" are alone
|
||||
|
||||
## v2.14.0 - 2023-11-24
|
||||
### Feature
|
||||
* ([#161](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/161)) Export: in filter "Filter accompanying period work (social action) by type, goal and result", order the items alphabetically or with the defined order
|
||||
### Fixed
|
||||
* ([#141](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/141)) Export: on filter "action by type goals, and results", restore the fields when editing a saved export
|
||||
* ([#219](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/219)) Export: fix the list of accompanying period work, when the "calc date" is null
|
||||
* ([#222](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/222)) Fix rendering of custom fields
|
||||
* Fix various errors in custom fields administration
|
||||
|
||||
## v2.13.0 - 2023-11-21
|
||||
### Feature
|
||||
* ([#173](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/173)) Allow user to add a phonenumber to their profile which will be included in automatically generated documents
|
||||
### Fixed
|
||||
* ([#211](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/211)) Export: fix loading of "Group activity by type"
|
||||
* ([#190](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/190)) Export: fix loading of "group activity by reasons"
|
||||
* ([#213](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/213)) Export: fix usage of some Collection returned instead of array in export filters
|
||||
* ([#215](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/215)) Use only the string 'both' for gender (with a database migration)
|
||||
* ([#212](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/212)) Clean the database to make working the "Group people by gender" aggregator
|
||||
|
||||
## v2.12.1 - 2023-11-16
|
||||
### Fixed
|
||||
* ([#208](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/208)) Export: fix loading of form for "filter action by type, goal and result"
|
||||
|
||||
## v2.12.0 - 2023-11-15
|
||||
### Feature
|
||||
* ([#199](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/199)) Export: add an aggregator "group activities by presence"
|
||||
* ([#199](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/199)) Export: add a filter "filter activity by activity presence"
|
||||
* ([#199](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/199)) Export: add an aggregator "group activities by person" (only for the activities saved in a person context)
|
||||
* ([#199](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/199)) Export: add a new aggregator "group peoples by postal code"
|
||||
* ([#200](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/200)) Export: split export about person on accompanying period work: one with the people associated with the work, another one with the people associated with the accompanying period
|
||||
* ([#204](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/204)) Add 3 new filters and 3 new aggregators for work action creator (with jobs and scopes)
|
||||
|
||||
* ([#202](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/202)) Create export for the average duration of social work actions
|
||||
* ([#206](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/206)) Export: add a export which count persons on accompanying period work
|
||||
* ([#206](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/206)) Export: add an export which count persons on activity
|
||||
* ([#203](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/203)) Export: add clauses on the social work start date and end date within the filter "Filter accompanying period by accompanying period work"
|
||||
### Fixed
|
||||
* Export: fix typo in filter "filter accompanying period work on end date"
|
||||
* ([#189](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/189)) Export: Fix failure in export linked to household
|
||||
* ([#205](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/205)) Fix loading of accompanying period work referrers
|
||||
### Traduction francophone des principaux changements
|
||||
* export: ajout d'un regroupement "grouper les échanges par présence de l'usager";
|
||||
* export: ajout d'un filtre "filtre les échanges par présence de l'usager";
|
||||
* export: ajout d'un regroupement "regrouper les échanges par personne" (seulement pour les échanges enregistrés dans le contexte de l'usager);
|
||||
* export: ajout d'un regroupement "grouper les usagers par codes postaux"
|
||||
* export: séparation des exports sur les actions: dans l'un, les filtres des usagers portent sur les usagers concernés par l'action, dans l'autre, les filtres portent sur les usagers concernés par le parcours de l'action;
|
||||
* export: ajout de 3 nouveaux filtres et regroupements sur le créateur de l'action, son métier et son service;
|
||||
* export: correction de l'export sur les ménages liés aux parcours;
|
||||
* correction du chargement des actions d'accompagnement
|
||||
|
||||
## v2.11.0 - 2023-11-07
|
||||
### Feature
|
||||
* ([#194](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/194)) Export: add a filter "filter activity by creator job"
|
||||
### Fixed
|
||||
* ([#185](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/185)) Export: fix "group accompanying period by geographical unit": take into account the accompanying periods when the period is not located within an unit
|
||||
* Fix "group activity by creator job" aggregator
|
||||
|
||||
## v2.10.6 - 2023-11-07
|
||||
### Fixed
|
||||
* ([#182](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/182)) Fix merging of double person files. Adjustement relationship sql statement
|
||||
* ([#185](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/185)) Export: fix aggregator by geographical unit on person: avoid inconsistencies
|
||||
|
||||
## v2.10.5 - 2023-11-05
|
||||
### Fixed
|
||||
* ([#183](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/183)) Fix "problem during download" on some filters, which used a wrong data type
|
||||
* ([#184](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/184)) Fix filter "activity by date"
|
||||
|
||||
## v2.10.4 - 2023-10-26
|
||||
### Fixed
|
||||
* Fix null value constraint errors when merging relationships in doubles
|
||||
|
@@ -12,7 +12,6 @@
|
||||
"ext-json": "*",
|
||||
"ext-openssl": "*",
|
||||
"ext-redis": "*",
|
||||
"champs-libres/async-uploader-bundle": "dev-sf4#d57134aee8e504a83c902ff0cf9f8d36ac418290",
|
||||
"champs-libres/wopi-bundle": "dev-master@dev",
|
||||
"champs-libres/wopi-lib": "dev-master@dev",
|
||||
"doctrine/doctrine-bundle": "^2.1",
|
||||
|
@@ -1,13 +1,15 @@
|
||||
|
||||
Configure Chill for calendar sync and SSO with Microsoft Graph (Outlook)
|
||||
========================================================================
|
||||
Configure Chill for calendar and absence synchronisation and SSO with Microsoft Graph (Outlook)
|
||||
===============================================================================================
|
||||
|
||||
Chill offers the possibility to:
|
||||
|
||||
* authenticate users using Microsoft Graph, with relatively small adaptations;
|
||||
* synchronize calendar in both ways (`see the user manual for a large description of the feature <https://gitea.champs-libres.be/Chill-project/manuals>`_).
|
||||
|
||||
Both can be configured separately (synchronising calendars without SSO, or SSO without calendar). When calendar sync is configured without SSL, the user's email address is the key to associate Chill's users with Microsoft's ones.
|
||||
Both can be configured separately (synchronising calendars without SSO, or SSO without calendar).
|
||||
|
||||
Please note that the user's email address is the key to associate Chill's users with Microsoft's ones.
|
||||
|
||||
Configure SSO
|
||||
-------------
|
||||
@@ -46,7 +48,7 @@ Do not forget to provider user's accesses to your app, using the "Utilisateurs e
|
||||
|
||||
You must know have gathered all the required variables for SSO:
|
||||
|
||||
.. code-block::
|
||||
.. code-block::
|
||||
|
||||
SAML_BASE_URL=https://test.chill.be # must be
|
||||
SAML_ENTITY_ID=https://test.chill.be # must match the one entered
|
||||
@@ -186,20 +188,27 @@ Configure chill app
|
||||
|
||||
|
||||
|
||||
Configure sync
|
||||
--------------
|
||||
Configure sync and calendar access
|
||||
----------------------------------
|
||||
|
||||
The sync processe might be configured in the same app, or into a different app.
|
||||
The purpose of this configuration is the following:
|
||||
|
||||
The synchronization processes use Oauth2.0 for authentication and authorization.
|
||||
- let user read their calendar and shared calendar within Chill (with the same permissions as the one configured in Outlook / Azure);
|
||||
- allow chill instance to write appointment ("Rendez-vous") into their calendar, and invite other users to their appointment;
|
||||
- allow chill instance to be notified if an appoint is added or removed by the user within another interface than Chill: if the appointment match another one created in the Chill interface, the date and time are updated in Chill;
|
||||
- allow chill instance to read the absence of the user and, if set, mark the user as absent in Chill;
|
||||
|
||||
The sync processe might be configured in the same app, or into a different app on the Azure side.
|
||||
|
||||
The synchronization processes use Oauth 2.0 / OpenID Connect for authentication and authorization.
|
||||
|
||||
.. note::
|
||||
|
||||
Two flows are in use:
|
||||
|
||||
* we authenticate "on behalf of a user", to allow users to see their own calendar or other user's calendar into the web interface.
|
||||
* we authenticate "on behalf of a user", to allow users to see their own calendar or other user's calendar into the web interface.
|
||||
|
||||
Typically, when the page is loaded, Chill first check that an authorization token exists. If not, the user is redirected to Microsoft Azure for authentification and a new token is grabbed (most of the times, this is transparent for users).
|
||||
Typically, when the page is loaded, Chill first check that an authorization token exists. If not, the user is redirected to Microsoft Azure for authentification and a new token is grabbed (most of the times, this is transparent for users).
|
||||
|
||||
* Chill also acts "as a machine", to synchronize calendars with a daemon background.
|
||||
|
||||
@@ -229,8 +238,9 @@ Some explanation:
|
||||
The sync daemon must have write access:
|
||||
|
||||
* the daemon must be allowed to read all users and their profile, to establish a link between them and the Chill's users: (:code:`Users.Read.All`);
|
||||
* it must also be allowed to read and write into the calendars (:code:`Calendars.ReadWrite.All`)
|
||||
* for sending invitation to other users, the permission (:code:`Mail.Send`) must be granted.
|
||||
* it must also be allowed to read and write into the calendars (:code:`Calendars.ReadWrite.All`);
|
||||
* for sending invitation to other users, the permission (:code:`Mail.Send`) must be granted;
|
||||
* and, for reading the absence status of the user and sync it with chill, it must be able to read the mailboxSettings (:code:`MailboxSettings.Read`).
|
||||
|
||||
At this step, you might choose to accept those permissions for all users, or let them do it by yourself.
|
||||
|
||||
@@ -301,7 +311,7 @@ The calendar synchronization is processed using symfony messenger. It seems to b
|
||||
|
||||
The association between chill's users and Microsoft's users is done by this cli command:
|
||||
|
||||
.. code-block::
|
||||
.. code-block::
|
||||
|
||||
bin/console chill:calendar:msgraph-user-map-subscribe
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 166 KiB |
@@ -0,0 +1,72 @@
|
||||
<?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\Export\Aggregator;
|
||||
|
||||
use Chill\ActivityBundle\Export\Declarations;
|
||||
use Chill\ActivityBundle\Repository\ActivityPresenceRepositoryInterface;
|
||||
use Chill\MainBundle\Export\AggregatorInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
final readonly class ActivityPresenceAggregator implements AggregatorInterface
|
||||
{
|
||||
public function __construct(private ActivityPresenceRepositoryInterface $activityPresenceRepository, private TranslatableStringHelperInterface $translatableStringHelper) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder) {}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getLabels($key, array $values, mixed $data)
|
||||
{
|
||||
return function (null|int|string $value): string {
|
||||
if ('_header' === $value) {
|
||||
return 'export.aggregator.activity.by_activity_presence.header';
|
||||
}
|
||||
|
||||
if (null === $value || '' === $value || null === $presence = $this->activityPresenceRepository->find($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->translatableStringHelper->localize($presence->getName());
|
||||
};
|
||||
}
|
||||
|
||||
public function getQueryKeys($data)
|
||||
{
|
||||
return ['activity_presence_aggregator_attendee'];
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'export.aggregator.activity.by_activity_presence.Group activity by presence';
|
||||
}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data): void
|
||||
{
|
||||
$qb->addSelect('IDENTITY(activity.attendee) AS activity_presence_aggregator_attendee');
|
||||
$qb->addGroupBy('activity_presence_aggregator_attendee');
|
||||
}
|
||||
|
||||
public function applyOn()
|
||||
{
|
||||
return Declarations::ACTIVITY;
|
||||
}
|
||||
}
|
@@ -56,20 +56,15 @@ class ActivityTypeAggregator implements AggregatorInterface
|
||||
|
||||
public function getLabels($key, array $values, $data): \Closure
|
||||
{
|
||||
// for performance reason, we load data from db only once
|
||||
$this->activityTypeRepository->findBy(['id' => $values]);
|
||||
|
||||
return function ($value): string {
|
||||
return function (null|int|string $value): string {
|
||||
if ('_header' === $value) {
|
||||
return 'Activity type';
|
||||
}
|
||||
|
||||
if (null === $value || '' === $value) {
|
||||
if (null === $value || '' === $value || null === $t = $this->activityTypeRepository->find($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$t = $this->activityTypeRepository->find($value);
|
||||
|
||||
return $this->translatableStringHelper->localize($t->getName());
|
||||
};
|
||||
}
|
||||
|
@@ -14,18 +14,18 @@ namespace Chill\ActivityBundle\Export\Aggregator;
|
||||
use Chill\ActivityBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Entity\User\UserJobHistory;
|
||||
use Chill\MainBundle\Export\AggregatorInterface;
|
||||
use Chill\MainBundle\Repository\ScopeRepository;
|
||||
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class JobScopeAggregator implements AggregatorInterface
|
||||
class CreatorJobAggregator implements AggregatorInterface
|
||||
{
|
||||
private const PREFIX = 'acp_agg_creator_job';
|
||||
|
||||
public function __construct(
|
||||
private readonly ScopeRepository $scopeRepository,
|
||||
private readonly UserJobRepositoryInterface $userJobRepository,
|
||||
private readonly TranslatableStringHelper $translatableStringHelper
|
||||
) {}
|
||||
|
||||
@@ -76,17 +76,15 @@ class JobScopeAggregator implements AggregatorInterface
|
||||
{
|
||||
return function ($value): string {
|
||||
if ('_header' === $value) {
|
||||
return 'Scope';
|
||||
return 'Job';
|
||||
}
|
||||
|
||||
if (null === $value || '' === $value) {
|
||||
if (null === $value || '' === $value || null === $s = $this->userJobRepository->find($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$s = $this->scopeRepository->find($value);
|
||||
|
||||
return $this->translatableStringHelper->localize(
|
||||
$s->getName()
|
||||
$s->getLabel()
|
||||
);
|
||||
};
|
||||
}
|
@@ -99,12 +99,6 @@ class ActivityReasonAggregator implements AggregatorInterface, ExportElementVali
|
||||
|
||||
public function getLabels($key, array $values, $data)
|
||||
{
|
||||
match ($data['level']) {
|
||||
'reasons' => $this->activityReasonRepository->findBy(['id' => $values]),
|
||||
'categories' => $this->activityReasonCategoryRepository->findBy(['id' => $values]),
|
||||
default => throw new \RuntimeException(sprintf("The level data '%s' is invalid.", $data['level'])),
|
||||
};
|
||||
|
||||
return function ($value) use ($data) {
|
||||
if ('_header' === $value) {
|
||||
return 'reasons' === $data['level'] ? 'Group by reasons' : 'Group by categories of reason';
|
||||
|
@@ -0,0 +1,65 @@
|
||||
<?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\Export\Aggregator\PersonAggregators;
|
||||
|
||||
use Chill\ActivityBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Export\AggregatorInterface;
|
||||
use Chill\PersonBundle\Export\Helper\LabelPersonHelper;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
final readonly class PersonAggregator implements AggregatorInterface
|
||||
{
|
||||
public function __construct(private LabelPersonHelper $labelPersonHelper) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
// nothing to add here
|
||||
}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getLabels($key, array $values, mixed $data)
|
||||
{
|
||||
return $this->labelPersonHelper->getLabel($key, $values, 'export.aggregator.person.by_person.person');
|
||||
}
|
||||
|
||||
public function getQueryKeys($data)
|
||||
{
|
||||
return ['activity_by_person_agg'];
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return 'export.aggregator.person.by_person.title';
|
||||
}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data)
|
||||
{
|
||||
$qb
|
||||
->addSelect('IDENTITY(activity.person) AS activity_by_person_agg')
|
||||
->addGroupBy('activity_by_person_agg');
|
||||
}
|
||||
|
||||
public function applyOn()
|
||||
{
|
||||
return Declarations::ACTIVITY_PERSON;
|
||||
}
|
||||
}
|
@@ -0,0 +1,147 @@
|
||||
<?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\Export\Export\LinkedToACP;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\ActivityBundle\Export\Declarations;
|
||||
use Chill\ActivityBundle\Security\Authorization\ActivityStatsVoter;
|
||||
use Chill\MainBundle\Export\AccompanyingCourseExportHelper;
|
||||
use Chill\MainBundle\Export\ExportInterface;
|
||||
use Chill\MainBundle\Export\FormatterInterface;
|
||||
use Chill\MainBundle\Export\GroupedExportInterface;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
|
||||
use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
||||
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
|
||||
use Chill\PersonBundle\Export\Declarations as PersonDeclarations;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Query;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
final readonly class CountHouseholdOnActivity implements ExportInterface, GroupedExportInterface
|
||||
{
|
||||
private EntityRepository $repository;
|
||||
|
||||
private bool $filterStatsByCenters;
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $em,
|
||||
ParameterBagInterface $parameterBag,
|
||||
) {
|
||||
$this->repository = $em->getRepository(Activity::class);
|
||||
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder) {}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getAllowedFormattersTypes(): array
|
||||
{
|
||||
return [FormatterInterface::TYPE_TABULAR];
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'export.export.count_household_on_activity.description';
|
||||
}
|
||||
|
||||
public function getGroup(): string
|
||||
{
|
||||
return 'Exports of activities linked to an accompanying period';
|
||||
}
|
||||
|
||||
public function getLabels($key, array $values, $data)
|
||||
{
|
||||
if ('export_count_activity' !== $key) {
|
||||
throw new \LogicException("the key {$key} is not used by this export");
|
||||
}
|
||||
|
||||
return static fn ($value) => '_header' === $value ? 'export.export.count_household_on_activity.header' : $value;
|
||||
}
|
||||
|
||||
public function getQueryKeys($data): array
|
||||
{
|
||||
return ['export_count_activity'];
|
||||
}
|
||||
|
||||
public function getResult($query, $data)
|
||||
{
|
||||
return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'export.export.count_household_on_activity.title';
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return Declarations::ACTIVITY;
|
||||
}
|
||||
|
||||
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
|
||||
{
|
||||
$centers = array_map(static fn ($el) => $el['center'], $acl);
|
||||
|
||||
$qb = $this->repository
|
||||
->createQueryBuilder('activity')
|
||||
->join('activity.persons', 'person')
|
||||
->join('activity.accompanyingPeriod', 'acp')
|
||||
->join(
|
||||
HouseholdMember::class,
|
||||
'householdmember',
|
||||
Query\Expr\Join::WITH,
|
||||
'person.id = IDENTITY(householdmember.person) AND householdmember.startDate <= activity.date AND (householdmember.endDate IS NULL OR householdmember.endDate > activity.date)'
|
||||
)
|
||||
->join('householdmember.household', 'household');
|
||||
|
||||
if ($this->filterStatsByCenters) {
|
||||
$qb
|
||||
->andWhere(
|
||||
$qb->expr()->exists(
|
||||
'SELECT 1 FROM '.AccompanyingPeriodParticipation::class.' acl_count_part
|
||||
JOIN '.PersonCenterHistory::class.' acl_count_person_history WITH IDENTITY(acl_count_person_history.person) = IDENTITY(acl_count_part.person)
|
||||
WHERE acl_count_part.accompanyingPeriod = acp.id AND acl_count_person_history.center IN (:authorized_centers)
|
||||
'
|
||||
)
|
||||
)
|
||||
->setParameter('authorized_centers', $centers);
|
||||
}
|
||||
|
||||
AccompanyingCourseExportHelper::addClosingMotiveExclusionClause($qb);
|
||||
|
||||
$qb->select('COUNT(DISTINCT household.id) as export_count_activity');
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function requiredRole(): string
|
||||
{
|
||||
return ActivityStatsVoter::STATS;
|
||||
}
|
||||
|
||||
public function supportsModifiers(): array
|
||||
{
|
||||
return [
|
||||
Declarations::ACTIVITY,
|
||||
Declarations::ACTIVITY_ACP,
|
||||
PersonDeclarations::ACP_TYPE,
|
||||
PersonDeclarations::PERSON_TYPE,
|
||||
PersonDeclarations::HOUSEHOLD_TYPE,
|
||||
];
|
||||
}
|
||||
}
|
@@ -0,0 +1,138 @@
|
||||
<?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\Export\Export\LinkedToACP;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\ActivityBundle\Export\Declarations;
|
||||
use Chill\ActivityBundle\Security\Authorization\ActivityStatsVoter;
|
||||
use Chill\MainBundle\Export\AccompanyingCourseExportHelper;
|
||||
use Chill\MainBundle\Export\ExportInterface;
|
||||
use Chill\MainBundle\Export\FormatterInterface;
|
||||
use Chill\MainBundle\Export\GroupedExportInterface;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
|
||||
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
|
||||
use Chill\PersonBundle\Export\Declarations as PersonDeclarations;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Query;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class CountPersonsOnActivity implements ExportInterface, GroupedExportInterface
|
||||
{
|
||||
protected EntityRepository $repository;
|
||||
|
||||
private readonly bool $filterStatsByCenters;
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $em,
|
||||
ParameterBagInterface $parameterBag,
|
||||
) {
|
||||
$this->repository = $em->getRepository(Activity::class);
|
||||
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder) {}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getAllowedFormattersTypes(): array
|
||||
{
|
||||
return [FormatterInterface::TYPE_TABULAR];
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'export.export.count_person_on_activity.description';
|
||||
}
|
||||
|
||||
public function getGroup(): string
|
||||
{
|
||||
return 'Exports of activities linked to an accompanying period';
|
||||
}
|
||||
|
||||
public function getLabels($key, array $values, $data)
|
||||
{
|
||||
if ('export_count_activity' !== $key) {
|
||||
throw new \LogicException("the key {$key} is not used by this export");
|
||||
}
|
||||
|
||||
return static fn ($value) => '_header' === $value ? 'export.export.count_person_on_activity.header' : $value;
|
||||
}
|
||||
|
||||
public function getQueryKeys($data): array
|
||||
{
|
||||
return ['export_count_activity'];
|
||||
}
|
||||
|
||||
public function getResult($query, $data)
|
||||
{
|
||||
return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'export.export.count_person_on_activity.title';
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return Declarations::ACTIVITY;
|
||||
}
|
||||
|
||||
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
|
||||
{
|
||||
$centers = array_map(static fn ($el) => $el['center'], $acl);
|
||||
|
||||
$qb = $this->repository
|
||||
->createQueryBuilder('activity')
|
||||
->join('activity.persons', 'person')
|
||||
->join('activity.accompanyingPeriod', 'acp');
|
||||
|
||||
if ($this->filterStatsByCenters) {
|
||||
$qb
|
||||
->andWhere(
|
||||
$qb->expr()->exists(
|
||||
'SELECT 1 FROM '.AccompanyingPeriodParticipation::class.' acl_count_part
|
||||
JOIN '.PersonCenterHistory::class.' acl_count_person_history WITH IDENTITY(acl_count_person_history.person) = IDENTITY(acl_count_part.person)
|
||||
WHERE acl_count_part.accompanyingPeriod = acp.id AND acl_count_person_history.center IN (:authorized_centers)
|
||||
'
|
||||
)
|
||||
)
|
||||
->setParameter('authorized_centers', $centers);
|
||||
}
|
||||
|
||||
AccompanyingCourseExportHelper::addClosingMotiveExclusionClause($qb);
|
||||
|
||||
$qb->select('COUNT(DISTINCT person.id) as export_count_activity');
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function requiredRole(): string
|
||||
{
|
||||
return ActivityStatsVoter::STATS;
|
||||
}
|
||||
|
||||
public function supportsModifiers(): array
|
||||
{
|
||||
return [
|
||||
Declarations::ACTIVITY,
|
||||
Declarations::ACTIVITY_ACP,
|
||||
PersonDeclarations::ACP_TYPE,
|
||||
PersonDeclarations::PERSON_TYPE,
|
||||
];
|
||||
}
|
||||
}
|
@@ -20,23 +20,18 @@ use Chill\MainBundle\Export\AccompanyingCourseExportHelper;
|
||||
use Chill\MainBundle\Export\GroupedExportInterface;
|
||||
use Chill\MainBundle\Export\Helper\TranslatableStringExportLabelHelper;
|
||||
use Chill\MainBundle\Export\ListInterface;
|
||||
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
|
||||
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelperInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class ListActivity implements ListInterface, GroupedExportInterface
|
||||
final readonly class ListActivity implements ListInterface, GroupedExportInterface
|
||||
{
|
||||
private readonly bool $filterStatsByCenters;
|
||||
|
||||
public function __construct(
|
||||
private readonly ListActivityHelper $helper,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly TranslatableStringExportLabelHelper $translatableStringExportLabelHelper,
|
||||
ParameterBagInterface $parameterBag,
|
||||
) {
|
||||
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
|
||||
}
|
||||
private ListActivityHelper $helper,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private TranslatableStringExportLabelHelper $translatableStringExportLabelHelper,
|
||||
private FilterListAccompanyingPeriodHelperInterface $filterListAccompanyingPeriodHelper,
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
@@ -119,19 +114,7 @@ class ListActivity implements ListInterface, GroupedExportInterface
|
||||
->leftJoin('acppart.person', 'person')
|
||||
->andWhere('acppart.startDate != acppart.endDate OR acppart.endDate IS NULL');
|
||||
|
||||
if ($this->filterStatsByCenters) {
|
||||
$qb
|
||||
->andWhere(
|
||||
$qb->expr()->exists(
|
||||
'SELECT 1
|
||||
FROM '.PersonCenterHistory::class.' acl_count_person_history
|
||||
WHERE acl_count_person_history.person = person
|
||||
AND acl_count_person_history.center IN (:authorized_centers)
|
||||
'
|
||||
)
|
||||
)
|
||||
->setParameter('authorized_centers', $centers);
|
||||
}
|
||||
$this->filterListAccompanyingPeriodHelper->addFilterAccompanyingPeriods($qb, $requiredModifiers, $acl, $data);
|
||||
|
||||
$qb
|
||||
// some grouping are necessary
|
||||
|
@@ -0,0 +1,138 @@
|
||||
<?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\Export\Export\LinkedToPerson;
|
||||
|
||||
use Chill\ActivityBundle\Export\Declarations;
|
||||
use Chill\ActivityBundle\Repository\ActivityRepository;
|
||||
use Chill\ActivityBundle\Security\Authorization\ActivityStatsVoter;
|
||||
use Chill\MainBundle\Export\ExportInterface;
|
||||
use Chill\MainBundle\Export\FormatterInterface;
|
||||
use Chill\MainBundle\Export\GroupedExportInterface;
|
||||
use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
||||
use Chill\PersonBundle\Export\Declarations as PersonDeclarations;
|
||||
use Doctrine\ORM\Query;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
final readonly class CountHouseholdOnActivity implements ExportInterface, GroupedExportInterface
|
||||
{
|
||||
private bool $filterStatsByCenters;
|
||||
|
||||
public function __construct(
|
||||
private ActivityRepository $activityRepository,
|
||||
ParameterBagInterface $parameterBag,
|
||||
) {
|
||||
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder) {}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getAllowedFormattersTypes()
|
||||
{
|
||||
return [FormatterInterface::TYPE_TABULAR];
|
||||
}
|
||||
|
||||
public function getDescription()
|
||||
{
|
||||
return 'export.export.count_household_on_activity_person.description';
|
||||
}
|
||||
|
||||
public function getGroup(): string
|
||||
{
|
||||
return 'Exports of activities linked to a person';
|
||||
}
|
||||
|
||||
public function getLabels($key, array $values, $data)
|
||||
{
|
||||
if ('export_count_activity' !== $key) {
|
||||
throw new \LogicException("the key {$key} is not used by this export");
|
||||
}
|
||||
|
||||
return static fn ($value) => '_header' === $value ? 'export.export.count_household_on_activity_person.header' : $value;
|
||||
}
|
||||
|
||||
public function getQueryKeys($data)
|
||||
{
|
||||
return ['export_count_activity'];
|
||||
}
|
||||
|
||||
public function getResult($query, $data)
|
||||
{
|
||||
return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return 'export.export.count_household_on_activity_person.title';
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return Declarations::ACTIVITY;
|
||||
}
|
||||
|
||||
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
|
||||
{
|
||||
$centers = array_map(static fn ($el) => $el['center'], $acl);
|
||||
|
||||
$qb = $this->activityRepository
|
||||
->createQueryBuilder('activity')
|
||||
->join('activity.person', 'person')
|
||||
->join(
|
||||
HouseholdMember::class,
|
||||
'householdmember',
|
||||
Query\Expr\Join::WITH,
|
||||
'person = householdmember.person AND householdmember.startDate <= activity.date AND (householdmember.endDate IS NULL OR householdmember.endDate > activity.date)'
|
||||
)
|
||||
->join('householdmember.household', 'household');
|
||||
|
||||
$qb->select('COUNT(DISTINCT household.id) as export_count_activity');
|
||||
|
||||
if ($this->filterStatsByCenters) {
|
||||
$qb
|
||||
->join('person.centerHistory', 'centerHistory')
|
||||
->where(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->lte('centerHistory.startDate', 'activity.date'),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isNull('centerHistory.endDate'),
|
||||
$qb->expr()->gt('centerHistory.endDate', 'activity.date')
|
||||
)
|
||||
)
|
||||
)
|
||||
->andWhere($qb->expr()->in('centerHistory.center', ':centers'))
|
||||
->setParameter('centers', $centers);
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function requiredRole(): string
|
||||
{
|
||||
return ActivityStatsVoter::STATS;
|
||||
}
|
||||
|
||||
public function supportsModifiers()
|
||||
{
|
||||
return [
|
||||
Declarations::ACTIVITY,
|
||||
Declarations::ACTIVITY_PERSON,
|
||||
PersonDeclarations::PERSON_TYPE,
|
||||
PersonDeclarations::HOUSEHOLD_TYPE,
|
||||
];
|
||||
}
|
||||
}
|
@@ -15,17 +15,22 @@ use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\ActivityBundle\Entity\ActivityType;
|
||||
use Chill\ActivityBundle\Repository\ActivityTypeRepositoryInterface;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Chill\PersonBundle\Export\Declarations;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class ActivityTypeFilter implements FilterInterface
|
||||
final readonly class ActivityTypeFilter implements FilterInterface
|
||||
{
|
||||
private const BASE_EXISTS = 'SELECT 1 FROM '.Activity::class.' act_type_filter_activity WHERE act_type_filter_activity.accompanyingPeriod = acp';
|
||||
|
||||
public function __construct(
|
||||
private readonly ActivityTypeRepositoryInterface $activityTypeRepository,
|
||||
private readonly TranslatableStringHelperInterface $translatableStringHelper
|
||||
private ActivityTypeRepositoryInterface $activityTypeRepository,
|
||||
private TranslatableStringHelperInterface $translatableStringHelper,
|
||||
private RollingDateConverterInterface $rollingDateConverter,
|
||||
) {}
|
||||
|
||||
public function addRole(): ?string
|
||||
@@ -35,13 +40,26 @@ class ActivityTypeFilter implements FilterInterface
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data)
|
||||
{
|
||||
$qb->andWhere(
|
||||
$qb->expr()->exists(
|
||||
'SELECT 1 FROM '.Activity::class.' act_type_filter_activity
|
||||
WHERE act_type_filter_activity.activityType IN (:act_type_filter_activity_types) AND act_type_filter_activity.accompanyingPeriod = acp'
|
||||
)
|
||||
);
|
||||
$qb->setParameter('act_type_filter_activity_types', $data['accepted_activitytypes']);
|
||||
$exists = self::BASE_EXISTS;
|
||||
|
||||
if (count($data['accepted_activitytypes']) > 0) {
|
||||
$exists .= ' AND act_type_filter_activity.activityType IN (:act_type_filter_activity_types)';
|
||||
$qb->setParameter('act_type_filter_activity_types', $data['accepted_activitytypes']);
|
||||
}
|
||||
|
||||
if (null !== $data['date_after']) {
|
||||
$exists .= ' AND act_type_filter_activity.date >= :act_type_filter_activity_date_after';
|
||||
$qb->setParameter('act_type_filter_activity_date_after', $this->rollingDateConverter->convert($data['date_after']));
|
||||
}
|
||||
|
||||
if (null !== $data['date_before']) {
|
||||
$exists .= ' AND act_type_filter_activity.date >= :act_type_filter_activity_date_before';
|
||||
$qb->setParameter('act_type_filter_activity_date_before', $this->rollingDateConverter->convert($data['date_before']));
|
||||
}
|
||||
|
||||
if (self::BASE_EXISTS !== $exists) {
|
||||
$qb->andWhere($qb->expr()->exists($exists));
|
||||
}
|
||||
}
|
||||
|
||||
public function applyOn()
|
||||
@@ -60,11 +78,27 @@ class ActivityTypeFilter implements FilterInterface
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
]);
|
||||
|
||||
$builder->add('date_after', PickRollingDateType::class, [
|
||||
'label' => 'export.filter.activity.acp_by_activity_type.activity after',
|
||||
'help' => 'export.filter.activity.acp_by_activity_type.activity after help',
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
$builder->add('date_before', PickRollingDateType::class, [
|
||||
'label' => 'export.filter.activity.acp_by_activity_type.activity before',
|
||||
'help' => 'export.filter.activity.acp_by_activity_type.activity before help',
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return [];
|
||||
return [
|
||||
'accepted_activitytypes' => [],
|
||||
'date_after' => null,
|
||||
'date_before' => null,
|
||||
];
|
||||
}
|
||||
|
||||
public function describeAction($data, $format = 'string'): array
|
||||
@@ -75,8 +109,12 @@ class ActivityTypeFilter implements FilterInterface
|
||||
$types[] = $this->translatableStringHelper->localize($aty->getName());
|
||||
}
|
||||
|
||||
return ['export.filter.activity.acp_by_activity_type.acp_containing_at_least_one_%activitytypes%', [
|
||||
'%activitytypes%' => implode(', ', $types),
|
||||
return ['export.filter.activity.acp_by_activity_type.acp_containing_at_least_one_activitytypes', [
|
||||
'activitytypes' => implode(', ', $types),
|
||||
'has_date_after' => null !== $data['date_after'] ? 1 : 0,
|
||||
'date_after' => $this->rollingDateConverter->convert($data['date_after']),
|
||||
'has_date_before' => null !== $data['date_before'] ? 1 : 0,
|
||||
'date_before' => $this->rollingDateConverter->convert($data['date_before']),
|
||||
]];
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,82 @@
|
||||
<?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\Export\Filter;
|
||||
|
||||
use Chill\ActivityBundle\Entity\ActivityPresence;
|
||||
use Chill\ActivityBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final readonly class ActivityPresenceFilter implements FilterInterface
|
||||
{
|
||||
public function __construct(
|
||||
private TranslatableStringHelperInterface $translatableStringHelper,
|
||||
private TranslatorInterface $translator
|
||||
) {}
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return 'export.filter.activity.by_presence.Filter activity by activity presence';
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('presences', EntityType::class, [
|
||||
'class' => ActivityPresence::class,
|
||||
'choice_label' => fn (ActivityPresence $presence) => $this->translatableStringHelper->localize($presence->getName())
|
||||
.($presence->isActive() ? '' : ' ('.$this->translator->trans('inactive').')'),
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'label' => 'export.filter.activity.by_presence.presences',
|
||||
]);
|
||||
}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function describeAction($data, $format = 'string')
|
||||
{
|
||||
$presences = array_map(
|
||||
fn (ActivityPresence $presence) => $this->translatableStringHelper->localize($presence->getName()),
|
||||
$data['presences'] instanceof Collection ? $data['presences']->toArray() : $data['presences']
|
||||
);
|
||||
|
||||
return [
|
||||
'export.filter.activity.by_presence.Filtered by activity presence: only %presences%',
|
||||
['%presences%' => implode(', ', $presences)],
|
||||
];
|
||||
}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data)
|
||||
{
|
||||
$qb
|
||||
->andWhere('activity.attendee IN (:activity_presence_filter_presences)')
|
||||
->setParameter('activity_presence_filter_presences', $data['presences']);
|
||||
}
|
||||
|
||||
public function applyOn()
|
||||
{
|
||||
return Declarations::ACTIVITY;
|
||||
}
|
||||
}
|
@@ -0,0 +1,116 @@
|
||||
<?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\Export\Filter;
|
||||
|
||||
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\TranslatableStringHelper;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final readonly class CreatorJobFilter implements FilterInterface
|
||||
{
|
||||
private const PREFIX = 'acp_act_filter_creator_job';
|
||||
|
||||
public function __construct(
|
||||
private TranslatableStringHelper $translatableStringHelper,
|
||||
private TranslatorInterface $translator,
|
||||
private UserJobRepositoryInterface $userJobRepository,
|
||||
) {}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data)
|
||||
{
|
||||
$p = self::PREFIX;
|
||||
|
||||
$qb
|
||||
->leftJoin('activity.createdBy', "{$p}_user")
|
||||
->leftJoin(
|
||||
UserJobHistory::class,
|
||||
"{$p}_history",
|
||||
Join::WITH,
|
||||
$qb->expr()->eq("{$p}_history.user", "{$p}_user")
|
||||
)
|
||||
// job_at based on activity.date
|
||||
->andWhere(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->lte("{$p}_history.startDate", 'activity.date'),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isNull("{$p}_history.endDate"),
|
||||
$qb->expr()->gt("{$p}_history.endDate", 'activity.date')
|
||||
)
|
||||
)
|
||||
)
|
||||
->andWhere(
|
||||
$qb->expr()->in("{$p}_history.job", ":{$p}_jobs")
|
||||
)
|
||||
->setParameter(
|
||||
"{$p}_jobs",
|
||||
$data['jobs'],
|
||||
);
|
||||
}
|
||||
|
||||
public function applyOn(): string
|
||||
{
|
||||
return Declarations::ACTIVITY;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder
|
||||
->add('jobs', EntityType::class, [
|
||||
'choices' => $this->userJobRepository->findAllOrderedByName(),
|
||||
'class' => UserJob::class,
|
||||
'choice_label' => fn (UserJob $s) => $this->translatableStringHelper->localize(
|
||||
$s->getLabel()
|
||||
).($s->isActive() ? '' : '('.$this->translator->trans('inactive').')'),
|
||||
'label' => 'export.filter.activity.by_creator_job.job_form_label',
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function describeAction($data, $format = 'string'): array
|
||||
{
|
||||
$jobs = array_map(
|
||||
fn (UserJob $job) => $this->translatableStringHelper->localize($job->getLabel()),
|
||||
$data['jobs'] instanceof Collection ? $data['jobs']->toArray() : $data['jobs']
|
||||
);
|
||||
|
||||
return ['export.filter.activity.by_creator_job.Filtered activity by user job: only %jobs%', [
|
||||
'%jobs%' => implode(', ', $jobs),
|
||||
]];
|
||||
}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return [
|
||||
'jobs' => [],
|
||||
];
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'export.filter.activity.by_creator_job.Filter activity by user job';
|
||||
}
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
<?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\Export\Aggregator;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\ActivityBundle\Export\Aggregator\ActivityPresenceAggregator;
|
||||
use Chill\ActivityBundle\Repository\ActivityPresenceRepositoryInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class ActivityPresenceAggregatorTest extends AbstractAggregatorTest
|
||||
{
|
||||
private TranslatableStringHelperInterface $translatableStringHelper;
|
||||
private ActivityPresenceRepositoryInterface $activityPresenceRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->translatableStringHelper = self::$container->get(TranslatableStringHelperInterface::class);
|
||||
$this->activityPresenceRepository = self::$container->get(ActivityPresenceRepositoryInterface::class);
|
||||
}
|
||||
|
||||
public function getAggregator()
|
||||
{
|
||||
return new ActivityPresenceAggregator($this->activityPresenceRepository, $this->translatableStringHelper);
|
||||
}
|
||||
|
||||
public function getFormData()
|
||||
{
|
||||
return [
|
||||
[],
|
||||
];
|
||||
}
|
||||
|
||||
public function getQueryBuilders()
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
|
||||
return [
|
||||
$em->createQueryBuilder()
|
||||
->select('count(activity.id)')
|
||||
->from(Activity::class, 'activity'),
|
||||
];
|
||||
}
|
||||
}
|
@@ -12,7 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\ActivityBundle\Tests\Export\Aggregator;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\ActivityBundle\Export\Aggregator\JobScopeAggregator;
|
||||
use Chill\ActivityBundle\Export\Aggregator\CreatorJobAggregator;
|
||||
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
@@ -21,15 +21,15 @@ use Doctrine\ORM\EntityManagerInterface;
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
final class JobScopeAggregatorTest extends AbstractAggregatorTest
|
||||
final class CreatorJobAggregatorTest extends AbstractAggregatorTest
|
||||
{
|
||||
private JobScopeAggregator $aggregator;
|
||||
private CreatorJobAggregator $aggregator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$this->aggregator = self::$container->get(JobScopeAggregator::class);
|
||||
$this->aggregator = self::$container->get(CreatorJobAggregator::class);
|
||||
}
|
||||
|
||||
public function getAggregator()
|
@@ -0,0 +1,57 @@
|
||||
<?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\Export\Aggregator\PersonAggregators;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\ActivityBundle\Export\Aggregator\PersonAggregators\PersonAggregator;
|
||||
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
|
||||
use Chill\PersonBundle\Export\Helper\LabelPersonHelper;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class PersonAggregatorTest extends AbstractAggregatorTest
|
||||
{
|
||||
private LabelPersonHelper $labelPersonHelper;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->labelPersonHelper = self::$container->get(LabelPersonHelper::class);
|
||||
}
|
||||
|
||||
public function getAggregator()
|
||||
{
|
||||
return new PersonAggregator($this->labelPersonHelper);
|
||||
}
|
||||
|
||||
public function getFormData()
|
||||
{
|
||||
return [[]];
|
||||
}
|
||||
|
||||
public function getQueryBuilders()
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
|
||||
return [
|
||||
$em->createQueryBuilder()
|
||||
->select('count(activity.id)')
|
||||
->from(Activity::class, 'activity'),
|
||||
];
|
||||
}
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
<?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\Export\Export\LinkedToACP;
|
||||
|
||||
use Chill\ActivityBundle\Export\Declarations;
|
||||
use Chill\ActivityBundle\Export\Export\LinkedToACP\CountHouseholdOnActivity;
|
||||
use Chill\MainBundle\Test\Export\AbstractExportTest;
|
||||
use Chill\PersonBundle\Export\Declarations as PersonDeclarations;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class CountHouseholdOnActivityTest extends AbstractExportTest
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->entityManager = self::$container->get(EntityManagerInterface::class);
|
||||
}
|
||||
|
||||
public function getExport()
|
||||
{
|
||||
yield new CountHouseholdOnActivity($this->entityManager, $this->getParameters(true));
|
||||
yield new CountHouseholdOnActivity($this->entityManager, $this->getParameters(false));
|
||||
}
|
||||
|
||||
public function getFormData()
|
||||
{
|
||||
return [
|
||||
[],
|
||||
];
|
||||
}
|
||||
|
||||
public function getModifiersCombination()
|
||||
{
|
||||
return [
|
||||
[
|
||||
Declarations::ACTIVITY,
|
||||
Declarations::ACTIVITY_ACP,
|
||||
PersonDeclarations::ACP_TYPE,
|
||||
PersonDeclarations::PERSON_TYPE,
|
||||
PersonDeclarations::HOUSEHOLD_TYPE,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
<?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\Export\Export\LinkedToACP;
|
||||
|
||||
use Chill\ActivityBundle\Export\Declarations;
|
||||
use Chill\ActivityBundle\Export\Export\LinkedToACP\CountPersonsOnActivity;
|
||||
use Chill\MainBundle\Test\Export\AbstractExportTest;
|
||||
use Chill\PersonBundle\Export\Declarations as PersonDeclarations;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class CountPersonsOnActivityTest extends AbstractExportTest
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
}
|
||||
|
||||
public function getExport()
|
||||
{
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
|
||||
yield new CountPersonsOnActivity($em, $this->getParameters(true));
|
||||
yield new CountPersonsOnActivity($em, $this->getParameters(false));
|
||||
}
|
||||
|
||||
public function getFormData()
|
||||
{
|
||||
return [[]];
|
||||
}
|
||||
|
||||
public function getModifiersCombination()
|
||||
{
|
||||
return [[
|
||||
Declarations::ACTIVITY,
|
||||
Declarations::ACTIVITY_ACP,
|
||||
PersonDeclarations::ACP_TYPE,
|
||||
PersonDeclarations::PERSON_TYPE,
|
||||
]];
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\ActivityBundle\Tests\Export\Export\LinkedToPerson;
|
||||
|
||||
use Chill\ActivityBundle\Export\Declarations;
|
||||
use Chill\ActivityBundle\Export\Export\LinkedToPerson\CountHouseholdOnActivity;
|
||||
use Chill\ActivityBundle\Repository\ActivityRepository;
|
||||
use Chill\MainBundle\Test\Export\AbstractExportTest;
|
||||
use Chill\PersonBundle\Export\Declarations as PersonDeclarations;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class CountHouseholdOnActivityTest extends AbstractExportTest
|
||||
{
|
||||
private ActivityRepository $activityRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->activityRepository = self::$container->get(ActivityRepository::class);
|
||||
}
|
||||
|
||||
public function getExport()
|
||||
{
|
||||
yield new CountHouseholdOnActivity($this->activityRepository, $this->getParameters(true));
|
||||
yield new CountHouseholdOnActivity($this->activityRepository, $this->getParameters(false));
|
||||
}
|
||||
|
||||
public function getFormData()
|
||||
{
|
||||
return [
|
||||
[],
|
||||
];
|
||||
}
|
||||
|
||||
public function getModifiersCombination()
|
||||
{
|
||||
return [
|
||||
[
|
||||
Declarations::ACTIVITY,
|
||||
Declarations::ACTIVITY_PERSON,
|
||||
PersonDeclarations::PERSON_TYPE,
|
||||
PersonDeclarations::HOUSEHOLD_TYPE,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@@ -13,6 +13,7 @@ namespace Chill\ActivityBundle\Tests\Export\Filter\ACPFilters;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\ActivityBundle\Entity\ActivityType;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||
use Chill\MainBundle\Test\Export\AbstractFilterTest;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
@@ -55,8 +56,30 @@ final class ActivityTypeFilterTest extends AbstractFilterTest
|
||||
$data = [];
|
||||
|
||||
foreach ($array as $a) {
|
||||
$data[] = [
|
||||
'accepted_activitytypes' => [],
|
||||
'date_after' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START),
|
||||
'date_before' => new RollingDate(RollingDate::T_TODAY),
|
||||
];
|
||||
$data[] = [
|
||||
'accepted_activitytypes' => new ArrayCollection([$a]),
|
||||
'date_after' => null,
|
||||
'date_before' => null,
|
||||
];
|
||||
$data[] = [
|
||||
'accepted_activitytypes' => [$a],
|
||||
'date_after' => null,
|
||||
'date_before' => null,
|
||||
];
|
||||
$data[] = [
|
||||
'accepted_activitytypes' => [$a],
|
||||
'date_after' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START),
|
||||
'date_before' => new RollingDate(RollingDate::T_TODAY),
|
||||
];
|
||||
$data[] = [
|
||||
'accepted_activitytypes' => [],
|
||||
'date_after' => null,
|
||||
'date_before' => null,
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,76 @@
|
||||
<?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\Export\Filter;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\ActivityBundle\Export\Filter\ActivityPresenceFilter;
|
||||
use Chill\ActivityBundle\Repository\ActivityPresenceRepositoryInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Chill\MainBundle\Test\Export\AbstractFilterTest;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class ActivityPresenceFilterTest extends AbstractFilterTest
|
||||
{
|
||||
private TranslatableStringHelperInterface $translatableStringHelper;
|
||||
private TranslatorInterface $translator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
self::bootKernel();
|
||||
|
||||
$this->translator = self::$container->get(TranslatorInterface::class);
|
||||
$this->translatableStringHelper = self::$container->get(TranslatableStringHelperInterface::class);
|
||||
}
|
||||
|
||||
public function getFilter()
|
||||
{
|
||||
return new ActivityPresenceFilter($this->translatableStringHelper, $this->translator);
|
||||
}
|
||||
|
||||
public function getFormData()
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$presences = self::$container->get(ActivityPresenceRepositoryInterface::class)
|
||||
->findAll();
|
||||
|
||||
return [
|
||||
[
|
||||
'presences' => $presences,
|
||||
],
|
||||
[
|
||||
'presences' => new ArrayCollection($presences),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getQueryBuilders()
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
|
||||
yield $em->createQueryBuilder()
|
||||
->select('count(activity.id)')
|
||||
->from(Activity::class, 'activity');
|
||||
|
||||
self::ensureKernelShutdown();
|
||||
}
|
||||
}
|
@@ -0,0 +1,75 @@
|
||||
<?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\Export\Filter;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\ActivityBundle\Export\Filter\CreatorJobFilter;
|
||||
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Chill\MainBundle\Test\Export\AbstractFilterTest;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class CreatorJobFilterTest extends AbstractFilterTest
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
private TranslatableStringHelperInterface $translatableStringHelper;
|
||||
private TranslatorInterface $translator;
|
||||
private UserJobRepositoryInterface $userJobRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
self::bootKernel();
|
||||
|
||||
$this->entityManager = self::$container->get(EntityManagerInterface::class);
|
||||
$this->translatableStringHelper = self::$container->get(TranslatableStringHelperInterface::class);
|
||||
$this->translator = self::$container->get(TranslatorInterface::class);
|
||||
$this->userJobRepository = self::$container->get(UserJobRepositoryInterface::class);
|
||||
}
|
||||
|
||||
public function getFilter()
|
||||
{
|
||||
return new CreatorJobFilter(
|
||||
$this->translatableStringHelper,
|
||||
$this->translator,
|
||||
$this->userJobRepository
|
||||
);
|
||||
}
|
||||
|
||||
public function getFormData()
|
||||
{
|
||||
$this->setUp();
|
||||
$jobs = $this->userJobRepository->findAll();
|
||||
|
||||
return [
|
||||
['jobs' => $jobs],
|
||||
];
|
||||
}
|
||||
|
||||
public function getQueryBuilders()
|
||||
{
|
||||
self::setUp();
|
||||
|
||||
return [
|
||||
$this->entityManager->createQueryBuilder()
|
||||
->select('count(activity.id)')
|
||||
->from(Activity::class, 'activity')
|
||||
->join('activity.user', 'actuser'),
|
||||
];
|
||||
}
|
||||
}
|
@@ -16,6 +16,18 @@ services:
|
||||
tags:
|
||||
- { name: chill.export, alias: 'list_activity_linked_to_person' }
|
||||
|
||||
Chill\ActivityBundle\Export\Export\LinkedToACP\CountPersonsOnActivity:
|
||||
tags:
|
||||
- { name: chill.export, alias: 'count_person_on_activity' }
|
||||
|
||||
Chill\ActivityBundle\Export\Export\LinkedToACP\CountHouseholdOnActivity:
|
||||
tags:
|
||||
- { name: chill.export, alias: 'count_household_on_activity_acp' }
|
||||
|
||||
Chill\ActivityBundle\Export\Export\LinkedToPerson\CountHouseholdOnActivity:
|
||||
tags:
|
||||
- { name: chill.export, alias: 'count_household_on_activity_person' }
|
||||
|
||||
chill.activity.export.count_activity_linked_to_acp:
|
||||
class: Chill\ActivityBundle\Export\Export\LinkedToACP\CountActivity
|
||||
tags:
|
||||
@@ -115,6 +127,10 @@ services:
|
||||
# affect all saved exports (unless we write a migration for that)
|
||||
- { name: chill.export_filter, alias: 'activity_userscope_filter' }
|
||||
|
||||
Chill\ActivityBundle\Export\Filter\CreatorJobFilter:
|
||||
tags:
|
||||
- { name: chill.export_filter, alias: 'activity_creatorjob_filter' }
|
||||
|
||||
Chill\ActivityBundle\Export\Filter\UsersJobFilter:
|
||||
tags:
|
||||
- { name: chill.export_filter, alias: 'activity_usersjob_filter' }
|
||||
@@ -131,6 +147,11 @@ services:
|
||||
tags:
|
||||
- { name: chill.export_filter, alias: 'period_having_activity_betw_dates_filter' }
|
||||
|
||||
Chill\ActivityBundle\Export\Filter\ActivityPresenceFilter:
|
||||
tags:
|
||||
- { name: chill.export_filter, alias: 'activity_presence_filter' }
|
||||
|
||||
|
||||
## Aggregators
|
||||
Chill\ActivityBundle\Export\Aggregator\PersonAggregators\ActivityReasonAggregator:
|
||||
tags:
|
||||
@@ -179,7 +200,7 @@ services:
|
||||
tags:
|
||||
- { name: chill.export_aggregator, alias: activity_creator_scope_aggregator }
|
||||
|
||||
Chill\ActivityBundle\Export\Aggregator\JobScopeAggregator:
|
||||
Chill\ActivityBundle\Export\Aggregator\CreatorJobAggregator:
|
||||
tags:
|
||||
- { name: chill.export_aggregator, alias: activity_creator_job_aggregator }
|
||||
|
||||
@@ -214,3 +235,11 @@ services:
|
||||
Chill\ActivityBundle\Export\Aggregator\ACPAggregators\ByActivityTypeAggregator:
|
||||
tags:
|
||||
- { name: chill.export_aggregator, alias: acp_by_activity_type_aggregator }
|
||||
|
||||
Chill\ActivityBundle\Export\Aggregator\ActivityPresenceAggregator:
|
||||
tags:
|
||||
- { name: chill.export_aggregator, alias: activity_presence_agg }
|
||||
|
||||
Chill\ActivityBundle\Export\Aggregator\PersonAggregators\PersonAggregator:
|
||||
tags:
|
||||
- { name: chill.export_aggregator, alias: activity_person_agg }
|
||||
|
@@ -3,7 +3,12 @@ export:
|
||||
activity:
|
||||
course_having_activity_between_date:
|
||||
Only course having an activity between from and to: Seulement les parcours ayant reçu au moins un échange entre le {from, date, short} et le {to, date, short}
|
||||
person_between_dates:
|
||||
|
||||
acp_by_activity_type:
|
||||
'acp_containing_at_least_one_activitytypes': >-
|
||||
Parcours filtrés: uniquement ceux qui contiennent au moins un échange d'un des types suivants: {activitytypes}
|
||||
{has_date_after, select, 1 {, après le {date_after, date}} other {}}
|
||||
{has_date_before, select, 1 {, avant le {date_before, date}} other {}}
|
||||
describe_action_with_no_subject: >-
|
||||
Filtré par personne ayant eu un échange entre le {date_from, date} et le {date_to, date}
|
||||
describe_action_with_subject: >-
|
||||
|
@@ -332,6 +332,19 @@ docgen:
|
||||
myWorksOnly: Prendre en compte uniquement les actions d'accompagnement dont je suis référent
|
||||
|
||||
export:
|
||||
export:
|
||||
count_person_on_activity:
|
||||
title: Nombre d'usagers concernés par les échanges
|
||||
description: Compte le nombre d'usagers concernés par les échanges. Si un usager est présent dans plusieurs échanges, il n'est comptabilisé qu'une seule fois.
|
||||
header: Nombre d'usagers concernés par des échanges
|
||||
count_household_on_activity:
|
||||
title: Nombre de ménages concernés par les échanges
|
||||
description: Compte le nombre de ménages concernés par les échanges. Si un ménage est présent dans plusieurs échanges, il n'est comptabilisé qu'une seule fois. Les usagers sans ménages ne sont pas comptabilisés.
|
||||
header: Nombre de ménage concernés par des échanges
|
||||
count_household_on_activity_person:
|
||||
title: Nombre de ménages concernés par les échanges
|
||||
description: Compte le nombre de ménages concernés par les échanges. Si un ménage est présent dans plusieurs échanges, il n'est comptabilisé qu'une seule fois. Les usagers sans ménages ne sont pas comptabilisés. Lorsqu'un usager change de ménage, chaque ménage est comptabilisé une fois.
|
||||
header: Nombre de ménage concernés par des échanges
|
||||
list:
|
||||
activity:
|
||||
users name: Nom des utilisateurs
|
||||
@@ -366,7 +379,10 @@ export:
|
||||
Receiving an activity after: Ayant reçu un échange après le
|
||||
Receiving an activity before: Ayant reçu un échange avant le
|
||||
acp_by_activity_type:
|
||||
'acp_containing_at_least_one_%activitytypes%': 'Parcours filtrés: uniquement ceux qui contiennent au moins un échange d''un des types suivants: %activitytypes%'
|
||||
'activity after': Échanges après le
|
||||
activity after help: Si laissé vide, ne sera pas pris en compte
|
||||
activity before: Echanges avant le
|
||||
activity before help: Si laissé vide, ne sera pas pris en compte
|
||||
person_between_dates:
|
||||
Implied in an activity after this date: Impliqué dans un échange après cette date
|
||||
Implied in an activity before this date: Impliqué dans un échange avant cette date
|
||||
@@ -376,7 +392,11 @@ export:
|
||||
date mismatch: La date de fin de la période doit être supérieure à la date du début
|
||||
by_creator_scope:
|
||||
Filter activity by user scope: Filtrer les échanges par service du créateur de l'échange
|
||||
'Filtered activity by user scope: only %scopes%': "Filtré par service du créateur: uniquement %scopes%"
|
||||
'Filtered activity by user scope: only %scopes%': "Filtré par service du créateur de l'échange: uniquement %scopes%"
|
||||
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%"
|
||||
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%'
|
||||
@@ -385,8 +405,16 @@ export:
|
||||
Sent or received: Envoyé ou reçu
|
||||
is sent: envoyé
|
||||
is received: reçu
|
||||
by_presence:
|
||||
Filter activity by activity presence: Filtrer les échanges par présence de l'usager
|
||||
presences: Présences
|
||||
'Filtered by activity presence: only %presences%': 'Filtré par présence de l''usager: seulement %presences%'
|
||||
|
||||
aggregator:
|
||||
person:
|
||||
by_person:
|
||||
title: Grouper les échanges par usager (dossier d'usager dans lequel l'échange est enregistré)
|
||||
person: Usager
|
||||
acp:
|
||||
by_activity_type:
|
||||
title: Grouper les parcours par type d'échange
|
||||
@@ -412,11 +440,14 @@ export:
|
||||
Group activity by creator scope: Grouper les échanges par service du créateur de l'échange
|
||||
Calc date: Date de calcul du service du créateur de l'échange
|
||||
by_creator_job:
|
||||
Group activity by creator job: Grouper les échanges par service du créateur de l'échange
|
||||
Calc date: Date de calcul du service du créateur de l'échange
|
||||
Group activity by creator job: Grouper les échanges par métier du créateur de l'échange
|
||||
Calc date: Date de calcul du métier du créateur de l'échange
|
||||
by_persons:
|
||||
Group activity by persons: Grouper les échanges par usager participant
|
||||
Persons: Usagers participants
|
||||
by_activity_presence:
|
||||
Group activity by presence: Grouper les échanges par présence de l'usager
|
||||
header: Présence de(s) usager(s)
|
||||
|
||||
generic_doc:
|
||||
filter:
|
||||
|
@@ -13,15 +13,11 @@ namespace Chill\AsideActivityBundle\Export\Filter;
|
||||
|
||||
use Chill\AsideActivityBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Form\Type\Export\FilterType;
|
||||
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class ByDateFilter implements FilterInterface
|
||||
@@ -66,51 +62,14 @@ class ByDateFilter implements FilterInterface
|
||||
->add('date_to', PickRollingDateType::class, [
|
||||
'label' => 'export.filter.Aside activities before this date',
|
||||
]);
|
||||
|
||||
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) {
|
||||
/** @var \Symfony\Component\Form\FormInterface $filterForm */
|
||||
$filterForm = $event->getForm()->getParent();
|
||||
$enabled = $filterForm->get(FilterType::ENABLED_FIELD)->getData();
|
||||
|
||||
if (true === $enabled) {
|
||||
// if the filter is enabled, add some validation
|
||||
$form = $event->getForm();
|
||||
$date_from = $form->get('date_from')->getData();
|
||||
$date_to = $form->get('date_to')->getData();
|
||||
|
||||
// check that fields are not empty
|
||||
if (null === $date_from) {
|
||||
$form->get('date_from')->addError(new FormError(
|
||||
$this->translator->trans('This field '
|
||||
.'should not be empty')
|
||||
));
|
||||
}
|
||||
|
||||
if (null === $date_to) {
|
||||
$form->get('date_to')->addError(new FormError(
|
||||
$this->translator->trans('This field '
|
||||
.'should not be empty')
|
||||
));
|
||||
}
|
||||
|
||||
// check that date_from is before date_to
|
||||
if (
|
||||
(null !== $date_from && null !== $date_to)
|
||||
&& $date_from >= $date_to
|
||||
) {
|
||||
$form->get('date_to')->addError(new FormError(
|
||||
$this->translator->trans('export.filter.This date should be after '
|
||||
.'the date given in "Implied in an aside activity after '
|
||||
.'this date" field')
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return ['date_from' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), 'date_to' => new RollingDate(RollingDate::T_TODAY)];
|
||||
return [
|
||||
'date_from' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START),
|
||||
'date_to' => new RollingDate(RollingDate::T_TODAY),
|
||||
];
|
||||
}
|
||||
|
||||
public function describeAction($data, $format = 'string'): array
|
||||
|
@@ -23,6 +23,6 @@ class ChargeKindController extends CRUDController
|
||||
/* @var QueryBuilder $query */
|
||||
$query->addOrderBy('e.ordering', 'ASC');
|
||||
|
||||
return $query;
|
||||
return parent::orderQuery($action, $query, $request, $paginator);
|
||||
}
|
||||
}
|
||||
|
@@ -23,6 +23,6 @@ class ResourceKindController extends CRUDController
|
||||
/* @var QueryBuilder $query */
|
||||
$query->addOrderBy('e.ordering', 'ASC');
|
||||
|
||||
return $query;
|
||||
return parent::orderQuery($action, $query, $request, $paginator);
|
||||
}
|
||||
}
|
||||
|
@@ -356,20 +356,6 @@ final class CalendarSyncerTest extends TestCase
|
||||
}
|
||||
JSON;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// all tests should run when timezone = +02:00
|
||||
$brussels = new \DateTimeZone('Europe/Brussels');
|
||||
|
||||
if (7200 === $brussels->getOffset(new \DateTimeImmutable())) {
|
||||
date_default_timezone_set('Europe/Brussels');
|
||||
} else {
|
||||
date_default_timezone_set('Europe/Moscow');
|
||||
}
|
||||
}
|
||||
|
||||
public function testHandleAttendeesConfirmingCalendar(): void
|
||||
{
|
||||
$machineHttpClient = new MockHttpClient([
|
||||
|
@@ -30,7 +30,7 @@ class CustomFieldController extends AbstractController
|
||||
/**
|
||||
* Creates a new CustomField entity.
|
||||
*
|
||||
* @Route("/{_locale}/admin/customfield/new", name="customfield_new")
|
||||
* @Route("/{_locale}/admin/customfield/new", name="customfield_create")
|
||||
*/
|
||||
public function createAction(Request $request)
|
||||
{
|
||||
|
@@ -232,7 +232,7 @@ class CustomFieldsGroupController extends AbstractController
|
||||
/**
|
||||
* Finds and displays a CustomFieldsGroup entity.
|
||||
*
|
||||
* @Route("/{_locale}/admin/customfieldsgroup/{id}/show", name="customfieldsgroup/show")
|
||||
* @Route("/{_locale}/admin/customfieldsgroup/{id}/show", name="customfieldsgroup_show")
|
||||
*/
|
||||
public function showAction(mixed $id)
|
||||
{
|
||||
@@ -256,7 +256,7 @@ class CustomFieldsGroupController extends AbstractController
|
||||
/**
|
||||
* Edits an existing CustomFieldsGroup entity.
|
||||
*
|
||||
* @Route("/{_locale}/admin/customfieldsgroup/{id}/update", name="customfieldsgroup/update")
|
||||
* @Route("/{_locale}/admin/customfieldsgroup/{id}/update", name="customfieldsgroup_update")
|
||||
*/
|
||||
public function updateAction(Request $request, mixed $id)
|
||||
{
|
||||
@@ -383,7 +383,7 @@ class CustomFieldsGroupController extends AbstractController
|
||||
$em = $this->getDoctrine()->getManager();
|
||||
|
||||
$customFieldsGroupIds = $em->createQuery('SELECT g.id FROM '
|
||||
.'ChillCustomFieldsBundle:CustomFieldsDefaultGroup d '
|
||||
.CustomFieldsDefaultGroup::class.' d '
|
||||
.'JOIN d.customFieldsGroup g')
|
||||
->getResult(Query::HYDRATE_SCALAR);
|
||||
|
||||
|
@@ -280,7 +280,7 @@ class CustomFieldChoice extends AbstractCustomField
|
||||
$template = '@ChillCustomFields/CustomFieldsRendering/choice.html.twig';
|
||||
|
||||
if ('csv' === $documentType) {
|
||||
$template = 'ChillCustomFieldsBundle:CustomFieldsRendering:choice.csv.twig';
|
||||
$template = '@ChillCustomFields/CustomFieldsRendering/choice.csv.twig';
|
||||
}
|
||||
|
||||
return $this->templating
|
||||
|
@@ -68,7 +68,7 @@ class CustomFieldDate extends AbstractCustomField
|
||||
{
|
||||
$validatorFunction = static function ($value, ExecutionContextInterface $context) {
|
||||
try {
|
||||
$date = new \DateTime($value);
|
||||
$date = new \DateTime((string) $value);
|
||||
} catch (\Exception) {
|
||||
$context->buildViolation('The expression "%expression%" is invalid', [
|
||||
'%expression%' => $value,
|
||||
@@ -125,7 +125,7 @@ class CustomFieldDate extends AbstractCustomField
|
||||
return $date->format('Y-m-d');
|
||||
|
||||
default:
|
||||
$template = 'ChillCustomFieldsBundle:CustomFieldsRendering:date.'
|
||||
$template = '@ChillCustomFields/CustomFieldsRendering/date.'
|
||||
.$documentType.'.twig';
|
||||
|
||||
return $this->templating
|
||||
|
@@ -96,7 +96,7 @@ class CustomFieldLongChoice extends AbstractCustomField
|
||||
public function render($value, CustomField $customField, $documentType = 'html')
|
||||
{
|
||||
$option = $this->deserialize($value, $customField);
|
||||
$template = 'ChillCustomFieldsBundle:CustomFieldsRendering:choice_long.'
|
||||
$template = '@ChillCustomFields/CustomFieldsRendering/choice_long.'
|
||||
.$documentType.'.twig';
|
||||
|
||||
return $this->templating
|
||||
|
@@ -95,7 +95,7 @@ class CustomFieldNumber extends AbstractCustomField
|
||||
|
||||
public function render($value, CustomField $customField, $documentType = 'html')
|
||||
{
|
||||
$template = 'ChillCustomFieldsBundle:CustomFieldsRendering:number.'
|
||||
$template = '@ChillCustomFields/CustomFieldsRendering/number.'
|
||||
.$documentType.'.twig';
|
||||
$options = $customField->getOptions();
|
||||
|
||||
|
@@ -89,7 +89,7 @@ class CustomFieldText extends AbstractCustomField
|
||||
$template = '@ChillCustomFields/CustomFieldsRendering/text.html.twig';
|
||||
|
||||
if ('csv' === $documentType) {
|
||||
$template = 'ChillCustomFieldsBundle:CustomFieldsRendering:text.csv.twig';
|
||||
$template = '@ChillCustomFields/CustomFieldsRendering/text.csv.twig';
|
||||
}
|
||||
|
||||
return $this->templating
|
||||
|
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\CustomFieldsBundle\Form;
|
||||
|
||||
use Chill\CustomFieldsBundle\Entity\CustomFieldsGroup;
|
||||
use Chill\CustomFieldsBundle\Form\DataTransformer\CustomFieldsGroupToIdTransformer;
|
||||
use Chill\CustomFieldsBundle\Service\CustomFieldProvider;
|
||||
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
|
||||
@@ -45,7 +46,7 @@ class CustomFieldType extends AbstractType
|
||||
|
||||
if ('entity' === $options['group_widget']) {
|
||||
$builder->add('customFieldsGroup', EntityType::class, [
|
||||
'class' => 'ChillCustomFieldsBundle:CustomFieldsGroup',
|
||||
'class' => CustomFieldsGroup::class,
|
||||
'choice_label' => fn ($g) => $this->translatableStringHelper->localize($g->getName()),
|
||||
]);
|
||||
} elseif ('hidden' === $options['group_widget']) {
|
||||
|
@@ -1,4 +1,8 @@
|
||||
services:
|
||||
_defaults:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\CustomFieldsBundle\Controller\:
|
||||
resource: '../../Controller'
|
||||
tags: ['controller.service_arguments']
|
||||
|
@@ -11,14 +11,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocGeneratorBundle\Serializer\Helper;
|
||||
|
||||
use Symfony\Component\Serializer\Mapping\ClassMetadata;
|
||||
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
class NormalizeNullValueHelper
|
||||
{
|
||||
public function __construct(private readonly NormalizerInterface $normalizer, private readonly ?string $discriminatorType = null, private readonly ?string $discriminatorValue = null) {}
|
||||
|
||||
public function normalize(array $attributes, string $format = 'docgen', ?array $context = [], ClassMetadata $classMetadata = null)
|
||||
public function normalize(array $attributes, string $format = 'docgen', ?array $context = [], ClassMetadataInterface $classMetadata = null)
|
||||
{
|
||||
$data = [];
|
||||
$data['isNull'] = true;
|
||||
@@ -44,7 +44,7 @@ class NormalizeNullValueHelper
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function getContextForAttribute(string $key, array $initialContext, ?ClassMetadata $classMetadata): array
|
||||
private function getContextForAttribute(string $key, array $initialContext, ?ClassMetadataInterface $classMetadata): array
|
||||
{
|
||||
if (null === $classMetadata) {
|
||||
return $initialContext;
|
||||
|
@@ -19,6 +19,7 @@ use Symfony\Component\PropertyAccess\PropertyAccessor;
|
||||
use Symfony\Component\Serializer\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Serializer\Exception\LogicException;
|
||||
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
|
||||
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
|
||||
use Symfony\Component\Serializer\Mapping\ClassMetadata;
|
||||
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
@@ -52,12 +53,15 @@ class DocGenObjectNormalizer implements NormalizerAwareInterface, NormalizerInte
|
||||
}
|
||||
|
||||
$metadata = $this->classMetadataFactory->getMetadataFor($classMetadataKey);
|
||||
if (!$metadata instanceof ClassMetadata) {
|
||||
throw new \LogicException('ClassMetadata should be the only one implementation for ClassMetadataInterface. See https://github.com/symfony/symfony/pull/17114');
|
||||
}
|
||||
$expectedGroups = \array_key_exists(AbstractNormalizer::GROUPS, $context) ?
|
||||
\is_array($context[AbstractNormalizer::GROUPS]) ? $context[AbstractNormalizer::GROUPS] : [$context[AbstractNormalizer::GROUPS]]
|
||||
: [];
|
||||
$attributes = \array_filter(
|
||||
$metadata->getAttributesMetadata(),
|
||||
static function (AttributeMetadata $a) use ($expectedGroups) {
|
||||
static function (AttributeMetadataInterface $a) use ($expectedGroups) {
|
||||
foreach ($a->getGroups() as $g) {
|
||||
if (\in_array($g, $expectedGroups, true)) {
|
||||
return true;
|
||||
@@ -119,7 +123,7 @@ class DocGenObjectNormalizer implements NormalizerAwareInterface, NormalizerInte
|
||||
return $type->getName();
|
||||
}
|
||||
if ($type instanceof \ReflectionIntersectionType) {
|
||||
foreach (array_map(fn (\ReflectionNamedType $t) => $t->getName(), $type->getTypes()) as $classString) {
|
||||
foreach (array_map(fn (\ReflectionType $t) => $t->getName(), $type->getTypes()) as $classString) {
|
||||
if (ReadableCollection::class === $classString) {
|
||||
return ReadableCollection::class;
|
||||
}
|
||||
@@ -211,7 +215,7 @@ class DocGenObjectNormalizer implements NormalizerAwareInterface, NormalizerInte
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|AttributeMetadata[] $attributes
|
||||
* @param array<AttributeMetadata> $attributes
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
|
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\AsyncUpload\Command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class ConfigureOpenstackObjectStorageCommand extends Command
|
||||
{
|
||||
private readonly string $basePath;
|
||||
|
||||
private readonly string $tempUrlKey;
|
||||
|
||||
public function __construct(private readonly HttpClientInterface $client, ParameterBagInterface $parameterBag)
|
||||
{
|
||||
$config = $parameterBag->get('chill_doc_store')['openstack']['temp_url'];
|
||||
|
||||
$this->tempUrlKey = $config['temp_url_key'];
|
||||
$this->basePath = $config['temp_url_base_path'];
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this->setDescription('Configure openstack container to store documents')
|
||||
->setName('chill:doc-store:configure-openstack')
|
||||
->addOption('os_token', 'o', InputOption::VALUE_REQUIRED, 'Openstack token')
|
||||
->addOption('domain', 'd', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Domain name')
|
||||
;
|
||||
}
|
||||
|
||||
protected function interact(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
if (!$input->hasOption('os_token')) {
|
||||
$output->writeln('The option os_token is required');
|
||||
|
||||
throw new \RuntimeException('The option os_token is required');
|
||||
}
|
||||
|
||||
if (0 === count($input->getOption('domain'))) {
|
||||
$output->writeln('At least one occurence of option domain is required');
|
||||
|
||||
throw new \RuntimeException('At least one occurence of option domain is required');
|
||||
}
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$domains = trim(implode(' ', $input->getOption('domain')));
|
||||
|
||||
if ($output->isVerbose()) {
|
||||
$output->writeln(['Domains configured will be', $domains]);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->client->request('POST', $this->basePath, [
|
||||
'headers' => [
|
||||
'X-Auth-Token' => $input->getOption('os_token'),
|
||||
'X-Container-Meta-Access-Control-Allow-Origin' => $domains,
|
||||
'X-Container-Meta-Temp-URL-Key' => $this->tempUrlKey,
|
||||
],
|
||||
]);
|
||||
$response->getContent();
|
||||
} catch (HttpExceptionInterface $e) {
|
||||
$output->writeln('Error');
|
||||
$output->writeln($e->getMessage());
|
||||
|
||||
if ($output->isVerbose()) {
|
||||
$output->writeln($e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\AsyncUpload\Driver\OpenstackObjectStore;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\Event\TempUrlGenerateEvent;
|
||||
use Chill\DocStoreBundle\AsyncUpload\Exception\TempUrlGeneratorException;
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrlPost;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* Generate a temp url.
|
||||
*/
|
||||
final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterface
|
||||
{
|
||||
private const CHARACTERS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
private string $base_url;
|
||||
private string $key;
|
||||
private int $max_expire_delay;
|
||||
private int $max_submit_delay;
|
||||
private int $max_post_file_size;
|
||||
private int $max_file_count;
|
||||
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
private EventDispatcherInterface $event_dispatcher,
|
||||
private ClockInterface $clock,
|
||||
ParameterBagInterface $parameterBag,
|
||||
) {
|
||||
$config = $parameterBag->get('chill_doc_store')['openstack']['temp_url'];
|
||||
|
||||
$this->key = $config['temp_url_key'];
|
||||
$this->base_url = $config['temp_url_base_path'];
|
||||
$this->max_expire_delay = $config['max_expires_delay'];
|
||||
$this->max_submit_delay = $config['max_submit_delay'];
|
||||
$this->max_post_file_size = $config['max_post_file_size'];
|
||||
$this->max_file_count = $config['max_post_file_count'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TempUrlGeneratorException
|
||||
*/
|
||||
public function generatePost(
|
||||
int $expire_delay = null,
|
||||
int $submit_delay = null,
|
||||
int $max_file_count = 1,
|
||||
): SignedUrlPost {
|
||||
$delay = $expire_delay ?? $this->max_expire_delay;
|
||||
$submit_delay ??= $this->max_submit_delay;
|
||||
|
||||
if ($delay < 2) {
|
||||
throw new TempUrlGeneratorException("The delay of {$delay} is too ".'short (<2 sec) to properly use this token');
|
||||
}
|
||||
|
||||
if ($delay > $this->max_expire_delay) {
|
||||
throw new TempUrlGeneratorException('The given delay is greater than the max delay authorized.');
|
||||
}
|
||||
|
||||
if ($submit_delay < 15) {
|
||||
throw new TempUrlGeneratorException("The submit delay of {$delay} is too ".'short (<15 sec) to properly use this token');
|
||||
}
|
||||
|
||||
if ($submit_delay > $this->max_submit_delay) {
|
||||
throw new TempUrlGeneratorException('The given submit delay is greater than the max submit delay authorized.');
|
||||
}
|
||||
|
||||
if ($max_file_count > $this->max_file_count) {
|
||||
throw new TempUrlGeneratorException('The number of files is greater than the authorized number of files');
|
||||
}
|
||||
|
||||
$expires = $this->clock->now()->add(new \DateInterval('PT'.(string) $delay.'S'));
|
||||
|
||||
$object_name = $this->generateObjectName();
|
||||
|
||||
$g = new SignedUrlPost(
|
||||
$url = $this->generateUrl($object_name),
|
||||
$expires,
|
||||
$this->max_post_file_size,
|
||||
$max_file_count,
|
||||
$submit_delay,
|
||||
'',
|
||||
$object_name,
|
||||
$this->generateSignaturePost($url, $expires)
|
||||
);
|
||||
|
||||
$this->event_dispatcher->dispatch(
|
||||
new TempUrlGenerateEvent($g),
|
||||
);
|
||||
|
||||
$this->logger->info(
|
||||
'generate signature for url',
|
||||
(array) $g
|
||||
);
|
||||
|
||||
return $g;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an absolute public url for a GET request on the object.
|
||||
*/
|
||||
public function generate(string $method, string $object_name, int $expire_delay = null): SignedUrl
|
||||
{
|
||||
if ($expire_delay > $this->max_expire_delay) {
|
||||
throw new TempUrlGeneratorException(sprintf('The expire delay (%d) is greater than the max_expire_delay (%d)', $expire_delay, $this->max_expire_delay));
|
||||
}
|
||||
$url = $this->generateUrl($object_name);
|
||||
|
||||
$expires = $this->clock->now()
|
||||
->add(new \DateInterval(sprintf('PT%dS', $expire_delay ?? $this->max_expire_delay)));
|
||||
$args = [
|
||||
'temp_url_sig' => $this->generateSignature($method, $url, $expires),
|
||||
'temp_url_expires' => $expires->format('U'),
|
||||
];
|
||||
$url = $url.'?'.\http_build_query($args);
|
||||
|
||||
$signature = new SignedUrl(strtoupper($method), $url, $expires);
|
||||
|
||||
$this->event_dispatcher->dispatch(
|
||||
new TempUrlGenerateEvent($signature)
|
||||
);
|
||||
|
||||
return $signature;
|
||||
}
|
||||
|
||||
private function generateUrl($relative_path): string
|
||||
{
|
||||
return match (str_ends_with($this->base_url, '/')) {
|
||||
true => $this->base_url.$relative_path,
|
||||
false => $this->base_url.'/'.$relative_path
|
||||
};
|
||||
}
|
||||
|
||||
private function generateObjectName()
|
||||
{
|
||||
// inspiration from https://stackoverflow.com/a/4356295/1572236
|
||||
$charactersLength = strlen(self::CHARACTERS);
|
||||
$randomString = '';
|
||||
for ($i = 0; $i < 21; ++$i) {
|
||||
$randomString .= self::CHARACTERS[random_int(0, $charactersLength - 1)];
|
||||
}
|
||||
|
||||
return $randomString;
|
||||
}
|
||||
|
||||
private function generateSignaturePost($url, \DateTimeImmutable $expires)
|
||||
{
|
||||
$path = \parse_url((string) $url, PHP_URL_PATH);
|
||||
|
||||
$body = sprintf(
|
||||
"%s\n%s\n%s\n%s\n%s",
|
||||
$path,
|
||||
'', // redirect is empty
|
||||
(string) $this->max_post_file_size,
|
||||
(string) $this->max_file_count,
|
||||
$expires->format('U')
|
||||
)
|
||||
;
|
||||
|
||||
$this->logger->debug(
|
||||
'generate signature post',
|
||||
['url' => $body, 'method' => 'POST']
|
||||
);
|
||||
|
||||
return \hash_hmac('sha512', $body, $this->key, false);
|
||||
}
|
||||
|
||||
private function generateSignature($method, $url, \DateTimeImmutable $expires)
|
||||
{
|
||||
if ('POST' === $method) {
|
||||
return $this->generateSignaturePost($url, $expires);
|
||||
}
|
||||
|
||||
$path = \parse_url((string) $url, PHP_URL_PATH);
|
||||
|
||||
$body = sprintf(
|
||||
"%s\n%s\n%s",
|
||||
$method,
|
||||
$expires->format('U'),
|
||||
$path
|
||||
)
|
||||
;
|
||||
|
||||
$this->logger->debug(
|
||||
'generate signature GET',
|
||||
['url' => $body, 'method' => 'GET']
|
||||
);
|
||||
|
||||
return \hash_hmac('sha512', $body, $this->key, false);
|
||||
}
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\AsyncUpload\Event;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
|
||||
final class TempUrlGenerateEvent extends \Symfony\Contracts\EventDispatcher\Event
|
||||
{
|
||||
final public const NAME_GENERATE = 'async_uploader.generate_url';
|
||||
|
||||
public function __construct(private readonly SignedUrl $data) {}
|
||||
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->data->method;
|
||||
}
|
||||
|
||||
public function getData(): SignedUrl
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\AsyncUpload\Exception;
|
||||
|
||||
class BadCallToRemoteServer extends \LogicException
|
||||
{
|
||||
public function __construct(string $content, int $statusCode, int $code = 0, \Throwable $previous = null)
|
||||
{
|
||||
parent::__construct("Bad call to remote server: {$statusCode}, {$content}", $code, $previous);
|
||||
}
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\AsyncUpload\Exception;
|
||||
|
||||
class TempUrlGeneratorException extends \RuntimeException {}
|
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\AsyncUpload\Exception;
|
||||
|
||||
class TempUrlRemoteServerException extends \RuntimeException
|
||||
{
|
||||
public function __construct(int $statusCode, int $code = 0, \Throwable $previous = null)
|
||||
{
|
||||
parent::__construct('Could not reach remote server: '.(string) $statusCode, $code, $previous);
|
||||
}
|
||||
}
|
38
src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php
Normal file
38
src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\AsyncUpload;
|
||||
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
|
||||
readonly class SignedUrl
|
||||
{
|
||||
public function __construct(
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public string $method,
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public string $url,
|
||||
public \DateTimeImmutable $expires,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public function getExpires(): int
|
||||
{
|
||||
return $this->expires->getTimestamp();
|
||||
}
|
||||
}
|
54
src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrlPost.php
Normal file
54
src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrlPost.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\AsyncUpload;
|
||||
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
|
||||
readonly class SignedUrlPost extends SignedUrl
|
||||
{
|
||||
public function __construct(
|
||||
string $url,
|
||||
\DateTimeImmutable $expires,
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public int $max_file_size,
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public int $max_file_count,
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public int $submit_delay,
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public string $redirect,
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public string $prefix,
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public string $signature,
|
||||
) {
|
||||
parent::__construct('POST', $url, $expires);
|
||||
}
|
||||
}
|
@@ -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\DocStoreBundle\AsyncUpload;
|
||||
|
||||
interface TempUrlGeneratorInterface
|
||||
{
|
||||
public function generatePost(
|
||||
int $expire_delay = null,
|
||||
int $submit_delay = null,
|
||||
int $max_file_count = 1
|
||||
): SignedUrlPost;
|
||||
|
||||
public function generate(string $method, string $object_name, int $expire_delay = null): SignedUrl;
|
||||
}
|
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\AsyncUpload\Templating;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
|
||||
/**
|
||||
* This class extends the AbstractExtension class and provides Twig filter functions for generating URLs for asynchronous
|
||||
* file uploads.
|
||||
*/
|
||||
class AsyncUploadExtension extends AbstractExtension
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TempUrlGeneratorInterface $tempUrlGenerator,
|
||||
private readonly UrlGeneratorInterface $routingUrlGenerator
|
||||
) {}
|
||||
|
||||
public function getFilters()
|
||||
{
|
||||
return [
|
||||
new TwigFilter('file_url', $this->computeSignedUrl(...)),
|
||||
new TwigFilter('generate_url', $this->computeGenerateUrl(...)),
|
||||
];
|
||||
}
|
||||
|
||||
public function computeSignedUrl(StoredObject|string $file, string $method = 'GET', int $expiresDelay = null): string
|
||||
{
|
||||
if ($file instanceof StoredObject) {
|
||||
$object_name = $file->getFilename();
|
||||
} else {
|
||||
$object_name = $file;
|
||||
}
|
||||
|
||||
return $this->tempUrlGenerator->generate($method, $object_name, $expiresDelay)->url;
|
||||
}
|
||||
|
||||
public function computeGenerateUrl(StoredObject|string $file, string $method = 'GET', int $expiresDelay = null): string
|
||||
{
|
||||
if ($file instanceof StoredObject) {
|
||||
$object_name = $file->getFilename();
|
||||
} else {
|
||||
$object_name = $file;
|
||||
}
|
||||
|
||||
$args = [
|
||||
'method' => $method,
|
||||
'object_name' => $object_name,
|
||||
];
|
||||
|
||||
if (null !== $expiresDelay) {
|
||||
$args['expires_delay'] = $expiresDelay;
|
||||
}
|
||||
|
||||
return $this->routingUrlGenerator->generate('async_upload.generate_url', $args);
|
||||
}
|
||||
}
|
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\Exception\TempUrlGeneratorException;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\AsyncUploadVoter;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
final readonly class AsyncUploadController
|
||||
{
|
||||
public function __construct(
|
||||
private TempUrlGeneratorInterface $tempUrlGenerator,
|
||||
private SerializerInterface $serializer,
|
||||
private Security $security,
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @Route("/asyncupload/temp_url/generate/{method}",
|
||||
* name="async_upload.generate_url")
|
||||
*/
|
||||
public function getSignedUrl(string $method, Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
switch (strtolower($method)) {
|
||||
case 'post':
|
||||
$p = $this->tempUrlGenerator
|
||||
->generatePost(
|
||||
$request->query->has('expires_delay') ? $request->query->getInt('expires_delay') : null,
|
||||
$request->query->has('submit_delay') ? $request->query->getInt('submit_delay') : null
|
||||
)
|
||||
;
|
||||
break;
|
||||
case 'get':
|
||||
case 'head':
|
||||
$object_name = $request->query->get('object_name', null);
|
||||
|
||||
if (null === $object_name) {
|
||||
return (new JsonResponse((object) [
|
||||
'message' => 'the object_name is null',
|
||||
]))
|
||||
->setStatusCode(JsonResponse::HTTP_BAD_REQUEST);
|
||||
}
|
||||
$p = $this->tempUrlGenerator->generate(
|
||||
$method,
|
||||
$object_name,
|
||||
$request->query->has('expires_delay') ? $request->query->getInt('expires_delay') : null
|
||||
);
|
||||
break;
|
||||
default:
|
||||
return (new JsonResponse((object) ['message' => 'the method '
|
||||
."{$method} is not valid"]))
|
||||
->setStatusCode(JsonResponse::HTTP_BAD_REQUEST);
|
||||
}
|
||||
} catch (TempUrlGeneratorException $e) {
|
||||
$this->logger->warning('The client requested a temp url'
|
||||
.' which sparkle an error.', [
|
||||
'message' => $e->getMessage(),
|
||||
'expire_delay' => $request->query->getInt('expire_delay', 0),
|
||||
'file_count' => $request->query->getInt('file_count', 1),
|
||||
'method' => $method,
|
||||
]);
|
||||
|
||||
$p = new \stdClass();
|
||||
$p->message = $e->getMessage();
|
||||
$p->status = JsonResponse::HTTP_BAD_REQUEST;
|
||||
|
||||
return new JsonResponse($p, JsonResponse::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (!$this->security->isGranted(AsyncUploadVoter::GENERATE_SIGNATURE, $p)) {
|
||||
throw new AccessDeniedHttpException('not allowed to generate this signature');
|
||||
}
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize($p, 'json', [AbstractNormalizer::GROUPS => ['read']]),
|
||||
Response::HTTP_OK,
|
||||
[],
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
@@ -32,9 +32,10 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
|
||||
$configuration = new Configuration();
|
||||
$config = $this->processConfiguration($configuration, $configs);
|
||||
|
||||
$container->setParameter('chill_doc_store', $config);
|
||||
|
||||
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
|
||||
$loader->load('services.yaml');
|
||||
$loader->load('services/media.yaml');
|
||||
$loader->load('services/controller.yaml');
|
||||
$loader->load('services/menu.yaml');
|
||||
$loader->load('services/fixtures.yaml');
|
||||
@@ -94,7 +95,6 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
|
||||
'routing' => [
|
||||
'resources' => [
|
||||
'@ChillDocStoreBundle/config/routes.yaml',
|
||||
'@ChampsLibresAsyncUploaderBundle/config/routes.yaml',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocStoreBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
|
||||
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
|
||||
use Symfony\Component\Config\Definition\ConfigurationInterface;
|
||||
|
||||
@@ -24,11 +25,68 @@ class Configuration implements ConfigurationInterface
|
||||
public function getConfigTreeBuilder()
|
||||
{
|
||||
$treeBuilder = new TreeBuilder('chill_doc_store');
|
||||
/** @var ArrayNodeDefinition $rootNode */
|
||||
$rootNode = $treeBuilder->getRootNode();
|
||||
|
||||
// Here you should define the parameters that are allowed to
|
||||
// configure your bundle. See the documentation linked above for
|
||||
// more information on that topic.
|
||||
/* @phpstan-ignore-next-line As there are inconsistencies in return types, but the code works... */
|
||||
$rootNode->children()
|
||||
// openstack node
|
||||
->arrayNode('openstack')
|
||||
->info('parameters to authenticate and generate temp url against the openstack object storage service')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
// openstack.temp_url
|
||||
->arrayNode('temp_url')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
// openstack.temp_url.temp_url_key
|
||||
->scalarNode('temp_url_key')
|
||||
->isRequired()->cannotBeEmpty()
|
||||
->info('the temp url key')
|
||||
->end()
|
||||
|
||||
->scalarNode('temp_url_base_path')
|
||||
->isRequired()->cannotBeEmpty()
|
||||
->info('the base path to add **before** the path to media. Must contains the container')
|
||||
->end()
|
||||
|
||||
->scalarNode('container')
|
||||
->info('the container name')
|
||||
->isRequired()->cannotBeEmpty()
|
||||
->end()
|
||||
|
||||
->integerNode('max_post_file_size')
|
||||
->defaultValue(15_000_000)
|
||||
->info('Maximum size of the posted file, in bytes')
|
||||
->end()
|
||||
|
||||
->integerNode('max_post_file_count')
|
||||
->defaultValue(1)
|
||||
->info('Maximum number of files which may be posted at once using a POST operation using async upload')
|
||||
->end()
|
||||
|
||||
->integerNode('max_expires_delay')
|
||||
->defaultValue(180)
|
||||
->info('the maximum of seconds a cryptographic signature '
|
||||
.'will be valid for submitting a file. This should be '
|
||||
.'short, to avoid uploading multiple files')
|
||||
->end()
|
||||
|
||||
->integerNode('max_submit_delay')
|
||||
->defaultValue(3600)
|
||||
->info('the maximum of seconds between the upload of a file and '
|
||||
.'a the submission of the form. This delay will also prevent '
|
||||
.'the check of persistence of uploaded file. Should be long '
|
||||
.'enough for keeping user-friendly forms')
|
||||
->end()
|
||||
|
||||
->end() // end of children 's temp_url
|
||||
->end() // end array temp_url
|
||||
|
||||
->end() // end of children's openstack
|
||||
->end() // end of openstack
|
||||
->end() // end of root's children
|
||||
->end();
|
||||
|
||||
return $treeBuilder;
|
||||
}
|
||||
|
@@ -11,8 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocStoreBundle\Entity;
|
||||
|
||||
use ChampsLibres\AsyncUploaderBundle\Model\AsyncFileInterface;
|
||||
use ChampsLibres\AsyncUploaderBundle\Validator\Constraints\AsyncFileExists;
|
||||
use Chill\DocStoreBundle\Validator\Constraints\AsyncFileExists;
|
||||
use ChampsLibres\WopiLib\Contract\Entity\Document;
|
||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
@@ -33,7 +32,7 @@ use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
* message="The file is not stored properly"
|
||||
* )
|
||||
*/
|
||||
class StoredObject implements AsyncFileInterface, Document, TrackCreationInterface
|
||||
class StoredObject implements Document, TrackCreationInterface
|
||||
{
|
||||
use TrackCreationTrait;
|
||||
final public const STATUS_READY = 'ready';
|
||||
|
@@ -14,13 +14,10 @@ namespace Chill\DocStoreBundle\Form;
|
||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
||||
use Chill\DocStoreBundle\Entity\Document;
|
||||
use Chill\DocStoreBundle\Entity\DocumentCategory;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Form\Type\ChillDateType;
|
||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
@@ -29,33 +26,9 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class AccompanyingCourseDocumentType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @var AuthorizationHelper
|
||||
*/
|
||||
protected $authorizationHelper;
|
||||
|
||||
/**
|
||||
* @var ObjectManager
|
||||
*/
|
||||
protected $om;
|
||||
|
||||
/**
|
||||
* @var TranslatableStringHelper
|
||||
*/
|
||||
protected $translatableStringHelper;
|
||||
|
||||
/**
|
||||
* the user running this form.
|
||||
*
|
||||
* @var User
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
public function __construct(
|
||||
TranslatableStringHelper $translatableStringHelper
|
||||
) {
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
}
|
||||
private readonly TranslatableStringHelperInterface $translatableStringHelper
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
|
@@ -20,23 +20,15 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class DocumentCategoryType extends AbstractType
|
||||
{
|
||||
private $chillBundlesFlipped;
|
||||
|
||||
public function __construct($kernelBundles)
|
||||
{
|
||||
// TODO faire un service dans CHillMain
|
||||
foreach ($kernelBundles as $key => $value) {
|
||||
if (str_starts_with((string) $key, 'Chill')) {
|
||||
$this->chillBundlesFlipped[$value] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$bundles = [
|
||||
'chill-doc-store' => 'chill-doc-store',
|
||||
];
|
||||
|
||||
$builder
|
||||
->add('bundleId', ChoiceType::class, [
|
||||
'choices' => $this->chillBundlesFlipped,
|
||||
'choices' => $bundles,
|
||||
'disabled' => false,
|
||||
])
|
||||
->add('idInsideBundle', null, [
|
||||
@@ -44,7 +36,7 @@ class DocumentCategoryType extends AbstractType
|
||||
])
|
||||
->add('documentClass', null, [
|
||||
'disabled' => false,
|
||||
]) // cahcerh par default PersonDocument
|
||||
])
|
||||
->add('name', TranslatableStringFormType::class);
|
||||
}
|
||||
|
||||
|
@@ -11,7 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocStoreBundle\Form;
|
||||
|
||||
use ChampsLibres\AsyncUploaderBundle\Form\Type\AsyncUploaderType;
|
||||
use Chill\DocStoreBundle\Form\Type\AsyncUploaderType;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
@@ -26,15 +26,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
*/
|
||||
class StoredObjectType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
protected $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
public function __construct(private readonly EntityManagerInterface $em) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
|
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Form\Type;
|
||||
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
class AsyncUploaderType extends AbstractType
|
||||
{
|
||||
private readonly int $expires_delay;
|
||||
private readonly int $max_submit_delay;
|
||||
private readonly int $max_post_file_size;
|
||||
|
||||
public function __construct(
|
||||
private readonly UrlGeneratorInterface $url_generator,
|
||||
ParameterBagInterface $parameters,
|
||||
) {
|
||||
$config = $parameters->get('chill_doc_store')['openstack']['temp_url'];
|
||||
|
||||
$this->expires_delay = $config['max_expires_delay'];
|
||||
$this->max_submit_delay = $config['max_submit_delay'];
|
||||
$this->max_post_file_size = $config['max_post_file_size'];
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'expires_delay' => $this->expires_delay,
|
||||
'max_post_size' => $this->max_post_file_size,
|
||||
'submit_delay' => $this->max_submit_delay,
|
||||
'max_files' => 1,
|
||||
'error_bubbling' => false,
|
||||
]);
|
||||
|
||||
$resolver->setAllowedTypes('expires_delay', ['int']);
|
||||
$resolver->setAllowedTypes('max_post_size', ['int']);
|
||||
$resolver->setAllowedTypes('max_files', ['int']);
|
||||
$resolver->setAllowedTypes('submit_delay', ['int']);
|
||||
}
|
||||
|
||||
public function buildView(
|
||||
FormView $view,
|
||||
FormInterface $form,
|
||||
array $options
|
||||
) {
|
||||
$view->vars['attr']['data-async-file-upload'] = true;
|
||||
$view->vars['attr']['data-generate-temp-url-post'] = $this
|
||||
->url_generator->generate('async_upload.generate_url', [
|
||||
'expires_delay' => $options['expires_delay'],
|
||||
'method' => 'post',
|
||||
'submit_delay' => $options['submit_delay'],
|
||||
]);
|
||||
$view->vars['attr']['data-temp-url-get'] = $this->url_generator
|
||||
->generate('async_upload.generate_url', ['method' => 'GET']);
|
||||
$view->vars['attr']['data-max-files'] = $options['max_files'];
|
||||
$view->vars['attr']['data-max-post-size'] = $options['max_post_size'];
|
||||
}
|
||||
|
||||
public function getParent()
|
||||
{
|
||||
return HiddenType::class;
|
||||
}
|
||||
}
|
@@ -1,47 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Object;
|
||||
|
||||
use ChampsLibres\AsyncUploaderBundle\Form\AsyncFileTransformer\AsyncFileTransformerInterface;
|
||||
use ChampsLibres\AsyncUploaderBundle\Model\AsyncFileInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class ObjectToAsyncFileTransformer implements AsyncFileTransformerInterface
|
||||
{
|
||||
/**
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
protected $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
public function toAsyncFile($data)
|
||||
{
|
||||
if ($data instanceof StoredObject) {
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
public function toData(AsyncFileInterface $asyncFile)
|
||||
{
|
||||
$object = $this->em
|
||||
->getRepository(StoredObject::class)
|
||||
->findByFilename($asyncFile->getObjectName());
|
||||
|
||||
return $object ?? (new StoredObject())
|
||||
->setFilename($asyncFile->getObjectName());
|
||||
}
|
||||
}
|
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Object;
|
||||
|
||||
use ChampsLibres\AsyncUploaderBundle\Persistence\PersistenceCheckerInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class PersistenceChecker implements PersistenceCheckerInterface
|
||||
{
|
||||
/**
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
protected $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
public function isPersisted($object_name): bool
|
||||
{
|
||||
$qb = $this->em->createQueryBuilder();
|
||||
$qb->select('COUNT(m)')
|
||||
->from(StoredObject::class, 'm')
|
||||
->where($qb->expr()->eq('m.filename', ':object_name'))
|
||||
->setParameter('object_name', $object_name);
|
||||
|
||||
return 1 === $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Security\Authorization;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
final class AsyncUploadVoter extends Voter
|
||||
{
|
||||
public const GENERATE_SIGNATURE = 'CHILL_DOC_GENERATE_ASYNC_SIGNATURE';
|
||||
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
protected function supports($attribute, $subject): bool
|
||||
{
|
||||
return self::GENERATE_SIGNATURE === $attribute && $subject instanceof SignedUrl;
|
||||
}
|
||||
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||
{
|
||||
/** @var SignedUrl $subject */
|
||||
if (!in_array($subject->method, ['POST', 'GET', 'HEAD'], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN');
|
||||
}
|
||||
}
|
@@ -12,7 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\DocStoreBundle\Service;
|
||||
|
||||
use Base64Url\Base64Url;
|
||||
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -27,7 +27,10 @@ final class StoredObjectManager implements StoredObjectManagerInterface
|
||||
|
||||
private array $inMemory = [];
|
||||
|
||||
public function __construct(private readonly HttpClientInterface $client, private readonly TempUrlGeneratorInterface $tempUrlGenerator) {}
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $client,
|
||||
private readonly TempUrlGeneratorInterface $tempUrlGenerator
|
||||
) {}
|
||||
|
||||
public function getLastModified(StoredObject $document): \DateTimeInterface
|
||||
{
|
||||
|
@@ -0,0 +1,65 @@
|
||||
<?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 AsyncUpload\Command;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\Command\ConfigureOpenstackObjectStorageCommand;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class ConfigureOpenstackObjectStorageCommandTest extends TestCase
|
||||
{
|
||||
public function testRun(): void
|
||||
{
|
||||
$client = new MockHttpClient(function ($method, $url, $options): MockResponse {
|
||||
self::assertSame('POST', $method);
|
||||
self::assertSame($url, 'https://object.store.example/v1/AUTH/container');
|
||||
|
||||
$headers = $options['headers'];
|
||||
|
||||
self::assertContains('X-Auth-Token: abc', $headers);
|
||||
self::assertContains('X-Container-Meta-Temp-URL-Key: 12345679801234567890', $headers);
|
||||
self::assertContains('X-Container-Meta-Access-Control-Allow-Origin: https://chill.domain.social https://chill2.domain.social', $headers);
|
||||
|
||||
return new MockResponse('', ['http_code' => 204]);
|
||||
});
|
||||
|
||||
$parameters = new ParameterBag([
|
||||
'chill_doc_store' => [
|
||||
'openstack' => [
|
||||
'temp_url' => [
|
||||
'temp_url_key' => '12345679801234567890',
|
||||
'temp_url_base_path' => 'https://object.store.example/v1/AUTH/container',
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$command = new ConfigureOpenstackObjectStorageCommand($client, $parameters);
|
||||
|
||||
$tester = new CommandTester($command);
|
||||
|
||||
$status = $tester->execute([
|
||||
'--os_token' => 'abc',
|
||||
'--domain' => ['https://chill.domain.social', 'https://chill2.domain.social'],
|
||||
]);
|
||||
|
||||
self::assertSame(0, $status);
|
||||
}
|
||||
}
|
@@ -0,0 +1,176 @@
|
||||
<?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 AsyncUpload\Driver\OpenstackObjectStore;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\Driver\OpenstackObjectStore\TempUrlOpenstackGenerator;
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrlPost;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class TempUrlOpenstackGeneratorTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider dataProviderGenerate
|
||||
*/
|
||||
public function testGenerate(string $baseUrl, \DateTimeImmutable $now, string $key, string $method, string $objectName, int $expireDelay, SignedUrl $expected): void
|
||||
{
|
||||
$logger = new NullLogger();
|
||||
$eventDispatcher = new EventDispatcher();
|
||||
$clock = new MockClock($now);
|
||||
$parameters = new ParameterBag(
|
||||
[
|
||||
'chill_doc_store' => [
|
||||
'openstack' => [
|
||||
'temp_url' => [
|
||||
'temp_url_key' => $key,
|
||||
'temp_url_base_path' => $baseUrl,
|
||||
'max_post_file_size' => 150,
|
||||
'max_post_file_count' => 1,
|
||||
'max_expires_delay' => 1800,
|
||||
'max_submit_delay' => 1800,
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$generator = new TempUrlOpenstackGenerator(
|
||||
$logger,
|
||||
$eventDispatcher,
|
||||
$clock,
|
||||
$parameters,
|
||||
);
|
||||
|
||||
$signedUrl = $generator->generate($method, $objectName, $expireDelay);
|
||||
|
||||
self::assertEquals($expected, $signedUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataProviderGeneratePost
|
||||
*/
|
||||
public function testGeneratePost(string $baseUrl, \DateTimeImmutable $now, string $key, string $method, string $objectName, int $expireDelay, SignedUrl $expected): void
|
||||
{
|
||||
$logger = new NullLogger();
|
||||
$eventDispatcher = new EventDispatcher();
|
||||
$clock = new MockClock($now);
|
||||
$parameters = new ParameterBag(
|
||||
[
|
||||
'chill_doc_store' => [
|
||||
'openstack' => [
|
||||
'temp_url' => [
|
||||
'temp_url_key' => $key,
|
||||
'temp_url_base_path' => $baseUrl,
|
||||
'max_post_file_size' => 150,
|
||||
'max_post_file_count' => 1,
|
||||
'max_expires_delay' => 1800,
|
||||
'max_submit_delay' => 1800,
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$generator = new TempUrlOpenstackGenerator(
|
||||
$logger,
|
||||
$eventDispatcher,
|
||||
$clock,
|
||||
$parameters,
|
||||
);
|
||||
|
||||
$signedUrl = $generator->generatePost();
|
||||
|
||||
self::assertEquals('POST', $signedUrl->method);
|
||||
self::assertEquals((int) $clock->now()->format('U') + 1800, $signedUrl->expires->getTimestamp());
|
||||
self::assertEquals(150, $signedUrl->max_file_size);
|
||||
self::assertEquals(1, $signedUrl->max_file_count);
|
||||
self::assertEquals(1800, $signedUrl->submit_delay);
|
||||
self::assertEquals('', $signedUrl->redirect);
|
||||
self::assertGreaterThanOrEqual(20, strlen($signedUrl->prefix));
|
||||
}
|
||||
|
||||
public function dataProviderGenerate(): iterable
|
||||
{
|
||||
$now = \DateTimeImmutable::createFromFormat('U', '1702041743');
|
||||
$expireDelay = 1800;
|
||||
$baseUrls = [
|
||||
'https://objectstore.example/v1/my_account/container/',
|
||||
'https://objectstore.example/v1/my_account/container',
|
||||
];
|
||||
$objectName = 'object';
|
||||
$method = 'GET';
|
||||
$key = 'MYKEY';
|
||||
|
||||
$signedUrl = new SignedUrl(
|
||||
'GET',
|
||||
'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543',
|
||||
\DateTimeImmutable::createFromFormat('U', '1702043543')
|
||||
);
|
||||
|
||||
foreach ($baseUrls as $baseUrl) {
|
||||
yield [
|
||||
$baseUrl,
|
||||
$now,
|
||||
$key,
|
||||
$method,
|
||||
$objectName,
|
||||
$expireDelay,
|
||||
$signedUrl,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function dataProviderGeneratePost(): iterable
|
||||
{
|
||||
$now = \DateTimeImmutable::createFromFormat('U', '1702041743');
|
||||
$expireDelay = 1800;
|
||||
$baseUrls = [
|
||||
'https://objectstore.example/v1/my_account/container/',
|
||||
'https://objectstore.example/v1/my_account/container',
|
||||
];
|
||||
$objectName = 'object';
|
||||
$method = 'GET';
|
||||
$key = 'MYKEY';
|
||||
|
||||
$signedUrl = new SignedUrlPost(
|
||||
'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543',
|
||||
\DateTimeImmutable::createFromFormat('U', '1702043543'),
|
||||
150,
|
||||
1,
|
||||
1800,
|
||||
'',
|
||||
'abc',
|
||||
'abc'
|
||||
);
|
||||
|
||||
foreach ($baseUrls as $baseUrl) {
|
||||
yield [
|
||||
$baseUrl,
|
||||
$now,
|
||||
$key,
|
||||
$method,
|
||||
$objectName,
|
||||
$expireDelay,
|
||||
$signedUrl,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
<?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 AsyncUpload\Templating;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\AsyncUpload\Templating\AsyncUploadExtension;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class AsyncUploadExtensionTest extends KernelTestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private AsyncUploadExtension $asyncUploadExtension;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$generator = $this->prophesize(TempUrlGeneratorInterface::class);
|
||||
$generator->generate(Argument::in(['GET', 'POST']), Argument::type('string'), Argument::any())
|
||||
->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours')));
|
||||
|
||||
$urlGenerator = $this->prophesize(UrlGeneratorInterface::class);
|
||||
$urlGenerator->generate('async_upload.generate_url', Argument::type('array'))
|
||||
->willReturn('url');
|
||||
|
||||
$this->asyncUploadExtension = new AsyncUploadExtension(
|
||||
$generator->reveal(),
|
||||
$urlGenerator->reveal()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataProviderStoredObject
|
||||
*/
|
||||
public function testComputeSignedUrl(StoredObject|string $storedObject): void
|
||||
{
|
||||
$actual = $this->asyncUploadExtension->computeSignedUrl($storedObject);
|
||||
|
||||
self::assertStringContainsString('https://object.store.example/container', $actual);
|
||||
self::assertStringContainsString(is_string($storedObject) ? $storedObject : $storedObject->getFilename(), $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataProviderStoredObject
|
||||
*/
|
||||
public function testComputeGenerateUrl(StoredObject|string $storedObject): void
|
||||
{
|
||||
$actual = $this->asyncUploadExtension->computeGenerateUrl($storedObject);
|
||||
|
||||
self::assertEquals('url', $actual);
|
||||
}
|
||||
|
||||
public function dataProviderStoredObject(): iterable
|
||||
{
|
||||
yield [(new StoredObject())->setFilename('blabla')];
|
||||
|
||||
yield ['blabla'];
|
||||
}
|
||||
}
|
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrlPost;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Controller\AsyncUploadController;
|
||||
use Chill\DocStoreBundle\Security\Authorization\AsyncUploadVoter;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class AsyncUploadControllerTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testGenerateWhenUserIsNotGranted(): void
|
||||
{
|
||||
$this->expectException(AccessDeniedHttpException::class);
|
||||
$controller = $this->buildAsyncUploadController(false);
|
||||
|
||||
$controller->getSignedUrl('POST', new Request());
|
||||
}
|
||||
|
||||
public function testGeneratePost(): void
|
||||
{
|
||||
$controller = $this->buildAsyncUploadController(true);
|
||||
|
||||
$actual = $controller->getSignedUrl('POST', new Request());
|
||||
$decodedActual = json_decode($actual->getContent(), true, JSON_THROW_ON_ERROR, JSON_THROW_ON_ERROR);
|
||||
|
||||
self::assertArrayHasKey('method', $decodedActual);
|
||||
self::assertEquals('POST', $decodedActual['method']);
|
||||
}
|
||||
|
||||
public function testGenerateGet(): void
|
||||
{
|
||||
$controller = $this->buildAsyncUploadController(true);
|
||||
|
||||
$actual = $controller->getSignedUrl('GET', new Request(['object_name' => 'abc']));
|
||||
$decodedActual = json_decode($actual->getContent(), true, JSON_THROW_ON_ERROR, JSON_THROW_ON_ERROR);
|
||||
|
||||
self::assertArrayHasKey('method', $decodedActual);
|
||||
self::assertEquals('GET', $decodedActual['method']);
|
||||
}
|
||||
|
||||
private function buildAsyncUploadController(
|
||||
bool $isGranted,
|
||||
): AsyncUploadController {
|
||||
$tempUrlGenerator = new class () implements TempUrlGeneratorInterface {
|
||||
public function generatePost(int $expire_delay = null, int $submit_delay = null, int $max_file_count = 1): SignedUrlPost
|
||||
{
|
||||
return new SignedUrlPost(
|
||||
'https://object.store.example',
|
||||
new \DateTimeImmutable('1 hour'),
|
||||
150,
|
||||
1,
|
||||
1800,
|
||||
'',
|
||||
'abc',
|
||||
'abc'
|
||||
);
|
||||
}
|
||||
|
||||
public function generate(string $method, string $object_name, int $expire_delay = null): SignedUrl
|
||||
{
|
||||
return new SignedUrl(
|
||||
$method,
|
||||
'https://object.store.example',
|
||||
new \DateTimeImmutable('1 hour')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
$serializer = $this->prophesize(SerializerInterface::class);
|
||||
$serializer->serialize(Argument::type(SignedUrl::class), 'json', Argument::type('array'))
|
||||
->will(fn (array $args): string => json_encode(['method' => $args[0]->method], JSON_THROW_ON_ERROR, 3));
|
||||
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->isGranted(AsyncUploadVoter::GENERATE_SIGNATURE, Argument::type(SignedUrl::class))
|
||||
->willReturn($isGranted);
|
||||
|
||||
return new AsyncUploadController(
|
||||
$tempUrlGenerator,
|
||||
$serializer->reveal(),
|
||||
$security->reveal(),
|
||||
new NullLogger()
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Serializer\Normalizer;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class SignedUrlNormalizerTest extends KernelTestCase
|
||||
{
|
||||
public static NormalizerInterface $normalizer;
|
||||
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
self::bootKernel();
|
||||
self::$normalizer = self::$container->get(NormalizerInterface::class);
|
||||
}
|
||||
|
||||
public function testNormalizerSignedUrl(): void
|
||||
{
|
||||
$signedUrl = new SignedUrl(
|
||||
'GET',
|
||||
'https://object.store.example/container/object',
|
||||
\DateTimeImmutable::createFromFormat('U', '1700000')
|
||||
);
|
||||
|
||||
$actual = self::$normalizer->normalize($signedUrl, 'json', [AbstractNormalizer::GROUPS => ['read']]);
|
||||
|
||||
self::assertEqualsCanonicalizing(
|
||||
[
|
||||
'method' => 'GET',
|
||||
'expires' => 1_700_000,
|
||||
'url' => 'https://object.store.example/container/object',
|
||||
],
|
||||
$actual
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Serializer\Normalizer;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrlPost;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class SignedUrlPostNormalizerTest extends KernelTestCase
|
||||
{
|
||||
public static NormalizerInterface $normalizer;
|
||||
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
self::bootKernel();
|
||||
self::$normalizer = self::$container->get(NormalizerInterface::class);
|
||||
}
|
||||
|
||||
public function testNormalizerSignedUrl(): void
|
||||
{
|
||||
$signedUrl = new SignedUrlPost(
|
||||
'https://object.store.example/container/object',
|
||||
\DateTimeImmutable::createFromFormat('U', '1700000'),
|
||||
15000,
|
||||
1,
|
||||
180,
|
||||
'',
|
||||
'abc',
|
||||
'SiGnaTure'
|
||||
);
|
||||
|
||||
$actual = self::$normalizer->normalize($signedUrl, 'json', [AbstractNormalizer::GROUPS => ['read']]);
|
||||
|
||||
self::assertEqualsCanonicalizing(
|
||||
[
|
||||
'max_file_size' => 15000,
|
||||
'max_file_count' => 1,
|
||||
'submit_delay' => 180,
|
||||
'redirect' => '',
|
||||
'prefix' => 'abc',
|
||||
'signature' => 'SiGnaTure',
|
||||
'method' => 'POST',
|
||||
'expires' => 1_700_000,
|
||||
'url' => 'https://object.store.example/container/object',
|
||||
],
|
||||
$actual
|
||||
);
|
||||
}
|
||||
}
|
@@ -11,7 +11,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests;
|
||||
|
||||
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManager;
|
||||
@@ -163,8 +164,11 @@ final class StoredObjectManagerTest extends TestCase
|
||||
|
||||
private function getTempUrlGenerator(StoredObject $storedObject): TempUrlGeneratorInterface
|
||||
{
|
||||
$response = new \stdClass();
|
||||
$response->url = $storedObject->getFilename();
|
||||
$response = new SignedUrl(
|
||||
'PUT',
|
||||
'https://example.com/'.$storedObject->getFilename(),
|
||||
new \DateTimeImmutable('1 hours')
|
||||
);
|
||||
|
||||
$tempUrlGenerator = $this->createMock(TempUrlGeneratorInterface::class);
|
||||
|
||||
|
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Validator\Constraints;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Validator\Constraints\AsyncFileExists;
|
||||
use Chill\DocStoreBundle\Validator\Constraints\AsyncFileExistsValidator;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class AsyncFileExistsValidatorTest extends ConstraintValidatorTestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
protected function createValidator()
|
||||
{
|
||||
$client = new MockHttpClient(function ($method, $url, $options): MockResponse {
|
||||
if (str_contains((string) $url, '404')) {
|
||||
return new MockResponse('', ['http_code' => 404]);
|
||||
}
|
||||
|
||||
return new MockResponse('', ['http_code' => 200]);
|
||||
});
|
||||
|
||||
$generator = $this->prophesize(TempUrlGeneratorInterface::class);
|
||||
$generator->generate(Argument::in(['GET', 'HEAD']), Argument::type('string'), Argument::any())
|
||||
->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours')));
|
||||
|
||||
return new AsyncFileExistsValidator($generator->reveal(), $client);
|
||||
}
|
||||
|
||||
public function testWhenFileExistsIsValid(): void
|
||||
{
|
||||
$this->validator->validate((new StoredObject())->setFilename('present'), new AsyncFileExists());
|
||||
|
||||
$this->assertNoViolation();
|
||||
}
|
||||
|
||||
public function testWhenFileIsNotPresent(): void
|
||||
{
|
||||
$this->validator->validate(
|
||||
(new StoredObject())->setFilename('is_404'),
|
||||
new AsyncFileExists(['message' => 'my_message'])
|
||||
);
|
||||
|
||||
$this->buildViolation('my_message')->setParameter('{{ filename }}', 'is_404')->assertRaised();
|
||||
}
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Validator\Constraints;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
*/
|
||||
final class AsyncFileExists extends Constraint
|
||||
{
|
||||
public string $message = "The file '{{ filename }}' is not stored properly.";
|
||||
|
||||
public function validatedBy()
|
||||
{
|
||||
return AsyncFileExistsValidator::class;
|
||||
}
|
||||
|
||||
public function getTargets()
|
||||
{
|
||||
return [Constraint::CLASS_CONSTRAINT, Constraint::PROPERTY_CONSTRAINT];
|
||||
}
|
||||
}
|
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Validator\Constraints;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\Exception\BadCallToRemoteServer;
|
||||
use Chill\DocStoreBundle\AsyncUpload\Exception\TempUrlRemoteServerException;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedValueException;
|
||||
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
final class AsyncFileExistsValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TempUrlGeneratorInterface $tempUrlGenerator,
|
||||
private readonly HttpClientInterface $client
|
||||
) {}
|
||||
|
||||
public function validate($value, Constraint $constraint): void
|
||||
{
|
||||
if ($value instanceof StoredObject) {
|
||||
$this->validateObject($value->getFilename(), $constraint);
|
||||
} elseif (is_string($value)) {
|
||||
$this->validateObject($value, $constraint);
|
||||
} else {
|
||||
throw new UnexpectedValueException($value, StoredObject::class.' or string');
|
||||
}
|
||||
}
|
||||
|
||||
protected function validateObject(string $file, Constraint $constraint): void
|
||||
{
|
||||
if (!$constraint instanceof AsyncFileExists) {
|
||||
throw new UnexpectedTypeException($constraint, AsyncFileExists::class);
|
||||
}
|
||||
|
||||
$urlHead = $this->tempUrlGenerator->generate(
|
||||
'HEAD',
|
||||
$file,
|
||||
30
|
||||
);
|
||||
|
||||
try {
|
||||
$response = $this->client->request('HEAD', $urlHead->url);
|
||||
|
||||
if (404 === $status = $response->getStatusCode()) {
|
||||
$this->context->buildViolation($constraint->message)
|
||||
->setParameter('{{ filename }}', $file)
|
||||
->addViolation();
|
||||
} elseif (500 <= $status) {
|
||||
throw new TempUrlRemoteServerException($response->getStatusCode());
|
||||
} elseif (400 <= $status) {
|
||||
throw new BadCallToRemoteServer($response->getContent(false), $response->getStatusCode());
|
||||
}
|
||||
} catch (HttpExceptionInterface $exception) {
|
||||
if (404 !== $exception->getResponse()->getStatusCode()) {
|
||||
throw $exception;
|
||||
}
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
throw new TempUrlRemoteServerException(0, previous: $e);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,73 +1,58 @@
|
||||
parameters:
|
||||
# cl_chill_person.example.class: Chill\PersonBundle\Example
|
||||
|
||||
services:
|
||||
_defaults:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\DocStoreBundle\Repository\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: "../Repository/"
|
||||
|
||||
Chill\DocStoreBundle\Form\DocumentCategoryType:
|
||||
class: Chill\DocStoreBundle\Form\DocumentCategoryType
|
||||
arguments: [ "%kernel.bundles%" ]
|
||||
tags:
|
||||
- { name: form.type }
|
||||
- { name: doctrine.repository_service }
|
||||
|
||||
Chill\DocStoreBundle\Form\PersonDocumentType:
|
||||
class: Chill\DocStoreBundle\Form\PersonDocumentType
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
# arguments:
|
||||
# - "@chill.main.helper.translatable_string"
|
||||
tags:
|
||||
- { name: form.type, alias: chill_docstorebundle_form_document }
|
||||
|
||||
Chill\DocStoreBundle\Security\Authorization\:
|
||||
resource: "./../Security/Authorization"
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
tags:
|
||||
- { name: chill.role }
|
||||
|
||||
Chill\DocStoreBundle\Workflow\:
|
||||
resource: './../Workflow/'
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
|
||||
Chill\DocStoreBundle\Serializer\Normalizer\:
|
||||
autowire: true
|
||||
resource: '../Serializer/Normalizer/'
|
||||
tags:
|
||||
- { name: 'serializer.normalizer', priority: 16 }
|
||||
- { name: serializer.normalizer, priority: 16 }
|
||||
|
||||
Chill\DocStoreBundle\Service\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../Service/'
|
||||
|
||||
Chill\DocStoreBundle\GenericDoc\Manager:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
arguments:
|
||||
$providersForAccompanyingPeriod: !tagged_iterator chill_doc_store.generic_doc_accompanying_period_provider
|
||||
$providersForPerson: !tagged_iterator chill_doc_store.generic_doc_person_provider
|
||||
|
||||
Chill\DocStoreBundle\GenericDoc\Twig\GenericDocExtension:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
Chill\DocStoreBundle\GenericDoc\Twig\GenericDocExtension: ~
|
||||
|
||||
Chill\DocStoreBundle\GenericDoc\Twig\GenericDocExtensionRuntime:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
arguments:
|
||||
$renderers: !tagged_iterator chill_doc_store.generic_doc_renderer
|
||||
|
||||
Chill\DocStoreBundle\GenericDoc\Providers\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../GenericDoc/Providers/'
|
||||
|
||||
Chill\DocStoreBundle\GenericDoc\Renderer\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../GenericDoc/Renderer/'
|
||||
|
||||
Chill\DocStoreBundle\Validator\:
|
||||
resource: '../Validator'
|
||||
|
||||
Chill\DocStoreBundle\AsyncUpload\Driver\:
|
||||
resource: '../AsyncUpload/Driver/'
|
||||
|
||||
Chill\DocStoreBundle\AsyncUpload\Templating\:
|
||||
resource: '../AsyncUpload/Templating/'
|
||||
|
||||
Chill\DocStoreBundle\AsyncUpload\Command\:
|
||||
resource: '../AsyncUpload/Command/'
|
||||
|
||||
Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface:
|
||||
alias: Chill\DocStoreBundle\AsyncUpload\Driver\OpenstackObjectStore\TempUrlOpenstackGenerator
|
||||
|
@@ -1,13 +1,15 @@
|
||||
services:
|
||||
Chill\DocStoreBundle\Form\StoredObjectType:
|
||||
arguments:
|
||||
$em: '@Doctrine\ORM\EntityManagerInterface'
|
||||
tags:
|
||||
- { name: form.type }
|
||||
_defaults:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\DocStoreBundle\Form\AccompanyingCourseDocumentType:
|
||||
class: Chill\DocStoreBundle\Form\AccompanyingCourseDocumentType
|
||||
arguments:
|
||||
- "@chill.main.helper.translatable_string"
|
||||
tags:
|
||||
- { name: form.type, alias: chill_docstorebundle_form_document }
|
||||
Chill\DocStoreBundle\Form\:
|
||||
resource: '../../Form'
|
||||
|
||||
Chill\DocStoreBundle\Form\PersonDocumentType:
|
||||
tags:
|
||||
- { name: form.type, alias: chill_docstorebundle_form_document }
|
||||
|
||||
Chill\DocStoreBundle\Form\AccompanyingCourseDocumentType:
|
||||
tags:
|
||||
- { name: form.type, alias: chill_docstorebundle_form_document }
|
||||
|
@@ -1,9 +0,0 @@
|
||||
services:
|
||||
chill_doc_store.persistence_checker:
|
||||
class: Chill\DocStoreBundle\Object\PersistenceChecker
|
||||
arguments:
|
||||
$em: '@Doctrine\ORM\EntityManagerInterface'
|
||||
|
||||
Chill\DocStoreBundle\Object\ObjectToAsyncFileTransformer:
|
||||
arguments:
|
||||
$em: '@Doctrine\ORM\EntityManagerInterface'
|
@@ -304,7 +304,7 @@ class ExportController extends AbstractController
|
||||
FormType::class,
|
||||
$defaultFormData,
|
||||
[
|
||||
'method' => $isGenerate ? 'GET' : 'POST',
|
||||
'method' => $isGenerate ? Request::METHOD_GET : Request::METHOD_POST,
|
||||
'csrf_protection' => !$isGenerate,
|
||||
]
|
||||
);
|
||||
@@ -352,7 +352,7 @@ class ExportController extends AbstractController
|
||||
|
||||
$form = $this->createCreateFormExport($alias, 'export', $data, $savedExport);
|
||||
|
||||
if ('POST' === $request->getMethod()) {
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isValid()) {
|
||||
@@ -406,7 +406,7 @@ class ExportController extends AbstractController
|
||||
|
||||
$form = $this->createCreateFormExport($alias, 'formatter', $data, $savedExport);
|
||||
|
||||
if ('POST' === $request->getMethod()) {
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isValid()) {
|
||||
@@ -536,7 +536,7 @@ class ExportController extends AbstractController
|
||||
|
||||
$form = $this->createCreateFormExport($alias, 'centers', [], $savedExport);
|
||||
|
||||
if ('POST' === $request->getMethod()) {
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isValid()) {
|
||||
|
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Form\UserPhonenumberType;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
class UserProfileController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TranslatorInterface $translator,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* User profile that allows editing of phonenumber and visualization of certain data.
|
||||
*
|
||||
* @Route("/{_locale}/main/user/my-profile", name="chill_main_user_profile")
|
||||
*/
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$editForm = $this->createPhonenumberEditForm($user);
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
$phonenumber = $editForm->get('phonenumber')->getData();
|
||||
|
||||
$user->setPhonenumber($phonenumber);
|
||||
|
||||
$this->getDoctrine()->getManager()->flush();
|
||||
$this->addFlash('success', $this->translator->trans('user.profile.Phonenumber successfully updated!'));
|
||||
|
||||
return $this->redirectToRoute('chill_main_user_profile');
|
||||
}
|
||||
|
||||
return $this->render('@ChillMain/User/profile.html.twig', [
|
||||
'user' => $user,
|
||||
'form' => $editForm->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function createPhonenumberEditForm(UserInterface $user): FormInterface
|
||||
{
|
||||
return $this->createForm(
|
||||
UserPhonenumberType::class,
|
||||
$user,
|
||||
)
|
||||
->add('submit', SubmitType::class, ['label' => $this->translator->trans('Save')]);
|
||||
}
|
||||
}
|
@@ -18,9 +18,11 @@ use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\Common\Collections\Selectable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use libphonenumber\PhoneNumber;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
||||
|
||||
/**
|
||||
* User.
|
||||
@@ -161,6 +163,15 @@ class User implements UserInterface, \Stringable
|
||||
*/
|
||||
private ?string $usernameCanonical = null;
|
||||
|
||||
/**
|
||||
* The user's mobile phone number.
|
||||
*
|
||||
* @ORM\Column(type="phone_number", nullable=true)
|
||||
*
|
||||
* @PhonenumberConstraint()
|
||||
*/
|
||||
private ?PhoneNumber $phonenumber = null;
|
||||
|
||||
/**
|
||||
* User constructor.
|
||||
*/
|
||||
@@ -419,6 +430,11 @@ class User implements UserInterface, \Stringable
|
||||
}
|
||||
}
|
||||
|
||||
public function getPhonenumber(): ?PhoneNumber
|
||||
{
|
||||
return $this->phonenumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \RuntimeException if the groupCenter is not in the collection
|
||||
*/
|
||||
@@ -639,4 +655,11 @@ class User implements UserInterface, \Stringable
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setPhonenumber(?PhoneNumber $phonenumber): self
|
||||
{
|
||||
$this->phonenumber = $phonenumber;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@@ -92,6 +92,11 @@ class UserHelper
|
||||
}
|
||||
$asStrings = [];
|
||||
|
||||
if (array_key_exists('uid', $decoded) || is_numeric($decoded)) {
|
||||
// this is a single value. We have to wrap it into an array
|
||||
$decoded = [$decoded];
|
||||
}
|
||||
|
||||
foreach ($decoded as $userId) {
|
||||
if (is_array($userId)) {
|
||||
$uid = $userId['uid'];
|
||||
|
@@ -37,9 +37,13 @@ class RollingDateDataMapper implements DataMapperInterface
|
||||
{
|
||||
$forms = iterator_to_array($forms);
|
||||
|
||||
$viewData = new RollingDate(
|
||||
$forms['roll']->getData() ?? RollingDate::T_TODAY,
|
||||
$forms['fixedDate']->getData()
|
||||
);
|
||||
if (null === $forms['roll']->getData()) {
|
||||
$viewData = null;
|
||||
} else {
|
||||
$viewData = new RollingDate(
|
||||
$forms['roll']->getData(),
|
||||
$forms['fixedDate']->getData()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -54,7 +54,7 @@ class PickRollingDateType extends AbstractType
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'class' => RollingDate::class,
|
||||
'empty_data' => new RollingDate(RollingDate::T_TODAY),
|
||||
'empty_data' => null,
|
||||
'constraints' => [
|
||||
new Callback($this->validate(...)),
|
||||
],
|
||||
@@ -66,6 +66,10 @@ class PickRollingDateType extends AbstractType
|
||||
|
||||
public function validate($data, ExecutionContextInterface $context, $payload): void
|
||||
{
|
||||
if (null === $data) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var RollingDate $data */
|
||||
if (RollingDate::T_FIXED_DATE === $data->getRoll() && null === $data->getFixedDate()) {
|
||||
$context
|
||||
|
36
src/Bundle/ChillMainBundle/Form/UserPhonenumberType.php
Normal file
36
src/Bundle/ChillMainBundle/Form/UserPhonenumberType.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class UserPhonenumberType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('phonenumber', ChillPhoneNumberType::class, [
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => User::class,
|
||||
]);
|
||||
}
|
||||
}
|
@@ -16,6 +16,7 @@ use Chill\MainBundle\Entity\Location;
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Entity\UserJob;
|
||||
use Chill\MainBundle\Form\Type\ChillDateType;
|
||||
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
|
||||
use Chill\MainBundle\Form\Type\PickCivilityType;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
@@ -44,6 +45,9 @@ class UserType extends AbstractType
|
||||
->add('email', EmailType::class, [
|
||||
'required' => true,
|
||||
])
|
||||
->add('phonenumber', ChillPhoneNumberType::class, [
|
||||
'required' => false,
|
||||
])
|
||||
->add('label', TextType::class)
|
||||
->add('civility', PickCivilityType::class, [
|
||||
'required' => false,
|
||||
|
@@ -12,14 +12,15 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\UserJob;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
|
||||
class UserJobRepository implements UserJobRepositoryInterface
|
||||
readonly class UserJobRepository implements UserJobRepositoryInterface
|
||||
{
|
||||
private readonly EntityRepository $repository;
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
public function __construct(EntityManagerInterface $em, private TranslatableStringHelperInterface $translatableStringHelper)
|
||||
{
|
||||
$this->repository = $em->getRepository(UserJob::class);
|
||||
}
|
||||
@@ -42,6 +43,15 @@ class UserJobRepository implements UserJobRepositoryInterface
|
||||
return $this->repository->findBy(['active' => true]);
|
||||
}
|
||||
|
||||
public function findAllOrderedByName(): array
|
||||
{
|
||||
$jobs = $this->findAll();
|
||||
|
||||
usort($jobs, fn (UserJob $a, UserJob $b) => $this->translatableStringHelper->localize($a->getLabel()) <=> $this->translatableStringHelper->localize($b->getLabel()));
|
||||
|
||||
return $jobs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed|null $limit
|
||||
* @param mixed|null $offset
|
||||
|
@@ -28,6 +28,13 @@ interface UserJobRepositoryInterface extends ObjectRepository
|
||||
*/
|
||||
public function findAllActive(): array;
|
||||
|
||||
/**
|
||||
* a list of UserJob ordered by name.
|
||||
*
|
||||
* @return array<UserJob>
|
||||
*/
|
||||
public function findAllOrderedByName(): array;
|
||||
|
||||
/**
|
||||
* @param mixed|null $limit
|
||||
* @param mixed|null $offset
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import {ShowHide} from 'ChillMainAssets/lib/show_hide/index';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function(_e) {
|
||||
console.log('pick-rolling-date');
|
||||
document.querySelectorAll('div[data-rolling-date]').forEach( (picker) => {
|
||||
const
|
||||
roll_wrapper = picker.querySelector('div.roll-wrapper'),
|
||||
@@ -11,12 +10,8 @@ document.addEventListener('DOMContentLoaded', function(_e) {
|
||||
froms: [roll_wrapper],
|
||||
container: [fixed_wrapper],
|
||||
test: function (elems) {
|
||||
console.log('testing');
|
||||
console.log('elems', elems);
|
||||
for (let el of elems) {
|
||||
for (let select_roll of el.querySelectorAll('select[data-roll-picker]')) {
|
||||
console.log('select_roll', select_roll);
|
||||
console.log('value', select_roll.value);
|
||||
return select_roll.value === 'fixed_date';
|
||||
}
|
||||
}
|
||||
|
@@ -28,9 +28,7 @@
|
||||
{% block js %}
|
||||
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
||||
{{ encore_entry_script_tags('page_export') }}
|
||||
{% if export_alias == 'count_social_work_actions' %}
|
||||
{{ encore_entry_script_tags('vue_export_action_goal_result') }}
|
||||
{% endif %}
|
||||
{{ encore_entry_script_tags('vue_export_action_goal_result') }}
|
||||
{{ encore_entry_script_tags('mod_pick_rolling_date') }}
|
||||
{% endblock js %}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user