Compare commits

..

116 Commits

Author SHA1 Message Date
db3961275b git release 2.22.1 2024-07-01 09:53:41 +02:00
cd488d7576 Merge branch 'master' of gitlab.com:Chill-Projet/chill-bundles 2024-07-01 09:19:29 +02:00
436661d952 Remove debug word from code 2024-07-01 09:19:22 +02:00
c3b8d42047 Merge branch 'import_lux_addresses_command' into 'master'
DX import Luxembourg address command

See merge request Chill-Projet/chill-bundles!704
2024-06-28 09:07:32 +00:00
nobohan
9c28df25a1 DX: Improve Lux adress import command + register in config 2024-06-28 10:45:58 +02:00
nobohan
c5a24e8ac5 DX: Improve Lux address import command - WIP 2024-06-28 09:27:45 +02:00
nobohan
d9c50cffb7 DX import Luxembourg address command - phpstan 2024-06-27 10:34:41 +02:00
nobohan
25ccb16308 DX import Luxembourg address command - csfixer 2024-06-27 10:17:08 +02:00
nobohan
ba25c181f5 DX import Luxembourg address command 2024-06-27 10:07:24 +02:00
145419a76b CHANGELOG entry added for exports in event bundle 2024-06-25 13:29:24 +02:00
b1ba5cc608 Merge branch '216-exports-event-bundle' into 'master'
Add eventBundle exports

Closes #216

See merge request Chill-Projet/chill-bundles!688
2024-06-25 11:24:57 +00:00
87a6757e5e Add eventBundle exports 2024-06-25 11:24:57 +00:00
47f4cfddbb Prepare for 2.21.0 2024-06-18 10:16:39 +02:00
e95f9e9846 Merge branch '282-add-dates-job-filter' into 'master'
Add dates ranges to job and scope filter and aggregator (accompanying course's exports)

Closes #282

See merge request Chill-Projet/chill-bundles!699
2024-06-18 07:54:49 +00:00
1f4bef754d Refactor callback functions to arrow functions
The callback functions used in the addViewTransformer method in FilterType.php and AggregatorType.php were replaced with shorter arrow functions. This change was made to increase code readability and encourage consistency throughout the codebase.
2024-06-18 09:35:57 +02:00
19e34d5dc0 PHP CS Fixer updated (3.57.2 -> v3.59.3) 2024-06-17 17:28:29 +02:00
fab00f679c Add date range to UserJobAggregator
This update includes adding start_date and end_date to UserJobAggregator. This addition allows the selection of a date range in the export feature. Accompanying this change are associated translations and tests.
2024-06-17 17:16:02 +02:00
791b3776c5 Add date range filter to referrer scope aggregator
A date range filter was added to the 'ReferrerScopeAggregator' class. This new feature allows users to filter courses by their referrer's scope based on a specified date range. In addition, relevant unit tests and translations were updated to support this new functionality.
2024-06-17 17:15:53 +02:00
6bd38f1a58 Refactor assertions in AbstractAggregatorTest
The conditional checks in the AbstractAggregatorTest have been simplified. Instead of a complex inline condition with multiple checks, the test now uses straightforward assertions. This makes the code cleaner and easier to understand.
2024-06-17 15:31:33 +02:00
68d21c9267 Update ReferrerAggregator to specify a date range as parameter
The ReferrerAggregator in ChillPersonBundle has been updated to include start and end dates, replacing the previous single computation date. This provides greater flexibility in setting the timeframe for referrer data. The messages.fr.yml file has also been updated to reflect these changes. Relevant tests have been updated to match the new functionality.
2024-06-17 15:22:28 +02:00
e7ca89e0c1 Rename DataTransformerFilterInterface to DataTransformerInterface
The DataTransformerFilterInterface has been renamed to DataTransformerInterface to reflect expanded functionality. Now, this interface can be implemented not only by @see{FilterInterface}, but also by @see{AggregatorInterface}. This change allows transforming existing data in saved exports and replacing it with some default values, or new default values.
2024-06-17 15:20:54 +02:00
fc8bc33ba9 Add startDate and endDate on UserScopeFilter 2024-06-17 14:31:12 +02:00
cbd9489810 Add startDate and endDate on UserJobFilter 2024-06-17 14:10:32 +02:00
90b615c5b2 Add data transformation interface for filters
Introduced a new DataTransformerFilterInterface that allows transforming filter's form data before it is processed. Updated the FilterType file to add a view transformer if the filter implements this new interface. This new transformation process caters to transforming existing data in saved exports and replacing it with default values.
2024-06-14 14:38:10 +02:00
5ca222b501 Merge branch '122-improve-list-rendez-vous' into 'master'
Update calendar list display for the the next calendar in search results

Closes #122

See merge request Chill-Projet/chill-bundles!700
2024-06-13 16:12:57 +00:00
3e4495dd6e Refactor AccompanyingPeriod::getNextCalendarForPerson to enhance performance 2024-06-13 18:07:19 +02:00
bca0d04201 Update calendar list display for the the next calendar in search results
The calendar list display in ChillPersonBundle has been revamped, including a new view and style modifications. This update enables the display of calendars as a list for easy navigation with an added authorization check. Also, a new SCSS file named "calendar-list.scss" has been created and imported to enhance the UI/UX design.
2024-06-13 18:07:19 +02:00
f66ac50571 Merge branch '616_rapid-action' into 'master'
Flash menu rapid action in search results

See merge request Chill-Projet/chill-bundles!441
2024-06-13 10:32:30 +00:00
b454774836 add changie [ci-skip] 2024-06-13 12:21:19 +02:00
008f344e49 Update calendar and activity voters in security checks
This commit adjusts the conditions in CalendarVoter and ActivityVoter security checks. Now it takes into account both STEP_DRAFT and STEP_CLOSED statuses in determining permissions. This enhancement ensures tighter control over specific actions in these two scenarios, enhancing the overall application security.
2024-06-13 12:17:14 +02:00
90bfd87ec6 Implement security checks for menu options
The changes in this commit add security checks before displaying menu options for creating new objects on Accompanying Period.
2024-06-13 12:08:24 +02:00
cc0030c1cd Fix adding quick menus to list_with_period.html.twig
- update twig namespaces
- move twig file within Resources
2024-06-13 12:07:34 +02:00
d60ba3ecb2 Merge remote-tracking branch 'origin/master' into 616_rapid-action 2024-06-12 16:45:43 +02:00
cd5001ac74 Merge branch 'issue178_affichage_metiers' into 'master'
Display users and jobs at the date that they executed some task

See merge request Chill-Projet/chill-bundles!641
2024-06-12 14:41:40 +00:00
98f47ac512 fixes for normalising accompanying periods in docgen context 2024-06-12 16:35:53 +02:00
31b541d12f Update AccompanyingPeriodWorkNormalizer and related classes
Updated the AccompanyingPeriodWorkNormalizer, its test, and the related entity class. Now, the normalizer includes additional checks for different formats and conditions, and cleans the context accordingly before processing. AccompanyingPeriodWorkDocGenNormalizerTest now extends from a new abstract base class. Changes are made in AccompanyingPeriodWork entity for datetime handling and serialization.
2024-06-12 11:47:13 +02:00
72045ce082 Add DocGenNormalizerTestAbstract class
A new class, DocGenNormalizerTestAbstract, was added to the ChillDocGeneratorBundle. This abstract class tests the normalization of null values and ensures they comply with expected formats and behaviors. It implements key methods that allow for providing non-null objects, expectation setting, and normalization.
2024-06-12 11:46:49 +02:00
0bfb3de465 fix cs 2024-06-11 16:58:33 +02:00
9ec4c77fb7 systematically at a parameter 'at_date' when displaying createdBy in twig template
The rendering of the 'createdBy' entity has been updated across various .twig files to include the 'at_date' property. This makes the date an entity was created more explicit, providing clearer information to the user.
2024-06-11 16:55:15 +02:00
77c53972c8 Add DateTimeImmutable support in UserNormalizer
This commit introduces the use of DateTimeImmutable in the UserNormalizer class to ensure immutability of datetime objects. A check is also added to convert DateTime instances to DateTimeImmutable when normalizing data. This enhances the safety and predictability of datetimes used in the application.
2024-06-11 16:33:54 +02:00
350d991a85 Update AccompanyingPeriod normalization with UserHistory
The AccompanyingPeriod normalization now includes 'createdBy' and 'ref' fields from the UserHistory. AccompanyingPeriodDocGenNormalizer has been modified adding UserHistory retrieval and subsequently normalization. A new method was also added to the AccompanyingPeriod entity to retrieve the current UserHistory.
2024-06-11 16:33:37 +02:00
0ce9cdd07a Add label to main user selection in Calendar App
A new attribute `label` has been added to the `pick-entity` component in the Chill Calendar Bundle's Vue.js App. This label, set as 'Utilisateur principal', enhances user interaction and clarity in the main user selection process.
2024-06-11 09:39:51 +02:00
1993fac1c4 Update button rendering in AddPersons.vue
This commit modifies the button rendering in AddPersons.vue component to ensure that it doesn't crash if 'buttonTitle' is undefined. It does so by providing an empty string as a fallback in case 'buttonTitle' is unavailable, improving the component's stability.
2024-06-11 09:39:32 +02:00
83883567a2 Upgrade node dependencies and add necessary fixes 2024-06-11 09:38:56 +02:00
29d57934a1 Update workflow rendering with date context
Code updates have been made in multiple files to ensure that when entities are rendered, it includes the appropriate context relating to the date. This adjustment has been primarily made in template files where the `chill_entity_render_box` function is used. These changes help to provide users with more accurate information regarding the state of an entity at a specific time.
2024-06-10 17:19:34 +02:00
f43d79c940 Add notification date to entity render strings
The notification date has been added to the render strings of entities involved in the notifications, specifically for the sender, addressees, and normalizer. This is done by passing it as a parameter to the 'chill_entity_render_string' function and the 'normalize' function in NotificationNormalizer. This will help provide more context regarding the time of the events in the notification.
2024-06-10 17:08:30 +02:00
be730679c8 Update rendering of user information in AccompanyingCourse/Comment: show user at the comment's date 2024-06-10 15:23:12 +02:00
f62f1891d8 Fix displaying calendar: reproduce same getAtDate method as Activity 2024-06-07 13:07:25 +02:00
ebb856fe85 fix rendering of accompanying course commen with at_date 2024-06-07 13:06:46 +02:00
61877e0157 Refactor UserRenderTest and remove unused methods
The UserRenderTest class has been refactored significantly. Redundant methods related to the booting kernel of Symfony have been removed. The approach of mocking objects has been changed, swapping from traditional mocking to prophecy mocking.
2024-06-07 13:06:11 +02:00
4c3f082163 Merge remote-tracking branch 'origin/master' into issue178_affichage_metiers 2024-06-07 12:03:30 +02:00
a7ec7c9f37 fix pipeline for branch affichage metiers 2024-05-07 16:32:12 +02:00
c9e13be736 pipeline fixes for phpstan, cs-fixer, rector 2024-04-16 20:16:45 +02:00
b9b342fe44 Set isActive property for userjob 2024-04-16 20:09:00 +02:00
31f29f0bc5 Resolve merge conflicts 2024-04-16 12:43:07 +02:00
0bc9fff825 test still failing with error saying column phonenumber of relation users does not exist 2024-04-16 12:01:40 +02:00
25f93e8a89 fix typing 2024-04-16 12:01:40 +02:00
4e0d8e4def fix userNormalizerTest by adding clock in the construct of UserNormalizer 2024-04-16 12:01:40 +02:00
1ecc825945 Correct 2 phpstan errors, condition is always true and wrong typing 2024-04-16 12:01:40 +02:00
addc623add php style fixer 2024-04-16 12:01:37 +02:00
1b96deb4ee try to fix userRenderTest: mock not working 2024-04-16 11:55:54 +02:00
f510acd170 add back the annotation to edit accompanying period work for referrers 2024-04-16 11:55:54 +02:00
835409cb94 work on userRenderTest 2024-04-16 11:55:54 +02:00
2121b3ef28 Add at_date to userRender for rendering the text 2024-04-16 11:55:54 +02:00
6c9101c167 Adapt the rendering of user in accompanyingPeriodWork list and item templates 2024-04-16 11:55:54 +02:00
b46883fe36 Implement clockInterface in renderString method 2024-04-16 11:55:54 +02:00
8d58805abd work on user render test 2024-04-16 11:55:54 +02:00
c3a799cb7d work on test logic 2024-04-16 11:55:54 +02:00
bc683b28d6 update normalizers to take into account referrerHistory logic for accompanying period work 2024-04-16 11:55:50 +02:00
d91b1a70bf work on userRender test 2024-04-16 11:52:58 +02:00
853014d8d2 remove attempt to adjust accompanyingperiod work for display of user job and service at specific date 2024-04-16 11:52:58 +02:00
ad6154a1e4 Implement 'at date' for concerned groups in activity 2024-04-16 11:52:58 +02:00
50c04382ef Adjust view template for aside activity 2024-04-16 11:52:58 +02:00
d62e9ce269 Implement 'at date' for display of service and user job in accompanying period work entities (for twig templates) -> vue component still to fix 2024-04-16 11:52:58 +02:00
2149ef1cb4 Implement 'at date' for display of service and user job in aside activities entities 2024-04-16 11:52:58 +02:00
d15fbadd27 Implement 'at date' for display of service and user job in calendar entities 2024-04-16 11:52:58 +02:00
fbbf421d8b Handle DateTime type for activity while DateTimeImmutable is expected 2024-04-16 11:52:58 +02:00
fe695f1a14 Implement 'at date' in user render component for activities 2024-04-16 11:52:58 +02:00
d0ec6f9819 Improve naming for 'at date' in user render component 2024-04-16 11:52:54 +02:00
0b739fda34 test still failing with error saying column phonenumber of relation users does not exist 2024-02-12 18:56:05 +01:00
9b8e143855 fix typing 2024-02-12 18:55:00 +01:00
a533ab77ed fix userNormalizerTest by adding clock in the construct of UserNormalizer 2024-02-12 18:44:32 +01:00
087032881b Correct 2 phpstan errors, condition is always true and wrong typing 2024-02-12 14:50:26 +01:00
82667a1c0f php style fixer 2024-02-12 14:37:54 +01:00
db6408926b try to fix userRenderTest: mock not working 2024-02-12 14:36:41 +01:00
f5c7ab6ef0 add back the annotation to edit accompanying period work for referrers 2024-02-12 09:02:48 +01:00
a13ada2937 work on userRenderTest 2024-02-07 07:19:26 +01:00
3be8a39a1a Add at_date to userRender for rendering the text 2024-01-30 17:03:24 +01:00
d7eb1e01da Adapt the rendering of user in accompanyingPeriodWork list and item templates 2024-01-30 17:01:45 +01:00
bd62202d22 Implement clockInterface in renderString method 2024-01-30 16:31:29 +01:00
0e3de2ec8a work on user render test 2024-01-29 15:07:27 +01:00
aa2a398f9e work on test logic 2024-01-24 19:31:04 +01:00
33187448a0 update normalizers to take into account referrerHistory logic for accompanying period work 2024-01-24 19:30:50 +01:00
a4482ad28b work on userRender test 2024-01-23 18:13:33 +01:00
8ed5a023e8 remove attempt to adjust accompanyingperiod work for display of user job and service at specific date 2024-01-17 17:27:54 +01:00
653ac1d62b Implement 'at date' for concerned groups in activity 2024-01-08 16:51:06 +01:00
499009ac43 Adjust view template for aside activity 2024-01-08 16:38:50 +01:00
192b161e78 Implement 'at date' for display of service and user job in accompanying period work entities (for twig templates) -> vue component still to fix 2024-01-08 16:38:07 +01:00
1b1f355123 Implement 'at date' for display of service and user job in aside activities entities 2024-01-08 16:37:25 +01:00
39a863448c Implement 'at date' for display of service and user job in calendar entities 2024-01-08 12:35:41 +01:00
0c1a4a5f59 Handle DateTime type for activity while DateTimeImmutable is expected 2024-01-08 12:35:09 +01:00
6f358ee1a9 Implement 'at date' in user render component for activities 2024-01-08 11:25:33 +01:00
0f36b9349b Improve naming for 'at date' in user render component 2024-01-08 11:25:13 +01:00
d18cc29acf Revert "UX: [address details] improve button integration"
This reverts commit 89fb87f71f.
2023-07-12 17:45:06 +02:00
4220d1a2d3 Revert "UX: [vue][onTheFly] improve residential address position in modale"
This reverts commit 62d6106801.
2023-07-12 17:44:47 +02:00
1ae27152c2 Merge branch 'master' into 616_rapid-action 2023-07-12 15:38:51 +02:00
b946f8c10a Merge branch 'master' into 616_rapid-action 2023-05-24 19:56:24 +02:00
62d6106801 UX: [vue][onTheFly] improve residential address position in modale 2023-05-24 19:55:17 +02:00
89fb87f71f UX: [address details] improve button integration 2023-05-24 19:54:13 +02:00
1337360690 Fixed [search page] quickMenu: improve position and design, manage duplicate buttons 2023-05-24 18:25:35 +02:00
9324c33caf Merge branch 'master' into 616_rapid-action 2023-05-24 11:21:34 +02:00
c2dd9ef676 wip 2022-07-12 09:37:16 +02:00
a42d7231d9 not display flash menu if menu is empty 2022-07-11 17:23:34 +02:00
38deaf6f36 remove outdated deprecated message 2022-07-11 17:11:27 +02:00
04fc5b6614 flash menu rapid action built with chill_menu() 2022-07-11 17:11:21 +02:00
384b2be577 flash menu rapid action in search results: bootstrap dropdown integration 2022-07-11 12:35:00 +02:00
126 changed files with 2921 additions and 487 deletions

31
.changes/v2.21.0.md Normal file
View File

@@ -0,0 +1,31 @@
## v2.21.0 - 2024-06-18
### Feature
* Add flash menu buttons in search results, to open directly a new calendar, or a new activity in an accompanying period
* ([#122](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/122)) Improve the list of calendar in the search results: make all calendar clicable, and display a list of calendars
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add start date and end date on filters "filter course by referrer job" and "filter course by referrer scope"
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] the aggregator "Group by referrer" now accept a date range.
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's scope"
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's jobs"
* ([#168](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/168) In the UX, display user job and service at the time when he performs an action:
now, the job and service is shown:
* at the activity's date,
* at the appointment's date,
* when the user is marked as referrer for an accompanying period work,
* when the user apply a transition in a workflow,
* when the user updates or creates "something" ("created/updated by ... at ..."),
* or when he wrote a comment,
*
### Traduction francophone
* Ajout d'un menu "flash" dans les résultats de recherche, pour créer un rendez-vous ou un échange dans un parcours depuis les résultats de recherche;
* Améliore la liste des rendez-vous dans les résultats de recherche: les rendez-vous sont cliquables;
* [exports] Ajout d'intervalles de dates pour des filtres et regroupements des parcours par référent, métier du référent, service du référent;
* Affiche le métier et le service des utilisateurs à la date à laquelle il a exécuté une action. Le métier et le service est affiché:
* à la date d'un échange,
* au jour d'un rendez-vous,
* quand l'utilisateur est devenu référent d'un parcours d'accompagnement,
* quand il a appliqué une transition sur un workflow,
* quand il a mise à jour ou créé une fiche, dans les mentions "créé / mise à jour par ..., le ...",
* quand il a mis à jour un commentaire,
*

6
.changes/v2.22.0.md Normal file
View File

@@ -0,0 +1,6 @@
## v2.22.0 - 2024-06-25
### Feature
* ([#216](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/216)) [event bundle] exports added for the event module
### Traduction francophone
* Exports sont ajoutés pour la module événement.

5
.changes/v2.22.1.md Normal file
View File

@@ -0,0 +1,5 @@
## v2.22.1 - 2024-07-01
### Fixed
* Remove debug word
### DX
* Add a command for reading official address DB from Luxembourg and update chill addresses

View File

@@ -6,6 +6,45 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## v2.22.0 - 2024-06-25
### Feature
* ([#216](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/216)) [event bundle] exports added for the event module
### Traduction francophone
* Exports sont ajoutés pour la module événement.
## v2.21.0 - 2024-06-18
### Feature
* Add flash menu buttons in search results, to open directly a new calendar, or a new activity in an accompanying period
* ([#122](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/122)) Improve the list of calendar in the search results: make all calendar clicable, and display a list of calendars
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add start date and end date on filters "filter course by referrer job" and "filter course by referrer scope"
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] the aggregator "Group by referrer" now accept a date range.
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's scope"
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's jobs"
* ([#168](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/168) In the UX, display user job and service at the time when he performs an action:
now, the job and service is shown:
* at the activity's date,
* at the appointment's date,
* when the user is marked as referrer for an accompanying period work,
* when the user apply a transition in a workflow,
* when the user updates or creates "something" ("created/updated by ... at ..."),
* or when he wrote a comment,
*
### Traduction francophone
* Ajout d'un menu "flash" dans les résultats de recherche, pour créer un rendez-vous ou un échange dans un parcours depuis les résultats de recherche;
* Améliore la liste des rendez-vous dans les résultats de recherche: les rendez-vous sont cliquables;
* [exports] Ajout d'intervalles de dates pour des filtres et regroupements des parcours par référent, métier du référent, service du référent;
* Affiche le métier et le service des utilisateurs à la date à laquelle il a exécuté une action. Le métier et le service est affiché:
* à la date d'un échange,
* au jour d'un rendez-vous,
* quand l'utilisateur est devenu référent d'un parcours d'accompagnement,
* quand il a appliqué une transition sur un workflow,
* quand il a mise à jour ou créé une fiche, dans les mentions "créé / mise à jour par ..., le ...",
* quand il a mis à jour un commentaire,
*
## v2.20.1 - 2024-06-05
### Fixed
* Do not allow StoredObjectCreated for edit and convert buttons

View File

@@ -76,7 +76,7 @@
"phpunit/phpunit": ">= 7.5",
"psalm/plugin-phpunit": "^0.18.4",
"psalm/plugin-symfony": "^4.0.2",
"rector/rector": "^1.1.0",
"rector/rector": "1.1.1",
"symfony/debug-bundle": "^5.1",
"symfony/dotenv": "^4.4",
"symfony/maker-bundle": "^1.20",

View File

@@ -42,11 +42,13 @@
"@fullcalendar/vue3": "^6.1.4",
"@popperjs/core": "^2.9.2",
"@types/leaflet": "^1.9.3",
"@types/dompurify": "^3.0.5",
"dropzone": "^5.7.6",
"es6-promise": "^4.2.8",
"leaflet": "^1.7.1",
"marked": "^12.0.2",
"masonry-layout": "^4.2.2",
"mime": "^3.0.0",
"mime": "^4.0.0",
"swagger-ui": "^4.15.5",
"vis-network": "^9.1.0",
"vue": "^3.2.37",

View File

@@ -32,9 +32,13 @@ return static function (RectorConfig $rectorConfig): void {
//define sets of rules
$rectorConfig->sets([
LevelSetList::UP_TO_PHP_82,
\Rector\Symfony\Set\SymfonyLevelSetList::UP_TO_SYMFONY_44,
\Rector\Symfony\Set\SymfonySetList::SYMFONY_40,
\Rector\Symfony\Set\SymfonySetList::SYMFONY_41,
\Rector\Symfony\Set\SymfonySetList::SYMFONY_42,
\Rector\Symfony\Set\SymfonySetList::SYMFONY_43,
\Rector\Symfony\Set\SymfonySetList::SYMFONY_44,
\Rector\Doctrine\Set\DoctrineSetList::DOCTRINE_CODE_QUALITY,
\Rector\PHPUnit\Set\PHPUnitLevelSetList::UP_TO_PHPUNIT_90,
\Rector\PHPUnit\Set\PHPUnitSetList::PHPUNIT_90,
]);
// some routes are added twice if it remains activated

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ActivityBundle\Menu;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Security;
final readonly class AccompanyingCourseQuickMenuBuilder implements LocalMenuBuilderInterface
{
public function __construct(private Security $security)
{
}
public static function getMenuIds(): array
{
return ['accompanying_course_quick_menu'];
}
public function buildMenu($menuId, MenuItem $menu, array $parameters)
{
/** @var \Chill\PersonBundle\Entity\AccompanyingPeriod $accompanyingCourse */
$accompanyingCourse = $parameters['accompanying-course'];
if ($this->security->isGranted(ActivityVoter::CREATE, $accompanyingCourse)) {
$menu
->addChild('Create a new activity in accompanying course', [
'route' => 'chill_activity_activity_new',
'routeParameters' => [
// 'activityType_id' => '',
'accompanying_period_id' => $accompanyingCourse->getId(),
],
])
->setExtras([
'order' => 10,
'icon' => 'plus',
])
;
}
}
}

View File

@@ -68,7 +68,7 @@
<div class="wl-col title"><h3>{{ 'Referrer'|trans }}</h3></div>
<div class="wl-col list">
<p class="wl-item">
<span class="badge-user">{{ activity.user|chill_entity_render_box }}</span>
<span class="badge-user">{{ activity.user|chill_entity_render_box({'at_date': activity.date}) }}</span>
</p>
</div>
</div>

View File

@@ -87,7 +87,7 @@
<li>
{% if bloc.type == 'user' %}
<span class="badge-user">
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false }) }}
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }}
</span>
{% else %}
{{ _self.insert_onthefly(bloc.type, item) }}
@@ -114,7 +114,7 @@
<li>
{% if bloc.type == 'user' %}
<span class="badge-user">
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false }) }}
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }}
</span>
{% else %}
{{ _self.insert_onthefly(bloc.type, item) }}
@@ -142,7 +142,7 @@
<span class="wl-item">
{% if bloc.type == 'user' %}
<span class="badge-user">
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false }) }}
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }}
{%- if context == 'calendar_accompanyingCourse' or context == 'calendar_person' %}
{% set invite = entity.inviteForUser(item) %}
{% if invite is not null %}

