Compare commits

...

132 Commits

Author SHA1 Message Date
3fb04594eb add changie [ci-skip] 2023-12-12 15:55:14 +01:00
1065706e60 Re-configure test app 2023-12-12 15:55:14 +01:00
f131572344 Remove references to old async upload bundle 2023-12-12 15:55:14 +01:00
45e1ce034a Add ConfigureOpenstackObjectStorageCommand to ChillDocStoreBundle
Added new command file "ConfigureOpenstackObjectStorageCommand.php" under ChillDocStoreBundle that helps in setting up OpenStack container for document storage. Along with the command, added corresponding test file "ConfigureOpenstackObjectStorageCommandTest.php" to ensure the functionality is working as expected.
2023-12-12 15:55:14 +01:00
82ca321715 Add exceptions for async file handling failures
Introduced two new exceptions, `BadCallToRemoteServer` and `TempUrlRemoteServerException`, to improve error handling in asynchronous file operations. These exceptions are thrown when there are issues with the remote server during an async file operation such as HTTP status codes >= 400 and if the server is unreachable.
2023-12-12 15:55:13 +01:00
28d09a8206 Add form and types to handle async files 2023-12-12 15:55:13 +01:00
1195767eb3 Add Twig AsyncUploadExtension and its associated test
Created AsyncUploadExtension under Chill\DocStoreBundle\AsyncUpload\Templating to provide Twig filter functions for generating URLs for asynchronous file uploads. To ensure correctness, AsyncUploadExtensionTest was implemented in Bundle\ChillDocStoreBundle\Tests\AsyncUpload\Templating. Service definitions were also updated in services.yaml.
2023-12-12 15:55:13 +01:00
4fd5a37df3 Add serializer groups to SignedUrl and SignedUrlPost classes, include tests
Serializer groups have been added to the SignedUrl and SignedUrlPost classes to ensure correct serialization. Two new test files were also included: SignedUrlNormalizerTest and SignedUrlPostNormalizerTest which test the normalization of instances of the classes.
2023-12-12 15:55:13 +01:00
450e7c348b Fix and clean DI configuration for chill_doc_store for usage with AsyncUpload 2023-12-12 15:55:12 +01:00
a70572266f Refactor TempUrlOpenstackGenerator to use parameter bag and Bundle configuration
The TempUrlOpenstackGenerator now uses a ParameterBag for configuration instead of individual parameters. This simplifies the handling of configuration values and makes the code more maintainable. The parameter configuration has also been included in the chill_doc_store configuration array for a unified approach.
2023-12-12 15:55:12 +01:00
d688022825 Add Controller to get asyncUpload signatures
Added new files for handling asynchronous file uploads in ChillDocStoreBundle. The new files include a controller for generating temporary URLs (AsyncUploadController.php), a security authorization file (AsyncUploadVoter.php), and a corresponding test file (AsyncUploadControllerTest.php). These implementations permit asynchronous uploads via POST, GET, and HEAD methods while maintaining security protocols.
2023-12-12 11:36:14 +01:00
264fff5c36 Implement asynchronous upload feature in Openstack Object Store
Those features were previously stored in champs-libres/async-upload-bundle

This commit introduces several new classes within the ChillDocStore bundle for handling asynchronous uploads onto an Openstack Object Store. Specifically, the TempUrlOpenstackGenerator, SignedUrl, TempUrlGeneratorInterface, TempUrlGenerateEvent, and TempUrlGeneratorException classes have been created.

This implementation will allow for generating "temporary URLs", which assist in securely and temporarily uploading resources to the OpenStack Object Store. This feature enables the handling of file uploads in a more scalable and efficient manner in high-volume environments.

Additionally, corresponding unit tests have also been added to ensure the accuracy of this new feature.
2023-12-12 11:36:14 +01:00
91e6b035bd release 2.15.0 2023-12-11 17:23:47 +01:00
2adb6105eb Merge branch '246-do-not-show-confidential-in-list' into 'master'
Filter confidential courses from list of accompanying periods and related

Closes #246

See merge request Chill-Projet/chill-bundles!633
2023-12-11 16:22:38 +00:00
33d187f329 Merge branch '228-fix-export-activity-from-v1' into 'master'
Fix export for people created before 'createdAt' column introduction

Closes #228

See merge request Chill-Projet/chill-bundles!634
2023-12-11 16:22:12 +00:00
8cc93a8b07 add changie [ci-skip] 2023-12-07 23:25:38 +01:00
f7184ca7bb Fix export for people created before 'createdAt' column introduction
This commit introduces a migration to fix the export for individuals who were created before the introduction of the 'createdAt' column. The 'startdate' column is now updated to match the individual's first activity date. This migration is irreversible. It's been added as a response to Issue #228
2023-12-07 23:22:48 +01:00
dab80a84d8 Merge branch '235-add-date-filter-course-by-activity' into 'master'
Add dates in the filter "filter course by activity type"

Closes #235

See merge request Chill-Projet/chill-bundles!632
2023-12-07 15:25:09 +00:00
68e00dc42f Merge branch '234-fix-date-validation-aside-activity-filter-by-date' into 'master'
Fix "filter aside activity by date"

Closes #234

See merge request Chill-Projet/chill-bundles!631
2023-12-07 15:24:16 +00:00
5fae49821f Merge branch '191-export-number-of-household-by-activities' into 'master'
Export: add export "number of household associated with an activity"

Closes #191

See merge request Chill-Projet/chill-bundles!629
2023-12-07 15:23:43 +00:00
0e599a99a7 Merge branch '233-fix-filter-by-evaluation-type' into 'master'
Fix filter evaluation by evaluation type

Closes #233

See merge request Chill-Projet/chill-bundles!628
2023-12-07 15:23:24 +00:00
f0605c6b08 Use dedicated FilterListAccompanyingPeriodHelper to filter confidential course in different lists 2023-12-07 16:18:37 +01:00
54606403b4 Refactor confidential filtering on list to let it usable by other lists 2023-12-07 15:13:54 +01:00
47d829d72d Filter confidential courses from list of accompanying periods 2023-12-06 22:46:50 +01:00
fa3b305ab9 Merge branch 'issue214_fix_error_empty_comment' into 'master'
Notification: avoid the comment's content to be null

Closes #214

See merge request Chill-Projet/chill-bundles!623
2023-12-05 16:26:06 +00:00
b26b7a2706 Notification: avoid the comment's content to be null 2023-12-05 16:26:06 +00:00
fe54e51362 Add dates in the filter "filter course by activity type" 2023-12-04 18:17:00 +01:00
179e3e92ed Fix "filter aside activity by date" 2023-12-04 17:30:40 +01:00
c00c6066a9 Merge branch 'fix-rector-issues-20231204' into 'master'
Fix issues with new version of rector

See merge request Chill-Projet/chill-bundles!630
2023-12-04 16:11:38 +00:00
f424c5464f Fix issues with new version of rector 2023-12-04 16:46:48 +01:00
b17b2a8cfb Export: add export "number of household associated with an activity" 2023-12-04 16:39:01 +01:00
00de657cae Fix filter evaluation by evaluation type 2023-12-01 09:46:02 +01:00
673518e0eb Merge branch 'master' of gitlab.com:Chill-Projet/chill-bundles 2023-11-30 12:15:47 +01:00
4700a0fef7 update URL for postal code FR import 2023-11-30 12:15:22 +01:00
f4d258882f Release 2.14.1 2023-11-29 22:23:42 +01:00
fffc4a9c33 Fix error on listEvaluation
- force default calcDate
- Export / UserHelper: handle case when a single user is given, when we expect more than one user
2023-11-29 22:22:06 +01:00
a344f36592 Merge branch 'issue100_add_paginator_budget_admin' into 'master'
add paginator to budget admin pages

See merge request Chill-Projet/chill-bundles!624
2023-11-29 20:21:42 +00:00
eef93b8a0d add changie 2023-11-29 11:33:26 +01:00
75b78d3c99 add paginator to budget admin pages 2023-11-28 17:27:26 +01:00
3d4c439be4 Merge branch 'fix-list-person-custom-field' into 'master'
Fix "PersonList" with custom fields

See merge request Chill-Projet/chill-bundles!622
2023-11-28 11:03:26 +00:00
4727a57825 Export: ListPerson: avoid json_decoding the string '_header' 2023-11-27 21:02:39 +01:00
5f441eb5ac Fix path to csv template 2023-11-27 21:01:01 +01:00
2e4e5ee79a Release v2.14.0 2023-11-24 12:39:13 +01:00
1467c708f2 Merge branch '222-fix-custom-fields' into 'master'
Fix errors in custom field rendering and administration

See merge request Chill-Projet/chill-bundles!620
2023-11-24 11:38:07 +00:00
170bb9586d Fix errors in custom field rendering and administration 2023-11-24 12:30:18 +01:00
c704ffa379 Merge branch '161-fix-ordering-filter-social-action-type' into 'master'
Export: Fix vue app in "filter action by type, goals and result"

Closes #219, #141, and #161