View File

@@ -41,7 +41,7 @@
{% if activity.user and t.userVisible %}
<li>
<span class="item-key">{{ 'Referrer'|trans ~ ': ' }}</span>
<span class="badge-user">{{ activity.user|chill_entity_render_box }}</span>
<span class="badge-user">{{ activity.user|chill_entity_render_box({'at_date': activity.date}) }}</span>
</li>
{% endif %}

View File

@@ -37,7 +37,7 @@
{%- if entity.user is not null %}
<dt class="inline">{{ 'Referrer'|trans|capitalize }}</dt>
<dd>
<span class="badge-user">{{ entity.user|chill_entity_render_box }}</span>
<span class="badge-user">{{ entity.user|chill_entity_render_box({'at_date': entity.date}) }}</span>
</dd>
{% endif %}

View File

@@ -145,7 +145,7 @@ class ActivityVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn
throw new \RuntimeException('Could not determine context of activity.');
}
} elseif ($subject instanceof AccompanyingPeriod) {
if (AccompanyingPeriod::STEP_CLOSED === $subject->getStep()) {
if (AccompanyingPeriod::STEP_CLOSED === $subject->getStep() || AccompanyingPeriod::STEP_DRAFT === $subject->getStep()) {
if (\in_array($attribute, [self::UPDATE, self::CREATE, self::DELETE], true)) {
return false;
}

View File

@@ -60,7 +60,7 @@ final class TranslatableActivityTypeTest extends KernelTestCase
$this->assertInstanceOf(
ActivityType::class,
$form->getData()['type'],
'The data is an instance of Chill\\ActivityBundle\\Entity\\ActivityType'
'The data is an instance of Chill\ActivityBundle\Entity\ActivityType'
);
$this->assertEquals($type->getId(), $form->getData()['type']->getId());

View File

@@ -222,6 +222,7 @@ Documents label: Libellé du champ Documents
# activity type category admin
ActivityTypeCategory list: Liste des catégories des types d'échange
Create a new activity type category: Créer une nouvelle catégorie de type d'échange
Create a new activity in accompanying course: Créer un échange dans le parcours
# activity delete
Remove activity: Supprimer un échange

View File

@@ -49,13 +49,13 @@
<li>
<span>
<abbr class="referrer" title={{ 'Created by'|trans }}>{{ 'By'|trans }}:</abbr>
<b>{{ entity.createdBy|chill_entity_render_box }}</b>
<b>{{ entity.createdBy|chill_entity_render_box({'at_date': entity.date}) }}</b>
</span>
</li>
<li>
<span>
<abbr class="referrer" title={{ 'Created for'|trans }}>{{ 'For'|trans }}:</abbr>
<b>{{ entity.agent|chill_entity_render_box }}</b>
<b>{{ entity.agent|chill_entity_render_box({'at_date': entity.date}) }}</b>
</span>
</li>

View File

@@ -18,11 +18,11 @@
<dd>{{ entity.type|chill_entity_render_box }}</dd>
<dt class="inline">{{ 'Created by'|trans }}</dt>
<dd>{{ entity.createdBy }}</dd>
<dd>{{ entity.createdBy|chill_entity_render_box({'at_date': entity.date}) }}</dd>
<dt class="inline">{{ 'Created for'|trans }}</dt>
<dd>{{ entity.agent }}</dd>
<dd>{{ entity.agent|chill_entity_render_box({'at_date': entity.date}) }}</dd>
<dt class="inline">{{ 'Asideactivity location'|trans }}</dt>
{%- if entity.location.name is defined -%}
<dd>{{ entity.location.name }}</dd>

View File

@@ -524,6 +524,16 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
return $this->startDate;
}
/**
* get the date of the calendar.
*
* Useful for showing the date of the calendar event, required by twig in some places.
*/
public function getDate(): ?\DateTimeImmutable
{
return $this->getStartDate();
}
public function getStatus(): ?string
{
return $this->status;

View File

@@ -0,0 +1,50 @@
<?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\CalendarBundle\Menu;
use Chill\CalendarBundle\Security\Voter\CalendarVoter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Security;
final readonly class AccompanyingCourseQuickMenuBuilder implements LocalMenuBuilderInterface
{
public function __construct(private Security $security)
{
}
public static function getMenuIds(): array
{
return ['accompanying_course_quick_menu'];
}
public function buildMenu($menuId, MenuItem $menu, array $parameters)
{
/** @var \Chill\PersonBundle\Entity\AccompanyingPeriod $accompanyingCourse */
$accompanyingCourse = $parameters['accompanying-course'];
if ($this->security->isGranted(CalendarVoter::CREATE, $accompanyingCourse)) {
$menu
->addChild('Create a new calendar in accompanying course', [
'route' => 'chill_calendar_calendar_new',
'routeParameters' => [
'accompanying_period_id' => $accompanyingCourse->getId(),
],
])
->setExtras([
'order' => 20,
'icon' => 'plus',
])
;
}
}
}

View File

@@ -37,12 +37,12 @@ class RemoteEventConverter
* valid when the remote string contains also a timezone, like in
* lastModifiedDate.
*/
final public const REMOTE_DATETIMEZONE_FORMAT = 'Y-m-d\\TH:i:s.u?P';
final public const REMOTE_DATETIMEZONE_FORMAT = 'Y-m-d\TH:i:s.u?P';
/**
* Same as above, but sometimes the date is expressed with only 6 milliseconds.
*/
final public const REMOTE_DATETIMEZONE_FORMAT_ALT = 'Y-m-d\\TH:i:s.uP';
final public const REMOTE_DATETIMEZONE_FORMAT_ALT = 'Y-m-d\TH:i:s.uP';
private const REMOTE_DATE_FORMAT = 'Y-m-d\TH:i:s.u0';

View File

@@ -1 +1,2 @@
import './scss/badge.scss';
import './scss/calendar-list.scss';

View File

@@ -0,0 +1,26 @@
ul.calendar-list {
list-style-type: none;
padding: 0;
& > li {
display: inline-block;
}
& > li:nth-child(n+2) {
margin-left: 0.25rem;
}
}
div.calendar-list {
ul.calendar-list {
display: inline-block;
}
& > a.calendar-list__global {
display: inline-block;;
padding: 0.2rem;
min-width: 2rem;
border: 1px solid var(--bs-chill-blue);
border-radius: 0.25rem;
text-align: center;
}
}

View File

@@ -16,6 +16,7 @@
:removableIfSet="false"
:displayPicked="false"
:suggested="this.suggestedUsers"
:label="'Utilisateur principal'"
@addNewEntity="setMainUser"
></pick-entity>
</div>

View File

@@ -55,7 +55,7 @@
<div class="item-col">
<ul class="list-content">
{% if calendar.mainUser is not empty %}
<span class="badge-user">{{ calendar.mainUser|chill_entity_render_box }}</span>
<span class="badge-user">{{ calendar.mainUser|chill_entity_render_box({'at_date': calendar.startDate}) }}</span>
{% endif %}
</ul>
</div>
@@ -132,7 +132,7 @@
<li class="cancel">
<span class="createdBy">
{{ 'Created by'|trans }}
<b>{{ calendar.activity.createdBy|chill_entity_render_string }}</b>, {{ 'on'|trans }} {{ calendar.activity.createdAt|format_datetime('short', 'short') }}
<b>{{ calendar.activity.createdBy|chill_entity_render_string({'at_date': calendar.activity.createdAt}) }}</b>, {{ 'on'|trans }} {{ calendar.activity.createdAt|format_datetime('short', 'short') }}
</span>
</li>
{% if is_granted('CHILL_ACTIVITY_SEE', calendar.activity) %}

View File

@@ -89,7 +89,7 @@ class CalendarVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn
switch ($attribute) {
case self::SEE:
case self::CREATE:
if (AccompanyingPeriod::STEP_DRAFT === $subject->getStep()) {
if (AccompanyingPeriod::STEP_DRAFT === $subject->getStep() || AccompanyingPeriod::STEP_CLOSED === $subject->getStep()) {
return false;
}

View File

@@ -26,6 +26,7 @@ The calendar item has been successfully removed.: Le rendez-vous a été supprim
From the day: Du
to the day: au
Transform to activity: Transformer en échange
Create a new calendar in accompanying course: Créer un rendez-vous dans le parcours
Will send SMS: Un SMS de rappel sera envoyé
Will not send SMS: Aucun SMS de rappel ne sera envoyé
SMS already sent: Un SMS a été envoyé

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\DocGeneratorBundle\Test;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* @template T of object
*/
abstract class DocGenNormalizerTestAbstract extends KernelTestCase
{
public function testNullValueHasSameKeysAsNull(): void
{
$normalizedObject = $this->getNormalizer()->normalize($this->provideNotNullObject(), 'docgen', [
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => $this->provideDocGenExpectClass(),
]);
$nullNormalizedObject = $this->getNormalizer()->normalize(null, 'docgen', [
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => $this->provideDocGenExpectClass(),
]);
self::assertEqualsCanonicalizing(array_keys($normalizedObject), array_keys($nullNormalizedObject));
self::assertArrayHasKey('isNull', $nullNormalizedObject, 'each object must have an "isNull" key');
self::assertTrue($nullNormalizedObject['isNull'], 'isNull key must be true for null objects');
self::assertFalse($normalizedObject['isNull'], 'isNull key must be false for null objects');
foreach ($normalizedObject as $key => $value) {
if (in_array($key, ['isNull', 'type'])) {
continue;
}
if (is_array($value)) {
if (array_is_list($value)) {
self::assertEquals([], $nullNormalizedObject[$key], "list must be serialized as an empty array, in {$key}");
} else {
self::assertEqualsCanonicalizing(array_keys($value), array_keys($nullNormalizedObject[$key]), "sub-object must have the same keys, in {$key}");
}
} elseif (is_string($value)) {
self::assertEquals('', $nullNormalizedObject[$key], 'strings must be ');
}
}
}
/**
* @return T
*/
abstract public function provideNotNullObject(): object;
/**
* @return class-string<T>
*/
abstract public function provideDocGenExpectClass(): string;
abstract public function getNormalizer(): NormalizerInterface;
}

View File

@@ -15,7 +15,7 @@ use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\Participation;
use Chill\EventBundle\Form\EventType;
use Chill\EventBundle\Form\Type\PickEventType;
use Chill\EventBundle\Security\Authorization\EventVoter;
use Chill\EventBundle\Security\EventVoter;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Pagination\PaginatorFactory;
@@ -433,7 +433,6 @@ final class EventController extends AbstractController
$builder->add('event_id', HiddenType::class, [
'data' => $event->getId(),
]);
dump($event->getId());
return $builder->getForm();
}

View File

@@ -15,7 +15,7 @@ use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\Participation;
use Chill\EventBundle\Form\ParticipationType;
use Chill\EventBundle\Repository\EventRepository;
use Chill\EventBundle\Security\Authorization\ParticipationVoter;
use Chill\EventBundle\Security\ParticipationVoter;
use Chill\PersonBundle\Repository\PersonRepository;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Doctrine\Common\Collections\Collection;

View File

@@ -11,8 +11,8 @@ declare(strict_types=1);
namespace Chill\EventBundle\DependencyInjection;
use Chill\EventBundle\Security\Authorization\EventVoter;
use Chill\EventBundle\Security\Authorization\ParticipationVoter;
use Chill\EventBundle\Security\EventVoter;
use Chill\EventBundle\Security\ParticipationVoter;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
@@ -33,12 +33,13 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
$loader->load('services.yaml');
$loader->load('services/authorization.yaml');
$loader->load('services/security.yaml');
$loader->load('services/fixtures.yaml');
$loader->load('services/forms.yaml');
$loader->load('services/repositories.yaml');
$loader->load('services/search.yaml');
$loader->load('services/timeline.yaml');
$loader->load('services/export.yaml');
}
/** (non-PHPdoc).

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\EventBundle\Export\Aggregator;
use Chill\EventBundle\Export\Declarations;
use Chill\MainBundle\Export\AggregatorInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
class EventDateAggregator implements AggregatorInterface
{
private const CHOICES = [
'by month' => 'month',
'by week' => 'week',
'by year' => 'year',
];
private const DEFAULT_CHOICE = 'year';
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$order = null;
switch ($data['frequency']) {
case 'month':
$fmt = 'YYYY-MM';
break;
case 'week':
$fmt = 'YYYY-IW';
break;
case 'year':
$fmt = 'YYYY';
$order = 'DESC';
break;
default:
throw new \RuntimeException(sprintf("The frequency data '%s' is invalid.", $data['frequency']));
}
$qb->addSelect(sprintf("TO_CHAR(event.date, '%s') AS date_aggregator", $fmt));
$qb->addGroupBy('date_aggregator');
$qb->addOrderBy('date_aggregator', $order);
}
public function applyOn(): string
{
return Declarations::EVENT;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('frequency', ChoiceType::class, [
'choices' => self::CHOICES,
'multiple' => false,
'expanded' => true,
]);
}
public function getFormDefaultData(): array
{
return ['frequency' => self::DEFAULT_CHOICE];
}
public function getLabels($key, array $values, $data)
{
return static function ($value) use ($data): string {
if ('_header' === $value) {
return 'by '.$data['frequency'];
}
if (null === $value) {
return '';
}
return match ($data['frequency']) {
default => $value,
};
};
}
public function getQueryKeys($data): array
{
return ['date_aggregator'];
}
public function getTitle(): string
{
return 'Group event by date';
}
}

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export\Aggregator;
use Chill\EventBundle\Export\Declarations;
use Chill\EventBundle\Repository\EventTypeRepository;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class EventTypeAggregator implements AggregatorInterface
{
final public const KEY = 'event_type_aggregator';
public function __construct(protected EventTypeRepository $eventTypeRepository, protected TranslatableStringHelperInterface $translatableStringHelper)
{
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
if (!\in_array('eventtype', $qb->getAllAliases(), true)) {
$qb->leftJoin('event.type', 'eventtype');
}
$qb->addSelect(sprintf('IDENTITY(event.type) AS %s', self::KEY));
$qb->addGroupBy(self::KEY);
}
public function applyOn(): string
{
return Declarations::EVENT;
}
public function buildForm(FormBuilderInterface $builder)
{
// no form required for this aggregator
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data): \Closure
{
return function (int|string|null $value): string {
if ('_header' === $value) {
return 'Event type';
}
if (null === $value || '' === $value || null === $t = $this->eventTypeRepository->find($value)) {
return '';
}
return $this->translatableStringHelper->localize($t->getName());
};
}
public function getQueryKeys($data): array
{
return [self::KEY];
}
public function getTitle()
{
return 'Group by event type';
}
}

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export\Aggregator;
use Chill\EventBundle\Export\Declarations;
use Chill\EventBundle\Repository\RoleRepository;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class RoleAggregator implements AggregatorInterface
{
final public const KEY = 'part_role_aggregator';
public function __construct(protected RoleRepository $roleRepository, protected TranslatableStringHelperInterface $translatableStringHelper)
{
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
if (!\in_array('event_part', $qb->getAllAliases(), true)) {
$qb->leftJoin('event_part.role', 'role');
}
$qb->addSelect(sprintf('IDENTITY(event_part.role) AS %s', self::KEY));
$qb->addGroupBy(self::KEY);
}
public function applyOn(): string
{
return Declarations::EVENT_PARTICIPANTS;
}
public function buildForm(FormBuilderInterface $builder)
{
// no form required for this aggregator
}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, $data): \Closure
{
return function (int|string|null $value): string {
if ('_header' === $value) {
return 'Participant role';
}
if (null === $value || '' === $value || null === $r = $this->roleRepository->find($value)) {
return '';
}
return $this->translatableStringHelper->localize($r->getName());
};
}
public function getQueryKeys($data): array
{
return [self::KEY];
}
public function getTitle()
{
return 'Group by participant role';
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export;
/**
* This class declare constants used for the export framework.
*/
abstract class Declarations
{
final public const EVENT = 'event';
final public const EVENT_PARTICIPANTS = 'event_participants';
}

View File

@@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export\Export;
use Chill\EventBundle\Export\Declarations;
use Chill\EventBundle\Repository\ParticipationRepository;
use Chill\EventBundle\Security\ParticipationVoter;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\GroupedExportInterface;
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
use Doctrine\ORM\Query;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Chill\PersonBundle\Export\Declarations as PersonDeclarations;
readonly class CountEventParticipations implements ExportInterface, GroupedExportInterface
{
private bool $filterStatsByCenters;
public function __construct(
private ParticipationRepository $participationRepository,
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 'Count participants to an event by various parameters.';
}
public function getGroup(): string
{
return 'Exports of events';
}
public function getLabels($key, array $values, $data)
{
if ('export_count_event_participants' !== $key) {
throw new \LogicException("the key {$key} is not used by this export");
}
return static fn ($value) => '_header' === $value ? 'Count event participants' : $value;
}
public function getQueryKeys($data)
{
return ['export_count_event_participants'];
}
public function getResult($query, $data)
{
return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
}
public function getTitle()
{
return 'Count event participants';
}
public function getType(): string
{
return Declarations::EVENT_PARTICIPANTS;
}
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
{
$centers = array_map(static fn ($el) => $el['center'], $acl);
$qb = $this->participationRepository
->createQueryBuilder('event_part')
->join('event_part.person', 'person');
$qb->select('COUNT(event_part.id) as export_count_event_participants');
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);
}
return $qb;
}
public function requiredRole(): string
{
return ParticipationVoter::STATS;
}
public function supportsModifiers()
{
return [
Declarations::EVENT_PARTICIPANTS,
PersonDeclarations::PERSON_TYPE,
];
}
}

View File

@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export\Export;
use Chill\EventBundle\Repository\EventRepository;
use Chill\EventBundle\Security\EventVoter;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\GroupedExportInterface;
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
use Doctrine\ORM\Query;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Chill\EventBundle\Export\Declarations;
use Chill\PersonBundle\Export\Declarations as PersonDeclarations;
readonly class CountEvents implements ExportInterface, GroupedExportInterface
{
private bool $filterStatsByCenters;
public function __construct(
private EventRepository $eventRepository,
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 'Count events by various parameters.';
}
public function getGroup(): string
{
return 'Exports of events';
}
public function getLabels($key, array $values, $data)
{
if ('export_count_event' !== $key) {
throw new \LogicException("the key {$key} is not used by this export");
}
return static fn ($value) => '_header' === $value ? 'Number of events' : $value;
}
public function getQueryKeys($data)
{
return ['export_count_event'];
}
public function getResult($query, $data)
{
return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
}
public function getTitle()
{
return 'Count events';
}
public function getType(): string
{
return Declarations::EVENT;
}
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
{
$centers = array_map(static fn ($el) => $el['center'], $acl);
$qb = $this->eventRepository
->createQueryBuilder('event')
->leftJoin('event.participations', 'epart')
->leftJoin('epart.person', 'person');
$qb->select('COUNT(event.id) as export_count_event');
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);
}
return $qb;
}
public function requiredRole(): string
{
return EventVoter::STATS;
}
public function supportsModifiers()
{
return [
Declarations::EVENT,
PersonDeclarations::PERSON_TYPE,
];
}
}

View File

@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export\Filter;
use Chill\EventBundle\Export\Declarations;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class EventDateFilter implements FilterInterface
{
public function __construct(protected TranslatorInterface $translator, private readonly RollingDateConverterInterface $rollingDateConverter)
{
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$where = $qb->getDQLPart('where');
$clause = $qb->expr()->between(
'event.date',
':date_from',
':date_to'
);
if ($where instanceof Expr\Andx) {
$where->add($clause);
} else {
$where = $qb->expr()->andX($clause);
}
$qb->add('where', $where);
$qb->setParameter(
'date_from',
$this->rollingDateConverter->convert($data['date_from'])
);
$qb->setParameter(
'date_to',
$this->rollingDateConverter->convert($data['date_to'])
);
}
public function applyOn(): string
{
return Declarations::EVENT;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('date_from', PickRollingDateType::class, [
'label' => 'Events after this date',
])
->add('date_to', PickRollingDateType::class, [
'label' => 'Events before this date',
]);
}
public function getFormDefaultData(): array
{
return ['date_from' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), 'date_to' => new RollingDate(RollingDate::T_TODAY)];
}
public function describeAction($data, $format = 'string')
{
return [
'Filtered by date of event: only between %date_from% and %date_to%',
[
'%date_from%' => $this->rollingDateConverter->convert($data['date_from'])->format('d-m-Y'),
'%date_to%' => $this->rollingDateConverter->convert($data['date_to'])->format('d-m-Y'),
],
];
}
public function getTitle()
{
return 'Filtered by event date';
}
}