See merge request Chill-Projet/chill-bundles!616
2023-11-23 15:50:57 +00:00
947b7b90e2 Fix SocialWorkTypeFilter: allow null value in date, and avoid vuejs module ExportFormActionGoal to override the export form content 2023-11-23 16:02:55 +01:00
992f7761bb Fix cs 2023-11-21 15:17:02 +01:00
35170e1f7c release 2.13.0 2023-11-21 14:14:11 +01:00
7132dfa3f6 Merge branch '211-errors-exports' into 'master'
Resolve multiple errors in filters and aggregators

Closes #213 and #211

See merge request Chill-Projet/chill-bundles!617
2023-11-21 12:53:27 +00:00
7e09e0ea54 Merge branch 'issue215_gender_neuter_fix' into 'master'
change all instances of neuter to both

See merge request Chill-Projet/chill-bundles!618
2023-11-21 12:53:13 +00:00
ccf8cc4d6e Merge branch 'fix-phpstan-2023-11-21' into 'master'
Fix type-hinting of DocGenObjectNormalizer and related

See merge request Chill-Projet/chill-bundles!619
2023-11-21 12:48:30 +00:00
63124f8f92 Fix type-hinting of DocGenObjectNormalizer and related 2023-11-21 13:37:23 +01:00
75d80ebd98 Fix migration syntax 2023-11-21 12:13:27 +01:00
0ea6f36297 styles fixes 2023-11-21 11:59:03 +01:00
975ea417b7 Further update gender options 2023-11-21 11:37:19 +01:00
af8e02f76b Migration to change instances of gender neuter to both 2023-11-21 08:13:46 +01:00
cbaeb3d7e8 fix double translation keys 2023-11-21 08:13:25 +01:00
f609ddb315 change all instances of neuter to both 2023-11-21 07:58:45 +01:00
d0bceb59dc Export: fix usage of some Collection returned instead of array in export filters 2023-11-20 18:15:53 +01:00
2883e085ed DX: Fix aggregator: "group activity by reason" 2023-11-20 18:07:06 +01:00
b05ed86d1e DX: Fix aggregator: "group activity by type" 2023-11-20 18:06:55 +01:00
c855d0badc Merge branch 'add_phonenumber_to_user' into 'master'
Add phonenumber to user

See merge request Chill-Projet/chill-bundles!600
2023-11-16 15:12:12 +00:00
be57c96a2f Add phonenumber to user 2023-11-16 15:12:12 +00:00
eb01c7c203 Export: on filter "action by type goals, and results", restore the fields when editing a saved export 2023-11-16 14:22:52 +01:00
53b4747697 fixup! Export: in filter "Filter accompanying period work (social action) by type, goal and result", order the items alphabetically or with the defined order 2023-11-16 11:55:15 +01:00
89e19502d3 Export: in filter "Filter accompanying period work (social action) by type, goal and result", order the items alphabetically or with the defined order 2023-11-16 11:53:57 +01:00
ff344dbb0c Release 2.12.1 2023-11-16 11:07:34 +01:00
8719b4dedd Merge branch '208-export-fix-loading-by-type-goal-result' into 'master'
Resolve "Export: le chargement de l'app vue de "filtrer les actions par type, objectif et résultat" ne se charge pas sur certains exports"

Closes #208

See merge request Chill-Projet/chill-bundles!615
2023-11-16 10:05:56 +00:00
d8fa743bc9 Export: fix loading of form for "filter action by type, goal and result" 2023-11-16 10:57:59 +01:00
eaa40d6725 DX: remove some unnecessary console.log 2023-11-16 10:57:32 +01:00
1f47f157ea Release v2.12.0 2023-11-15 14:03:46 +01:00
5ab0d3f8da add units on the headers for avg duration of accompanying period work 2023-11-15 13:57:48 +01:00
98fd5cae78 Merge branch '204-export-action-creator' into 'master'
[export] add filters/aggregators for creator works action

Closes #204

See merge request Chill-Projet/chill-bundles!610
2023-11-15 12:49:08 +00:00
2bb29242e4 Merge branch '206-add-exports' into 'master'
"Ajouter un export "usagers concernées par un échange" et "usagers concernés par une action""

Closes #203 and #206