View File

@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export\Filter;
use Chill\EventBundle\Entity\EventType;
use Chill\EventBundle\Export\Declarations;
use Chill\EventBundle\Repository\EventTypeRepository;
use Chill\MainBundle\Export\ExportElementValidatedInterface;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
class EventTypeFilter implements ExportElementValidatedInterface, FilterInterface
{
public function __construct(
protected TranslatableStringHelperInterface $translatableStringHelper,
protected EventTypeRepository $eventTypeRepository
) {
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$clause = $qb->expr()->in('event.type', ':selected_event_types');
$qb->andWhere($clause);
$qb->setParameter('selected_event_types', $data['types']);
}
public function applyOn(): string
{
return Declarations::EVENT;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('types', EntityType::class, [
'choices' => $this->eventTypeRepository->findAllActive(),
'class' => EventType::class,
'choice_label' => fn (EventType $ety) => $this->translatableStringHelper->localize($ety->getName()),
'multiple' => true,
'expanded' => false,
'attr' => [
'class' => 'select2',
],
]);
}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string')
{
$typeNames = array_map(
fn (EventType $t): string => $this->translatableStringHelper->localize($t->getName()),
$this->eventTypeRepository->findBy(['id' => $data['types'] instanceof \Doctrine\Common\Collections\Collection ? $data['types']->toArray() : $data['types']])
);
return ['Filtered by event type: only %list%', [
'%list%' => implode(', ', $typeNames),
]];
}
public function getTitle()
{
return 'Filtered by event type';
}
public function validateForm($data, ExecutionContextInterface $context)
{
if (null === $data['types'] || 0 === \count($data['types'])) {
$context
->buildViolation('At least one type must be chosen')
->addViolation();
}
}
}

View File

@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export\Filter;
use Chill\EventBundle\Entity\Role;
use Chill\EventBundle\Export\Declarations;
use Chill\EventBundle\Repository\RoleRepository;
use Chill\MainBundle\Export\ExportElementValidatedInterface;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
class RoleFilter implements ExportElementValidatedInterface, FilterInterface
{
public function __construct(
protected TranslatableStringHelperInterface $translatableStringHelper,
protected RoleRepository $roleRepository
) {
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$clause = $qb->expr()->in('event_part.role', ':selected_part_roles');
$qb->andWhere($clause);
$qb->setParameter('selected_part_roles', $data['part_roles']);
}
public function applyOn(): string
{
return Declarations::EVENT_PARTICIPANTS;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('part_roles', EntityType::class, [
'choices' => $this->roleRepository->findAllActive(),
'class' => Role::class,
'choice_label' => fn (Role $r) => $this->translatableStringHelper->localize($r->getName()),
'multiple' => true,
'expanded' => false,
'attr' => [
'class' => 'select2',
],
]);
}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string')
{
$roleNames = array_map(
fn (Role $r): string => $this->translatableStringHelper->localize($r->getName()),
$this->roleRepository->findBy(['id' => $data['part_roles'] instanceof \Doctrine\Common\Collections\Collection ? $data['part_roles']->toArray() : $data['part_roles']])
);
return ['Filtered by participant roles: only %list%', [
'%list%' => implode(', ', $roleNames),
]];
}
public function getTitle()
{
return 'Filter by participant roles';
}
public function validateForm($data, ExecutionContextInterface $context)
{
if (null === $data['part_roles'] || 0 === \count($data['part_roles'])) {
$context
->buildViolation('At least one role must be chosen')
->addViolation();
}
}
}

View File

@@ -11,7 +11,7 @@ declare(strict_types=1);
namespace Chill\EventBundle\Menu;
use Chill\EventBundle\Security\Authorization\EventVoter;
use Chill\EventBundle\Security\EventVoter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

View File

@@ -11,7 +11,7 @@ declare(strict_types=1);
namespace Chill\EventBundle\Menu;
use Chill\EventBundle\Security\Authorization\EventVoter;
use Chill\EventBundle\Security\EventVoter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Security;

View File

@@ -13,7 +13,7 @@ namespace Chill\EventBundle\Repository;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\Participation;
use Chill\EventBundle\Security\Authorization\EventVoter;
use Chill\EventBundle\Security\EventVoter;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
use Chill\PersonBundle\Entity\Person;

View File

@@ -12,13 +12,57 @@ declare(strict_types=1);
namespace Chill\EventBundle\Repository;
use Chill\EventBundle\Entity\Role;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;
class RoleRepository extends ServiceEntityRepository
readonly class RoleRepository implements ObjectRepository
{
public function __construct(ManagerRegistry $registry)
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager, private TranslatableStringHelper $translatableStringHelper)
{
parent::__construct($registry, Role::class);
$this->repository = $entityManager->getRepository(Role::class);
}
public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder
{
return $this->repository->createQueryBuilder($alias, $indexBy);
}
public function find($id)
{
return $this->repository->find($id);
}
public function findAll(): array
{
return $this->repository->findAll();
}
public function findAllActive(): array
{
$roles = $this->repository->findBy(['active' => true]);
usort($roles, fn (Role $a, Role $b) => $this->translatableStringHelper->localize($a->getName()) <=> $this->translatableStringHelper->localize($b->getName()));
return $roles;
}
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria)
{
return $this->repository->findOneBy($criteria);
}
public function getClassName(): string
{
return Role::class;
}
}

View File

@@ -9,18 +9,19 @@ declare(strict_types=1);
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Security\Authorization;
namespace Chill\EventBundle\Security;
use Chill\EventBundle\Entity\Event;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
/**
* Description of EventVoter.
@@ -42,61 +43,46 @@ class EventVoter extends AbstractChillVoter implements ProvideRoleHierarchyInter
final public const UPDATE = 'CHILL_EVENT_UPDATE';
/**
* @var AccessDecisionManagerInterface
*/
protected $accessDecisionManager;
final public const STATS = 'CHILL_EVENT_STATS';
/**
* @var AuthorizationHelper
*/
protected $authorizationHelper;
/**
* @var LoggerInterface
*/
protected $logger;
private readonly VoterHelperInterface $voterHelper;
public function __construct(
AccessDecisionManagerInterface $accessDecisionManager,
AuthorizationHelper $authorizationHelper,
LoggerInterface $logger
private readonly AuthorizationHelper $authorizationHelper,
private readonly LoggerInterface $logger,
VoterHelperFactoryInterface $voterHelperFactory
) {
$this->accessDecisionManager = $accessDecisionManager;
$this->authorizationHelper = $authorizationHelper;
$this->logger = $logger;
$this->voterHelper = $voterHelperFactory
->generate(self::class)
->addCheckFor(null, [self::SEE])
->addCheckFor(Event::class, [...self::ROLES])
->addCheckFor(Person::class, [self::SEE, self::CREATE])
->addCheckFor(Center::class, [self::STATS])
->build();
}
public function getRoles(): array
{
return self::ROLES;
return [...self::ROLES, self::STATS];
}
public function getRolesWithHierarchy(): array
{
return [
'Event' => self::ROLES,
'Event' => $this->getRoles(),
];
}
public function getRolesWithoutScope(): array
{
return [];
return [self::ROLES, self::STATS];
}
public function supports($attribute, $subject)
{
return ($subject instanceof Event && \in_array($attribute, self::ROLES, true))
|| ($subject instanceof Person && \in_array($attribute, [self::CREATE, self::SEE], true))
|| (null === $subject && self::SEE === $attribute);
return $this->voterHelper->supports($attribute, $subject);
}
/**
* @param string $attribute
* @param Event $subject
*
* @return bool
*/
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
$this->logger->debug(sprintf('Voting from %s class', self::class));
@@ -118,15 +104,5 @@ class EventVoter extends AbstractChillVoter implements ProvideRoleHierarchyInter
->getReachableCenters($token->getUser(), $attribute);
return \count($centers) > 0;
if (!$this->accessDecisionManager->decide($token, [PersonVoter::SEE], $person)) {
return false;
}
return $this->authorizationHelper->userHasAccess(
$token->getUser(),
$subject,
$attribute
);
}
}

View File

@@ -9,18 +9,19 @@ declare(strict_types=1);
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Security\Authorization;
namespace Chill\EventBundle\Security;
use Chill\EventBundle\Entity\Participation;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
class ParticipationVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface
{
@@ -39,40 +40,33 @@ class ParticipationVoter extends AbstractChillVoter implements ProvideRoleHierar
final public const UPDATE = 'CHILL_EVENT_PARTICIPATION_UPDATE';
/**
* @var AccessDecisionManagerInterface
*/
protected $accessDecisionManager;
final public const STATS = 'CHILL_EVENT_PARTICIPATION_STATS';
/**
* @var AuthorizationHelper
*/
protected $authorizationHelper;
/**
* @var LoggerInterface
*/
protected $logger;
private readonly VoterHelperInterface $voterHelper;
public function __construct(
AccessDecisionManagerInterface $accessDecisionManager,
AuthorizationHelper $authorizationHelper,
LoggerInterface $logger
private readonly AuthorizationHelper $authorizationHelper,
private readonly LoggerInterface $logger,
VoterHelperFactoryInterface $voterHelperFactory
) {
$this->accessDecisionManager = $accessDecisionManager;
$this->authorizationHelper = $authorizationHelper;
$this->logger = $logger;
$this->voterHelper = $voterHelperFactory
->generate(self::class)
->addCheckFor(null, [self::SEE])
->addCheckFor(Participation::class, [...self::ROLES])
->addCheckFor(Person::class, [self::SEE, self::CREATE])
->addCheckFor(Center::class, [self::STATS])
->build();
}
public function getRoles(): array
{
return self::ROLES;
return [...self::ROLES, self::STATS];
}
public function getRolesWithHierarchy(): array
{
return [
'Event' => self::ROLES,
'Participation' => $this->getRoles(),
];
}
@@ -83,14 +77,11 @@ class ParticipationVoter extends AbstractChillVoter implements ProvideRoleHierar
public function supports($attribute, $subject)
{
return ($subject instanceof Participation && \in_array($attribute, self::ROLES, true))
|| ($subject instanceof Person && \in_array($attribute, [self::CREATE, self::SEE], true))
|| (null === $subject && self::SEE === $attribute);
return $this->voterHelper->supports($attribute, $subject);
}
/**
* @param string $attribute
* @param Participation $subject
* @param string $attribute
*
* @return bool
*/
@@ -115,15 +106,5 @@ class ParticipationVoter extends AbstractChillVoter implements ProvideRoleHierar
->getReachableCenters($token->getUser(), $attribute);
return \count($centers) > 0;
if (!$this->accessDecisionManager->decide($token, [PersonVoter::SEE], $person)) {
return false;
}
return $this->authorizationHelper->userHasAccess(
$token->getUser(),
$subject,
$attribute
);
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Tests\Export;
use Chill\EventBundle\Export\Export\CountEventParticipations;
use Doctrine\ORM\AbstractQuery;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @internal
*
* @coversNothing
*/
class CountEventParticipationsTest extends KernelTestCase
{
private CountEventParticipations $countEventParticipations;
protected function setUp(): void
{
parent::setUp();
self::bootKernel();
$this->countEventParticipations = self::$container->get(CountEventParticipations::class);
}
public function testExecuteQuery(): void
{
$qb = $this->countEventParticipations->initiateQuery([], [], [])
->setMaxResults(1);
$results = $qb->getQuery()->getResult(AbstractQuery::HYDRATE_ARRAY);
self::assertIsArray($results, 'smoke test: test that the result is an array');
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Tests\Export;
use Chill\EventBundle\Export\Export\CountEvents;
use Doctrine\ORM\AbstractQuery;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @internal
*
* @coversNothing
*/
class CountEventTest extends KernelTestCase
{
private CountEvents $countEvents;
protected function setUp(): void
{
parent::setUp();
self::bootKernel();
$this->countEvents = self::$container->get(CountEvents::class);
}
public function testExecuteQuery(): void
{
$qb = $this->countEvents->initiateQuery([], [], [])
->setMaxResults(1);
$results = $qb->getQuery()->getResult(AbstractQuery::HYDRATE_ARRAY);
self::assertIsArray($results, 'smoke test: test that the result is an array');
}
}

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 Export\aggregators;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Export\Aggregator\EventDateAggregator;
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
use Doctrine\ORM\EntityManagerInterface;
/**
* @internal
*
* @coversNothing
*/
class EventDateAggregatorTest extends AbstractAggregatorTest
{
private $aggregator;
protected function setUp(): void
{
self::bootKernel();
$this->aggregator = self::$container->get(EventDateAggregator::class);
}
public function getAggregator()
{
return $this->aggregator;
}
public function getFormData(): array|\Generator
{
yield ['frequency' => 'YYYY'];
yield ['frequency' => 'YYYY-MM'];
yield ['frequency' => 'YYYY-IV'];
}
public function getQueryBuilders(): array
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
return [
$em->createQueryBuilder()
->select('event.id')
->from(Event::class, 'event'),
];
}
}

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 Export\aggregators;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Export\Aggregator\EventTypeAggregator;
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
use Doctrine\ORM\EntityManagerInterface;
/**
* @internal
*
* @coversNothing
*/
class EventTypeAggregatorTest extends AbstractAggregatorTest
{
private $aggregator;
protected function setUp(): void
{
self::bootKernel();
$this->aggregator = self::$container->get(EventTypeAggregator::class);
}
public function getAggregator()
{
return $this->aggregator;
}
public function getFormData(): array
{
return [
[],
];
}
public function getQueryBuilders(): array
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
return [
$em->createQueryBuilder()
->select('event.id')
->from(Event::class, 'event'),
];
}
}

View File

@@ -0,0 +1,63 @@
<?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 Export\aggregators;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\Participation;
use Chill\EventBundle\Export\Aggregator\RoleAggregator;
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
use Doctrine\ORM\EntityManagerInterface;
/**
* @internal
*
* @coversNothing
*/
class RoleAggregatorTest extends AbstractAggregatorTest
{
private $aggregator;
protected function setUp(): void
{
self::bootKernel();
$this->aggregator = self::$container->get(RoleAggregator::class);
}
public function getAggregator()
{
return $this->aggregator;
}
public function getFormData(): array
{
return [
[],
];
}
public function getQueryBuilders(): array
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
return [
$em->createQueryBuilder()
->select('event.id')
->from(Event::class, 'event'),
$em->createQueryBuilder()
->select('event_part')
->from(Participation::class, 'event_part'),
];
}
}

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 Export\filters;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Export\Filter\EventDateFilter;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\MainBundle\Test\Export\AbstractFilterTest;
use Doctrine\ORM\EntityManagerInterface;
/**
* @internal
*
* @coversNothing
*/
class EventDateFilterTest extends AbstractFilterTest
{
private RollingDateConverterInterface $rollingDateConverter;
protected function setUp(): void
{
parent::setUp();
self::bootKernel();
$this->rollingDateConverter = self::$container->get(RollingDateConverterInterface::class);
}
public function getFilter()
{
return new EventDateFilter($this->rollingDateConverter);
}
public function getFormData()
{
return [
[
'date_from' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'date_to' => new RollingDate(RollingDate::T_TODAY),
],
];
}
public function getQueryBuilders(): array
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
return [
$em->createQueryBuilder()
->select('event.id')
->from(Event::class, 'event'),
];
}
}

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 Export\filters;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\EventType;
use Chill\EventBundle\Export\Filter\EventTypeFilter;
use Chill\MainBundle\Test\Export\AbstractFilterTest;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
/**
* @internal
*
* @coversNothing
*/
class EventTypeFilterTest extends AbstractFilterTest
{
private EventTypeFilter $filter;
protected function setUp(): void
{
self::bootKernel();
$this->filter = self::$container->get(EventTypeFilter::class);
}
public function getFilter(): EventTypeFilter|\Chill\MainBundle\Export\FilterInterface
{
return $this->filter;
}
public function getFormData()
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
$array = $em->createQueryBuilder()
->from(EventType::class, 'et')
->select('et')
->getQuery()
->getResult();
$data = [];
foreach ($array as $a) {
$data[] = [
'types' => new ArrayCollection([$a]),
];
}
return $data;
}
public function getQueryBuilders()
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
return [
$em->createQueryBuilder()
->select('event.id')
->from(Event::class, 'event'),
];
}
}