See merge request Chill-Projet/chill-bundles!613
2023-11-15 12:45:29 +00:00
e6cab938c8 DX: increase max connection in postgres service 2023-11-15 13:41:35 +01:00
f5f4d8fcdd remove blank line in changie 2023-11-15 13:30:48 +01:00
e9df26c2f7 Export: add clauses on the social work start date and end date within the filter "Filter accompanying period by accompanying period work"' 2023-11-15 13:25:08 +01:00
af6bee2497 Export: add an export which count persons on activity 2023-11-15 13:25:08 +01:00
af585bada3 Export: add a export which count persons on accompanying period work 2023-11-15 13:24:59 +01:00
f9763b866d add 6 new tests for filters/aggrs 2023-11-15 13:00:45 +01:00
7f18a2fb7d [export] better translation since filters are sorted 2023-11-15 13:00:45 +01:00
3cdad6caff Revert "test cs-fixer"
This reverts commit 9aa1d22d69e0f67333a8c39574fa2efed71c2bc8.
2023-11-15 13:00:45 +01:00
200ab836c7 test cs-fixer 2023-11-15 13:00:45 +01:00
3e39c0ced7 rector and cs-fixer 2023-11-15 13:00:45 +01:00
83c3621c26 declare service 2023-11-15 13:00:45 +01:00
b790e2fcf1 complete aggregators 2023-11-15 13:00:45 +01:00
fde6000d0b filters alterQuery 2023-11-15 13:00:45 +01:00
3892d1e877 remove date field 2023-11-15 13:00:45 +01:00
6944773868 create 6 new filters/aggr files 2023-11-15 13:00:45 +01:00
d5bc9d10d5 Merge branch 'issue202_export_duration_actions' into 'master'
Export: create an export which add the average  duration of accompanying period work

Closes #202

See merge request Chill-Projet/chill-bundles!611
2023-11-15 11:42:35 +00:00
3743b336a6 Export: create an export which add the average duration of accompanying period work 2023-11-15 11:42:34 +00:00
3332413fe0 Merge branch '199-missing-exports' into 'master'
Add missing filters and grouping

Closes #189, #200, and #199

See merge request Chill-Projet/chill-bundles!609
2023-11-14 20:24:21 +00:00
86e659edd4 DX: replace some string by constants 2023-11-14 21:17:09 +01:00
2e60ba3137 Export: fix infinite loop in HouseholdVoter 2023-11-14 21:17:08 +01:00
1663c6f7c7 DX: rename CountHousehold => CountHouseholdInPeriod.php + add test 2023-11-14 21:17:08 +01:00
d01b6db5dc DX: remove unused property 2023-11-14 21:17:08 +01:00
847c8238e8 Export: fix typo in filter "filter accompanying period work on end date 2023-11-14 21:17:08 +01:00
2cc5fa06ae 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
2023-11-14 21:17:07 +01:00
0d741ab886 Export: add grouping "group peoples by postal code" 2023-11-14 21:17:07 +01:00
9cb794fef2 Merge branch 'documentation-configure-absence-sync' into 'master'
Update the description and screenshots for the configuration of calendar

See merge request Chill-Projet/chill-bundles!608
2023-11-14 19:41:04 +00:00
fd03855ee6 Merge branch '205-fix-acpwork-referrers-as-a-list' into 'master'
Fix normalization of accompanying period work

Closes #205

See merge request Chill-Projet/chill-bundles!612
2023-11-14 19:40:42 +00:00
ac9897d9e7 DX: add a test to ensure that accompanyingPeriodWork::getReferrers is a list 2023-11-14 20:33:18 +01:00
88469dbe8e Fix serialization of accompangying period work referrers 2023-11-14 18:34:28 +01:00
cbc83c0e63 Export: add grouping "group activity by person" (activites saved in person context) 2023-11-13 15:29:01 +01:00
27012d842d Export: add an ActivityPresenceFilter 2023-11-13 15:29:01 +01:00
4112e59af4 Export: add an ActivityPresenceAggregator 2023-11-09 17:35:34 +01:00
1b0a30baf8 Merge branch '188-fix-ci' into 'master'
debug ci

Closes #188

See merge request Chill-Projet/chill-bundles!606
2023-11-09 15:20:55 +00:00
9673b369ef DX: restore the requirements of phpunit's tests
The problem with timezone is now fixed
2023-11-09 16:13:58 +01:00
43966c4d5a remove the change of timezone in test's setUp 2023-11-09 16:01:34 +01:00
c9f438319c update the description and screenshots for the configuration of calendar
and absence synchronization
2023-11-08 10:21:03 +01:00
58b45c6f1b Prepare for v2.11.0 2023-11-07 16:18:33 +01:00
f756c0d71e Merge branch '185-fix-inconsistencies-person-geog-unit-aggregator' into 'master'
Fix geographical unit stat aggregator by course: show also the course if they...

Closes #194 and #185