View File

@@ -0,0 +1,81 @@
<?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 Export\filters;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\Participation;
use Chill\EventBundle\Entity\Role;
use Chill\EventBundle\Export\Filter\RoleFilter;
use Chill\MainBundle\Test\Export\AbstractFilterTest;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
/**
* @internal
*
* @coversNothing
*/
class RoleFilterTest extends AbstractFilterTest
{
private RoleFilter $filter;
protected function setUp(): void
{
self::bootKernel();
$this->filter = self::$container->get(RoleFilter::class);
}
public function getFilter()
{
return $this->filter;
}
public function getFormData(): array
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
$array = $em->createQueryBuilder()
->from(Role::class, 'r')
->select('r')
->getQuery()
->setMaxResults(1)
->getResult();
$data = [];
foreach ($array as $a) {
$data[] = [
'roles' => new ArrayCollection([$a]),
];
}
return $data;
}
public function getQueryBuilders()
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
return [
$em->createQueryBuilder()
->select('event.id')
->from(Event::class, 'event'),
$em->createQueryBuilder()
->select('event_part')
->from(Participation::class, 'event_part'),
];
}
}

View File

@@ -12,7 +12,7 @@ declare(strict_types=1);
namespace Chill\EventBundle\Tests\Repository;
use Chill\EventBundle\Repository\EventACLAwareRepository;
use Chill\EventBundle\Security\Authorization\EventVoter;
use Chill\EventBundle\Security\EventVoter;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;

View File

@@ -1,20 +0,0 @@
services:
chill_event.event_voter:
class: Chill\EventBundle\Security\Authorization\EventVoter
arguments:
- "@security.access.decision_manager"
- "@chill.main.security.authorization.helper"
- "@logger"
tags:
- { name: chill.role }
- { name: security.voter }
chill_event.event_participation:
class: Chill\EventBundle\Security\Authorization\ParticipationVoter
arguments:
- "@security.access.decision_manager"
- "@chill.main.security.authorization.helper"
- "@logger"
tags:
- { name: chill.role }
- { name: security.voter }

View File

@@ -0,0 +1,41 @@
services:
_defaults:
autowire: true
autoconfigure: true
# indicators
Chill\EventBundle\Export\Export\CountEvents:
tags:
- { name: chill.export, alias: 'count_events' }
Chill\EventBundle\Export\Export\CountEventParticipations:
tags:
- { name: chill.export, alias: 'count_event_participants' }
# filters
Chill\EventBundle\Export\Filter\EventDateFilter:
tags:
- { name: chill.export_filter, alias: 'event_date_filter' }
Chill\EventBundle\Export\Filter\EventTypeFilter:
tags:
- { name: chill.export_filter, alias: 'event_type_filter' }
Chill\EventBundle\Export\Filter\RoleFilter:
tags:
- { name: chill.export_filter, alias: 'role_filter' }
# aggregators
Chill\EventBundle\Export\Aggregator\EventTypeAggregator:
tags:
- { name: chill.export_aggregator, alias: event_type_aggregator }
Chill\EventBundle\Export\Aggregator\EventDateAggregator:
tags:
- { name: chill.export_aggregator, alias: event_date_aggregator }
Chill\EventBundle\Export\Aggregator\RoleAggregator:
tags:
- { name: chill.export_aggregator, alias: role_aggregator }

View File

@@ -0,0 +1,14 @@
services:
Chill\EventBundle\Security\EventVoter:
autowire: true
autoconfigure: true
tags:
- { name: security.voter }
- { name: chill.role }
Chill\EventBundle\Security\ParticipationVoter:
autowire: true
autoconfigure: true
tags:
- { name: security.voter }
- { name: chill.role }

View File

@@ -81,9 +81,31 @@ Pick an event: Choisir un événement
Pick a type of event: Choisir un type d'événement
Pick a moderator: Choisir un animateur
# exports
Select a format: Choisir un format
Export: Exporter
Count events: Nombre d'événements
Count events by various parameters.: Compte le nombre d'événements selon divers critères
Exports of events: Exports d'événements
Filtered by event date: Filtrer par date d'événement
'Filtered by date of event: only between %date_from% and %date_to%': "Filtré par date d'événement: uniquement entre le %date_from% et le %date_to%"
Events after this date: Événements après cette date
Events before this date: Événements avant cette date
Filtered by event type: Filtrer par type d'événement
'Filtered by event type: only %list%': "Filtré par type: uniquement %list%"
Group event by date: Grouper par date d'événement
Group by event type: Grouper par type d'événement
Count event participants: Nombre de participations
Count participants to an event by various parameters.: Compte le nombre de participations selon divers critères
Exports of event participants: Exports de participations
'Filtered by participant roles: only %list%': "Filtré par rôles de participation: uniquement %list%"
Filter by participant roles: Filtrer par rôles de participation
Part roles: Rôles de participation
Group by participant role: Grouper par rôle de participation
Events configuration: Configuration des événements
Events configuration menu: Menu des événements

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Command;
use Chill\MainBundle\Service\Import\AddressReferenceLU;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class LoadAddressesLUFromBDAddressCommand extends Command
{
protected static $defaultDescription = 'Import LUX addresses from BD addresses (see https://data.public.lu/fr/datasets/adresses-georeferencees-bd-adresses/)';
public function __construct(
private readonly AddressReferenceLU $addressImporter,
) {
parent::__construct();
}
protected function configure()
{
$this->setName('chill:main:address-ref-lux');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->addressImporter->import();
return 0;
}
}

View File

@@ -43,7 +43,7 @@ class ShortMessageCompilerPass implements CompilerPassInterface
$defaultTransporter = new Reference(NullShortMessageSender::class);
} elseif ('ovh' === $dsn['scheme']) {
if (!class_exists('\\'.\Ovh\Api::class)) {
throw new RuntimeException('Class \\Ovh\\Api not found');
throw new RuntimeException('Class \Ovh\Api not found');
}
foreach (['user', 'host', 'pass'] as $component) {

View File

@@ -274,13 +274,13 @@ class User implements UserInterface, \Stringable
return $this->mainLocation;
}
public function getMainScope(?\DateTimeImmutable $at = null): ?Scope
public function getMainScope(?\DateTimeImmutable $atDate = null): ?Scope
{
$at ??= new \DateTimeImmutable('now');
$atDate ??= new \DateTimeImmutable('now');
foreach ($this->scopeHistories as $scopeHistory) {
if ($at >= $scopeHistory->getStartDate() && (
null === $scopeHistory->getEndDate() || $at < $scopeHistory->getEndDate()
if ($atDate >= $scopeHistory->getStartDate() && (
null === $scopeHistory->getEndDate() || $atDate < $scopeHistory->getEndDate()
)) {
return $scopeHistory->getScope();
}
@@ -326,13 +326,13 @@ class User implements UserInterface, \Stringable
return $this->salt;
}
public function getUserJob(?\DateTimeImmutable $at = null): ?UserJob
public function getUserJob(?\DateTimeImmutable $atDate = null): ?UserJob
{
$at ??= new \DateTimeImmutable('now');
$atDate ??= new \DateTimeImmutable('now');
foreach ($this->jobHistories as $jobHistory) {
if ($at >= $jobHistory->getStartDate() && (
null === $jobHistory->getEndDate() || $at < $jobHistory->getEndDate()
if ($atDate >= $jobHistory->getStartDate() && (
null === $jobHistory->getEndDate() || $atDate < $jobHistory->getEndDate()
)) {
return $jobHistory->getJob();
}
@@ -346,6 +346,11 @@ class User implements UserInterface, \Stringable
return $this->jobHistories;
}
public function getUserScopeHistories(): Collection
{
return $this->scopeHistories;
}
/**
* @return ArrayCollection|UserJobHistory[]
*/

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Export;
/**
* Transform data from filter.
*
* This interface defines a method for transforming filter's form data before it is processed.
*
* You can implement this interface on @see{FilterInterface} or @see{AggregatorInterface}, to allow to transform existing data in saved exports
* and replace it with some default values, or new default values.
*/
interface DataTransformerInterface
{
public function transformData(?array $before): array;
}

View File

@@ -190,7 +190,7 @@ class ExportManager
// throw an error if the export require other modifier, which is
// not allowed when the export return a `NativeQuery`
if (\count($export->supportsModifiers()) > 0) {
throw new \LogicException("The export with alias `{$exportAlias}` return ".'a `\\Doctrine\\ORM\\NativeQuery` and supports modifiers, which is not allowed. Either the method `supportsModifiers` should return an empty array, or return a `Doctrine\\ORM\\QueryBuilder`');
throw new \LogicException("The export with alias `{$exportAlias}` return ".'a `\Doctrine\ORM\NativeQuery` and supports modifiers, which is not allowed. Either the method `supportsModifiers` should return an empty array, or return a `Doctrine\ORM\QueryBuilder`');
}
} elseif ($query instanceof QueryBuilder) {
// handle filters
@@ -203,7 +203,7 @@ class ExportManager
'dql' => $query->getDQL(),
]);
} else {
throw new \UnexpectedValueException('The method `intiateQuery` should return a `\\Doctrine\\ORM\\NativeQuery` or a `Doctrine\\ORM\\QueryBuilder` object.');
throw new \UnexpectedValueException('The method `intiateQuery` should return a `\Doctrine\ORM\NativeQuery` or a `Doctrine\ORM\QueryBuilder` object.');
}
$result = $export->getResult($query, $data[ExportType::EXPORT_KEY]);

View File

@@ -32,6 +32,9 @@ interface FilterInterface extends ModifierInterface
/**
* Get the default data, that can be use as "data" for the form.
*
* In case of adding new parameters to a filter, you can implement a @see{DataTransformerFilterInterface} to
* transforme the filters's data saved in an export to the desired state.
*/
public function getFormDefaultData(): array;

View File

@@ -11,7 +11,9 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form\Type\Export;
use Chill\MainBundle\Export\DataTransformerInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormBuilderInterface;
@@ -19,11 +21,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class AggregatorType extends AbstractType
{
public function __construct()
{
}
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$exportManager = $options['export_manager'];
$aggregator = $exportManager->getAggregator($options['aggregator_alias']);
@@ -34,17 +32,24 @@ class AggregatorType extends AbstractType
'required' => false,
]);
$filterFormBuilder = $builder->create('form', FormType::class, [
$aggregatorFormBuilder = $builder->create('form', FormType::class, [
'compound' => true,
'required' => false,
'error_bubbling' => false,
]);
$aggregator->buildForm($filterFormBuilder);
$aggregator->buildForm($aggregatorFormBuilder);
$builder->add($filterFormBuilder);
if ($aggregator instanceof DataTransformerInterface) {
$aggregatorFormBuilder->addViewTransformer(new CallbackTransformer(
fn (?array $data) => $data,
fn (?array $data) => $aggregator->transformData($data),
));
}
$builder->add($aggregatorFormBuilder);
}
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setRequired('aggregator_alias')
->setRequired('export_manager')

View File

@@ -11,8 +11,10 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form\Type\Export;
use Chill\MainBundle\Export\DataTransformerInterface;
use Chill\MainBundle\Export\FilterInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormBuilderInterface;
@@ -43,6 +45,13 @@ class FilterType extends AbstractType
]);
$filter->buildForm($filterFormBuilder);
if ($filter instanceof DataTransformerInterface) {
$filterFormBuilder->addViewTransformer(new CallbackTransformer(
fn (?array $data) => $data,
fn (?array $data) => $filter->transformData($data),
));
}
$builder->add($filterFormBuilder);
}

View File

@@ -43,7 +43,14 @@ var download_report = (url, container) => {
content = URL.createObjectURL(blob);
}
extension = mime.getExtension(type);
const extensions = new Map();
extensions.set('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'xlsx');
extensions.set('application/vnd.oasis.opendocument.spreadsheet', 'ods');
extensions.set('application/vnd.ms-excel', 'xlsx');
extensions.set('text/csv', 'csv');
extensions.set('text/csv; charset=utf-8', 'csv');
extension = extensions.get(type);
link.appendChild(document.createTextNode(download_text));
link.classList.add("btn", "btn-action");
@@ -55,7 +62,7 @@ var download_report = (url, container) => {
container.innerHTML = "";
container.appendChild(link);
}).catch(function(error) {
console.log(error);
console.error(error);
var problem_text =
document.createTextNode("Problem during download");

View File

@@ -139,8 +139,8 @@ const postprocess = (html: string): string => {
}
const convertMarkdownToHtml = (markdown: string): string => {
marked.use({'hooks': {postprocess, preprocess}});
const rawHtml = marked(markdown);
marked.use({'hooks': {postprocess, preprocess}, 'async': false});
const rawHtml = marked(markdown) as string;
return rawHtml;
};

View File

@@ -40,10 +40,10 @@
{{ 'by_user'|trans ~ ' ' }}
{% endif %}
<span class="user">
{{ user|chill_entity_render_box(options['user']) }}
{{ user|chill_entity_render_box({'at_date': comment.date}) }}
</span>
{% endif %}
</div>
{% endif %}
</blockquote>
{{ closing_box|raw }}
{{ closing_box|raw }}

View File

@@ -1,10 +1,10 @@
<span class="chill-entity entity-user">
{{- user.label }}
{%- if opts['user_job'] and user.userJob(opts['at']) is not null %}
<span class="user-job">({{ user.userJob(opts['at']).label|localize_translatable_string }})</span>
{%- if opts['user_job'] and user.userJob(opts['at_date']) is not null %}
<span class="user-job">({{ user.userJob(opts['at_date']).label|localize_translatable_string }})</span>
{%- endif -%}
{%- if opts['main_scope'] and user.mainScope(opts['at']) is not null %}
<span class="main-scope">({{ user.mainScope(opts['at']).name|localize_translatable_string }})</span>
{%- if opts['main_scope'] and user.mainScope(opts['at_date']) is not null %}
<span class="main-scope">({{ user.mainScope(opts['at_date']).name|localize_translatable_string }})</span>
{%- endif -%}
{%- if opts['absence'] and user.isAbsent %}
<span class="badge bg-danger rounded-pill" title="{{ 'absence.Absent'|trans|escape('html_attr') }}">{{ 'absence.A'|trans }}</span>

View File

@@ -0,0 +1,20 @@
{% if menus|length > 0 %}
<li class="dropdown">
<a class="dropdown-toggle btn btn-sm btn-outline-primary"
href="#"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false">
<i class="fa fa-flash"></i>
</a>
<div class="dropdown-menu">
{% for menu in menus %}
<a class="dropdown-item"
href="{{ menu.uri }}"
><i class="fa fa-{{- menu.extras.icon }} fa-fw"></i>
{{ menu.label|trans }}
</a>
{% endfor %}
</div>
</li>
{% endif %}

View File

@@ -21,7 +21,7 @@
</span>
{% if not c.notification.isSystem %}
<span class="badge-user">
{{ c.notification.sender|chill_entity_render_string }}
{{ c.notification.sender|chill_entity_render_string({'at_date': c.notification.date}) }}
</span>
{% else %}
<span class="badge-user system">{{ 'notification.is_system'|trans }}</span>
@@ -53,7 +53,7 @@
{% endif %}
{% for a in c.notification.addressees %}
<span class="badge-user">
{{ a|chill_entity_render_string }}
{{ a|chill_entity_render_string({'at_date': c.notification.date}) }}
</span>
{% endfor %}
{% for a in c.notification.addressesEmails %}

View File

@@ -1,7 +1,7 @@
{% extends "@ChillMain/layout.html.twig" %}
{% block title 'notification.show notification from %sender%'|trans(
{ '%sender%': notification.sender|chill_entity_render_string }
{ '%sender%': notification.sender|chill_entity_render_string({'at_date': notification.date}) }
) ~ ' ' ~ notification.title %}
{% block js %}

View File

@@ -31,14 +31,14 @@
<div class="row">
<div class="col-sm-12">
{{ 'By'|trans }}
{{ step.previous.transitionBy|chill_entity_render_box }},
{{ step.previous.transitionBy|chill_entity_render_box({'at_date': step.previous.transitionAt }) }},
{{ step.previous.transitionAt|format_datetime('short', 'short') }}
</div>
</div>
{% else %}
<div class="row">
<div class="col-sm-4">{{ 'workflow.Created by'|trans }}</div>
<div class="col-sm-8">{{ step.entityWorkflow.createdBy|chill_entity_render_box }}</div>
<div class="col-sm-8">{{ step.entityWorkflow.createdBy|chill_entity_render_box({'at_date': step.entityWorkflow.createdAt}) }}</div>
</div>
<div class="row">
<div class="col-sm-4">{{ 'Le'|trans }}</div>
@@ -110,8 +110,8 @@
{% if entity_workflow.currentStep.destUserByAccessKey|length > 0 %}
<p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }}&nbsp;:</b></p>
<ul>
{% for u in entity_workflow.currentStep.destUserByAccessKey %}
<li>{{ u|chill_entity_render_box }}</li>
{% for u in entity_workflow.currentStepChained.destUserByAccessKey %}
<li>{{ u|chill_entity_render_box({'at_date': entity_workflow.currentStepChained.previous.transitionAt }) }}</li>
{% endfor %}
</ul>
{% endif %}

View File

@@ -42,7 +42,7 @@
<div class="item-col" style="width: inherit;">
{% if step.transitionBy is not null %}
<div>
{{ step.transitionBy|chill_entity_render_box }}
{{ step.transitionBy|chill_entity_render_box({'at_date': step.transitionAt}) }}
</div>
{% endif %}
<div>
@@ -76,7 +76,7 @@
<p><b>{{ 'workflow.Users allowed to apply transition'|trans }}&nbsp;: </b></p>
<ul>
{% for u in step.destUser %}
<li>{{ u|chill_entity_render_box }}</li>
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li>
{% endfor %}
</ul>
{% endif %}
@@ -85,7 +85,7 @@
<p><b>{{ 'workflow.Users put in Cc'|trans }}&nbsp;: </b></p>
<ul>
{% for u in step.ccUser %}
<li>{{ u|chill_entity_render_box }}</li>
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li>
{% endfor %}
</ul>
{% endif %}
@@ -103,7 +103,7 @@
<p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }}&nbsp;:</b></p>
<ul>
{% for u in step.destUserByAccessKey %}
<li>{{ u|chill_entity_render_box }}</li>
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li>
{% endfor %}
</ul>
{% endif %}

View File

@@ -3,7 +3,7 @@
{% if step.previous is not null %}
<li>
<span class="item-key">{{ 'By'|trans ~ ' : ' }}</span>
<b>{{ step.previous.transitionBy|chill_entity_render_box }}</b>
<b>{{ step.previous.transitionBy|chill_entity_render_box({'at_date': step.previous.transitionAt }) }}</b>
</li>
<li>
<span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span>
@@ -12,19 +12,19 @@
<li>
<span class="item-key">{{ 'workflow.For'|trans ~ ' : ' }}</span>
<b>
{% for d in step.destUser %}{{ d|chill_entity_render_string }}{% if not loop.last %}, {% endif %}{% endfor %}
{% for d in step.destUser %}{{ d|chill_entity_render_string({'at_date': step.previous.transitionAt}) }}{% if not loop.last %}, {% endif %}{% endfor %}
</b>
</li>
<li>
<span class="item-key">{{ 'workflow.Cc'|trans ~ ' : ' }}</span>
<b>
{% for u in step.ccUser %}{{ u|chill_entity_render_string }}{% if not loop.last %}, {% endif %}{% endfor %}
{% for u in step.ccUser %}{{ u|chill_entity_render_string({'at_date': step.previous.transitionAt }) }}{% if not loop.last %}, {% endif %}{% endfor %}
</b>
</li>
{% else %}
<li>
<span class="item-key">{{ 'workflow.Created by'|trans ~ ' : ' }}</span>
<b>{{ step.entityWorkflow.createdBy|chill_entity_render_box }}</b>
<b>{{ step.entityWorkflow.createdBy|chill_entity_render_box({'at_date': step.entityWorkflow.createdAt }) }}</b>
</li>
<li>
<span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span>

View File