See merge request Chill-Projet/chill-bundles!607
2023-11-07 15:15:12 +00:00
bb1602934c Fix "group activity by creator job" aggregator 2023-11-07 16:07:24 +01:00
83fe3ec3fc Export: add a "filter activity by creator job" filter 2023-11-07 16:06:22 +01:00
abebb79e8b Fix geographical unit stat aggregator by course: show also the course if they are not located within a location 2023-11-07 14:41:56 +01:00
3a269ba953 update changelog 2023-11-07 13:28:11 +01:00
a4a4aa119e Merge branch 'issue182_fix_doublon_bug' into 'master'
Adjust query to transfer relationship from one person to another

See merge request Chill-Projet/chill-bundles!603
2023-11-07 12:26:18 +00:00
139f5a1ff9 Merge branch 'issue182_fix_doublon_bug' of gitlab.com:Chill-Projet/chill-bundles into issue182_fix_doublon_bug 2023-11-07 10:54:36 +01:00
1e43772b17 php cs fixer 2023-11-07 10:54:22 +01:00
c29b356c02 minor change to rerun pipeline 2023-11-07 10:54:22 +01:00
050a2955ca Add a comment 2023-11-07 10:54:22 +01:00
a1132cf82f add changie 2023-11-07 10:54:22 +01:00
db70ce4a61 Adjust query to transfer relationship from one person to another depending on whether original person was in the to or from position 2023-11-07 10:54:22 +01:00
4d20a46717 Fix geographical unit stat aggregator: date range on association with left join 2023-11-06 22:03:37 +01:00
dd75cb695c Fix date range operator in GeographicalUnitStatAggregator 2023-11-06 22:03:08 +01:00
3ce78d0c16 php cs fixer 2023-11-01 10:52:42 +01:00
f43bd2bc58 minor change to rerun pipeline 2023-11-01 09:48:48 +01:00
ec85f05e0b Php cs fixer 2023-10-30 14:09:03 +01:00
bfe0d9a137 Add a comment 2023-10-30 13:52:50 +01:00
cac092c45a add changie 2023-10-30 12:00:25 +01:00
39bb06b991 Adjust query to transfer relationship from one person to another depending on whether original person was in the to or from position 2023-10-30 11:53:41 +01:00
198 changed files with 7076 additions and 790 deletions

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View File

@@ -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:

View File

@@ -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

View File

@@ -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",

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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());
};
}

View File

@@ -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()
);
};
}

View File

@@ -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';

View File

@@ -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;
}
}

View File

@@ -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,
];
}
}

View File

@@ -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,
];
}
}

View File

@@ -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

View File

@@ -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,
];
}
}

View File

@@ -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']),
]];
}

View File

@@ -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;
}
}

View File

@@ -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';
}
}

View File

@@ -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'),
];
}
}

View File

@@ -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()

View File

@@ -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'),
];
}
}

View File

@@ -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,
],
];
}
}

View 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\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,
]];
}
}

View File

@@ -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,
],
];
}
}

View File

@@ -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,
];
}

View File

@@ -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();
}
}

View File

@@ -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'),
];
}
}

View File

@@ -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 }

View File

@@ -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: >-

View File

@@ -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:

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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([

View File

@@ -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)
{

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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();

View File

@@ -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

View File

@@ -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']) {

View File

@@ -1,4 +1,8 @@
services:
_defaults:
autowire: true
autoconfigure: true
Chill\CustomFieldsBundle\Controller\:
resource: '../../Controller'
tags: ['controller.service_arguments']

View File

@@ -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;

View File

@@ -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
*

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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 {}

View File

@@ -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);
}
}

View 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();
}
}

View 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);
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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
);
}
}

View File

@@ -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',
],
],
]);

View File

@@ -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;
}

View File

@@ -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';

View File

@@ -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)
{

View File

@@ -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);
}

View File

@@ -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)
{

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}
}

View File

@@ -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');
}
}

View File

@@ -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
{

View File

@@ -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);
}
}

View File

@@ -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,
];
}
}
}

View File

@@ -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'];
}
}

View File

@@ -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()
);
}
}

View File

@@ -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
);
}
}

View File

@@ -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
);
}
}

View File

@@ -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);

View File

@@ -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();
}
}

View File

@@ -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];
}
}

View File

@@ -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);
}
}
}

View File

@@ -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

View File

@@ -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 }

View File

@@ -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'

View File

@@ -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()) {

View File

@@ -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')]);
}
}

View File

@@ -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;
}
}

View File

@@ -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'];

View File

@@ -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()
);
}
}
}

View File

@@ -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

View 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,
]);
}
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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';
}
}

View File

@@ -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