@@ -11,8 +11,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Routing;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Twig\Environment;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
@@ -20,10 +18,8 @@ use Twig\TwigFunction;
/**
* Add the filter 'chill_menu'.
*/
class MenuTwig extends AbstractExtension implements ContainerAwareInterface
class MenuTwig extends AbstractExtension
{
private ?ContainerInterface $container = null;
/**
* the default parameters for chillMenu.
*
@@ -86,9 +82,4 @@ class MenuTwig extends AbstractExtension implements ContainerAwareInterface
{
return 'chill_menu';
}
public function setContainer(?ContainerInterface $container = null)
{
$this->container = $container;
}
}

View File

@@ -14,9 +14,9 @@ namespace Chill\MainBundle\Search\Utils;
class ExtractDateFromPattern
{
private const DATE_PATTERN = [
['([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))', 'Y-m-d'], // 1981-05-12
['((0[1-9]|[12]\\d|3[01])\\/(0[1-9]|1[0-2])\\/([12]\\d{3}))', 'd/m/Y'], // 15/12/1980
['((0[1-9]|[12]\\d|3[01])-(0[1-9]|1[0-2])-([12]\\d{3}))', 'd-m-Y'], // 15/12/1980
['([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))', 'Y-m-d'], // 1981-05-12
['((0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([12]\d{3}))', 'd/m/Y'], // 15/12/1980
['((0[1-9]|[12]\d|3[01])-(0[1-9]|1[0-2])-([12]\d{3}))', 'd-m-Y'], // 15/12/1980
];
public function extractDates(string $subject): SearchExtractionResult

View File

@@ -16,7 +16,7 @@ use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
class ExtractPhonenumberFromPattern
{
private const PATTERN = '([\\+]{0,1}[0-9\\ ]{5,})';
private const PATTERN = '([\+]{0,1}[0-9\ ]{5,})';
private readonly string $defaultCarrierCode;

View File

@@ -54,7 +54,7 @@ class EntityWorkflowStepNormalizer implements NormalizerAwareInterface, Normaliz
$data['transitionPreviousBy'] = $this->normalizer->normalize(
$previous->getTransitionBy(),
$format,
$context
[...$context, UserNormalizer::AT_DATE => $previous->getTransitionAt()]
);
$data['transitionPreviousAt'] = $this->normalizer->normalize(
$previous->getTransitionAt(),

View File

@@ -47,7 +47,7 @@ class NotificationNormalizer implements NormalizerAwareInterface, NormalizerInte
'message' => $object->getMessage(),
'relatedEntityClass' => $object->getRelatedEntityClass(),
'relatedEntityId' => $object->getRelatedEntityId(),
'sender' => $this->normalizer->normalize($object->getSender(), $format, $context),
'sender' => $this->normalizer->normalize($object->getSender(), $format, [...$context, UserNormalizer::AT_DATE => $object->getDate()]),
'title' => $object->getTitle(),
'entity' => null !== $entity ? $this->normalizer->normalize($entity, $format, $context) : null,
];

View File

@@ -19,6 +19,7 @@ use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Templating\Entity\UserRender;
use libphonenumber\PhoneNumber;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
@@ -27,6 +28,8 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
{
use NormalizerAwareTrait;
final public const AT_DATE = 'chill:user:at_date';
final public const NULL_USER = [
'type' => 'user',
'id' => '',
@@ -38,12 +41,18 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
'isAbsent' => false,
];
public function __construct(private readonly UserRender $userRender)
public function __construct(private readonly UserRender $userRender, private readonly ClockInterface $clock)
{
}
/**
* @param mixed|null $format
*
* @throws \Symfony\Component\Serializer\Exception\ExceptionInterface
*/
public function normalize($object, $format = null, array $context = [])
{
/** @var array{"chill:user:at_date"?: \DateTimeImmutable|\DateTime} $context */
/** @var User $object */
$userJobContext = array_merge(
$context,
@@ -74,18 +83,23 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
return [...self::NULL_USER, 'phonenumber' => $this->normalizer->normalize(null, $format, $phonenumberContext), 'civility' => $this->normalizer->normalize(null, $format, $civilityContext), 'user_job' => $this->normalizer->normalize(null, $format, $userJobContext), 'main_center' => $this->normalizer->normalize(null, $format, $centerContext), 'main_scope' => $this->normalizer->normalize(null, $format, $scopeContext), 'current_location' => $this->normalizer->normalize(null, $format, $locationContext), 'main_location' => $this->normalizer->normalize(null, $format, $locationContext)];
}
$at = $context[self::AT_DATE] ?? $this->clock->now();
if ($at instanceof \DateTime) {
$at = \DateTimeImmutable::createFromMutable($at);
}
$data = [
'type' => 'user',
'id' => $object->getId(),
'username' => $object->getUsername(),
'text' => $this->userRender->renderString($object, []),
'text' => $this->userRender->renderString($object, ['at_date' => $at]),
'text_without_absent' => $this->userRender->renderString($object, ['absence' => false]),
'label' => $object->getLabel(),
'email' => (string) $object->getEmail(),
'phonenumber' => $this->normalizer->normalize($object->getPhonenumber(), $format, $phonenumberContext),
'user_job' => $this->normalizer->normalize($object->getUserJob(), $format, $userJobContext),
'user_job' => $this->normalizer->normalize($object->getUserJob($at), $format, $userJobContext),
'main_center' => $this->normalizer->normalize($object->getMainCenter(), $format, $centerContext),
'main_scope' => $this->normalizer->normalize($object->getMainScope(), $format, $scopeContext),
'main_scope' => $this->normalizer->normalize($object->getMainScope($at), $format, $scopeContext),
'isAbsent' => $object->isAbsent(),
];

View File

@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Service\Import;
use League\Csv\Reader;
use League\Csv\Statement;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class AddressReferenceLU
{
private const RELEASE = 'https://data.public.lu/fr/datasets/r/5cadc5b8-6a7d-4283-87bc-f9e58dd771f7';
public function __construct(private readonly HttpClientInterface $client, private readonly AddressReferenceBaseImporter $addressBaseImporter, private readonly PostalCodeBaseImporter $postalCodeBaseImporter, private readonly AddressToReferenceMatcher $addressToReferenceMatcher)
{
}
public function import(): void
{
$downloadUrl = self::RELEASE;
$response = $this->client->request('GET', $downloadUrl);
if (200 !== $response->getStatusCode()) {
throw new \Exception('Could not download CSV: '.$response->getStatusCode());
}
$file = tmpfile();
foreach ($this->client->stream($response) as $chunk) {
fwrite($file, $chunk->getContent());
}
fseek($file, 0);
$csv = Reader::createFromStream($file);
$csv->setDelimiter(';');
$csv->setHeaderOffset(0);
$this->process_postal_code($csv);
$this->process_address($csv);
$this->addressToReferenceMatcher->checkAddressesMatchingReferences();
fclose($file);
}
private function process_address(Reader $csv): void
{
$stmt = Statement::create()->process($csv);
foreach ($stmt as $record) {
$this->addressBaseImporter->importAddress(
$record['id_geoportail'],
$record['code_postal'],
$record['code_postal'],
$record['rue'],
$record['numero'],
'bd-addresses.lux',
(float) $record['lat_wgs84'],
(float) $record['lon_wgs84'],
4326
);
}
$this->addressBaseImporter->finalize();
}
private function process_postal_code(Reader $csv): void
{
$stmt = Statement::create()->process($csv);
$arr_postal_codes = [];
foreach ($stmt as $record) {
if (false === \array_key_exists($record['code_postal'], $arr_postal_codes)) {
$this->postalCodeBaseImporter->importCode(
'LU',
trim((string) $record['localite']),
trim((string) $record['code_postal']),
trim((string) $record['code_postal']),
'bd-addresses.lux',
(float) $record['lat_wgs84'],
(float) $record['lon_wgs84'],
4326
);
$arr_postal_codes[$record['code_postal']] = 1;
}
}
}
}

View File

@@ -12,8 +12,14 @@ declare(strict_types=1);
namespace Chill\MainBundle\Templating\Entity;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use DateTime;
use DateTimeImmutable;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
/**
* @implements ChillEntityRenderInterface<User>
@@ -24,17 +30,32 @@ class UserRender implements ChillEntityRenderInterface
'main_scope' => true,
'user_job' => true,
'absence' => true,
'at' => null,
'at_date' => null, // instanceof DateTimeInterface
];
public function __construct(private readonly TranslatableStringHelper $translatableStringHelper, private readonly \Twig\Environment $engine, private readonly TranslatorInterface $translator)
{
public function __construct(
private readonly TranslatableStringHelperInterface $translatableStringHelper,
private readonly \Twig\Environment $engine,
private readonly TranslatorInterface $translator,
private readonly ClockInterface $clock,
) {
}
/**
* @throws LoaderError
* @throws RuntimeError
* @throws SyntaxError
*/
public function renderBox($entity, array $options): string
{
$opts = \array_merge(self::DEFAULT_OPTIONS, $options);
if (null === $opts['at_date']) {
$opts['at_date'] = $this->clock->now();
} elseif ($opts['at_date'] instanceof \DateTime) {
$opts['at_date'] = \DateTimeImmutable::createFromMutable($opts['at_date']);
}
return $this->engine->render('@ChillMain/Entity/user.html.twig', [
'user' => $entity,
'opts' => $opts,
@@ -45,16 +66,24 @@ class UserRender implements ChillEntityRenderInterface
{
$opts = \array_merge(self::DEFAULT_OPTIONS, $options);
$str = $entity->getLabel();
// $immutableAtDate = $opts['at_date'] instanceOf DateTime ? DateTimeImmutable::createFromMutable($opts['at_date']) : $opts['at_date'];
if (null !== $entity->getUserJob($opts['at']) && $opts['user_job']) {
$str .= ' ('.$this->translatableStringHelper
->localize($entity->getUserJob($opts['at'])->getLabel()).')';
if (null === $opts['at_date']) {
$opts['at_date'] = $this->clock->now();
} elseif ($opts['at_date'] instanceof \DateTime) {
$opts['at_date'] = \DateTimeImmutable::createFromMutable($opts['at_date']);
}
if (null !== $entity->getMainScope($opts['at']) && $opts['main_scope']) {
$str = $entity->getLabel();
if (null !== $entity->getUserJob($opts['at_date']) && $opts['user_job']) {
$str .= ' ('.$this->translatableStringHelper
->localize($entity->getMainScope($opts['at'])->getName()).')';
->localize($entity->getUserJob($opts['at_date'])->getLabel()).')';
}
if (null !== $entity->getMainScope($opts['at_date']) && $opts['main_scope']) {
$str .= ' ('.$this->translatableStringHelper
->localize($entity->getMainScope($opts['at_date'])->getName()).')';
}
if ($entity->isAbsent() && $opts['absence']) {

View File

@@ -338,15 +338,11 @@ abstract class AbstractAggregatorTest extends KernelTestCase
.'is a string or an be converted to a string', $key)
);
$this->assertTrue(
// conditions
\is_string((string) \call_user_func($closure, '_header'))
&& !empty(\call_user_func($closure, '_header'))
&& '_header' !== \call_user_func($closure, '_header'),
// message
sprintf('Test that the callable return by `getLabels` for key %s '
.'can provide an header', $key)
);
$head = \call_user_func($closure, '_header');
self::assertIsString($head);
self::assertNotEquals('', $head);
self::assertNotEquals('_header', $head);
}
}

View File

@@ -25,6 +25,7 @@ use libphonenumber\PhoneNumberUtil;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Clock\MockClock;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
@@ -122,7 +123,9 @@ final class UserNormalizerTest extends TestCase
$userRender = $this->prophesize(UserRender::class);
$userRender->renderString(Argument::type(User::class), Argument::type('array'))->willReturn($user ? $user->getLabel() : '');
$normalizer = new UserNormalizer($userRender->reveal());
$clock = new MockClock(new \DateTimeImmutable('now'));
$normalizer = new UserNormalizer($userRender->reveal(), $clock);
$normalizer->setNormalizer(new class () implements NormalizerInterface {
public function normalize($object, ?string $format = null, array $context = [])
{

View File

@@ -0,0 +1,109 @@
<?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 Templating\Entity;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Templating\Entity\UserRender;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Clock\MockClock;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment;
/**
* @internal
*
* @coversNothing
*/
class UserRenderTest extends TestCase
{
use ProphecyTrait;
public function testRenderUserWithJobAndScopeAtCertainDate(): void
{
// Create a user with a certain user job
$user = new User();
$userJobA = new UserJob();
$scopeA = new Scope();
$userJobA->setLabel(['fr' => 'assistant social'])
->setActive(true);
$scopeA->setName(['fr' => 'service A']);
$user->setLabel('BOB ISLA');
$userJobB = new UserJob();
$scopeB = new Scope();
$userJobB->setLabel(['fr' => 'directrice'])
->setActive(true);
$scopeB->setName(['fr' => 'service B']);
$userJobHistoryA = (new User\UserJobHistory())
->setUser($user)
->setJob($userJobA)
->setStartDate(new \DateTimeImmutable('2023-11-01 12:00:00'))
->setEndDate(new \DateTimeImmutable('2023-11-30 00:00:00'));
$userScopeHistoryA = (new User\UserScopeHistory())
->setUser($user)
->setScope($scopeA)
->setStartDate(new \DateTimeImmutable('2023-11-01 12:00:00'))
->setEndDate(new \DateTimeImmutable('2023-11-30 00:00:00'));
$userJobHistoryB = (new User\UserJobHistory())
->setUser($user)
->setJob($userJobB)
->setStartDate(new \DateTimeImmutable('2023-12-01 12:00:00'));
$userScopeHistoryB = (new User\UserScopeHistory())
->setUser($user)
->setScope($scopeB)
->setStartDate(new \DateTimeImmutable('2023-12-01 12:00:00'));
$user->getUserJobHistories()->add($userJobHistoryA);
$user->getUserScopeHistories()->add($userScopeHistoryA);
$user->getUserJobHistories()->add($userJobHistoryB);
$user->getUserScopeHistories()->add($userScopeHistoryB);
// Create renderer
$translatableStringHelperMock = $this->prophesize(TranslatableStringHelperInterface::class);
$translatableStringHelperMock->localize(Argument::type('array'))->will(fn ($args) => $args[0]['fr']);
$engineMock = $this->createMock(Environment::class);
$translatorMock = $this->createMock(TranslatorInterface::class);
$clock = new MockClock(new \DateTimeImmutable('2023-12-15 12:00:00'));
$renderer = new UserRender($translatableStringHelperMock->reveal(), $engineMock, $translatorMock, $clock);
$optionsNoDate['at_date'] = null;
$options['at_date'] = new \DateTime('2023-11-25 12:00:00');
$optionsTwo['at_date'] = new \DateTime('2024-01-30 12:00:00');
// Check that the user render for the first activity corresponds with the first user job
$expectedStringA = 'BOB ISLA (assistant social) (service A)';
$this->assertEquals($expectedStringA, $renderer->renderString($user, $options));
// Check that the user render for the second activity corresponds with the second user job
$expectedStringB = 'BOB ISLA (directrice) (service B)';
$this->assertEquals($expectedStringB, $renderer->renderString($user, $optionsTwo));
// Check that the user renders the job and scope that is active now, when no date is given
$expectedStringC = 'BOB ISLA (directrice) (service B)';
$this->assertEquals($expectedStringC, $renderer->renderString($user, $optionsNoDate));
}
}

View File

@@ -59,6 +59,12 @@ services:
tags:
- { name: console.command }
Chill\MainBundle\Command\LoadAddressesLUFromBDAddressCommand:
autoconfigure: true
autowire: true
tags:
- { name: console.command }
Chill\MainBundle\Command\ExecuteCronJobCommand:
autoconfigure: true
autowire: true

View File

@@ -20,9 +20,5 @@ services:
chill.main.twig.chill_menu:
class: Chill\MainBundle\Routing\MenuTwig
arguments:
- "@chill.main.menu_composer"
calls:
- [setContainer, ["@service_container"]]
tags:
- { name: twig.extension }

View File

@@ -869,19 +869,24 @@ class AccompanyingPeriod implements
public function getNextCalendarsForPerson(Person $person, $limit = 5): ReadableCollection
{
$today = new \DateTimeImmutable('today');
$criteria = Criteria::create()
->where(Criteria::expr()->gte('startDate', $today))
// ->andWhere(Criteria::expr()->memberOf('persons', $person))
->orderBy(['startDate' => 'DESC'])
->setMaxResults($limit * 2);
return $this->calendars->matching($criteria)
->matching(
// due to a bug, filter two times
Criteria::create()
->where(Criteria::expr()->memberOf('persons', $person))
->setMaxResults($limit)
);
$criteria = Criteria::create();
$expr = Criteria::expr();
$criteria
->where(
$expr->gte('startDate', $today),
)
->orderBy(['startDate' => 'ASC']);
$criteriaByPerson = Criteria::create();
$criteriaByPerson
->where(
$expr->memberOf('persons', $person)
)
->setMaxResults($limit);
return $this->calendars->matching($criteria)->matching($criteriaByPerson);
}
/**
@@ -1507,6 +1512,16 @@ class AccompanyingPeriod implements
return $this;
}
public function getUserHistories(): ReadableCollection
{
return $this->userHistories;
}
public function getCurrentUserHistory(): ?UserHistory
{
return $this->getUserHistories()->findFirst(fn (int $key, UserHistory $userHistory) => null === $userHistory->getEndDate());
}
private function addStepHistory(AccompanyingPeriodStepHistory $stepHistory, array $context = []): self
{
if (!$this->stepHistories->contains($stepHistory)) {

View File

@@ -60,13 +60,14 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
* orphanRemoval=true
* )
*
* @Serializer\Groups({"read", "docgen:read"})
* @Serializer\Groups({"read"})
*
* @ORM\OrderBy({"startDate": "DESC", "id": "DESC"})
*
* @var Collection<AccompanyingPeriodWorkEvaluation>
*
* @internal /!\ the serialization for write evaluations is handled in `AccompanyingPeriodWorkDenormalizer`
* @internal the serialization for write evaluations is handled in `accompanyingperiodworkdenormalizer`
* @internal the serialization for context docgen:read is handled in `accompanyingperiodworknormalizer`
*/
private Collection $accompanyingPeriodWorkEvaluations;
@@ -395,21 +396,21 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
/**
* @return ReadableCollection<int, User>
*
* @Serializer\Groups({"read", "docgen:read", "read:accompanyingPeriodWork:light"})
* @Serializer\Groups({"accompanying_period_work:edit"})
* @Serializer\Groups({"accompanying_period_work:create"})
*/
public function getReferrers(): ReadableCollection
{
$users = $this->referrersHistory
->filter(fn (AccompanyingPeriodWorkReferrerHistory $h) => null === $h->getEndDate())
->map(fn (AccompanyingPeriodWorkReferrerHistory $h) => $h->getUser())
->getValues()
;
->getValues();
return new ArrayCollection(array_values($users));
}
/**
* @return Collection<int, AccompanyingPeriodWorkReferrerHistory>
*/
public function getReferrersHistory(): Collection
{
return $this->referrersHistory;
@@ -577,9 +578,9 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
return $this;
}
public function setCreatedBy(?User $createdBy): self
public function setCreatedBy(?User $user): self
{
$this->createdBy = $createdBy;
$this->createdBy = $user;
return $this;
}
@@ -621,14 +622,14 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
public function setStartDate(\DateTimeInterface $startDate): self
{
$this->startDate = $startDate;
$this->startDate = $startDate instanceof \DateTime ? \DateTimeImmutable::createFromMutable($startDate) : $startDate;
return $this;
}
public function setUpdatedAt(\DateTimeInterface $datetime): TrackUpdateInterface
{
$this->updatedAt = $datetime;
$this->updatedAt = $datetime instanceof \DateTime ? \DateTimeImmutable::createFromMutable($datetime) : $datetime;
return $this;
}

View File

@@ -167,6 +167,11 @@ class Comment implements TrackCreationInterface, TrackUpdateInterface
return $this;
}
public function getCreatedBy(): ?User
{
return $this->getCreator();
}
public function setUpdatedAt(\DateTimeInterface $updatedAt): self
{
$this->updatedAt = $updatedAt;

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Export\DataTransformerInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Repository\UserRepository;
use Chill\MainBundle\Service\RollingDate\RollingDate;
@@ -21,14 +22,17 @@ use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
final readonly class ReferrerAggregator implements AggregatorInterface
final readonly class ReferrerAggregator implements AggregatorInterface, DataTransformerInterface
{
private const A = 'acp_ref_agg_uhistory';
private const P = 'acp_ref_agg_date';
public function __construct(private UserRepository $userRepository, private UserRender $userRender, private RollingDateConverterInterface $rollingDateConverter)
{
public function __construct(
private UserRepository $userRepository,
private UserRender $userRender,
private RollingDateConverterInterface $rollingDateConverter
) {
}
public function addRole(): ?string
@@ -46,18 +50,16 @@ final readonly class ReferrerAggregator implements AggregatorInterface
$qb->expr()->orX(
$qb->expr()->isNull(self::A),
$qb->expr()->andX(
$qb->expr()->lte(self::A.'.startDate', ':'.self::P),
$qb->expr()->lt(self::A.'.startDate', ':'.self::P.'_end_date'),
$qb->expr()->orX(
$qb->expr()->isNull(self::A.'.endDate'),
$qb->expr()->gt(self::A.'.endDate', ':'.self::P)
$qb->expr()->gte(self::A.'.endDate', ':'.self::P.'_start_date')
)
)
)
)
->setParameter(
self::P,
$this->rollingDateConverter->convert($data['date_calc'])
);
->setParameter(':'.self::P.'_end_date', $this->rollingDateConverter->convert($data['end_date']))
->setParameter(':'.self::P.'_start_date', $this->rollingDateConverter->convert($data['end_date']));
}
public function applyOn(): string
@@ -68,15 +70,37 @@ final readonly class ReferrerAggregator implements AggregatorInterface
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('date_calc', PickRollingDateType::class, [
'label' => 'export.aggregator.course.by_referrer.Computation date for referrer',
->add('start_date', PickRollingDateType::class, [
'label' => 'export.aggregator.course.by_referrer.Referrer after',
'required' => true,
])
->add('end_date', PickRollingDateType::class, [
'label' => 'export.aggregator.course.by_referrer.Until',
'required' => true,
]);
}
public function getFormDefaultData(): array
{
return ['date_calc' => new RollingDate(RollingDate::T_TODAY)];
return [
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'end_date' => new RollingDate(RollingDate::T_TODAY),
];
}
public function transformData(?array $before): array
{
$default = $this->getFormDefaultData();
$data = [];
if (null === $before) {
return $default;
}
$data['start_date'] = $before['date_calc'] ?? $before['start_date'] ?? $default['start_date'];
$data['end_date'] = $before['date_calc'] ?? $before['end_date'] ?? $default['end_date'];
return $data;
}
public function getLabels($key, array $values, $data)

View File

@@ -13,20 +13,25 @@ namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators;
use Chill\MainBundle\Entity\User\UserScopeHistory;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Export\DataTransformerInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
readonly class ReferrerScopeAggregator implements AggregatorInterface
readonly class ReferrerScopeAggregator implements AggregatorInterface, DataTransformerInterface
{
private const PREFIX = 'acp_agg_referrer_scope';
public function __construct(
private ScopeRepositoryInterface $scopeRepository,
private TranslatableStringHelperInterface $translatableStringHelper,
private RollingDateConverterInterface $rollingDateConverter,
) {
}
@@ -47,11 +52,16 @@ readonly class ReferrerScopeAggregator implements AggregatorInterface
$qb->expr()->andX(
$qb->expr()->eq("{$p}_userHistory.accompanyingPeriod", 'acp.id'),
$qb->expr()->andX(
// check that the user is referrer when the accompanying period is opened
$qb->expr()->gte('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.startDate"),
$qb->expr()->orX(
$qb->expr()->isNull("{$p}_userHistory.endDate"),
$qb->expr()->lt('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.endDate")
)
),
$qb->expr()->andX(
"{$p}_userHistory.startDate <= :{$p}_endDate",
"COALESCE({$p}_userHistory.endDate, CURRENT_TIMESTAMP()) > :{$p}_startDate"
)
)
)
@@ -67,9 +77,15 @@ readonly class ReferrerScopeAggregator implements AggregatorInterface
$qb->expr()->isNull("{$p}_scopeHistory.endDate"),
$qb->expr()->gt("{$p}_scopeHistory.endDate", "{$p}_userHistory.startDate")
)
),
$qb->expr()->andX(
"{$p}_scopeHistory.startDate <= :{$p}_endDate",
"COALESCE({$p}_scopeHistory.endDate, CURRENT_TIMESTAMP()) > :{$p}_startDate"
)
)
)
->setParameter("{$p}_startDate", $this->rollingDateConverter->convert($data['start_date']))
->setParameter("{$p}_endDate", $this->rollingDateConverter->convert($data['end_date']))
->addSelect("IDENTITY({$p}_scopeHistory.scope) AS {$p}_select")
->addGroupBy("{$p}_select");
}
@@ -81,11 +97,34 @@ readonly class ReferrerScopeAggregator implements AggregatorInterface
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('start_date', PickRollingDateType::class, [
'label' => 'export.aggregator.course.by_referrer_scope.Referrer and scope after',
'required' => true,
])
->add('end_date', PickRollingDateType::class, [
'label' => 'export.aggregator.course.by_referrer_scope.Until',
'required' => true,
]);
}
public function getFormDefaultData(): array
{
return [];
return [
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'end_date' => new RollingDate(RollingDate::T_TODAY),
];
}
public function transformData(?array $before): array
{
$default = $this->getFormDefaultData();
$data = [];
$data['start_date'] = $before['start_date'] ?? new RollingDate(RollingDate::T_FIXED_DATE, new \DateTimeImmutable('1970-01-01'));
$data['end_date'] = $before['end_date'] ?? $default['end_date'];
return $data;
}
public function getLabels($key, array $values, $data)

View File

@@ -13,20 +13,25 @@ namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators;
use Chill\MainBundle\Entity\User\UserJobHistory;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Export\DataTransformerInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Repository\UserJobRepository;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
final readonly class UserJobAggregator implements AggregatorInterface
final readonly class UserJobAggregator implements AggregatorInterface, DataTransformerInterface
{
private const PREFIX = 'acp_agg_user_job';
public function __construct(
private UserJobRepository $jobRepository,
private TranslatableStringHelper $translatableStringHelper
private TranslatableStringHelper $translatableStringHelper,
private RollingDateConverterInterface $rollingDateConverter,
) {
}
@@ -52,6 +57,10 @@ final readonly class UserJobAggregator implements AggregatorInterface
$qb->expr()->isNull("{$p}_userHistory.endDate"),
$qb->expr()->lt('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.endDate")
)
),
$qb->expr()->andX(
"{$p}_userHistory.startDate <= :{$p}_endDate",
"COALESCE({$p}_userHistory.endDate, CURRENT_TIMESTAMP()) > :{$p}_startDate"
)
)
)
@@ -67,9 +76,15 @@ final readonly class UserJobAggregator implements AggregatorInterface
$qb->expr()->isNull("{$p}_jobHistory.endDate"),
$qb->expr()->gt("{$p}_jobHistory.endDate", "{$p}_userHistory.startDate")
)
),
$qb->expr()->andX(
"{$p}_jobHistory.startDate <= :{$p}_endDate",
"COALESCE({$p}_jobHistory.endDate, CURRENT_TIMESTAMP()) > :{$p}_startDate"
)
)
)
->setParameter("{$p}_startDate", $this->rollingDateConverter->convert($data['start_date']))
->setParameter("{$p}_endDate", $this->rollingDateConverter->convert($data['end_date']))
->addSelect("IDENTITY({$p}_jobHistory.job) AS {$p}_select")
->addGroupBy("{$p}_select");
}
@@ -81,11 +96,34 @@ final readonly class UserJobAggregator implements AggregatorInterface
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('start_date', PickRollingDateType::class, [
'label' => 'export.aggregator.course.by_referrer_job.Referrer and job after',
'required' => true,
])
->add('end_date', PickRollingDateType::class, [
'label' => 'export.aggregator.course.by_referrer_job.Until',
'required' => true,
]);
}
public function getFormDefaultData(): array
{
return [];
return [
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'end_date' => new RollingDate(RollingDate::T_TODAY),
];
}
public function transformData(?array $before): array
{
$default = $this->getFormDefaultData();
$data = [];
$data['start_date'] = $before['start_date'] ?? new RollingDate(RollingDate::T_FIXED_DATE, new \DateTimeImmutable('1970-01-01'));
$data['end_date'] = $before['end_date'] ?? $default['end_date'];
return $data;
}
public function getLabels($key, array $values, $data)

View File

@@ -13,23 +13,28 @@ namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Entity\User\UserJobHistory;
use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Export\DataTransformerInterface;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Entity\AccompanyingPeriod\UserHistory;
use Chill\PersonBundle\Export\Declarations;
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;
class UserJobFilter implements FilterInterface
final readonly class UserJobFilter implements FilterInterface, DataTransformerInterface
{
private const PREFIX = 'acp_filter_user_job';
public function __construct(
private readonly TranslatableStringHelper $translatableStringHelper,
private readonly UserJobRepositoryInterface $userJobRepository,
private TranslatableStringHelper $translatableStringHelper,
private UserJobRepositoryInterface $userJobRepository,
private RollingDateConverterInterface $rollingDateConverter,
) {
}
@@ -42,42 +47,31 @@ class UserJobFilter implements FilterInterface
{
$p = self::PREFIX;
$qb
->leftJoin(
'acp.userHistories',
"{$p}_userHistory",
Join::WITH,
$qb->expr()->andX(
$qb->expr()->eq("{$p}_userHistory.accompanyingPeriod", 'acp.id'),
$qb->expr()->andX(
$qb->expr()->gte('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.startDate"),
$qb->expr()->orX(
$qb->expr()->isNull("{$p}_userHistory.endDate"),
$qb->expr()->lt('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.endDate")
)
)
)
)
->leftJoin(
UserJobHistory::class,
"{$p}_jobHistory",
Join::WITH,
$qb->expr()->andX(
$qb->expr()->eq("{$p}_jobHistory.user", "{$p}_userHistory.user"),
$qb->expr()->andX(
$qb->expr()->lte("{$p}_jobHistory.startDate", "{$p}_userHistory.startDate"),
$qb->expr()->orX(
$qb->expr()->isNull("{$p}_jobHistory".'.endDate'),
$qb->expr()->gt("{$p}_jobHistory.endDate", "{$p}_userHistory.startDate")
)
)
)
)
->andWhere($qb->expr()->in("{$p}_jobHistory.job", ":{$p}_job"))
->setParameter(
"{$p}_job",
$data['jobs'],
$qb->andWhere(
$qb->expr()->exists(
sprintf(
<<<DQL
SELECT 1
FROM %s {$p}_userHistory
JOIN %s {$p}_userJobHistory
WITH
{$p}_userHistory.user = {$p}_userJobHistory.user
AND OVERLAPSI({$p}_userHistory.startDate, {$p}_userHistory.endDate),({$p}_userJobHistory.startDate, {$p}_userJobHistory.endDate) = TRUE
WHERE {$p}_userHistory.accompanyingPeriod = acp
AND {$p}_userHistory.startDate <= :{$p}_endDate
AND ({$p}_userHistory.endDate IS NULL OR {$p}_userHistory.endDate > :{$p}_startDate)
AND {$p}_userJobHistory.startDate <= :{$p}_endDate
AND ({$p}_userJobHistory.endDate IS NULL OR {$p}_userJobHistory.endDate > :{$p}_startDate)
AND {$p}_userJobHistory.job IN (:{$p}_jobs)
DQL,
UserHistory::class,
UserJobHistory::class,
),
)
)
->setParameter("{$p}_jobs", $data['jobs'])
->setParameter("{$p}_startDate", $this->rollingDateConverter->convert($data['start_date']))
->setParameter("{$p}_endDate", $this->rollingDateConverter->convert($data['end_date']))
;
}
@@ -96,20 +90,29 @@ class UserJobFilter implements FilterInterface
'expanded' => true,
'choice_label' => fn (UserJob $job) => $this->translatableStringHelper->localize($job->getLabel()),
'label' => 'Job',
]);
])
->add('start_date', PickRollingDateType::class, [
'label' => 'export.filter.course.by_user_job.Start from',
])
->add('end_date', PickRollingDateType::class, [
'label' => 'export.filter.course.by_user_job.Until',
])
;
}
public function describeAction($data, $format = 'string')
{
return [
'export.filter.course.by_user_job.Filtered by user job: only %job%', [
'%job%' => implode(
'exports.filter.course.by_user_job.Filtered by user job: only job', [
'job' => implode(
', ',
array_map(
fn (UserJob $job) => $this->translatableStringHelper->localize($job->getLabel()),
$data['jobs'] instanceof Collection ? $data['jobs']->toArray() : $data['jobs']
)
),
'startDate' => $this->rollingDateConverter->convert($data['start_date']),
'endDate' => $this->rollingDateConverter->convert($data['end_date']),
],
];
}
@@ -118,9 +121,30 @@ class UserJobFilter implements FilterInterface
{
return [
'jobs' => [],
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'end_date' => new RollingDate(RollingDate::T_TODAY),
];
}
public function transformData(?array $before): array
{
$default = $this->getFormDefaultData();
if (null === $before) {
return $default;
}
if (!array_key_exists('start_date', $before) || null === $before['start_date']) {
$before['start_date'] = $default['start_date'];
}
if (!array_key_exists('end_date', $before) || null === $before['end_date']) {
$before['end_date'] = $default['end_date'];
}
return $before;
}
public function getTitle(): string
{
return 'export.filter.course.by_user_job.Filter by user job';

View File

@@ -13,23 +13,28 @@ namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User\UserScopeHistory;
use Chill\MainBundle\Export\DataTransformerInterface;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Entity\AccompanyingPeriod\UserHistory;
use Chill\PersonBundle\Export\Declarations;
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;
class UserScopeFilter implements FilterInterface
final readonly class UserScopeFilter implements FilterInterface, DataTransformerInterface
{
private const PREFIX = 'acp_filter_main_scope';
public function __construct(
private readonly ScopeRepositoryInterface $scopeRepository,
private readonly TranslatableStringHelper $translatableStringHelper,
private ScopeRepositoryInterface $scopeRepository,
private TranslatableStringHelper $translatableStringHelper,
private RollingDateConverterInterface $rollingDateConverter,
) {
}
@@ -38,47 +43,35 @@ class UserScopeFilter implements FilterInterface
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
public function alterQuery(QueryBuilder $qb, $data): void
{
$p = self::PREFIX;
$qb
->join(
'acp.userHistories',
"{$p}_userHistory",
Join::WITH,
$qb->expr()->andX(
$qb->expr()->eq("{$p}_userHistory.accompanyingPeriod", 'acp.id'),
$qb->expr()->andX(
$qb->expr()->gte('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.startDate"),
$qb->expr()->orX(
$qb->expr()->isNull("{$p}_userHistory.endDate"),
$qb->expr()->lt('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.endDate")
)
)
)
$qb->andWhere(
$qb->expr()->exists(
sprintf(
<<<DQL
SELECT 1
FROM %s {$p}_userHistory
JOIN %s {$p}_userScopeHistory
WITH
{$p}_userHistory.user = {$p}_userScopeHistory.user
AND OVERLAPSI({$p}_userHistory.startDate, {$p}_userHistory.endDate),({$p}_userScopeHistory.startDate, {$p}_userScopeHistory.endDate) = TRUE
WHERE {$p}_userHistory.accompanyingPeriod = acp
AND {$p}_userHistory.startDate <= :{$p}_endDate
AND ({$p}_userHistory.endDate IS NULL OR {$p}_userHistory.endDate > :{$p}_startDate)
AND {$p}_userScopeHistory.startDate <= :{$p}_endDate
AND ({$p}_userScopeHistory.endDate IS NULL OR {$p}_userScopeHistory.endDate > :{$p}_startDate)
AND {$p}_userScopeHistory.scope IN (:{$p}_scopes)
DQL,
UserHistory::class,
UserScopeHistory::class,
),
)
->join(
UserScopeHistory::class,
"{$p}_scopeHistory",
Join::WITH,
$qb->expr()->andX(
$qb->expr()->eq("{$p}_scopeHistory.user", "{$p}_userHistory.user"),
$qb->expr()->andX(
$qb->expr()->lte("{$p}_scopeHistory.startDate", "{$p}_userHistory.startDate"),
$qb->expr()->orX(
$qb->expr()->isNull("{$p}_scopeHistory.endDate"),
$qb->expr()->gt("{$p}_scopeHistory.endDate", "{$p}_userHistory.startDate")
)
)
)
)
->andWhere($qb->expr()->in("{$p}_scopeHistory.scope", ":{$p}_scopes"))
->setParameter(
"{$p}_scopes",
$data['scopes'],
)
;
)
->setParameter("{$p}_scopes", $data['scopes'])
->setParameter("{$p}_startDate", $this->rollingDateConverter->convert($data['start_date']))
->setParameter("{$p}_endDate", $this->rollingDateConverter->convert($data['end_date']));
}
public function applyOn(): string
@@ -95,20 +88,28 @@ class UserScopeFilter implements FilterInterface
'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()),
'multiple' => true,
'expanded' => true,
])
->add('start_date', PickRollingDateType::class, [
'label' => 'export.filter.course.by_user_scope.Start from',
])
->add('end_date', PickRollingDateType::class, [
'label' => 'export.filter.course.by_user_scope.Until',
]);
}
public function describeAction($data, $format = 'string')
{
return [
'export.filter.course.by_user_scope.Filtered by user main scope: only %scope%', [
'%scope%' => implode(
'exports.filter.course.by_user_scope.Filtered by user main scope: only scopes', [
'scopes' => implode(
', ',
array_map(
fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()),
$data['scopes'] instanceof Collection ? $data['scopes']->toArray() : $data['scopes']
)
),
'startDate' => $this->rollingDateConverter->convert($data['start_date']),
'endDate' => $this->rollingDateConverter->convert($data['end_date']),
],
];
}
@@ -117,9 +118,30 @@ class UserScopeFilter implements FilterInterface
{
return [
'scopes' => [],
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'end_date' => new RollingDate(RollingDate::T_TODAY),
];
}
public function transformData(?array $before): array
{
$default = $this->getFormDefaultData();
if (null === $before) {
return $default;
}
if (!array_key_exists('start_date', $before) || null === $before['start_date']) {
$before['start_date'] = $default['start_date'];
}
if (!array_key_exists('end_date', $before) || null === $before['end_date']) {
$before['end_date'] = $default['end_date'];
}
return $before;
}
public function getTitle(): string
{
return 'export.filter.course.by_user_scope.Filter by user scope';

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Menu;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
final readonly class PersonQuickMenuBuilder implements LocalMenuBuilderInterface
{
public function __construct(private AuthorizationCheckerInterface $authorizationChecker)
{
}
public static function getMenuIds(): array
{
return ['person_quick_menu'];
}
public function buildMenu($menuId, MenuItem $menu, array $parameters)
{
/** @var \Chill\PersonBundle\Entity\Person $person */
$person = $parameters['person'];
if ($this->authorizationChecker->isGranted(AccompanyingPeriodVoter::CREATE, $person)) {
$menu->addChild(
'Create Accompanying Course',
[
'route' => 'chill_person_accompanying_course_new',
'routeParameters' => [
'person_id' => [$person->getId()],
],
]
)
->setExtras([
'order' => 10,
'icon' => 'plus',
]);
}
}
}

View File

@@ -30,7 +30,7 @@ div.list-with-period {
// override wrap-list
div.wrap-list.periods-list {
padding-right: 1rem;
padding-right: 0;
div.wl-row {
flex-wrap: nowrap;
div.wl-col {

View File

@@ -1,6 +1,6 @@
<template>
<a class="btn" :class="getClassButton" :title="$t(buttonTitle)" @click="openModal">
<span v-if="displayTextButton">{{ $t(buttonTitle) }}</span>
<a class="btn" :class="getClassButton" :title="$t(buttonTitle || '')" @click="openModal">
<span v-if="displayTextButton">{{ $t(buttonTitle || '') }}</span>
</a>
<teleport to="body">

Some files were not shown because too many files have changed in this diff Show More