Compare commits

...

194 Commits

Author SHA1 Message Date
8a2272f93b release v3.4.1 2024-11-22 09:50:27 +01:00
9ef884349a Add workflow title to notification content
Updated the French template for workflow transition notifications to include the workflow title. This ensures users have more context in the notification email.
2024-11-21 21:26:08 +01:00
5acf9432d6 Add workflow title to notification content
Updated the French template for workflow transition notifications to include the workflow title. This ensures users have more context in the notification email.
2024-11-21 21:21:09 +01:00
4fdb722dc6 Update chill bundles to version v3.4.0 2024-11-20 14:50:41 +01:00
e113e3dce5 Merge branch '314-improve-admin' into 'master'
Resolve "[admin] Improve admin for document categories"

Closes #314 and #313

See merge request Chill-Projet/chill-bundles!753
2024-11-20 13:45:41 +00:00
6536662aba add changie for admin improvements 2024-11-20 14:07:07 +01:00
4127ce1d97 Fix conflicts after rebase 2024-11-20 12:45:55 +01:00
b327f65ef8 Fix the logic of adding a permission group to avoid duplicates being made 2024-11-20 12:43:50 +01:00
0f1604817b Allow the selection of multiple centers to create multiple groupcenters at once 2024-11-20 12:16:29 +01:00
63fc4f1089 Add translations for gender form in admin 2024-11-20 12:16:29 +01:00
ba3fe6af8c Fix pipeline phpstan, rector, php-cs-fixer 2024-11-20 12:16:29 +01:00
bc4c2c1471 Improve layout of event admin section 2024-11-20 12:16:29 +01:00
3f381c207d Add a notnull constraint on property 'type' when creating an event status 2024-11-20 12:16:29 +01:00
df30ca2c4f Improve user experience for creation of document categories with select field for document class 2024-11-20 12:16:29 +01:00
2573c32160 Update chill-bundles to v3.3.0 2024-11-20 12:10:45 +01:00
38886cd0b6 Rector correction in NotificationOnTransition 2024-11-20 10:23:33 +01:00
875d3293d2 Workflow: set comment on previous step 2024-11-20 09:47:11 +01:00
16d5f121db Merge branch 'master' of gitlab.com:Chill-Projet/chill-bundles 2024-11-19 16:29:55 +01:00
10999a2077 Add assets to task lists to render dynamic user picker 2024-11-19 16:29:44 +01:00
128f8b8852 Fix normalization of Date: use the same timezone as the original date 2024-11-19 15:40:53 +01:00
6ca4b91e1e Refactor breadcrumb macro parameters in Workflow templates
Updated the breadcrumb macro's parameters to accept `entity_workflow` directly instead of a context object. This change affects the index, list, and macro_breadcrumb templates, ensuring consistent parameter usage and improving readability.

This avoid exploiting a bug which was solved in twig 3.15.0
2024-11-19 12:58:20 +01:00
3a1947df9e Add entity workflow title to notification
Introduced `EntityWorkflowManager` to `NotificationOnTransition` to fetch and include the entity workflow title in notifications. Updated relevant tests to support the new functionality and verify the entity title retrieval process.
2024-11-19 12:01:31 +01:00
9012e68b70 Merge branch '1304-save-comments-on-workflow-step' into 'master'
Save the comments on a workflow step

See merge request Chill-Projet/chill-bundles!760
2024-11-19 10:15:15 +00:00
04b2def8a5 Save the comments on a workflow step 2024-11-19 10:15:15 +00:00
39b918e7eb Merge branch '326-fix-document-page-acc-course' into 'master'
Add unique constraints to prevent person_document and accompanying_course document with the same object_id

Closes #326

See merge request Chill-Projet/chill-bundles!759
2024-11-19 09:52:26 +00:00
4be6c09d4d Add unique constraints to prevent person_documnt and accompanyingcourse_document with duplicated object_id
This change ensures `object_id` uniqueness within `person_document` and `accompanyingcourse_document` tables. It includes migration scripts and entity annotations to enforce these constraints. This helps maintain data integrity and consistency across the database.
2024-11-19 10:50:46 +01:00
9a44cf060f Add workflow title to notification emails for workflow transition, to user groups
Incorporated the workflow title into notification emails to provide more context to users. Updated the NotificationToUserGroupsOnTransition class and its tests to include the title from EntityWorkflowManager. Adjusted the French email templates to display the workflow title correctly.
2024-11-18 15:05:03 +01:00
723ca8db6a Remove date validation on deathDate
This change removes the `Assert\Date` validation on the `deathDate` property in the `Person` entity. The adjustment allows for more flexible input by not strictly enforcing the date format, which can resolve issues where the date string validation was previously causing errors.
2024-11-18 15:05:02 +01:00
f04ef3c3e3 Merge branch '324-convert-to-pdf-on-signature-only' into 'master'
Lors de la procédure de signature, le document ne doit être converti qu'à partir du moment où la première signature est apposée

Closes #324

See merge request Chill-Projet/chill-bundles!758
2024-11-15 13:33:09 +00:00
b5f1f3153f Convert non-PDF documents before applying a signature
Enhanced the `addConvertedVersion` method in `StoredObjectToPdfConverter` to optionally include converted content in the response. Updated `SignatureRequestController` to handle non-PDF documents by converting them to PDF before processing the signature request.
2024-11-15 14:27:37 +01:00
03fe9a6d86 Update Signature App.vue to use a converted pdf
Introduced a new helper ts function to download documents as PDFs and integrated with the existing workflow. Enhanced `PDFSignatureZoneAvailable` to implement `LocaleAwareInterface` for better locale management and added PDF conversion handling for non-PDF documents. Updated App.vue to use the new PDF download method.
2024-11-15 14:27:36 +01:00
a312b45777 Remove ConvertToPdfBeforeSignatureStepEventSubscriber
This commit deletes the ConvertToPdfBeforeSignatureStepEventSubscriber.php file from the workflow directory. .
2024-11-15 14:27:35 +01:00
5d5150faa7 Add 'btn-sm' class to download button
The 'btn-sm' class was added to the download button in HistoryButtonListItem.vue to adjust its size. Additionally, a bug was fixed in DownloadButton.vue to correctly reference the 'type' property from 'atVersion'.
2024-11-15 14:27:34 +01:00
9b661c3b8f Let ConvertController return a pdf file directly without trying to converting it
Those changes improve performance when the file is already in pdf format.
2024-11-15 14:27:34 +01:00
f90fae4e14 Merge branch 'signature-app-master' into 'master'
Fixes for signature

See merge request Chill-Projet/chill-bundles!755
2024-11-14 11:23:37 +00:00
b7e27536bd Merge remote-tracking branch 'origin/master' into signature-app-master 2024-11-14 12:18:07 +01:00
887f3e0aa2 Merge branch '323-related-entity-permission-give-from-workflow' into 'master'
Permettre aux utilisateurs concerné par un workflow de modifier un document, même si, en dehors du workflow, ils ne possèdent pas de droits d'édition et de lecture sur ce document

Closes #323

See merge request Chill-Projet/chill-bundles!756
2024-11-14 11:12:37 +00:00
829fb669fe Update Workflow Permission Handling
Refactor the `WorkflowRelatedEntityPermissionHelper` to enhance permission checks for workflow-related entities. This includes updating methods, improving test coverage, and incorporating `MockClock` for date-sensitive operations.
2024-11-14 12:07:21 +01:00
a8660ecdb2 Merge branch 'adjust_list_household_export' into 'master'
Adjust list household export

See merge request Chill-Projet/chill-bundles!750
2024-11-14 08:53:44 +00:00
903a87c589 Merge branch '323-related-entity-permission-give-from-workflow' into signature-app-master 2024-11-13 22:44:29 +01:00
aad10cc61f Add workflow permission check to StoredObjectVoter
This commit introduces logic to grant permissions based on workflow conditions in the `AbstractStoredObjectVoter`. It also includes a new test case to ensure the workflow-based permission check functions correctly.
2024-11-13 22:41:30 +01:00
c99dda0126 Rename WorkflowStoredObjectPermissionHelper to WorkflowRelatedEntityPermissionHelper and create method isAllowedByWorkflow
Refactored class names and namespaces from WorkflowStoredObjectPermissionHelper to WorkflowRelatedEntityPermissionHelper across various modules. Updated associated tests and voter classes to reflect the changes, ensuring consistency and clarity in the codebase.
2024-11-13 22:41:23 +01:00
94f9ebd726 Add UserRender option to SignatureRequestController
Integrated a new UserRender option 'SPLIT_LINE_BEFORE_CHARACTER' with a value of 30 in the SignatureRequestController. This enhances the rendering format for user details.
2024-11-13 17:54:58 +01:00
5ad11041e0 Add line splitting in user render string functionality
Introduce a new option to split lines in user job and scope render strings based on a specified character limit. Implement a corresponding test to verify the correct behavior of this feature.
2024-11-13 17:54:58 +01:00
d50b169ab8 Add UserRender option to SignatureRequestController
Integrated a new UserRender option 'SPLIT_LINE_BEFORE_CHARACTER' with a value of 30 in the SignatureRequestController. This enhances the rendering format for user details.
2024-11-13 17:54:28 +01:00
ba2d8663f1 Add line splitting in user render string functionality
Introduce a new option to split lines in user job and scope render strings based on a specified character limit. Implement a corresponding test to verify the correct behavior of this feature.
2024-11-13 17:54:20 +01:00
d6c55c830b Persist EntityWorkflow and its steps with EntityManager
Refactored NotificationToUserGroupsOnTransition to utilize EntityManager for persisting EntityWorkflowStep. This ensures entities have generated IDs, leading to proper email notifications during workflow transitions.
2024-11-13 12:40:53 +01:00
937caa878e remove Date constraint on marital status date 2024-11-13 12:20:10 +01:00
21ec3121ec Merge branch 'signature-app-master' into 'master'
Signature app master

Closes #307

See merge request Chill-Projet/chill-bundles!743
2024-11-12 20:30:00 +00:00
261d47a8a4 Add changies
[ci-skip]
2024-11-12 21:25:23 +01:00
6453237340 Mark class as final and readonly 2024-11-12 21:22:51 +01:00
79621e8ab7 Add guard to block signatures on related entities without objects
Implemented a guard to prevent signatures on related entities that lack a stored object. It also includes corresponding tests and added translation for the error message in French.
2024-11-12 18:14:30 +01:00
bfa58177e0 fix url for granting access to workflow by key 2024-11-12 17:48:44 +01:00
ddf73e1a48 Updated URL in French workflow notification template
The absolute url in the French version of the workflow notification content to user group has been updated. It now points to 'chill_main_workflow_grant_access_by_key' instead of 'chill_main_workflow_show'. This update improves the user experience by taking the user directly to the desired page.
2024-11-12 14:52:39 +01:00
c3cc6c8353 Do not fail displaying notification when workflow has been deleted 2024-11-12 13:57:48 +01:00
3ec0d26001 Fix type error for displaying comment in accompanying period work detail view page 2024-11-12 10:13:20 +01:00
64d91e2afe Allow users to edit a document if they were added to the previous step of a workflow.
OP#826 Workflow - les utilisateurs des étapes précédentes sur un workflow doivent aussi avoir la main sur les étapes en cours du workflow (Vendee/accent-suivi-developpement/1289)

https://champs-libres.openproject.com/work_packages/826
2024-11-08 20:34:45 +01:00
5339d4f5d9 Refactor button to add a manual zone
- do not show the button if a zone is selected (to avoid to create two selected zones on the document);
- change the button while waiting for the user to create a new zone: this make visible the fact that the app is waiting for a user action
2024-11-08 19:35:03 +01:00
0439c29305 Prevent workflow execution for finalized transitions
Added a check in the workflow-show page script to stop the execution if a transition is marked as finalized (`toFinal`). This ensures that transitions leading to final states are not processed further.
2024-11-07 21:52:44 +01:00
8e34f6962a Add conditional cursor style for canvas when a zone is added within the canvas
Modified the canvas element to conditionally apply a CSS class when adding a zone. This change ensures the cursor changes to 'copy' for visual feedback during the add operation.
2024-11-07 21:34:49 +01:00
e5148f603b Add TransitionHasSignerIfSignature validator
Introduced a new validator `TransitionHasSignerIfSignature` to enforce that a signature transition must have a designated signer. Included related tests, updated DTO annotations, and added translations for new validation messages.
2024-11-07 19:55:09 +01:00
e2e24090ab Allow the selection of multiple centers to create multiple groupcenters at once 2024-11-07 18:50:02 +01:00
b6c141a785 Change zoomLevel from const to let.
Replaced `const` with `let` for `zoomLevel` in `App.vue` to allow reassignment. This change ensures proper handling of dynamic zoom levels during document signature operations.
2024-11-06 19:51:56 +01:00
db4d7669f1 Add duplication feature for evaluation documents within generic doc
Introduced a new method to duplicate evaluation documents and forward return path URLs. Updated templates to include duplication buttons and adjusted routing for handling the duplication process.
2024-11-06 19:34:41 +01:00
9526d016c6 Merge branch 'signature-app/zoom-button' into 'signature-app-master'
Signature app/zoom button

See merge request Chill-Projet/chill-bundles!751
2024-11-06 18:34:14 +00:00
b2f6dbbe30 Show the zoom level also if there is only one page in the document 2024-11-06 19:32:12 +01:00
5447ad2961 Fix casting zoom level from string to floatInt
Changed the type of the zoomLevel parameter from number to string and updated the assignment to parse the zoomLevel as a float. This ensures that the zoom functionality properly handles string inputs by converting them to numerical values.
2024-11-06 19:31:01 +01:00
d2b3ee0a2f Update version chill-bundles to v3.2.4 2024-11-06 18:10:55 +01:00
66b87358c8 remove push sass styles from job bundle in webpack config 2024-11-06 18:04:51 +01:00
83f0044eba Remove index.js file from webpack config in jobBundle 2024-11-06 18:01:50 +01:00
ac353ec3bc fix cs 2024-11-06 17:10:53 +01:00
7aca08c89e Allow to remove the canceled workflow to be deleted, if canceled
- OP#812
- https://champs-libres.openproject.com/work_packages/812
2024-11-06 17:10:48 +01:00
4d53c8a295 Add translations for gender form in admin 2024-11-06 14:08:21 +01:00
1ac9d32565 Fix pipeline phpstan, rector, php-cs-fixer 2024-11-05 15:43:22 +01:00
63c2578012 Improve layout of event admin section 2024-11-05 15:34:01 +01:00
884b3684fe Add a notnull constraint on property 'type' when creating an event status 2024-11-05 15:28:52 +01:00
456d29e605 Improve user experience for creation of document categories with select field for document class 2024-11-05 15:28:11 +01:00
8cb2bb1ef4 Fix rector pipeline 2024-11-05 14:52:28 +01:00
cc7e9235b5 Update bundles version to v3.2.3 2024-11-05 14:41:19 +01:00
973ffcbffa Fix text color and background color of footer, was being overwritten 2024-11-05 14:39:09 +01:00
8c3de682d6 Change color of footer text to dark 2024-11-05 14:30:26 +01:00
e71c2f162c Fix display of accompanying period work referrers 2024-11-05 14:24:43 +01:00
43b70fd773 Rename translation key for workflow signature titles
Updated the translation key from 'workflow.signature_required_title' to 'workflow.signatures_title' in the workflow history view. This change ensures consistency and proper usage of the translation key across different parts of the application.
2024-11-04 15:26:07 +01:00
5c0a383909 Add null check to canRun method in CancelStaleWorkflowCronJob
Ensure the canRun method in CancelStaleWorkflowCronJob returns true when the cron job execution is null. This change includes updating the corresponding test case to cover the new behavior.
2024-11-04 14:16:52 +01:00
4dc2348893 Change behaviour to allow to add a new workflow (if available) and open the modal to list existing workflows in the same button
Use the feature of splitted dropdown buttons, from bootstrap.

See:
- OP#776
- https://champs-libres.openproject.com/work_packages/776
2024-11-04 14:05:57 +01:00
32459e6092 Fix gender translation for unknown 2024-10-31 14:14:37 +01:00
1e02fed32b Update chill bundles to v3.2.1 2024-10-31 12:20:07 +01:00
2c3818258a Fix fusion of person doubles and add changies for fixes 2024-10-31 12:13:21 +01:00
64f3b40694 Add the possibility of unknown to the gender entity 2024-10-31 12:10:35 +01:00
nobohan
76458cf375 signature: cancel button at the left + changed style 2024-10-30 10:38:34 +01:00
nobohan
5259ea71a1 signature vue app: improve responsive design 2024-10-30 10:29:08 +01:00
1cadc71d5a Update bundles to v3.2.0 : gender entity added 2024-10-30 10:11:36 +01:00
2b45a51f57 Merge branch 'create_gender_entity' into 'master'
Add gender entity

See merge request Chill-Projet/chill-bundles!740
2024-10-30 09:10:26 +00:00
4c66adee86 Add changie for gender entity creation 2024-10-30 09:57:35 +01:00
6c8fd99cd1 php cs fixes of personDocGenNormalizerTest 2024-10-30 09:31:53 +01:00
e886387f17 Fix PersonDocGenNormalizerTest 2024-10-29 19:05:30 +01:00
c79f030310 Fix person list exports 2024-10-29 18:04:14 +01:00
a648fd09b0 php cs fix 2024-10-29 17:10:00 +01:00
1bd5e6d582 Fix PersonControllerCreateTest 2024-10-29 17:02:32 +01:00
80940a7b19 LoadGenders fixture access string value of enum 2024-10-29 16:51:14 +01:00
7541238c1e Fix pipeline for LoadGenders file 2024-10-29 16:09:04 +01:00
34748dca76 Create a gender fixture 2024-10-29 15:55:25 +01:00
12bb264eb5 Php cs fixes 2024-10-29 15:44:11 +01:00
ac3ac432e1 Fix phpunit pipeline 2024-10-29 15:30:20 +01:00
a00f47c312 changie added 2024-10-29 14:38:47 +01:00
b503f58089 Rector fix 2024-10-29 14:38:29 +01:00
5629a0c124 Adjust query to also add households in the list that do not have an address 2024-10-29 14:32:37 +01:00
nobohan
3bc6595f58 add a zoom button for zooming to the document 2024-10-26 12:09:00 +02:00
989fdad561 Add chill_path_force_return_path method to routing helper
Implemented a new Twig function `chill_path_force_return_path` to enforce a specific return path within the URL. Updated relevant template to utilize this new function for better control over URL routing and return path management.

OP#787
https://champs-libres.openproject.com/work_packages/787
2024-10-25 18:33:22 +02:00
d7174cdb95 Reintroduce pending signatures section in person list view
Moved the pending signatures section within the person list view template to the correct location.

OP#771
https://champs-libres.openproject.com/work_packages/771
2024-10-25 18:20:58 +02:00
182e2fc3af Set 'required' to false for Email field in UserGroupType form.
Changed the Email field's 'required' option to false, allowing it to be optional. This adjustment aims to improve the usability and flexibility of the UserGroupType form.
2024-10-25 11:18:38 +02:00
7df5a22b14 Merge branch 'signature-app/OP768-alter-sending-after-signature' into 'signature-app-master'
When a user applies a signature in a workflow, the signer is the futureDestUser of the next step

See merge request Chill-Projet/chill-bundles!749
2024-10-24 16:04:44 +00:00
f750cfecac Show userGroup in workflow breadcrumb
related:

- https://champs-libres.openproject.com/wp/774
- OP#774
- Vendee/accent-suivi-developpement#1274
2024-10-24 16:15:49 +02:00
bf85e9bb71 When a user applies a signature in a workflow, the signer is the futureDestUser of the next step
See:
- Vendee/accent-suivi-developpement#1252
- https://champs-libres.openproject.com/wp/768
- OP#768
2024-10-24 15:49:59 +02:00
97729de66d Update conditional rendering logic for displaying list of workflows
Ensure the workflow modal is displayed when either workflows_availables or workflows are non-empty. This improves the user interface by correctly triggering the modal in more scenarios.

- OP#762
- https://champs-libres.openproject.com/work_packages/762
- Vendee/accent-suivi-developpement#1254
2024-10-24 15:11:07 +02:00
4e0a421a03 Refactor PDF mounting process to use ArrayBuffer
Changed mountPdf function to accept an ArrayBuffer instead of a URL. This improves handling of documents by simplifying the data flow and eliminates the need for URL object creation.
2024-10-23 22:08:25 +02:00
00408b91a9 Merge branch 'signature-app/OP#767-block-external-send-when-no-document' into 'signature-app-master'
Add workflow guard to block external send when the handler does not provide a public view

See merge request Chill-Projet/chill-bundles!748
2024-10-23 13:45:18 +00:00
d04f9ae9ff Fix LoadPeople for gender 2024-10-22 17:51:41 +02:00
086f391dc9 Cs fix and phpstan fix 2024-10-22 17:25:51 +02:00
06cbfdd0c3 style fixes 2024-10-22 16:17:34 +02:00
f1844ae02b Php cs fixes and phpstan 2024-10-22 15:56:41 +02:00
73b0dd6009 Fix transformation of data in gender filter 2024-10-22 14:54:18 +02:00
4d8bcc5a5a Fix fixture for persons 2024-10-22 14:51:02 +02:00
5dfa5e1e7f Advanced search fixed to work with new gender entity 2024-10-22 14:50:16 +02:00
588f02cdf4 Take Null value for gender into account and fix OnTheFly makeFetch 2024-10-22 14:48:16 +02:00
30b66d5806 Php cs fixes 2024-10-22 09:16:15 +02:00
5786759daa Rector changes 2024-10-22 09:15:44 +02:00
0c1c1cbf8b Fix form type to use in advanced search 2024-10-22 09:15:25 +02:00
9741794f7a Add bootstrap icons to package.json 2024-10-22 07:42:02 +02:00
5ca558bba3 Fix accidental removal of -> in GenderAggregator 2024-10-22 07:31:52 +02:00
9d05f2ac2b set GenderFilterTest back to using accepted_genders key to check if data is transformed correctly 2024-10-22 07:09:00 +02:00
566c40dd84 transform gender data for saved exports 2024-10-22 06:46:39 +02:00
0d2e0b4e91 Customize genderFilter to include a NULL choice + add translation and adjust test 2024-10-21 16:45:45 +02:00
30ebd00693 Customize query to return ordered active gender entities 2024-10-21 16:18:50 +02:00
8b1d73356f Add condition to check if value passed to translatableStringHelper is not null 2024-10-21 16:18:12 +02:00
ddfaa2861e Remove unused method getGenderNumeric that creates phpstan errors 2024-10-21 15:39:27 +02:00
9416a19d85 Sanitize html for good measure 2024-10-21 15:39:05 +02:00
34bbee2031 Use makeFetch method 2024-10-21 15:38:46 +02:00
7f1764658a Add extends template on repository 2024-10-21 15:27:53 +02:00
43c3cc26ea Fix translation with gender 2024-10-21 15:27:37 +02:00
74593a7d28 Use enumType in gender admin form 2024-10-21 15:14:38 +02:00
e629dbf994 Merge branch 'create_gender_entity' of https://gitlab.com/Chill-Projet/chill-bundles into create_gender_entity 2024-10-09 17:03:28 +02:00
8e02db6c85 Fix phpstan 2024-10-09 17:00:33 +02:00
7183d9a3b1 Fix genderfilter to work with array of gender entity values 2024-10-09 16:49:40 +02:00
70335a6360 rector fixes 2024-10-09 16:49:40 +02:00
fa64f44cf1 php cs fixes 2024-10-09 16:49:40 +02:00
f34c94fd65 Use new gender entity in personDocGenNormalizer 2024-10-09 16:49:40 +02:00
73d80af80a Fix getter in gender entity 2024-10-09 16:49:40 +02:00
363cbc8a76 Translate gender terms for admin templates 2024-10-09 16:49:40 +02:00
9f3893243e Adjust gender aggregator 2024-10-09 16:49:40 +02:00
7520d746e8 Correct repository and normalizer 2024-10-09 16:49:40 +02:00
052e09cf64 Fixes phpstan 2024-10-09 16:49:40 +02:00
77ece243c0 Fix twig template, remove ul tag 2024-10-09 16:49:40 +02:00
a47c8d916b Adjust translation logic for gender in vue components 2024-10-09 16:49:40 +02:00
5fce9ee9fb Fix reactivity issue for genderIcon rendering 2024-10-09 16:49:40 +02:00
47d954fe9f Adjust display of gender in twig templates 2024-10-09 16:49:40 +02:00
d9dc2d1f4e Integrate gender entity into vue components upon creation of persons 2024-10-09 16:49:40 +02:00
37cfc035a3 Implement gender icon renderbox for vue components 2024-10-09 16:49:40 +02:00
2eb686ffdb Create gender API and adjust serialization of gender property 2024-10-09 16:49:40 +02:00
34e2a26d1e Fix display of icon field in gender admin form 2024-10-09 16:49:40 +02:00
789c977aba Use EnumType in form instead of ChoiceType for field genderTranslation 2024-10-09 16:49:40 +02:00
0ba93ec7c6 Remove 'unknown' gender enum 2024-10-09 16:49:40 +02:00
b9d2f5efa3 Use renderInterface to render gender icons in twig 2024-10-09 16:49:40 +02:00
567c01f395 wip: use GenderIconEnum to allow user to select bootstrap icon 2024-10-09 16:49:40 +02:00
67a6eb17db Change PickGenderType form field to use in Person creation form 2024-10-09 16:49:40 +02:00
b78f0980f5 Create genderEnum, add genderTranslation property to Gender entity and new gender property to Person entity
Also migrations were created to handle the changes in the database.
2024-10-09 16:49:40 +02:00
f428afc7ca Create gender admin entity and add configuration to use it
entity, migration, controller, repository, templates, form added
2024-10-09 16:49:40 +02:00
18899d665d rector fixes 2024-10-08 16:57:39 +02:00
59f9ac25ba php cs fixes 2024-10-08 16:49:54 +02:00
2da6b746fb Use new gender entity in personDocGenNormalizer 2024-10-08 16:48:47 +02:00
11f75bf6f1 Fix getter in gender entity 2024-10-08 16:39:31 +02:00
a1db1a1d65 Translate gender terms for admin templates 2024-10-08 16:30:25 +02:00
bdfdabe10e Adjust gender aggregator 2024-10-08 16:29:24 +02:00
843a2a4b5b Correct repository and normalizer 2024-10-01 20:02:58 +02:00
6781fdbd9b Fixes phpstan 2024-10-01 15:51:45 +02:00
e6102d339b Fix twig template, remove ul tag 2024-10-01 14:08:35 +02:00
06cb3ddcd1 Adjust translation logic for gender in vue components 2024-10-01 13:35:06 +02:00
05d56c6eeb Fix reactivity issue for genderIcon rendering 2024-10-01 13:09:38 +02:00
23e7f4a120 Adjust display of gender in twig templates 2024-10-01 12:16:16 +02:00
3eeb105913 Integrate gender entity into vue components upon creation of persons 2024-10-01 12:15:56 +02:00
726cdb385f Implement gender icon renderbox for vue components 2024-10-01 12:15:31 +02:00
406eba80d2 Create gender API and adjust serialization of gender property 2024-10-01 11:41:19 +02:00
236e8117d4 Fix display of icon field in gender admin form 2024-10-01 11:39:10 +02:00
e6bfcddae2 Use EnumType in form instead of ChoiceType for field genderTranslation 2024-10-01 11:38:43 +02:00
d61c090cee Remove 'unknown' gender enum 2024-10-01 11:38:20 +02:00
43dd94dad6 Use renderInterface to render gender icons in twig 2024-10-01 11:36:00 +02:00
f7f8319749 Update bundles version to v3.1.1 2024-10-01 10:38:22 +02:00
376ce59917 Fix typing errors in customfieldbundle 2024-10-01 10:35:38 +02:00
06d6227d0e Merge branch 'upgrade-sf5' into 'master'
Upgrade chill to symfony 5

See merge request Chill-Projet/chill-bundles!735
2024-09-26 14:19:40 +00:00
de914f4f17 wip: use GenderIconEnum to allow user to select bootstrap icon 2024-09-26 15:45:44 +02:00
e831cb1656 Change PickGenderType form field to use in Person creation form 2024-09-26 13:26:30 +02:00
94875d83b3 Create genderEnum, add genderTranslation property to Gender entity and new gender property to Person entity
Also migrations were created to handle the changes in the database.
2024-09-26 12:20:36 +02:00
8e30873001 Create gender admin entity and add configuration to use it
entity, migration, controller, repository, templates, form added
2024-09-25 16:02:40 +02:00
be8901a5c4 Fix referrer scope date comparison in aggregator
Correct the date comparison logic to use openingDate instead of closingDate when evaluating user history end dates. This ensures accurate grouping by referrer in the accompanying course aggregators. Added a changelog entry for Issue #309.
2024-09-16 15:52:48 +02:00
171 changed files with 3403 additions and 1313 deletions

View File

@@ -1,8 +0,0 @@
kind: Feature
body: |-
Electronic signature
Implementation of the electronic signature for documents within chill.
time: 2024-06-14T15:32:36.875891692+02:00
custom:
Issue: ""

View File

@@ -1,7 +0,0 @@
kind: Feature
body: The behavoir of the voters for stored objects is adjusted so as to limit edit
and delete possibilities to users related to the activity, social action or workflow
entity.
time: 2024-06-14T15:35:37.582159301+02:00
custom:
Issue: "286"

View File

@@ -1,5 +0,0 @@
kind: Feature
body: Metadata form added for person signatures
time: 2024-07-18T15:12:33.8134266+02:00
custom:
Issue: "288"

View File

@@ -1,6 +0,0 @@
kind: Fixed
body: Show only the current referrer in the page "show" for an accompanying period
workf
time: 2024-09-16T15:18:43.017401122+02:00
custom:
Issue: "308"

6
.changes/v3.1.1.md Normal file
View File

@@ -0,0 +1,6 @@
## v3.1.1 - 2024-10-01
### Fixed
* ([#308](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/308)) Show only the current referrer in the page "show" for an accompanying period workf
* ([#309](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/309)) Correctly compute the grouping by referrer aggregator
* Fixed typing of custom field long choice and custom field group

3
.changes/v3.2.0.md Normal file
View File

@@ -0,0 +1,3 @@
## v3.2.0 - 2024-10-30
### Feature
* Introduce a gender entity

4
.changes/v3.2.1.md Normal file
View File

@@ -0,0 +1,4 @@
## v3.2.1 - 2024-10-31
### Fixed
* Add the possibility of unknown to the gender entity
* Fix the fusion of person doubles by excluding accompanyingPeriod work entities to be deleted. They are moved instead.

3
.changes/v3.2.2.md Normal file
View File

@@ -0,0 +1,3 @@
## v3.2.2 - 2024-10-31
### Fixed
* Fix gender translation for unknown

4
.changes/v3.2.3.md Normal file
View File

@@ -0,0 +1,4 @@
## v3.2.3 - 2024-11-05
### Fixed
* ([#315](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/315)) Fix display of accompanying period work referrers. Only current referrers should be displayed.
Fix color of Chill footer

3
.changes/v3.2.4.md Normal file
View File

@@ -0,0 +1,3 @@
## v3.2.4 - 2024-11-06
### Fixed
* Fix compilation of chill assets

13
.changes/v3.3.0.md Normal file
View File

@@ -0,0 +1,13 @@
## v3.3.0 - 2024-11-20
### Feature
* Electronic signature
Implementation of the electronic signature for documents within chill.
* ([#286](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/286)) The behavoir of the voters for stored objects is adjusted so as to limit edit and delete possibilities to users related to the activity, social action or workflow entity.
* ([#288](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/288)) Metadata form added for person signatures
* Add a signature step in workflow, which allow to apply an electronic signature on documents
* Keep an history of each version of a stored object.
* Add a "send external" step in workflow, which allow to send stored objects and other elements to remote people, by sending them a public url
### Fixed
* Adjust household list export to include households even if their address is NULL
* Remove validation of date string on deathDate

4
.changes/v3.4.0.md Normal file
View File

@@ -0,0 +1,4 @@
## v3.4.0 - 2024-11-20
### Feature
* ([#314](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/314)) Admin: improve document type admin form with a select field for related class.
Admin: Allow administrator to assign multiple group centers in one go to a user.

3
.changes/v3.4.1.md Normal file
View File

@@ -0,0 +1,3 @@
## v3.4.1 - 2024-11-22
### Fixed
* Set the workflow's title to notification content and subject

View File

@@ -6,22 +6,80 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## v3.4.1 - 2024-11-22
### Fixed
* Set the workflow's title to notification content and subject
## v3.4.0 - 2024-11-20
### Feature
* ([#314](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/314)) Admin: improve document type admin form with a select field for related class.
Admin: Allow administrator to assign multiple group centers in one go to a user.
## v3.3.0 - 2024-11-20
### Feature
* Electronic signature
Implementation of the electronic signature for documents within chill.
* ([#286](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/286)) The behavoir of the voters for stored objects is adjusted so as to limit edit and delete possibilities to users related to the activity, social action or workflow entity.
* ([#288](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/288)) Metadata form added for person signatures
* Add a signature step in workflow, which allow to apply an electronic signature on documents
* Keep an history of each version of a stored object.
* Add a "send external" step in workflow, which allow to send stored objects and other elements to remote people, by sending them a public url
### Fixed
* Adjust household list export to include households even if their address is NULL
* Remove validation of date string on deathDate
## v3.2.4 - 2024-11-06
### Fixed
* Fix compilation of chill assets
## v3.2.3 - 2024-11-05
### Fixed
* ([#315](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/315)) Fix display of accompanying period work referrers. Only current referrers should be displayed.
Fix color of Chill footer
## v3.2.2 - 2024-10-31
### Fixed
* Fix gender translation for unknown
## v3.2.1 - 2024-10-31
### Fixed
* Add the possibility of unknown to the gender entity
* Fix the fusion of person doubles by excluding accompanyingPeriod work entities to be deleted. They are moved instead.
## v3.2.0 - 2024-10-30
### Feature
* Introduce a gender entity
## v3.1.1 - 2024-10-01
### Fixed
* ([#308](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/308)) Show only the current referrer in the page "show" for an accompanying period workf
* ([#309](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/309)) Correctly compute the grouping by referrer aggregator
* Fixed typing of custom field long choice and custom field group
## v3.1.0 - 2024-08-30
### Feature
* Add export aggregator to aggregate activities by household + filter persons that are not part of an accompanyingperiod during a certain timeframe.
* Add export aggregator to aggregate activities by household + filter persons that are not part of an accompanyingperiod during a certain timeframe.
## v3.0.0 - 2024-08-26
### Fixed
* Fix delete action for accompanying periods in draft state
* Fix connection to azure when making an calendar event in chill
* CollectionType js fixes for remove button and adding multiple entries
* Fix delete action for accompanying periods in draft state
* Fix connection to azure when making an calendar event in chill
* CollectionType js fixes for remove button and adding multiple entries
## v2.24.0 - 2024-09-11
### Feature
* ([#306](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/306)) When a document is converted or downloaded in the browser, this document is removed from the browser memory after 45s. Future click on the button re-download the document.
* ([#306](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/306)) When a document is converted or downloaded in the browser, this document is removed from the browser memory after 45s. Future click on the button re-download the document.
## v2.23.0 - 2024-07-19 & 2024-07-23
## v2.23.0 - 2024-07-23 & 2024-07-19
### Feature
* ([#221](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/221)) [DX] move async-upload-bundle features into chill-bundles
* Add job bundle (module emploi)
* Upgrade import of address list to the last version of compiled addresses of belgian-best-address
* Upgrade CKEditor and refactor configuration with use of typescript
* ([#123](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/123)) Add a button to duplicate calendar ranges from a week to another one
* [admin] filter users by active / inactive in the admin user's list
* ([#273](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/273)) Add the possibility to mark all notifications as read
@@ -31,6 +89,8 @@ and is generated by [Changie](https://github.com/miniscruff/changie).
* Do not update the "createdAt" column when importing postal code which does not change
* Display filename on file upload within the UI interface
### Fixed
* Fix resolving of centers for an household, which will fix in turn the access control
* Resolved type hinting error in activity list export
* ([#271](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/271)) Take into account the acp closing date in the acp works date filter
### Traduction française des principaux changements
@@ -43,25 +103,15 @@ and is generated by [Changie](https://github.com/miniscruff/changie).
- Agrandit l'icône du type de fichier dans l'interface de dépôt de fichier;
- correction: tient compte de la date de fermeture du parcours dans les filtres sur les actions d'accompagnement.
* ([#221](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/221)) [DX] move async-upload-bundle features into chill-bundles
* Add job bundle (module emploi)
* Upgrade import of address list to the last version of compiled addresses of belgian-best-address
* Upgrade CKEditor and refactor configuration with use of typescript
### Fixed
* Fix resolving of centers for an household, which will fix in turn the access control
* Resolved type hinting error in activity list export
## v2.22.2 - 2024-07-03
### Fixed
* Remove scope required for event participation stats
* Remove scope required for event participation stats
## v2.22.1 - 2024-07-01
### Fixed
* Remove debug word
* Remove debug word
### DX
* Add a command for reading official address DB from Luxembourg and update chill addresses
* Add a command for reading official address DB from Luxembourg and update chill addresses
## v2.22.0 - 2024-06-25
### Feature
@@ -104,7 +154,7 @@ and is generated by [Changie](https://github.com/miniscruff/changie).
## v2.20.1 - 2024-06-05
### Fixed
* Do not allow StoredObjectCreated for edit and convert buttons
* Do not allow StoredObjectCreated for edit and convert buttons
## v2.20.0 - 2024-06-05
### Fixed
@@ -151,96 +201,96 @@ and is generated by [Changie](https://github.com/miniscruff/changie).
## v2.18.2 - 2024-04-12
### Fixed
* ([#250](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/250)) Postal codes import : fix the source URL and the keys to handle each record
* ([#250](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/250)) Postal codes import : fix the source URL and the keys to handle each record
## v2.18.1 - 2024-03-26
### Fixed
* Fix layout issue in document generation for admin (minor)
* Fix layout issue in document generation for admin (minor)
## v2.18.0 - 2024-03-26
### Feature
* ([#268](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/268)) Improve admin UX to configure document templates for document generation
* ([#268](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/268)) Improve admin UX to configure document templates for document generation
### Fixed
* ([#267](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/267)) Fix the join between job and user in the user list (admin): show only the current user job
* ([#267](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/267)) Fix the join between job and user in the user list (admin): show only the current user job
## v2.17.0 - 2024-03-19
### Feature
* ([#237](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/237)) New export filter for social actions with an evaluation created between two dates
* ([#258](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/258)) In the list of accompangying period, add the list of person's centers and the duration of the course
* ([#238](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/238)) Allow to customize list person with new fields
* ([#237](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/237)) New export filter for social actions with an evaluation created between two dates
* ([#258](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/258)) In the list of accompangying period, add the list of person's centers and the duration of the course
* ([#238](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/238)) Allow to customize list person with new fields
* ([#159](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/159)) Admin can publish news on the homepage
### Fixed
* ([#264](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/264)) Fix languages: load the languages in all availables languages configured for Chill
* ([#259](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/259)) Keep a consistent behaviour between the filtering of activities within the document generation (model "accompanying period with activities"), and the same filter in the list of activities for an accompanying period
* ([#264](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/264)) Fix languages: load the languages in all availables languages configured for Chill
* ([#259](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/259)) Keep a consistent behaviour between the filtering of activities within the document generation (model "accompanying period with activities"), and the same filter in the list of activities for an accompanying period
## v2.16.3 - 2024-02-26
### Fixed
* ([#236](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/236)) Fix translation of user job -> 'service' must be 'métier'
* ([#236](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/236)) Fix translation of user job -> 'service' must be 'métier'
### UX
* ([#232](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/232)) Order user jobs and services alphabetically in export filters
* ([#232](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/232)) Order user jobs and services alphabetically in export filters
## v2.16.2 - 2024-02-21
### Fixed
* Check for null values in closing motive of parcours d'accompagnement for correct rendering of template
* Check for null values in closing motive of parcours d'accompagnement for correct rendering of template
## v2.16.1 - 2024-02-09
### Fixed
* Force bootstrap version to avoid error in builds with newer version
* Force bootstrap version to avoid error in builds with newer version
## v2.16.0 - 2024-02-08
### Feature
* ([#231](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/231)) Create new filter for persons having a participation in an accompanying period during a certain time span
* ([#241](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/241)) [Export][List of accompanyign period] Add two columns: the list of persons participating to the period, and their ids
* ([#244](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/244)) Add capability to generate export about change of steps of accompanying period, and generate exports for this
* ([#253](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/253)) Export: group accompanying period by person participating
* ([#243](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/243)) Export: add filter for courses not linked to a reference address
* ([#229](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/229)) Allow to group activities linked with accompanying period by reason
* ([#115](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/115)) Prevent social work to be saved when another user edited conccurently the social work
* Modernize the event bundle, with some new fields and multiple improvements
* ([#231](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/231)) Create new filter for persons having a participation in an accompanying period during a certain time span
* ([#241](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/241)) [Export][List of accompanyign period] Add two columns: the list of persons participating to the period, and their ids
* ([#244](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/244)) Add capability to generate export about change of steps of accompanying period, and generate exports for this
* ([#253](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/253)) Export: group accompanying period by person participating
* ([#243](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/243)) Export: add filter for courses not linked to a reference address
* ([#229](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/229)) Allow to group activities linked with accompanying period by reason
* ([#115](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/115)) Prevent social work to be saved when another user edited conccurently the social work
* Modernize the event bundle, with some new fields and multiple improvements
### Fixed
* ([#220](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/220)) Fix error in logs about wrong typing of eventArgs in onEditNotificationComment method
* ([#256](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/256)) Fix the conditions upon which social actions should be optional or required in relation to social issues within the activity creation form
* ([#220](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/220)) Fix error in logs about wrong typing of eventArgs in onEditNotificationComment method
* ([#256](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/256)) Fix the conditions upon which social actions should be optional or required in relation to social issues within the activity creation form
### UX
* ([#260](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/260)) Order list of centers alphabetically in dropdown 'user' section admin.
* ([#260](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/260)) Order list of centers alphabetically in dropdown 'user' section admin.
## v2.15.2 - 2024-01-11
### Fixed
* Fix the id_seq used when creating a new accompanying period participation during fusion of two person files
* Fix the id_seq used when creating a new accompanying period participation during fusion of two person files
### DX
* Set placeholder to False for expanded EntityType form fields where required is set to False.
* Set placeholder to False for expanded EntityType form fields where required is set to False.
## v2.15.1 - 2023-12-20
### Fixed
* Fix the household export query to exclude accompanying periods that are in draft state.
* Fix the household export query to exclude accompanying periods that are in draft state.
### DX
* ([#167](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/167)) Fixed readthedocs compilation by updating readthedocs config file and requirements for Sphinx
* ([#167](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/167)) Fixed readthedocs compilation by updating readthedocs config file and requirements for Sphinx
## v2.15.0 - 2023-12-11
### Feature
* ([#191](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/191)) Add export "number of household associate with an exchange"
* ([#235](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/235)) Export: add dates on the filter "filter course by activity type"
* ([#191](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/191)) Add export "number of household associate with an exchange"
* ([#235](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/235)) Export: add dates on the filter "filter course by activity type"
### Fixed
* ([#214](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/214)) Fix error when posting an empty comment on an accompanying period.
* ([#233](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/233)) Fix "filter evaluation by evaluation type" (and add select2 to the list of evaluation types to pick)
* ([#214](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/214)) Fix error when posting an empty comment on an accompanying period.
* ([#233](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/233)) Fix "filter evaluation by evaluation type" (and add select2 to the list of evaluation types to pick)
* ([#234](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/234)) Fix "filter aside activity by date"
* ([#228](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/228)) Fix export of activity for people created before the introduction of the createdAt column on person (during v1)
* ([#246](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/246)) Do not show activities, evaluations and social work when associated to a confidential accompanying period, except for the users which are allowed to see them
* ([#228](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/228)) Fix export of activity for people created before the introduction of the createdAt column on person (during v1)
* ([#246](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/246)) Do not show activities, evaluations and social work when associated to a confidential accompanying period, except for the users which are allowed to see them
## v2.14.1 - 2023-11-29
### Fixed
* Export: fix list person with custom fields
* ([#100](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/100)) Add a paginator to budget elements (resource and charge types) in the admin
* Fix error in ListEvaluation when "handling agents" are alone
* Export: fix list person with custom fields
* ([#100](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/100)) Add a paginator to budget elements (resource and charge types) in the admin
* Fix error in ListEvaluation when "handling agents" are alone
## v2.14.0 - 2023-11-24
### Feature
* ([#161](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/161)) Export: in filter "Filter accompanying period work (social action) by type, goal and result", order the items alphabetically or with the defined order
* ([#161](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/161)) Export: in filter "Filter accompanying period work (social action) by type, goal and result", order the items alphabetically or with the defined order
### Fixed
* ([#141](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/141)) Export: on filter "action by type goals, and results", restore the fields when editing a saved export
* ([#219](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/219)) Export: fix the list of accompanying period work, when the "calc date" is null
* ([#222](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/222)) Fix rendering of custom fields
* Fix various errors in custom fields administration
* ([#141](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/141)) Export: on filter "action by type goals, and results", restore the fields when editing a saved export
* ([#219](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/219)) Export: fix the list of accompanying period work, when the "calc date" is null
* ([#222](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/222)) Fix rendering of custom fields
* Fix various errors in custom fields administration
## v2.13.0 - 2023-11-21
### Feature
@@ -254,7 +304,7 @@ and is generated by [Changie](https://github.com/miniscruff/changie).
## v2.12.1 - 2023-11-16
### Fixed
* ([#208](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/208)) Export: fix loading of form for "filter action by type, goal and result"
* ([#208](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/208)) Export: fix loading of form for "filter action by type, goal and result"
## v2.12.0 - 2023-11-15
### Feature
@@ -285,36 +335,36 @@ and is generated by [Changie](https://github.com/miniscruff/changie).
## v2.11.0 - 2023-11-07
### Feature
* ([#194](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/194)) Export: add a filter "filter activity by creator job"
* ([#194](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/194)) Export: add a filter "filter activity by creator job"
### Fixed
* ([#185](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/185)) Export: fix "group accompanying period by geographical unit": take into account the accompanying periods when the period is not located within an unit
* Fix "group activity by creator job" aggregator
* ([#185](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/185)) Export: fix "group accompanying period by geographical unit": take into account the accompanying periods when the period is not located within an unit
* Fix "group activity by creator job" aggregator
## v2.10.6 - 2023-11-07
### Fixed
* ([#182](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/182)) Fix merging of double person files. Adjustement relationship sql statement
* ([#185](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/185)) Export: fix aggregator by geographical unit on person: avoid inconsistencies
* ([#182](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/182)) Fix merging of double person files. Adjustement relationship sql statement
* ([#185](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/185)) Export: fix aggregator by geographical unit on person: avoid inconsistencies
## v2.10.5 - 2023-11-05
### Fixed
* ([#183](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/183)) Fix "problem during download" on some filters, which used a wrong data type
* ([#184](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/184)) Fix filter "activity by date"
* ([#183](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/183)) Fix "problem during download" on some filters, which used a wrong data type
* ([#184](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/184)) Fix filter "activity by date"
## v2.10.4 - 2023-10-26
### Fixed
* Fix null value constraint errors when merging relationships in doubles
* Fix null value constraint errors when merging relationships in doubles
## v2.10.3 - 2023-10-26
### Fixed
* ([#175](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/175)) Replace old method of getting translator with injection of translatorInterface
* ([#175](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/175)) Replace old method of getting translator with injection of translatorInterface
## v2.10.2 - 2023-10-26
### Fixed
* ([#175](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/175)) Use injection of translator instead of ->get().
* ([#175](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/175)) Use injection of translator instead of ->get().
## v2.10.1 - 2023-10-24
### Fixed
* Fix export controller when generating an export without any data in session
* Fix export controller when generating an export without any data in session
## v2.10.0 - 2023-10-24
### Feature
@@ -339,11 +389,11 @@ and is generated by [Changie](https://github.com/miniscruff/changie).
## v2.9.2 - 2023-10-17
### Fixed
* Fix possible null values in string's entities
* Fix possible null values in string's entities
## v2.9.1 - 2023-10-17
### Fixed
* Fix the handling of activity form when editing or creating an activity in an accompanying period with multiple centers
* Fix the handling of activity form when editing or creating an activity in an accompanying period with multiple centers
## v2.9.0 - 2023-10-17
### Feature
@@ -391,57 +441,57 @@ But if you do not need this any more, you must ensure that the configuration key
## v2.7.0 - 2023-09-27
### Feature
* ([#155](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/155)) The regulation list load accompanying periods by exact postal code (address associated with postal code), and not by the content of the postal code (postal code with same code's string)
* ([#155](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/155)) The regulation list load accompanying periods by exact postal code (address associated with postal code), and not by the content of the postal code (postal code with same code's string)
### Fixed
* ([#142](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/142)) Fix the label of filter ActivityTypeFilter to a more obvious one
* ([#140](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/140)) [export] Fix association of filter "filter location by type" which did not appears on "list of activities"
* ([#142](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/142)) Fix the label of filter ActivityTypeFilter to a more obvious one
* ([#140](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/140)) [export] Fix association of filter "filter location by type" which did not appears on "list of activities"
## v2.6.3 - 2023-09-19
### Fixed
* Remove id property from document
mappedsuperclass
* Remove id property from document
mappedsuperclass
## v2.6.2 - 2023-09-18
### Fixed
* Fix doctrine mapping of AbstractTaskPlaceEvent and SingleTaskPlaceEvent: id property moved.
* Fix doctrine mapping of AbstractTaskPlaceEvent and SingleTaskPlaceEvent: id property moved.
## v2.6.1 - 2023-09-14
### Fixed
* Filter out active centers in exports, which uses a different PickCenterType.
* Filter out active centers in exports, which uses a different PickCenterType.
## v2.6.0 - 2023-09-14
### Feature
* ([#133](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/133)) Add locations in Aside Activity. By default, suggest user location, otherwise a select with all locations.
* ([#133](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/133)) Adapt Aside Activity exports: display location, filter by location, group by location
* Use the CRUD controller for center entity + add the isActive property to be able to mask instances of Center that are no longer in use.
* ([#133](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/133)) Add locations in Aside Activity. By default, suggest user location, otherwise a select with all locations.
* ([#133](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/133)) Adapt Aside Activity exports: display location, filter by location, group by location
* Use the CRUD controller for center entity + add the isActive property to be able to mask instances of Center that are no longer in use.
### Fixed
* ([#107](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/107)) reinstate the fusion of duplicate persons
* Missing translation in Work Actions exports
* Reimplement the mission type filter on tasks, only for instances that have a config parameter indicating true for this.
* ([#135](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/135)) Corrects a typing error in 2 filters, which caused an
* ([#107](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/107)) reinstate the fusion of duplicate persons
* Missing translation in Work Actions exports
* Reimplement the mission type filter on tasks, only for instances that have a config parameter indicating true for this.
* ([#135](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/135)) Corrects a typing error in 2 filters, which caused an
error when trying to reedit a saved export
* ([#136](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/136)) [household] when moving a person to a sharing position to a not-sharing position on the same household on the same date, remove the previous household membership on the same household. This fix duplicate member.
* ([#136](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/136)) [household] when moving a person to a sharing position to a not-sharing position on the same household on the same date, remove the previous household membership on the same household. This fix duplicate member.
* Add missing translation for comment field placeholder in repositionning household editor.
* Do not send an email to creator twice when adding a comment to a notification
* ([#107](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/107)) Fix gestion doublon functionality to work with chill bundles v2
* Do not send an email to creator twice when adding a comment to a notification
* ([#107](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/107)) Fix gestion doublon functionality to work with chill bundles v2
### UX
* Uniformize badge-person in household banner (background, size)
## v2.5.3 - 2023-07-20
### Fixed
* ([#132](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/132)) Rendez-vous documents created would appear in all documents lists of all persons with an accompanying period. Or statements are now added to the where clause to filter out documents that come from unrelated accompanying period/ or person rendez-vous.
* ([#132](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/132)) Rendez-vous documents created would appear in all documents lists of all persons with an accompanying period. Or statements are now added to the where clause to filter out documents that come from unrelated accompanying period/ or person rendez-vous.
## v2.5.2 - 2023-07-15
### Fixed
* [Collate Address] when updating address point, do not use the point's address reference if the similarity is below the requirement for associating the address reference and the address (it uses the postcode's center instead)
* [Collate Address] when updating address point, do not use the point's address reference if the similarity is below the requirement for associating the address reference and the address (it uses the postcode's center instead)
## v2.5.1 - 2023-07-14
### Fixed
* [collate addresses] block collating addresses to another address reference where the address reference is already the best match
* [collate addresses] block collating addresses to another address reference where the address reference is already the best match
## v2.5.0 - 2023-07-14
### Feature

View File

@@ -59,7 +59,8 @@
"vue-i18n": "^9.1.6",
"vue-multiselect": "3.0.0-alpha.2",
"vue-toast-notification": "^3.1.2",
"vuex": "^4.0.0"
"vuex": "^4.0.0",
"bootstrap-icons": "^1.11.3"
},
"browserslist": [
"Firefox ESR"

View File

@@ -16,7 +16,7 @@ use Chill\ActivityBundle\Repository\ActivityRepository;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Symfony\Component\Security\Core\Security;
class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
@@ -24,7 +24,7 @@ class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
public function __construct(
private readonly ActivityRepository $repository,
Security $security,
WorkflowStoredObjectPermissionHelper $workflowDocumentService,
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
) {
parent::__construct($security, $workflowDocumentService);
}

View File

@@ -42,8 +42,8 @@ class CustomFieldLongChoice extends AbstractCustomField
$translatableStringHelper = $this->translatableStringHelper;
$builder->add($customField->getSlug(), Select2ChoiceType::class, [
'choices' => $entries,
'choice_label' => static fn (Option $option) => $translatableStringHelper->localize($option->getText()),
'choice_value' => static fn (Option $key): ?int => null === $key ? null : $key->getId(),
'choice_label' => static fn (?Option $option) => $translatableStringHelper->localize($option->getText()),
'choice_value' => static fn (?Option $key): ?int => $key?->getId(),
'multiple' => false,
'expanded' => false,
'required' => $customField->isRequired(),

View File

@@ -46,11 +46,8 @@ class CustomFieldsGroup
#[ORM\GeneratedValue(strategy: 'AUTO')]
private ?int $id = null;
/**
* @var array
*/
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON)]
private $name;
private array|string $name;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON)]
private array $options = [];
@@ -181,7 +178,7 @@ class CustomFieldsGroup
*
* @return CustomFieldsGroup
*/
public function setName($name)
public function setName(array|string $name)
{
$this->name = $name;

View File

@@ -15,11 +15,14 @@ use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessa
use Chill\DocStoreBundle\Service\Signature\PDFPage;
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Chill\DocStoreBundle\Service\StoredObjectToPdfConverter;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderManagerInterface;
use Chill\MainBundle\Security\Authorization\EntityWorkflowStepSignatureVoter;
use Chill\MainBundle\Templating\Entity\UserRender;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -38,6 +41,8 @@ class SignatureRequestController
private readonly ChillEntityRenderManagerInterface $entityRender,
private readonly NormalizerInterface $normalizer,
private readonly Security $security,
private readonly StoredObjectToPdfConverter $converter,
private readonly EntityManagerInterface $entityManager,
) {}
#[Route('/api/1.0/document/workflow/{id}/signature-request', name: 'chill_docstore_signature_request')]
@@ -52,11 +57,17 @@ class SignatureRequestController
if (EntityWorkflowSignatureStateEnum::PENDING !== $signature->getState()) {
return new JsonResponse([], status: Response::HTTP_CONFLICT);
}
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
$content = $this->storedObjectManager->read($storedObject);
$data = \json_decode((string) $request->getContent(), true, 512, JSON_THROW_ON_ERROR); // TODO parse payload: json_decode ou, mieux, dataTransfertObject
if ('application/pdf' !== $storedObject->getType()) {
[$storedObject, $storedObjectVersion, $content] = $this->converter->addConvertedVersion($storedObject, $request->getLocale(), includeConvertedContent: true);
$this->entityManager->persist($storedObjectVersion);
$this->entityManager->flush();
} else {
$content = $this->storedObjectManager->read($storedObject);
}
$data = \json_decode((string) $request->getContent(), true, 512, JSON_THROW_ON_ERROR);
$zone = new PDFSignatureZone(
$data['zone']['index'],
$data['zone']['x'],
@@ -75,6 +86,7 @@ class SignatureRequestController
// options for user render
'absence' => false,
'main_scope' => false,
UserRender::SPLIT_LINE_BEFORE_CHARACTER => 30,
// options for person render
'addAge' => false,
]),

View File

@@ -18,6 +18,7 @@ use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table('chill_doc.accompanyingcourse_document')]
#[ORM\UniqueConstraint(name: 'acc_course_document_unique_stored_object', columns: ['object_id'])]
class AccompanyingCourseDocument extends Document implements HasScopesInterface, HasCentersInterface
{
#[ORM\ManyToOne(targetEntity: AccompanyingPeriod::class)]

View File

@@ -40,6 +40,7 @@ class Document implements TrackCreationInterface, TrackUpdateInterface
#[Assert\Valid]
#[Assert\NotNull(message: 'Upload a document')]
#[ORM\ManyToOne(targetEntity: StoredObject::class, cascade: ['persist'])]
#[ORM\JoinColumn(name: 'object_id', referencedColumnName: 'id')]
private ?StoredObject $object = null;
#[ORM\ManyToOne(targetEntity: DocGeneratorTemplate::class)]

View File

@@ -19,6 +19,7 @@ use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table('chill_doc.person_document')]
#[ORM\UniqueConstraint(name: 'person_document_unique_stored_object', columns: ['object_id'])]
class PersonDocument extends Document implements HasCenterInterface, HasScopeInterface
{
#[ORM\Id]

View File

@@ -17,15 +17,23 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
class DocumentCategoryType extends AbstractType
{
public function __construct(private readonly TranslatorInterface $translator) {}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$bundles = [
'chill-doc-store' => 'chill-doc-store',
];
$documentClasses = [
$this->translator->trans('Accompanying period document') => \Chill\DocStoreBundle\Entity\AccompanyingCourseDocument::class,
$this->translator->trans('Person document') => \Chill\DocStoreBundle\Entity\PersonDocument::class,
];
$builder
->add('bundleId', ChoiceType::class, [
'choices' => $bundles,
@@ -34,7 +42,10 @@ class DocumentCategoryType extends AbstractType
->add('idInsideBundle', null, [
'disabled' => true,
])
->add('documentClass', null, [
->add('documentClass', ChoiceType::class, [
'choices' => $documentClasses,
'expanded' => false,
'required' => true,
'disabled' => false,
])
->add('name', TranslatableStringFormType::class);

View File

@@ -130,3 +130,12 @@ export interface CheckSignature {
}
export type CanvasEvent = "select" | "add";
export interface ZoomLevel {
id: number;
zoom: number;
label: {
fr?: string,
nl?: string
};
}

View File

@@ -28,24 +28,40 @@
</teleport>
<div class="col-12 m-auto">
<div class="row justify-content-center border-bottom pdf-tools d-md-none">
<div v-if="pageCount > 1" class="col text-center turn-page">
<button
class="btn btn-light btn-sm"
:disabled="page <= 1"
@click="turnPage(-1)"
<div class="col text-center turn-page">
<select
class="form-select form-select-sm"
id="zoomSelect"
v-model="zoomLevel"
@change="setZoomLevel(zoomLevel)"
>
</button>
<span>{{ page }}/{{ pageCount }}</span>
<button
class="btn btn-light btn-sm"
:disabled="page >= pageCount"
@click="turnPage(1)"
>
</button>
<option value="" selected disabled>Zoom</option>
<option v-for="z in zoomLevels" :value="z.zoom" :key="z.id">
{{ z.label.fr }}
</option>
</select>
<template v-if="pageCount > 1">
<button
class="btn btn-light btn-xs p-1"
:disabled="page <= 1"
@click="turnPage(-1)"
>
</button>
<span>{{ page }}/{{ pageCount }}</span>
<button
class="btn btn-light btn-xs p-1"
:disabled="page >= pageCount"
@click="turnPage(1)"
>
</button>
</template>
</div>
<div v-if="signature.zones.length > 1" class="col-3 p-0">
<div
v-if="signature.zones.length > 1"
class="col-5 p-0 text-center turnSignature"
>
<button
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
class="btn btn-light btn-sm"
@@ -53,8 +69,7 @@
>
{{ $t("last_zone") }}
</button>
</div>
<div v-if="signature.zones.length > 1" class="col-3 p-0">
<span>|</span>
<button
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
class="btn btn-light btn-sm"
@@ -63,7 +78,7 @@
{{ $t("next_zone") }}
</button>
</div>
<div class="col text-end p-0">
<div class="col text-end" v-if="signedState !== 'signed'">
<button
class="btn btn-misc btn-sm"
:hidden="!userSignatureZone"
@@ -81,40 +96,56 @@
>
{{ $t("cancel") }}
</button>
</div>
<div class="col-1" v-if="signedState !== 'signed'">
<button
class="btn btn-create btn-sm"
:class="{ active: canvasEvent === 'add' }"
<button v-if="userSignatureZone === null"
:class="{ btn: true, 'btn-sm': true, 'btn-create': canvasEvent !== 'add', 'btn-chill-green': canvasEvent === 'add', active: canvasEvent === 'add' }"
@click="toggleAddZone()"
:title="$t('add_sign_zone')"
></button>
>
<template v-if="canvasEvent === 'add'">
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</template>
</button>
</div>
</div>
<div
class="row justify-content-center border-bottom pdf-tools d-none d-md-flex"
>
<div v-if="pageCount > 1" class="col-2 text-center turn-page p-0">
<button
class="btn btn-light btn-sm"
:disabled="page <= 1"
@click="turnPage(-1)"
<div class="col-3 text-center turn-page ps-3">
<select
class="form-select form-select-sm"
id="zoomSelect"
v-model="zoomLevel"
@change="setZoomLevel(zoomLevel)"
>
</button>
<span>{{ page }} / {{ pageCount }}</span>
<button
class="btn btn-light btn-sm"
:disabled="page >= pageCount"
@click="turnPage(1)"
>
</button>
<option value="" selected disabled>Zoom</option>
<option v-for="z in zoomLevels" :value="z.zoom" :key="z.id">
{{ z.label.fr }}
</option>
</select>
<template v-if="pageCount > 1">
<button
class="btn btn-light btn-xs p-1"
:disabled="page <= 1"
@click="turnPage(-1)"
>
</button>
<span>{{ page }} / {{ pageCount }}</span>
<button
class="btn btn-light btn-xs p-1"
:disabled="page >= pageCount"
@click="turnPage(1)"
>
</button>
</template>
</div>
<div
v-if="signature.zones.length > 1 && signedState !== 'signed'"
class="col text-end d-xl-none"
class="col-4 d-xl-none text-center turnSignature p-0"
>
<button
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
@@ -123,11 +154,7 @@
>
{{ $t("last_zone") }}
</button>
</div>
<div
v-if="signature.zones.length > 1 && signedState !== 'signed'"
class="col text-start d-xl-none"
>
<span>|</span>
<button
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
class="btn btn-light btn-sm"
@@ -138,7 +165,7 @@
</div>
<div
v-if="signature.zones.length > 1 && signedState !== 'signed'"
class="col text-end d-none d-xl-flex p-0"
class="col-4 d-none d-xl-flex p-0 text-center turnSignature"
>
<button
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
@@ -147,11 +174,7 @@
>
{{ $t("last_sign_zone") }}
</button>
</div>
<div
v-if="signature.zones.length > 1 && signedState !== 'signed'"
class="col text-start d-none d-xl-flex p-0"
>
<span>|</span>
<button
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
class="btn btn-light btn-sm"
@@ -160,7 +183,7 @@
{{ $t("next_sign_zone") }}
</button>
</div>
<div class="col text-end p-0" v-if="signedState !== 'signed'">
<div class="col text-end" v-if="signedState !== 'signed'">
<button
class="btn btn-misc btn-sm"
:hidden="!userSignatureZone"
@@ -177,29 +200,43 @@
>
{{ $t("cancel") }}
</button>
</div>
<div
class="col text-end p-0 pe-2 pe-xxl-4"
v-if="signedState !== 'signed'"
>
<button
class="btn btn-create btn-sm"
:class="{ active: canvasEvent === 'add' }"
<button v-if="userSignatureZone === null"
:class="{ btn: true, 'btn-sm': true, 'btn-create': canvasEvent !== 'add', 'btn-chill-green': canvasEvent === 'add', active: canvasEvent === 'add' }"
@click="toggleAddZone()"
:title="$t('add_sign_zone')"
>
{{ $t("add_zone") }}
<template v-if="canvasEvent !== 'add'">
{{ $t("add_zone") }}
</template>
<template v-else>
{{ $t("click_on_document")}}
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</template>
</button>
</div>
</div>
</div>
<div class="col-xs-12 col-md-12 col-lg-9 m-auto my-5 text-center">
<canvas class="m-auto" id="canvas"></canvas>
<div class="col-xs-12 col-md-12 col-lg-9 m-auto my-5 text-center" :class="{onAddZone: canvasEvent === 'add'}">
<canvas class="m-auto" id="canvas" ></canvas>
</div>
<div class="col-xs-12 col-md-12 col-lg-9 m-auto p-4" id="action-buttons">
<div class="row">
<div class="col-4" v-if="signedState !== 'signed'">
<div class="col d-flex">
<a
class="btn btn-cancel"
v-if="signedState !== 'signed'"
:href="getReturnPath()"
>
{{ $t("cancel") }}
</a>
<a class="btn btn-misc" v-else :href="getReturnPath()">
{{ $t("return") }}
</a>
</div>
<div class="col text-end" v-if="signedState !== 'signed'">
<button
class="btn btn-action me-2"
:disabled="!userSignatureZone"
@@ -209,18 +246,6 @@
</button>
</div>
<div class="col-4" v-else></div>
<div class="col-8 d-flex justify-content-end">
<a
class="btn btn-delete"
v-if="signedState !== 'signed'"
:href="getReturnPath()"
>
{{ $t("cancel_signing") }}
</a>
<a class="btn btn-misc" v-else :href="getReturnPath()">
{{ $t("return") }}
</a>
</div>
</div>
</div>
</template>
@@ -235,6 +260,7 @@ import {
Signature,
SignatureZone,
SignedState,
ZoomLevel,
} from "../../types";
import { makeFetch } from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
import * as pdfjsLib from "pdfjs-dist";
@@ -251,7 +277,7 @@ console.log(PdfWorker); // incredible but this is needed
// pdfjsLib.GlobalWorkerOptions.workerSrc = PdfWorker;
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import { download_and_decrypt_doc } from "../StoredObjectButton/helpers";
import {download_and_decrypt_doc, download_doc_as_pdf} from "../StoredObjectButton/helpers";
pdfjsLib.GlobalWorkerOptions.workerSrc = "pdfjs-dist/build/pdf.worker.mjs";
@@ -262,6 +288,52 @@ const canvasEvent: Ref<CanvasEvent> = ref("select");
const signedState: Ref<SignedState> = ref("pending");
const page: Ref<number> = ref(1);
const pageCount: Ref<number> = ref(0);
const zoom: Ref<number> = ref(1);
let zoomLevel = "";
const zoomLevels: Ref<ZoomLevel[]> = ref([
{
id: 0,
zoom: 0.75,
label: {
fr: "75%",
},
},
{
id: 1,
zoom: zoom.value,
label: {
fr: "100%",
},
},
{
id: 2,
zoom: 1.25,
label: {
fr: "125%",
},
},
{
id: 3,
zoom: 1.5,
label: {
fr: "150%",
},
},
{
id: 4,
zoom: 2,
label: {
fr: "200%",
},
},
{
id: 5,
zoom: 3,
label: {
fr: "300%",
},
},
]);
let userSignatureZone: Ref<null | SignatureZone> = ref(null);
let pdf = {} as PDFDocumentProxy;
@@ -275,17 +347,21 @@ const $toast = useToast();
const signature = window.signature;
console.log(signature);
const setZoomLevel = (zoomLevel: string) => {
zoom.value = Number.parseFloat(zoomLevel);
setPage(page.value);
setTimeout(() => drawAllZones(page.value), 200);
};
const mountPdf = async (url: string) => {
const loadingTask = pdfjsLib.getDocument(url);
const mountPdf = async (doc: ArrayBuffer) => {
const loadingTask = pdfjsLib.getDocument(doc);
pdf = await loadingTask.promise;
pageCount.value = pdf.numPages;
await setPage(page.value);
};
const getRenderContext = (pdfPage: PDFPageProxy) => {
const scale = 1;
const scale = 1 * zoom.value;
const viewport = pdfPage.getViewport({ scale });
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
const context = canvas.getContext("2d") as CanvasRenderingContext2D;
@@ -309,15 +385,13 @@ const init = () => downloadAndOpen().then(initPdf);
async function downloadAndOpen(): Promise<Blob> {
let raw;
try {
raw = await download_and_decrypt_doc(
signature.storedObject,
signature.storedObject.currentVersion
);
raw = await download_doc_as_pdf(signature.storedObject);
} catch (e) {
console.error("error while downloading and decrypting document", e);
throw e;
}
await mountPdf(URL.createObjectURL(raw));
const doc = await raw.arrayBuffer();
await mountPdf(doc);
return raw;
}
@@ -342,12 +416,12 @@ const hitSignature = (
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) < xy[0] &&
xy[0] <
scaleXToCanvas(zone.x + zone.width, canvasWidth, zone.PDFPage.width) &&
zone.PDFPage.height -
zone.PDFPage.height * zoom.value -
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) <
xy[1] &&
xy[1] <
scaleYToCanvas(zone.height - zone.y, canvasHeight, zone.PDFPage.height) +
zone.PDFPage.height;
zone.PDFPage.height * zoom.value;
const selectZone = (z: SignatureZone, canvas: HTMLCanvasElement) => {
userSignatureZone.value = z;
@@ -424,19 +498,19 @@ const drawZone = (
ctx.lineJoin = "bevel";
ctx.strokeRect(
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width),
zone.PDFPage.height -
zone.PDFPage.height * zoom.value -
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height),
scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width),
scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height)
);
ctx.font = "bold 16px serif";
ctx.font = `bold ${16 * zoom.value}px serif`;
ctx.textAlign = "center";
ctx.fillStyle = "black";
const xText =
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) +
scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width) / 2;
const yText =
zone.PDFPage.height -
zone.PDFPage.height * zoom.value -
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) +
scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height) / 2;
if (userSignatureZone.value?.index === zone.index) {
@@ -444,8 +518,8 @@ const drawZone = (
ctx.fillText("Signer ici", xText, yText);
} else {
ctx.fillStyle = unselectedBlue;
ctx.fillText("Choisir cette", xText, yText - 12);
ctx.fillText("zone de signature", xText, yText + 12);
ctx.fillText("Choisir cette", xText, yText - 12 * zoom.value);
ctx.fillText("zone de signature", xText, yText + 12 * zoom.value);
}
};
@@ -607,6 +681,15 @@ init();
#canvas {
box-shadow: 0 20px 20px rgba(0, 0, 0, 0.2);
}
.onAddZone {
cursor: not-allowed;
#canvas {
cursor: copy;
}
}
div#action-buttons {
position: sticky;
bottom: 0px;
@@ -615,16 +698,29 @@ div#action-buttons {
}
div.pdf-tools {
background-color: #f3f3f3;
font-size: 0.8rem;
font-size: 0.6rem;
button {
font-size: 0.75rem !important;
}
div.turnSignature {
span {
font-size: 1rem;
}
}
@media (min-width: 1400px) {
// background: none;
// border: none !important;
}
}
div.turn-page {
display: flex;
span {
font-size: 0.8rem;
margin: 0 0.4rem;
font-size: 0.75rem;
margin: auto 0.4rem;
}
select {
width: 5rem;
font-size: 0.75rem;
}
}
div.signature-modal-body {

View File

@@ -12,10 +12,10 @@ const appMessages = {
sign: 'Signer',
choose_another_signature: 'Choisir une autre zone',
cancel: 'Annuler',
cancel_signing: 'Refuser de signer',
last_sign_zone: 'Zone de signature précédente',
next_sign_zone: 'Zone de signature suivante',
add_sign_zone: 'Ajouter une zone de signature',
click_on_document: 'Cliquer sur le document',
last_zone: 'Zone précédente',
next_zone: 'Zone suivante',
add_zone: 'Ajouter une zone',

View File

@@ -3,7 +3,7 @@
<i class="fa fa-download"></i>
<template v-if="displayActionStringInButton">Télécharger</template>
</a>
<a v-else :class="props.classes" target="_blank" :type="props.storedObject.type" :download="buildDocumentName()" :href="state.href_url" ref="open_button" title="Ouvrir">
<a v-else :class="props.classes" target="_blank" :type="props.atVersion.type" :download="buildDocumentName()" :href="state.href_url" ref="open_button" title="Ouvrir">
<i class="fa fa-external-link"></i>
<template v-if="displayActionStringInButton">Ouvrir</template>
</a>

View File

@@ -55,7 +55,7 @@ const classes = computed<{row: true, 'row-hover': true, 'blinking-1': boolean, '
<restore-version-button :stored-object-version="props.version" @restore-version="onRestore"></restore-version-button>
</li>
<li>
<download-button :stored-object="storedObject" :at-version="version" :classes="{btn: true, 'btn-outline-primary': true}" :display-action-string-in-button="false"></download-button>
<download-button :stored-object="storedObject" :at-version="version" :classes="{btn: true, 'btn-outline-primary': true, 'btn-sm': true}" :display-action-string-in-button="false"></download-button>
</li>
</ul>
</div>

View File

@@ -197,6 +197,32 @@ async function download_and_decrypt_doc(storedObject: StoredObject, atVersion: n
}
}
/**
* Fetch the stored object as a pdf.
*
* If the document is already in a pdf on the server side, the document is retrieved "as is" from the usual
* storage.
*/
async function download_doc_as_pdf(storedObject: StoredObject): Promise<Blob>
{
if (null === storedObject.currentVersion) {
throw new Error("the stored object does not count any version");
}
if (storedObject.currentVersion?.type === 'application/pdf') {
return download_and_decrypt_doc(storedObject, storedObject.currentVersion);
}
const convertLink = build_convert_link(storedObject.uuid);
const response = await fetch(convertLink);
if (!response.ok) {
throw new Error("Could not convert the document: " + response.status);
}
return response.blob();
}
async function is_object_ready(storedObject: StoredObject): Promise<StoredObjectStatusChange>
{
const new_status_response = await window
@@ -214,6 +240,7 @@ export {
build_wopi_editor_link,
download_and_decrypt_doc,
download_doc,
download_doc_as_pdf,
is_extension_editable,
is_extension_viewable,
is_object_ready,

View File

@@ -8,7 +8,7 @@
<table class="table table-bordered border-dark align-middle">
<thead>
<tr>
<th>{{ 'Creator bundle id' | trans }}</th>
{# <th>{{ 'Creator bundle id' | trans }}</th>#}
<th>{{ 'Internal id inside creator bundle' | trans }}</th>
<th>{{ 'Document class' | trans }}</th>
<th>{{ 'Name' | trans }}</th>
@@ -18,7 +18,7 @@
<tbody>
{% for document_category in document_categories %}
<tr>
<td>{{ document_category.bundleId }}</td>
{# <td>{{ document_category.bundleId }}</td>#}
<td>{{ document_category.idInsideBundle }}</td>
<td>{{ document_category.documentClass }}</td>
<td>{{ document_category.name | localize_translatable_string}}</td>

View File

@@ -15,7 +15,7 @@ use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Security;
@@ -34,7 +34,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
public function __construct(
private readonly Security $security,
private readonly ?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null,
private readonly ?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
) {}
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool
@@ -46,24 +46,27 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool
{
// Retrieve the related accompanying course document
// Retrieve the related entity
$entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);
// Determine the attribute to pass to AccompanyingCourseDocumentVoter
// Determine the attribute to pass to the voter for argument
$voterAttribute = $this->attributeToRole($attribute);
if (false === $this->security->isGranted($voterAttribute, $entity)) {
return false;
$regularPermission = $this->security->isGranted($voterAttribute, $entity);
if (!$this->canBeAssociatedWithWorkflow()) {
return $regularPermission;
}
if (StoredObjectRoleEnum::SEE !== $attribute && $this->canBeAssociatedWithWorkflow()) {
if (null === $this->workflowDocumentService) {
throw new \LogicException('Provide a workflow document service');
}
$workflowPermission = match ($attribute) {
StoredObjectRoleEnum::SEE => $this->workflowDocumentService->isAllowedByWorkflowForReadOperation($entity),
StoredObjectRoleEnum::EDIT => $this->workflowDocumentService->isAllowedByWorkflowForWriteOperation($entity),
};
return $this->workflowDocumentService->notBlockedByWorkflow($entity);
}
return true;
return match ($workflowPermission) {
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT => true,
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED => false,
WorkflowRelatedEntityPermissionHelper::ABSTAIN => $regularPermission,
};
}
}

View File

@@ -16,7 +16,7 @@ use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Symfony\Component\Security\Core\Security;
final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredObjectVoter
@@ -24,7 +24,7 @@ final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredOb
public function __construct(
private readonly AccompanyingCourseDocumentRepository $repository,
Security $security,
WorkflowStoredObjectPermissionHelper $workflowDocumentService,
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
) {
parent::__construct($security, $workflowDocumentService);
}

View File

@@ -16,7 +16,7 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Symfony\Component\Security\Core\Security;
class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter
@@ -24,7 +24,7 @@ class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter
public function __construct(
private readonly PersonDocumentRepository $repository,
Security $security,
WorkflowStoredObjectPermissionHelper $workflowDocumentService,
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
) {
parent::__construct($security, $workflowDocumentService);
}

View File

@@ -17,15 +17,30 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\WopiBundle\Service\WopiConverter;
use Symfony\Contracts\Translation\LocaleAwareInterface;
class PDFSignatureZoneAvailable
class PDFSignatureZoneAvailable implements LocaleAwareInterface
{
private string $locale;
public function __construct(
private readonly EntityWorkflowManager $entityWorkflowManager,
private readonly PDFSignatureZoneParser $pdfSignatureZoneParser,
private readonly StoredObjectManagerInterface $storedObjectManager,
private readonly WopiConverter $converter,
) {}
public function setLocale(string $locale)
{
$this->locale = $locale;
}
public function getLocale()
{
return $this->locale;
}
/**
* @return list<PDFSignatureZone>
*/
@@ -38,10 +53,16 @@ class PDFSignatureZoneAvailable
}
if ('application/pdf' !== $storedObject->getType()) {
throw new \RuntimeException('Only PDF documents are supported');
$content = $this->converter->convert($this->getLocale(), $this->storedObjectManager->read($storedObject), $storedObject->getType());
} else {
$content = $this->storedObjectManager->read($storedObject);
}
$zones = $this->pdfSignatureZoneParser->findSignatureZones($this->storedObjectManager->read($storedObject));
$zones = $this->pdfSignatureZoneParser->findSignatureZones($content);
// free some memory as soon as possible...
unset($content);
$signatureZonesIndexes = array_map(
fn (EntityWorkflowStepSignature $step) => $step->getZoneSignatureIndex(),
$this->collectSignaturesInUse($entityWorkflow)

View File

@@ -39,13 +39,13 @@ class StoredObjectToPdfConverter
* @param string $lang the language for the conversion context
* @param string $convertTo The target format for the conversion. Default is 'pdf'.
*
* @return array{0: StoredObjectPointInTime, 1: StoredObjectVersion} contains the point in time before conversion and the new version of the stored object
* @return array{0: StoredObjectPointInTime, 1: StoredObjectVersion, 2?: string} contains the point in time before conversion and the new version of the stored object. The converted content is included in the response if $includeConvertedContent is true
*
* @throws \UnexpectedValueException if the preferred mime type for the conversion is not found
* @throws \RuntimeException if the conversion or storage of the new version fails
* @throws StoredObjectManagerException
*/
public function addConvertedVersion(StoredObject $storedObject, string $lang, $convertTo = 'pdf'): array
public function addConvertedVersion(StoredObject $storedObject, string $lang, $convertTo = 'pdf', bool $includeConvertedContent = false): array
{
$newMimeType = $this->mimeTypes->getMimeTypes($convertTo)[0] ?? null;
@@ -70,6 +70,11 @@ class StoredObjectToPdfConverter
$pointInTime = new StoredObjectPointInTime($currentVersion, StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION);
$version = $this->storedObjectManager->write($storedObject, $converted, $newMimeType);
return [$pointInTime, $version];
if (!$includeConvertedContent) {
return [$pointInTime, $version];
}
return [$pointInTime, $version, $converted];
}
}

View File

@@ -1,62 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Service;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Workflow\Registry;
class WorkflowStoredObjectPermissionHelper
{
public function __construct(
private readonly Security $security,
private readonly EntityWorkflowManager $entityWorkflowManager,
private readonly Registry $registry,
) {}
public function notBlockedByWorkflow(object $entity): bool
{
$entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
$currentUser = $this->security->getUser();
foreach ($entityWorkflows as $entityWorkflow) {
if ($entityWorkflow->isFinal()) {
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$marking = $workflow->getMarkingStore()->getMarking($entityWorkflow);
foreach ($marking->getPlaces() as $place => $active) {
$metadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
if ($metadata['isFinalPositive'] ?? true) {
return false;
}
}
} else {
if (!$entityWorkflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) {
return false;
}
}
// as soon as there is one signatured applyied, we are not able to
// edit the document any more
foreach ($entityWorkflow->getSteps() as $step) {
foreach ($step->getSignatures() as $signature) {
if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) {
return false;
}
}
}
}
return true;
}
}

View File

@@ -15,10 +15,11 @@ use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Security;
/**
@@ -28,26 +29,21 @@ use Symfony\Component\Security\Core\Security;
*/
class AbstractStoredObjectVoterTest extends TestCase
{
private AssociatedEntityToStoredObjectInterface $repository;
private Security $security;
private WorkflowStoredObjectPermissionHelper $workflowDocumentService;
use ProphecyTrait;
protected function setUp(): void
{
$this->repository = $this->createMock(AssociatedEntityToStoredObjectInterface::class);
$this->security = $this->createMock(Security::class);
$this->workflowDocumentService = $this->createMock(WorkflowStoredObjectPermissionHelper::class);
}
private function buildStoredObjectVoter(bool $canBeAssociatedWithWorkflow, AssociatedEntityToStoredObjectInterface $repository, Security $security, ?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null): AbstractStoredObjectVoter
{
private function buildStoredObjectVoter(
bool $canBeAssociatedWithWorkflow,
AssociatedEntityToStoredObjectInterface $repository,
Security $security,
?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
): AbstractStoredObjectVoter {
// Anonymous class extending the abstract class
return new class ($canBeAssociatedWithWorkflow, $repository, $security, $workflowDocumentService) extends AbstractStoredObjectVoter {
public function __construct(
private readonly bool $canBeAssociatedWithWorkflow,
private readonly AssociatedEntityToStoredObjectInterface $repository,
Security $security,
?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null,
?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
) {
parent::__construct($security, $workflowDocumentService);
}
@@ -74,95 +70,89 @@ class AbstractStoredObjectVoterTest extends TestCase
};
}
private function setupMockObjects(): array
{
$user = new User();
$token = $this->createMock(TokenInterface::class);
$subject = new StoredObject();
$entity = new \stdClass();
return [$user, $token, $subject, $entity];
}
private function setupMocksForVoteOnAttribute(User $user, TokenInterface $token, bool $isGrantedForEntity, object $entity, bool $workflowAllowed): void
{
// Set up token to return user
$token->method('getUser')->willReturn($user);
// Mock the return of an AccompanyingCourseDocument by the repository
$this->repository->method('findAssociatedEntityToStoredObject')->willReturn($entity);
// Mock scenario where user is allowed to see_details of the AccompanyingCourseDocument
$this->security->method('isGranted')->willReturn($isGrantedForEntity);
// Mock case where user is blocked or not by workflow
$this->workflowDocumentService->method('notBlockedByWorkflow')->willReturn($workflowAllowed);
}
public function testSupportsOnAttribute(): void
{
[$user, $token, $subject, $entity] = $this->setupMockObjects();
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(new \stdClass()), $this->prophesize(Security::class)->reveal(), null);
// Setup mocks for voteOnAttribute method
$this->setupMocksForVoteOnAttribute($user, $token, true, $entity, true);
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
self::assertTrue($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject()));
self::assertTrue($voter->supports(StoredObjectRoleEnum::SEE, $subject));
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(new User()), $this->prophesize(Security::class)->reveal(), null);
self::assertFalse($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject()));
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(null), $this->prophesize(Security::class)->reveal(), null);
self::assertFalse($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject()));
}
public function testVoteOnAttributeAllowedAndWorkflowAllowed(): void
{
[$user, $token, $subject, $entity] = $this->setupMockObjects();
/**
* @dataProvider dataProviderVoteOnAttribute
*/
public function testVoteOnAttribute(
StoredObjectRoleEnum $attribute,
bool $expected,
bool $canBeAssociatedWithWorkflow,
bool $isGrantedRegularPermission,
?string $isGrantedWorkflowPermissionRead,
?string $isGrantedWorkflowPermissionWrite,
string $message,
): void {
$storedObject = new StoredObject();
$dummyRepository = new DummyRepository($related = new \stdClass());
$token = new UsernamePasswordToken(new User(), 'dummy');
// Setup mocks for voteOnAttribute method
$this->setupMocksForVoteOnAttribute($user, $token, true, $entity, true);
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
$security = $this->prophesize(Security::class);
$security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
// The voteOnAttribute method should return True when workflow is allowed
self::assertTrue($voter->voteOnAttribute(StoredObjectRoleEnum::SEE, $subject, $token));
$workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class);
if (null !== $isGrantedWorkflowPermissionRead) {
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
->willReturn($isGrantedWorkflowPermissionRead)->shouldBeCalled();
} else {
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)->shouldNotBeCalled();
}
if (null !== $isGrantedWorkflowPermissionWrite) {
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related)
->willReturn($isGrantedWorkflowPermissionWrite)->shouldBeCalled();
} else {
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related)->shouldNotBeCalled();
}
$voter = $this->buildStoredObjectVoter($canBeAssociatedWithWorkflow, $dummyRepository, $security->reveal(), $workflowRelatedEntityPermissionHelper->reveal());
self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message);
}
public function testVoteOnAttributeNotAllowed(): void
public static function dataProviderVoteOnAttribute(): iterable
{
[$user, $token, $subject, $entity] = $this->setupMockObjects();
// not associated on a workflow
yield [StoredObjectRoleEnum::SEE, true, false, true, null, null, 'not associated on a workflow, granted by regular access, must not rely on helper'];
yield [StoredObjectRoleEnum::SEE, false, false, false, null, null, 'not associated on a workflow, denied by regular access, must not rely on helper'];
// Setup mocks for voteOnAttribute method where isGranted() returns false
$this->setupMocksForVoteOnAttribute($user, $token, false, $entity, true);
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
// associated on a workflow, read operation
yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be granted'];
yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelper::ABSTAIN, null, 'associated on a workflow, read by regular, abstain by workflow, ask for read, should be granted'];
yield [StoredObjectRoleEnum::SEE, false, true, true, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be denied'];
yield [StoredObjectRoleEnum::SEE, true, true, false, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be granted'];
yield [StoredObjectRoleEnum::SEE, false, true, false, WorkflowRelatedEntityPermissionHelper::ABSTAIN, null, 'associated on a workflow, denied read by regular, abstain by workflow, ask for read, should be granted'];
yield [StoredObjectRoleEnum::SEE, false, true, false, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be denied'];
// The voteOnAttribute method should return True when workflow is allowed
self::assertFalse($voter->voteOnAttribute(StoredObjectRoleEnum::SEE, $subject, $token));
}
public function testVoteOnAttributeAllowedWorkflowNotAllowed(): void
{
[$user, $token, $subject, $entity] = $this->setupMockObjects();
// Setup mocks for voteOnAttribute method
$this->setupMocksForVoteOnAttribute($user, $token, true, $entity, false);
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
// Test voteOnAttribute method
$attribute = StoredObjectRoleEnum::EDIT;
$result = $voter->voteOnAttribute($attribute, $subject, $token);
// Assert that access is denied when workflow is not allowed
$this->assertFalse($result);
}
public function testVoteOnAttributeAllowedWorkflowAllowedToSeeDocument(): void
{
[$user, $token, $subject, $entity] = $this->setupMockObjects();
// Setup mocks for voteOnAttribute method
$this->setupMocksForVoteOnAttribute($user, $token, true, $entity, false);
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
// Test voteOnAttribute method
$attribute = StoredObjectRoleEnum::SEE;
$result = $voter->voteOnAttribute($attribute, $subject, $token);
// Assert that access is denied when workflow is not allowed
$this->assertTrue($result);
// association on a workflow, write operation
yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be granted'];
yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelper::ABSTAIN, 'associated on a workflow, write by regular, abstain by workflow, ask for write, should be granted'];
yield [StoredObjectRoleEnum::EDIT, false, true, true, null, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be denied'];
yield [StoredObjectRoleEnum::EDIT, true, true, false, null, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be granted'];
yield [StoredObjectRoleEnum::EDIT, false, true, false, null, WorkflowRelatedEntityPermissionHelper::ABSTAIN, 'associated on a workflow, denied write by regular, abstain by workflow, ask for write, should be granted'];
yield [StoredObjectRoleEnum::EDIT, false, true, false, null, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be denied'];
}
}
class DummyRepository implements AssociatedEntityToStoredObjectInterface
{
public function __construct(private readonly ?object $relatedEntity) {}
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object
{
return $this->relatedEntity;
}
}

View File

@@ -22,6 +22,7 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use Chill\PersonBundle\Entity\Person;
use Chill\WopiBundle\Service\WopiConverter;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Clock\MockClock;
@@ -65,6 +66,7 @@ class PDFSignatureZoneAvailableTest extends TestCase
$entityWorkflowManager->reveal(),
$parser->reveal(),
$storedObjectManager->reveal(),
$this->prophesize(WopiConverter::class)->reveal(),
);
$actual = $filter->getAvailableSignatureZones($entityWorkflow);

View File

@@ -1,161 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Tests\Service;
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use Chill\PersonBundle\Entity\Person;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Workflow\DefinitionBuilder;
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
use Symfony\Component\Workflow\Workflow;
use Symfony\Component\Workflow\WorkflowInterface;
/**
* @internal
*
* @coversNothing
*/
class WorkflowStoredObjectPermissionHelperTest extends TestCase
{
use ProphecyTrait;
/**
* @dataProvider provideDataNotBlockByWorkflow
*/
public function testNotBlockByWorkflow(EntityWorkflow $entityWorkflow, User $user, bool $expected, string $message): void
{
// all entities must have this workflow name, so we are ok to set it here
$entityWorkflow->setWorkflowName('dummy');
$object = new \stdClass();
$helper = $this->buildHelper($object, $entityWorkflow, $user);
self::assertEquals($expected, $helper->notBlockedByWorkflow($entityWorkflow), $message);
}
private function buildHelper(object $relatedEntity, EntityWorkflow $entityWorkflow, User $user): WorkflowStoredObjectPermissionHelper
{
$security = $this->prophesize(Security::class);
$security->getUser()->willReturn($user);
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
$entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn([$entityWorkflow]);
return new WorkflowStoredObjectPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $this->buildRegistry());
}
public static function provideDataNotBlockByWorkflow(): iterable
{
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable());
yield [$entityWorkflow, new User(), false, 'blocked because the user is not present as a dest user'];
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = $user = new User();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user);
yield [$entityWorkflow, $user, true, 'allowed because the user is present as a dest user'];
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = $user = new User();
$entityWorkflow->setStep('final_positive', $dto, 'to_final_positive', new \DateTimeImmutable(), $user);
$entityWorkflow->getCurrentStep()->setIsFinal(true);
yield [$entityWorkflow, $user, false, 'blocked because the step is final, and final positive'];
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = $user = new User();
$entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable(), $user);
$entityWorkflow->getCurrentStep()->setIsFinal(true);
yield [$entityWorkflow, $user, true, 'allowed because the step is final, and final negative'];
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = $user = new User();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user);
$step = $entityWorkflow->getCurrentStep();
new EntityWorkflowStepSignature($step, new Person());
yield [$entityWorkflow, $user, true, 'allow, a signature is present but still pending'];
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = $user = new User();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user);
$step = $entityWorkflow->getCurrentStep();
$signature = new EntityWorkflowStepSignature($step, new Person());
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED);
yield [$entityWorkflow, $user, false, 'blocked, a signature is present and signed'];
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = $user = new User();
$entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable(), $user);
$step = $entityWorkflow->getCurrentStep();
$signature = new EntityWorkflowStepSignature($step, new Person());
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED);
yield [$entityWorkflow, $user, false, 'blocked, a signature is present and signed, although the workflow is final negative'];
}
private static function buildRegistry(): Registry
{
$builder = new DefinitionBuilder();
$builder
->setInitialPlaces(['initial'])
->addPlaces(['initial', 'test', 'final_positive', 'final_negative'])
->setMetadataStore(
new InMemoryMetadataStore(
placesMetadata: [
'final_positive' => [
'isFinal' => true,
'isFinalPositive' => true,
],
'final_negative' => [
'isFinal' => true,
'isFinalPositive' => false,
],
]
)
);
$workflow = new Workflow($builder->build(), new EntityWorkflowMarkingStore(), name: 'dummy');
$registry = new Registry();
$registry->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface {
public function supports(WorkflowInterface $workflow, object $subject): bool
{
return true;
}
});
return $registry;
}
}

View File

@@ -1,208 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Tests\Workflow;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Entity\StoredObjectPointInTime;
use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum;
use Chill\DocStoreBundle\Service\StoredObjectToPdfConverter;
use Chill\DocStoreBundle\Workflow\ConvertToPdfBeforeSignatureStepEventSubscriber;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Workflow\DefinitionBuilder;
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
use Symfony\Component\Workflow\Transition;
use Symfony\Component\Workflow\Workflow;
use Symfony\Component\Workflow\WorkflowInterface;
/**
* @internal
*
* @coversNothing
*/
class ConvertToPdfBeforeSignatureStepEventSubscriberTest extends \PHPUnit\Framework\TestCase
{
use ProphecyTrait;
public function testConvertToPdfBeforeSignatureStepEventSubscriberToSignature(): void
{
$entityWorkflow = new EntityWorkflow();
$storedObject = new StoredObject();
$previousVersion = $storedObject->registerVersion();
$converter = $this->prophesize(StoredObjectToPdfConverter::class);
$converter->addConvertedVersion($storedObject, 'fr', 'pdf')
->shouldBeCalledOnce()
->will(function ($args) {
/** @var StoredObject $storedObject */
$storedObject = $args[0];
$pointInTime = new StoredObjectPointInTime($storedObject->getCurrentVersion(), StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION);
$newVersion = $storedObject->registerVersion(filename: 'next');
return [$pointInTime, $newVersion];
});
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
$entityWorkflowManager->getAssociatedStoredObject($entityWorkflow)->willReturn($storedObject);
$request = new Request();
$request->setLocale('fr');
$stack = new RequestStack();
$stack->push($request);
$eventSubscriber = new ConvertToPdfBeforeSignatureStepEventSubscriber($entityWorkflowManager->reveal(), $converter->reveal(), $stack);
$registry = $this->buildRegistry($eventSubscriber);
$workflow = $registry->get($entityWorkflow, 'dummy');
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$workflow->apply($entityWorkflow, 'to_signature', ['context' => $dto, 'transition' => 'to_signature', 'transitionAt' => new \DateTimeImmutable('now')]);
self::assertEquals('signature', $entityWorkflow->getStep());
self::assertNotSame($previousVersion, $storedObject->getCurrentVersion());
self::assertTrue($previousVersion->hasPointInTimes());
self::assertCount(2, $storedObject->getVersions());
self::assertEquals('next', $storedObject->getCurrentVersion()->getFilename());
}
public function testConvertToPdfBeforeSignatureStepEventSubscriberToNotASignatureStep(): void
{
$entityWorkflow = new EntityWorkflow();
$storedObject = new StoredObject();
$previousVersion = $storedObject->registerVersion();
$converter = $this->prophesize(StoredObjectToPdfConverter::class);
$converter->addConvertedVersion($storedObject, 'fr', 'pdf')
->shouldNotBeCalled();
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
$entityWorkflowManager->getAssociatedStoredObject($entityWorkflow)->willReturn($storedObject);
$request = new Request();
$request->setLocale('fr');
$stack = new RequestStack();
$stack->push($request);
$eventSubscriber = new ConvertToPdfBeforeSignatureStepEventSubscriber($entityWorkflowManager->reveal(), $converter->reveal(), $stack);
$registry = $this->buildRegistry($eventSubscriber);
$workflow = $registry->get($entityWorkflow, 'dummy');
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$workflow->apply($entityWorkflow, 'to_something', ['context' => $dto, 'transition' => 'to_something', 'transitionAt' => new \DateTimeImmutable('now')]);
self::assertEquals('something', $entityWorkflow->getStep());
self::assertSame($previousVersion, $storedObject->getCurrentVersion());
self::assertFalse($previousVersion->hasPointInTimes());
self::assertCount(1, $storedObject->getVersions());
}
public function testConvertToPdfBeforeSignatureStepEventSubscriberToSignatureAlreadyAPdf(): void
{
$entityWorkflow = new EntityWorkflow();
$storedObject = new StoredObject();
$previousVersion = $storedObject->registerVersion(type: 'application/pdf');
$converter = $this->prophesize(StoredObjectToPdfConverter::class);
$converter->addConvertedVersion($storedObject, 'fr', 'pdf')
->shouldNotBeCalled();
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
$entityWorkflowManager->getAssociatedStoredObject($entityWorkflow)->willReturn($storedObject);
$request = new Request();
$request->setLocale('fr');
$stack = new RequestStack();
$stack->push($request);
$eventSubscriber = new ConvertToPdfBeforeSignatureStepEventSubscriber($entityWorkflowManager->reveal(), $converter->reveal(), $stack);
$registry = $this->buildRegistry($eventSubscriber);
$workflow = $registry->get($entityWorkflow, 'dummy');
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$workflow->apply($entityWorkflow, 'to_signature', ['context' => $dto, 'transition' => 'to_signature', 'transitionAt' => new \DateTimeImmutable('now')]);
self::assertEquals('signature', $entityWorkflow->getStep());
self::assertSame($previousVersion, $storedObject->getCurrentVersion());
self::assertFalse($previousVersion->hasPointInTimes());
self::assertCount(1, $storedObject->getVersions());
}
public function testConvertToPdfBeforeSignatureStepEventSubscriberToSignatureWithNoStoredObject(): void
{
$entityWorkflow = new EntityWorkflow();
$converter = $this->prophesize(StoredObjectToPdfConverter::class);
$converter->addConvertedVersion(Argument::type(StoredObject::class), 'fr', 'pdf')
->shouldNotBeCalled();
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
$entityWorkflowManager->getAssociatedStoredObject($entityWorkflow)->willReturn(null);
$request = new Request();
$request->setLocale('fr');
$stack = new RequestStack();
$stack->push($request);
$eventSubscriber = new ConvertToPdfBeforeSignatureStepEventSubscriber($entityWorkflowManager->reveal(), $converter->reveal(), $stack);
$registry = $this->buildRegistry($eventSubscriber);
$workflow = $registry->get($entityWorkflow, 'dummy');
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$workflow->apply($entityWorkflow, 'to_signature', ['context' => $dto, 'transition' => 'to_signature', 'transitionAt' => new \DateTimeImmutable('now')]);
self::assertEquals('signature', $entityWorkflow->getStep());
}
private function buildRegistry(EventSubscriberInterface $eventSubscriber): Registry
{
$builder = new DefinitionBuilder();
$builder
->setInitialPlaces('initial')
->addPlaces(['initial', 'signature', 'something'])
->addTransition(new Transition('to_something', 'initial', 'something'))
->addTransition(new Transition('to_signature', 'initial', 'signature'));
$metadataStore = new InMemoryMetadataStore([], ['signature' => ['isSignature' => ['user']]]);
$builder->setMetadataStore($metadataStore);
$eventDispatcher = new EventDispatcher();
$eventDispatcher->addSubscriber($eventSubscriber);
$workflow = new Workflow($builder->build(), new EntityWorkflowMarkingStore(), $eventDispatcher, 'dummy');
$supports = new class () implements WorkflowSupportStrategyInterface {
public function supports(WorkflowInterface $workflow, object $subject): bool
{
return true;
}
};
$registry = new Registry();
$registry->addWorkflow($workflow, $supports);
return $registry;
}
}

View File

@@ -1,75 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Workflow;
use Chill\DocStoreBundle\Service\StoredObjectToPdfConverter;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Workflow\Event\CompletedEvent;
use Symfony\Component\Workflow\WorkflowEvents;
/**
* Event subscriber to convert objects to PDF when the document reach a signature step.
*/
class ConvertToPdfBeforeSignatureStepEventSubscriber implements EventSubscriberInterface
{
public function __construct(
private readonly EntityWorkflowManager $entityWorkflowManager,
private readonly StoredObjectToPdfConverter $storedObjectToPdfConverter,
private readonly RequestStack $requestStack,
) {}
public static function getSubscribedEvents(): array
{
return [
WorkflowEvents::COMPLETED => 'convertToPdfBeforeSignatureStepEvent',
];
}
public function convertToPdfBeforeSignatureStepEvent(CompletedEvent $event): void
{
$entityWorkflow = $event->getSubject();
if (!$entityWorkflow instanceof EntityWorkflow) {
return;
}
$tos = $event->getTransition()->getTos();
$workflow = $event->getWorkflow();
$metadataStore = $workflow->getMetadataStore();
foreach ($tos as $to) {
$metadata = $metadataStore->getPlaceMetadata($to);
if (array_key_exists('isSignature', $metadata) && 0 < count($metadata['isSignature'])) {
$this->convertToPdf($entityWorkflow);
return;
}
}
}
private function convertToPdf(EntityWorkflow $entityWorkflow): void
{
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
if (null === $storedObject) {
return;
}
if ('application/pdf' === $storedObject->getCurrentVersion()->getType()) {
return;
}
$this->storedObjectToPdfConverter->addConvertedVersion($storedObject, $this->requestStack->getCurrentRequest()->getLocale(), 'pdf');
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\Migrations\DocStore;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20241118151618 extends AbstractMigration
{
public function getDescription(): string
{
return 'Force no duplicated object_id within person_document and accompanyingcourse_document';
}
public function up(Schema $schema): void
{
$this->addSql(<<<'SQL'
WITH ranked AS (
SELECT id, rank() OVER (PARTITION BY object_id ORDER BY id ASC) FROM chill_doc.accompanyingcourse_document
)
DELETE FROM chill_doc.accompanyingcourse_document WHERE id IN (SELECT id FROM ranked where "rank" <> 1)
SQL);
$this->addSql('CREATE UNIQUE INDEX acc_course_document_unique_stored_object ON chill_doc.accompanyingcourse_document (object_id)');
$this->addSql('CREATE UNIQUE INDEX person_document_unique_stored_object ON chill_doc.person_document (object_id)');
}
public function down(Schema $schema): void
{
$this->addSql('DROP INDEX acc_course_document_unique_stored_object');
$this->addSql('DROP INDEX person_document_unique_stored_object');
}
}

View File

@@ -74,6 +74,8 @@ no records found:
Create new category: Créer une nouvelle catégorie
Back to the category list: Retour à la liste
Create new DocumentCategory: Créer une nouvelle catégorie de document
Accompanying period document: Document de parcours d'accompagnement
Person document: Document de personne
# WOPI EDIT
online_edit_document: Éditer en ligne

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\EventBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Class Status.
@@ -36,6 +37,7 @@ class Status
private $name;
#[ORM\ManyToOne(targetEntity: EventType::class, inversedBy: 'statuses')]
#[Assert\NotNull(message: 'An event status must be linked to an event type.')]
private ?EventType $type = null;
/**

View File

@@ -1,10 +1,10 @@
{% extends "@ChillEvent/Admin/index.html.twig" %}
{% extends '@ChillMain/Admin/layoutWithVerticalMenu.html.twig' %}
{% block admin_content -%}
<h1>{{ 'EventType list'|trans }}</h1>
<table class="records_list">
<table class="table table-bordered border-dark align-middle">
<thead>
<tr>
<th>{{ 'Id'|trans }}</th>

View File

@@ -1,10 +1,10 @@
{% extends "@ChillEvent/Admin/index.html.twig" %}
{% extends '@ChillMain/Admin/layoutWithVerticalMenu.html.twig' %}
{% block admin_content -%}
<h1>{{ 'Role list'|trans }}</h1>
<table class="records_list">
<table class="table table-bordered border-dark align-middle">
<thead>
<tr>
<th>{{ 'Id'|trans }}</th>

View File

@@ -1,10 +1,10 @@
{% extends "@ChillEvent/Admin/index.html.twig" %}
{% extends '@ChillMain/Admin/layoutWithVerticalMenu.html.twig' %}
{% block admin_content -%}
<h1>{{ 'Status list'|trans }}</h1>
<table class="records_list">
<table class="table table-bordered border-dark align-middle">
<thead>
<tr>
<th>{{ 'Id'|trans }}</th>

View File

@@ -14,7 +14,7 @@ namespace Chill\EventBundle\Security\Authorization;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Repository\EventRepository;
use Chill\EventBundle\Security\EventVoter;
@@ -25,7 +25,7 @@ class EventStoredObjectVoter extends AbstractStoredObjectVoter
public function __construct(
private readonly EventRepository $repository,
Security $security,
WorkflowStoredObjectPermissionHelper $workflowDocumentService,
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
) {
parent::__construct($security, $workflowDocumentService);
}

View File

@@ -1,19 +0,0 @@
footer.footer {
padding: 0;
background-color: white;
border-top: 1px solid grey;
div.sponsors {
p {
padding-bottom: 10px;
color: #000;
font-size: 16px;
}
background-color: white;
padding: 2em 0;
img {
display: block;
margin: auto;
}
}
}

View File

@@ -1 +0,0 @@
require('./csconnectes.scss');

View File

@@ -5,8 +5,7 @@ module.exports = function(encore, chillEntries)
personal_situation_edit_file = __dirname + '/Resources/public/module/personal_situation/index.js',
cv_edit_file = __dirname + '/Resources/public/module/cv_edit/index.js',
immersion_edit_file = __dirname + '/Resources/public/module/immersion_edit/index.js',
images = __dirname + '/Resources/public/images/index.js',
sass_styles = __dirname + '/Resources/public/sass/index.js'
images = __dirname + '/Resources/public/images/index.js'
;
encore.addEntry('dispositifs_edit', dispositif_edit_file);
@@ -15,6 +14,4 @@ module.exports = function(encore, chillEntries)
encore.addEntry('images', images);
encore.addEntry('cs_cv', cv_edit_file);
chillEntries.push(sass_styles);
};

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Symfony\Component\HttpFoundation\Request;
class GenderApiController extends ApiController
{
protected function customizeQuery(string $action, Request $request, $query): void
{
$query
->andWhere(
$query->expr()->eq('e.active', "'TRUE'")
);
}
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator, $_format)
{
return $query->addOrderBy('e.order', 'ASC');
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\CRUDController;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Symfony\Component\HttpFoundation\Request;
class GenderController extends CRUDController
{
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
{
$query->addOrderBy('e.order', 'ASC');
return parent::orderQuery($action, $query, $request, $paginator);
}
}

View File

@@ -12,7 +12,9 @@ declare(strict_types=1);
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\CRUDController;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\GroupCenter;
use Chill\MainBundle\Entity\PermissionsGroup;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\Type\ComposedGroupCenterType;
use Chill\MainBundle\Form\UserCurrentLocationType;
@@ -64,10 +66,14 @@ class UserController extends CRUDController
$form->handleRequest($request);
if ($form->isValid()) {
$groupCenter = $this->getPersistedGroupCenter(
$form[self::FORM_GROUP_CENTER_COMPOSED]->getData()
);
$user->addGroupCenter($groupCenter);
$formData = $form[self::FORM_GROUP_CENTER_COMPOSED]->getData();
$selectedCenters = $formData['center'];
foreach ($selectedCenters as $center) {
$groupCenter = $this->getPersistedGroupCenter($center, $formData['permissionsgroup']);
$user->addGroupCenter($groupCenter);
}
if (0 === $this->validator->validate($user)->count()) {
$em->flush();
@@ -419,17 +425,21 @@ class UserController extends CRUDController
}
}
private function getPersistedGroupCenter(GroupCenter $groupCenter)
private function getPersistedGroupCenter(Center $center, PermissionsGroup $permissionsGroup)
{
$em = $this->managerRegistry->getManager();
$groupCenterManaged = $em->getRepository(GroupCenter::class)
->findOneBy([
'center' => $groupCenter->getCenter(),
'permissionsGroup' => $groupCenter->getPermissionsGroup(),
'center' => $center,
'permissionsGroup' => $permissionsGroup,
]);
if (!$groupCenterManaged) {
$groupCenter = new GroupCenter();
$groupCenter->setCenter($center);
$groupCenter->setPermissionsGroup($permissionsGroup);
$em->persist($groupCenter);
return $groupCenter;

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 Chill\MainBundle\DataFixtures\ORM;
use Chill\MainBundle\Entity\Gender;
use Chill\MainBundle\Entity\GenderEnum;
use Chill\MainBundle\Entity\GenderIconEnum;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Persistence\ObjectManager;
class LoadGenders extends AbstractFixture implements OrderedFixtureInterface
{
private array $genders = [
[
'label' => ['en' => 'man', 'fr' => 'homme'],
'genderTranslation' => GenderEnum::MALE,
'icon' => GenderIconEnum::MALE,
],
[
'label' => ['en' => 'woman', 'fr' => 'femme'],
'genderTranslation' => GenderEnum::FEMALE,
'icon' => GenderIconEnum::FEMALE,
],
[
'label' => ['en' => 'neutral', 'fr' => 'neutre'],
'genderTranslation' => GenderEnum::NEUTRAL,
'icon' => GenderIconEnum::NEUTRAL,
],
];
public function getOrder()
{
return 100;
}
public function load(ObjectManager $manager)
{
echo "loading genders... \n";
foreach ($this->genders as $g) {
echo $g['label']['fr'].' ';
$new_g = new Gender();
$new_g->setGenderTranslation($g['genderTranslation']);
$new_g->setLabel($g['label']);
$new_g->setIcon($g['icon']);
$this->addReference('g_'.$g['genderTranslation']->value, $new_g);
$manager->persist($new_g);
}
$manager->flush();
}
}

View File

@@ -17,6 +17,8 @@ use Chill\MainBundle\Controller\CivilityApiController;
use Chill\MainBundle\Controller\CivilityController;
use Chill\MainBundle\Controller\CountryApiController;
use Chill\MainBundle\Controller\CountryController;
use Chill\MainBundle\Controller\GenderApiController;
use Chill\MainBundle\Controller\GenderController;
use Chill\MainBundle\Controller\GeographicalUnitApiController;
use Chill\MainBundle\Controller\LanguageController;
use Chill\MainBundle\Controller\LocationController;
@@ -54,6 +56,7 @@ use Chill\MainBundle\Doctrine\Type\PointType;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Civility;
use Chill\MainBundle\Entity\Country;
use Chill\MainBundle\Entity\Gender;
use Chill\MainBundle\Entity\GeographicalUnitLayer;
use Chill\MainBundle\Entity\Language;
use Chill\MainBundle\Entity\Location;
@@ -66,6 +69,7 @@ use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Form\CenterType;
use Chill\MainBundle\Form\CivilityType;
use Chill\MainBundle\Form\CountryType;
use Chill\MainBundle\Form\GenderType;
use Chill\MainBundle\Form\LanguageType;
use Chill\MainBundle\Form\LocationFormType;
use Chill\MainBundle\Form\LocationTypeType;
@@ -511,6 +515,28 @@ class ChillMainExtension extends Extension implements
],
],
],
[
'class' => Gender::class,
'name' => 'main_gender',
'base_path' => '/admin/main/gender',
'base_role' => 'ROLE_ADMIN',
'form_class' => GenderType::class,
'controller' => GenderController::class,
'actions' => [
'index' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillMain/Gender/index.html.twig',
],
'new' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillMain/Gender/new.html.twig',
],
'edit' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillMain/Gender/edit.html.twig',
],
],
],
[
'class' => Language::class,
'name' => 'main_language',
@@ -814,6 +840,21 @@ class ChillMainExtension extends Extension implements
],
],
],
[
'class' => Gender::class,
'name' => 'gender',
'base_path' => '/api/1.0/main/gender',
'base_role' => 'ROLE_USER',
'controller' => GenderApiController::class,
'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
],
],
],
],
[
'class' => GeographicalUnitLayer::class,
'controller' => GeographicalUnitApiController::class,

View File

@@ -0,0 +1,104 @@
<?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\Entity;
use Chill\MainBundle\Repository\GenderRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;
#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['chill_main_gender' => Gender::class])]
#[ORM\Entity(repositoryClass: GenderRepository::class)]
#[ORM\Table(name: 'chill_main_gender')]
class Gender
{
#[Serializer\Groups(['read'])]
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
private ?int $id = null;
#[Serializer\Groups(['read'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON)]
private array $label = [];
#[Serializer\Groups(['read'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN)]
private bool $active = true;
#[Assert\NotNull(message: 'You must choose a gender translation')]
#[Serializer\Groups(['read'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, enumType: GenderEnum::class)]
private GenderEnum $genderTranslation;
#[Serializer\Groups(['read'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, enumType: GenderIconEnum::class)]
private GenderIconEnum $icon;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::FLOAT, name: 'ordering', nullable: true, options: ['default' => '0.0'])]
private float $order = 0;
public function getId(): int
{
return $this->id;
}
public function getLabel(): array
{
return $this->label;
}
public function setLabel(array $label): void
{
$this->label = $label;
}
public function isActive(): bool
{
return $this->active;
}
public function setActive(bool $active): void
{
$this->active = $active;
}
public function getGenderTranslation(): GenderEnum
{
return $this->genderTranslation;
}
public function setGenderTranslation(GenderEnum $genderTranslation): void
{
$this->genderTranslation = $genderTranslation;
}
public function getIcon(): GenderIconEnum
{
return $this->icon;
}
public function setIcon(GenderIconEnum $icon): void
{
$this->icon = $icon;
}
public function getOrder(): float
{
return $this->order;
}
public function setOrder(float $order): void
{
$this->order = $order;
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Entity;
enum GenderEnum: string
{
case MALE = 'man';
case FEMALE = 'woman';
case NEUTRAL = 'neutral';
case UNKNOWN = 'unknown';
}

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\MainBundle\Entity;
enum GenderIconEnum: string
{
case MALE = 'bi bi-gender-male';
case FEMALE = 'bi bi-gender-female';
case NEUTRAL = 'bi bi-gender-neuter';
case AMBIGUOUS = 'bi bi-gender-ambiguous';
case TRANS = 'bi bi-gender-trans';
case UNKNOWN = 'bi bi-question';
}

View File

@@ -243,6 +243,9 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
throw new \RuntimeException();
}
/**
* @return Selectable<int, EntityWorkflowStep>&Collection<int, EntityWorkflowStep>
*/
public function getSteps(): Collection&Selectable
{
return $this->steps;
@@ -431,6 +434,7 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
$previousStep = $this->getCurrentStep();
$previousStep
->setComment($transitionContextDTO->comment)
->setTransitionAfter($transition)
->setTransitionAt($transitionAt)
->setTransitionBy($byUser);

View File

@@ -56,7 +56,7 @@ class EntityWorkflowSend implements TrackCreationInterface
/**
* @var Collection<int, EntityWorkflowSendView>
*/
#[ORM\OneToMany(targetEntity: EntityWorkflowSendView::class, mappedBy: 'send')]
#[ORM\OneToMany(mappedBy: 'send', targetEntity: EntityWorkflowSendView::class, cascade: ['remove'])]
private Collection $views;
public function __construct(

View File

@@ -66,7 +66,7 @@ class EntityWorkflowStep
/**
* @var Collection <int, EntityWorkflowStepSignature>
*/
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepSignature::class, cascade: ['persist'], orphanRemoval: true)]
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepSignature::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
private Collection $signatures;
#[ORM\ManyToOne(targetEntity: EntityWorkflow::class, inversedBy: 'steps')]
@@ -115,7 +115,7 @@ class EntityWorkflowStep
/**
* @var Collection<int, EntityWorkflowSend>
*/
#[ORM\OneToMany(mappedBy: 'entityWorkflowStep', targetEntity: EntityWorkflowSend::class, cascade: ['persist'], orphanRemoval: true)]
#[ORM\OneToMany(mappedBy: 'entityWorkflowStep', targetEntity: EntityWorkflowSend::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
private Collection $sends;
public function __construct()

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Form;
use Chill\MainBundle\Entity\Gender;
use Chill\MainBundle\Entity\GenderEnum;
use Chill\MainBundle\Entity\GenderIconEnum;
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class GenderType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('label', TranslatableStringFormType::class, [
'required' => true,
])
->add('icon', EnumType::class, [
'class' => GenderIconEnum::class,
'choices' => GenderIconEnum::cases(),
'expanded' => true,
'multiple' => false,
'mapped' => true,
'choice_label' => fn (GenderIconEnum $enum) => '<i class="'.strtolower($enum->value).'"></i>',
'choice_value' => fn (?GenderIconEnum $enum) => null !== $enum ? $enum->value : null,
'label' => 'gender.admin.Select gender icon',
'label_html' => true,
])
->add('genderTranslation', EnumType::class, [
'class' => GenderEnum::class,
'choice_label' => fn (GenderEnum $enum) => $enum->value,
'label' => 'gender.admin.Select gender translation',
])
->add('active', ChoiceType::class, [
'choices' => [
'Active' => true,
'Inactive' => false,
],
])
->add('order', NumberType::class);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Gender::class,
]);
}
}

View File

@@ -13,36 +13,30 @@ namespace Chill\MainBundle\Form\Type;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\PermissionsGroup;
use Doctrine\ORM\EntityRepository;
use Chill\MainBundle\Repository\CenterRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ComposedGroupCenterType extends AbstractType
{
public function __construct(private readonly CenterRepository $centerRepository) {}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$centers = $this->centerRepository->findActive();
$builder->add('permissionsgroup', EntityType::class, [
'class' => PermissionsGroup::class,
'choice_label' => static fn (PermissionsGroup $group) => $group->getName(),
])->add('center', EntityType::class, [
'class' => Center::class,
'query_builder' => static function (EntityRepository $er) {
$qb = $er->createQueryBuilder('c');
$qb->where($qb->expr()->eq('c.isActive', 'TRUE'))
->orderBy('c.name', 'ASC');
return $qb;
},
])->add('center', ChoiceType::class, [
'choices' => $centers,
'choice_label' => fn (Center $center) => $center->getName(),
'multiple' => true,
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('data_class', \Chill\MainBundle\Entity\GroupCenter::class);
}
public function getBlockPrefix()
{
return 'composed_groupcenter';

View File

@@ -39,6 +39,7 @@ class UserGroupType extends AbstractType
'label' => 'user_group.Email',
'help' => 'user_group.EmailHelp',
'empty_data' => '',
'required' => false,
])
->add('excludeKey', TextType::class, [
'label' => 'user_group.ExcludeKey',

View File

@@ -0,0 +1,47 @@
<?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\Repository;
use Chill\MainBundle\Entity\Gender;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Gender>
*/
class GenderRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Gender::class);
}
public function findByActiveOrdered(): array
{
return $this->createQueryBuilder('g')
->select('g')
->where('g.active = True')
->orderBy('g.order', 'ASC')
->getQuery()
->getResult();
}
public function findByGenderTranslation($gender): array
{
return $this->createQueryBuilder('g')
->select('g')
->where('g.genderTranslation = :gender')
->setParameter('gender', $gender)
->getQuery()
->getResult();
}
}

View File

@@ -10,6 +10,7 @@ import Modal from 'bootstrap/js/dist/modal';
import Collapse from 'bootstrap/js/src/collapse';
import Carousel from 'bootstrap/js/src/carousel';
import Popover from 'bootstrap/js/src/popover';
import 'bootstrap-icons/font/bootstrap-icons.css';
//
// Carousel: ACHeaderSlider is a small slider used in banner of AccompanyingCourse Section
@@ -59,4 +60,4 @@ const popoverList = triggerList.map(function (el) {
return new Popover(el, {
html: true,
});
});
});

View File

@@ -25,6 +25,10 @@ window.addEventListener('DOMContentLoaded', function() {
for (let transition of froms) {
for (let input of transition.querySelectorAll('input')) {
if (input.checked) {
if ('1' === input.dataset.toFinal) {
return false;
}
if ('1' === input.dataset.isSentExternal) {
return false;
}
@@ -151,6 +155,7 @@ window.addEventListener('DOMContentLoaded', function() {
}
}
}
return true;
},
toggle_callback: function (c, dir) {
for (let div of c) {

View File

@@ -0,0 +1,11 @@
<template>
<i :class="gender.icon"></i>
</template>
<script setup>
const props = defineProps({
gender: Object
})
</script>

View File

@@ -1,20 +1,13 @@
<template>
<button v-if="hasWorkflow"
class="btn btn-primary"
@click="openModal">
<b>{{ countWorkflows }}</b>
<template v-if="countWorkflows > 1">{{ $t('workflows') }}</template>
<template v-else>{{ $t('workflow') }}</template>
</button>
<pick-workflow v-else-if="allowCreate"
<pick-workflow
:relatedEntityClass="this.relatedEntityClass"
:relatedEntityId="this.relatedEntityId"
:workflowsAvailables="workflowsAvailables"
:preventDefaultMoveToGenerate="this.$props.preventDefaultMoveToGenerate"
:goToGenerateWorkflowPayload="this.goToGenerateWorkflowPayload"
:countExistingWorkflows="countWorkflows"
@go-to-generate-workflow="goToGenerateWorkflow"
@click-open-list="openModal"
></pick-workflow>
<teleport to="body">
@@ -39,6 +32,8 @@
:workflowsAvailables="workflowsAvailables"
:preventDefaultMoveToGenerate="this.$props.preventDefaultMoveToGenerate"
:goToGenerateWorkflowPayload="this.goToGenerateWorkflowPayload"
:countExistingWorkflows="countWorkflows"
:embedded-within-list-modal="true"
@go-to-generate-workflow="this.goToGenerateWorkflow"
></pick-workflow>
</template>

View File

@@ -1,6 +1,6 @@
<template>
<template v-if="props.workflowsAvailables.length >= 1">
<div class="dropdown d-grid gap-2">
<div v-if="countExistingWorkflows == 0 || embeddedWithinListModal" class="dropdown d-grid gap-2">
<button class="btn btn-primary dropdown-toggle" type="button" id="createWorkflowButton" data-bs-toggle="dropdown" aria-expanded="false">
Créer un workflow
</button>
@@ -10,6 +10,38 @@
</li>
</ul>
</div>
<div v-else>
<div class="btn-group">
<button @click="emit('clickOpenList')" class="btn btn-primary">
<template v-if="countExistingWorkflows === 1">
1 workflow associé
</template>
<template v-else>
{{ countExistingWorkflows }} workflows associés
</template>
</button>
<button class="btn btn-primary dropdown-toggle dropdown-toggle-split" type="button" id="createWorkflowButton" data-bs-toggle="dropdown" aria-expanded="false">
<span class="visually-hidden">Liste des workflows disponibles</span>
</button>
<ul class="dropdown-menu" aria-labelledby="createWorkflowButton">
<li v-for="w in props.workflowsAvailables" :key="w.name">
<button class="dropdown-item" type="button" @click.prevent="goToGenerateWorkflow($event, w.name)">{{ w.text }}</button>
</li>
</ul>
</div>
</div>
</template>
<template v-else>
<div v-if="countExistingWorkflows > 0" class="dropdown d-grid gap-2">
<button @click="emit('clickOpenList')" class="btn btn-primary" type="button" id="createWorkflowButton" data-bs-toggle="dropdown" aria-expanded="false">
<template v-if="countExistingWorkflows === 1">
1 workflow associé
</template>
<template v-else>
{{ countExistingWorkflows }} workflows associés
</template>
</button>
</div>
</template>
</template>
@@ -30,12 +62,18 @@ interface PickWorkflowConfig {
workflowsAvailables: WorkflowAvailable[];
preventDefaultMoveToGenerate: boolean;
goToGenerateWorkflowPayload: object;
countExistingWorkflows: number;
/**
* if true, this button will not present a splitted button
*/
embeddedWithinListModal: boolean;
}
const props = withDefaults(defineProps<PickWorkflowConfig>(), {preventDefaultMoveToGenerate: false, goToGenerateWorkflowPayload: {}});
const props = withDefaults(defineProps<PickWorkflowConfig>(), {preventDefaultMoveToGenerate: false, goToGenerateWorkflowPayload: {}, allowCreateWorkflow: false});
const emit = defineEmits<{
(e: 'goToGenerateWorkflow', {event: MouseEvent, workflowName: string, isLinkValid: boolean, link: string, payload: object}): void;
(e: 'clickOpenList'): void;
}>();
const makeLink = (workflowName: string): string => buildLinkCreate(workflowName, props.relatedEntityClass, props.relatedEntityId);

View File

@@ -38,7 +38,9 @@ const messages = {
person: "Usager",
birthday: {
man: "Né le",
woman: "Née le"
woman: "Née le",
neutral: "Né·e le",
unknown: "Né·e le",
},
deathdate: "Date de décès",
household_without_address: "Le ménage de l'usager est sans adresse",

View File

@@ -0,0 +1,75 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %}
{% include('@ChillMain/CRUD/_edit_title.html.twig') %}
{% endblock %}
{% form_theme form _self %}
{% block _gender_icon_widget %}
{% for child in form %}
<div class="form-check">
<input
type="radio"
id="{{ child.vars.id }}"
name="{{ child.vars.full_name }}"
value="{{ child.vars.value }}"
{% if child.vars.checked %}checked="checked"{% endif %}
/>
<label for="{{ child.vars.id }}">{{ child.vars.label|raw }}</label>
</div>
{% endfor %}
{% endblock %}
{% block admin_content %}
{% set formId = crudMainFormId|default('crud_main_form') %}
{% block crud_content_header %}
<h1 class="mb-5">{{ ('crud.'~crud_name~'.title_edit')|trans }}</h1>
{% endblock crud_content_header %}
{% block crud_content_form %}
{{ form_start(form, { 'attr' : { 'id': formId } }) }}
{{ form_row(form.label) }}
{{ form_row(form.genderTranslation) }}
{{ form_row(form.icon) }}
{{ form_row(form.active) }}
{{ form_row(form.order) }}
{{ form_end(form) }}
{% block crud_content_after_form %}{% endblock %}
{% block crud_content_form_actions %}
<ul class="record_actions sticky-form-buttons">
{% block content_form_actions_back %}
<li class="cancel">
{# <a class="btn btn-cancel" href="{{ chill_return_path_or('chill_crud_'~crud_name~'_index') }}">#}
{# {{ 'Cancel'|trans }}#}
{# </a>#}
</li>
{% endblock %}
{% block content_form_actions_before %}{% endblock %}
{% block content_form_actions_delete %}
{% if chill_crud_action_exists(crud_name, 'delete') %}
{% if is_granted(chill_crud_config('role', crud_name, 'delete'), entity) %}
<li class="">
<a class="btn btn-small btn-delete" href="{{ chill_path_add_return_path('chill_crud_'~crud_name~'_delete', { 'id': entity.id }) }}"></a>
</li>
{% endif %}
{% endif %}
{% endblock content_form_actions_delete %}
{% block content_form_actions_save_and_close %}
<li class="">
<button type="submit" name="submit" value="save-and-close" class="btn btn-update" form="{{ formId }}">
{{ 'crud.edit.save_and_close'|trans }}
</button>
</li>
{% endblock %}
{% block content_form_actions_after %}{% endblock %}
</ul>
{% endblock %}
{% endblock %}
{% endblock admin_content %}

View File

@@ -0,0 +1,46 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block admin_content %}
{% embed '@ChillMain/CRUD/_index.html.twig' %}
{% block table_entities_thead_tr %}
<th>id</th>
<th>{{ 'label'|trans }}</th>
<th>{{ 'icon'|trans }}</th>
<th>{{ 'gender.genderTranslation'|trans }}</th>
<th>{{ 'active'|trans }}</th>
<th>{{ 'ordering'|trans }}</th>
<th></th>
{% endblock %}
{% block table_entities_tbody %}
{% for entity in entities %}
<tr>
<td>{{ entity.id }}</td>
<td>{{ entity.label|localize_translatable_string }}</td>
<td>{{ entity.icon|chill_entity_render_box }}</td>
<td>{{ entity.genderTranslation.value }}</td>
<td style="text-align:center;">
{%- if entity.active -%}
<i class="fa fa-check-square-o"></i>
{%- else -%}
<i class="fa fa-square-o"></i>
{%- endif -%}
</td>
<td>{{ entity.order }}</td>
<td>
<ul class="record_actions">
<li>
<a href="{{ chill_path_add_return_path('chill_crud_main_gender_edit', { 'id': entity.id}) }}" class="btn btn-sm btn-edit btn-mini"></a>
</li>
</ul>
</td>
</tr>
{% endfor %}
{% endblock %}
{% block actions_before %}
<li class='cancel'>
<a href="{{ path('chill_main_admin_central') }}" class="btn btn-cancel">{{'Back to the admin'|trans}}</a>
</li>
{% endblock %}
{% endembed %}
{% endblock %}

View File

@@ -0,0 +1,79 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %}
{% include('@ChillMain/CRUD/_new_title.html.twig') %}
{% endblock %}
{% form_theme form _self %}
{% block _gender_icon_widget %}
{% for child in form %}
<div class="form-check">
<input
type="radio"
id="{{ child.vars.id }}"
name="{{ child.vars.full_name }}"
value="{{ child.vars.value }}"
{% if child.vars.checked %}checked="checked"{% endif %}
/>
<label for="{{ child.vars.id }}">{{ child.vars.label|raw }}</label>
</div>
{% endfor %}
{% endblock %}
{% block admin_content %}
{% set formId = crudMainFormId|default('crud_main_form') %}
{% block crud_content_header %}
<h1>{{ ('crud.' ~ crud_name ~ '.title_new')|trans({'%crud_name%' : crud_name }) }}</h1>
{% endblock crud_content_header %}
{% block crud_content_form %}
{{ form_start(form, { 'attr' : { 'id': formId } }) }}
{{ form_row(form.label) }}
{{ form_row(form.genderTranslation) }}
{{ form_row(form.icon) }}
{{ form_row(form.active) }}
{{ form_row(form.order) }}
{{ form_end(form) }}
{% block crud_content_after_form %}{% endblock %}
{% block crud_content_form_actions %}
<ul class="record_actions sticky-form-buttons">
{% block content_form_actions_back %}
<li class="cancel">
<a class="btn btn-cancel" href="{{ chill_return_path_or('chill_crud_'~crud_name~'_index') }}">
{{ 'Cancel'|trans }}
</a>
</li>
{% endblock %}
{% block content_form_actions_save_and_close %}
<li class="">
<button type="submit" name="submit" value="save-and-close" class="btn btn-create" form="{{ formId }}">
{{ 'crud.new.save_and_close'|trans }}
</button>
</li>
{% endblock %}
{% block content_form_actions_save_and_show %}
<li class="">
<button type="submit" name="submit" value="save-and-show" class="btn btn-create" form="{{ formId }}">
{{ 'crud.new.save_and_show'|trans }}
</button>
</li>
{% endblock %}
{% block content_form_actions_save_and_new %}
<li class="">
<button type="submit" name="submit" value="save-and-new" class="btn btn-create" form="{{ formId }}">
{{ 'crud.new.save_and_new'|trans }}
</button>
</li>
{% endblock %}
</ul>
{% endblock %}
{{ form_end(form) }}
{% endblock %}
{% endblock admin_content %}

View File

@@ -42,7 +42,7 @@
<div class="item-col" style="width: inherit;">
<div>
{%- if step.transitionBy is not null -%}
{{ step.transitionBy|chill_entity_render_box({'at_date': step.transitionAt}) }}
<span class="badge-user">{{ step.transitionBy|chill_entity_render_box({'at_date': step.transitionAt}) }}</span>
{% else %}
<span class="chill-no-data-statement">{{ 'workflow.Automated transition'|trans }}</span>
{%- endif -%}
@@ -74,7 +74,7 @@
{% if not loop.last and step.signatures|length > 0 %}
<div class="separator">
<div>
<p><b>{{ 'workflow.signature_required_title'|trans({'nb_signatures': step.signatures|length}) }}&nbsp;:</b></p>
<p><b>{{ 'workflow.signatures_title'|trans({'nb_signatures': step.signatures|length}) }}&nbsp;:</b></p>
<div>
{{ include('@ChillMain/Workflow/_signature_list.html.twig', {'signatures': step.signatures, is_small: true }) }}
</div>
@@ -105,7 +105,7 @@
<p><b>{{ 'workflow.Users put in Cc'|trans }}&nbsp;: </b></p>
<ul>
{% for u in step.ccUser %}
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li>
<li><span class="badge-user">{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</span></li>
{% endfor %}
</ul>
{% endif %}
@@ -123,14 +123,14 @@
<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({'at_date': step.previous.transitionAt}) }}</li>
<li><span class="badge-user">{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</span></li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
{% if step.signatures|length > 0 %}
<p><b>{{ 'workflow.signature_required_title'|trans({'nb_signatures': step.signatures|length}) }}&nbsp;:</b></p>
<p><b>{{ 'workflow.signatures_title'|trans({'nb_signatures': step.signatures|length}) }}&nbsp;:</b></p>
{{ include('@ChillMain/Workflow/_signature_list.html.twig', {'signatures': step.signatures, is_small: true }) }}
{% endif %}
{% endif %}

View File

@@ -1,11 +1,15 @@
<h2>
{{ 'workflow_'|trans }}
</h2>
{% if handler is not null %}
<h2>
{{ 'workflow_'|trans }}
</h2>
<div class="item-row col" style="display: block;">
{% include handler.template(entity_workflow) with handler.templateData(entity_workflow)|merge({
'description': true,
'breadcrumb': true,
'add_classes': ''
}) %}
</div>
<div class="item-row col" style="display: block;">
{% include handler.template(entity_workflow) with handler.templateData(entity_workflow)|merge({
'description': true,
'breadcrumb': true,
'add_classes': ''
}) %}
</div>
{% else %}
<h2>{{ 'workflow.deleted_title'|trans }}</h2>
{% endif %}

View File

@@ -37,7 +37,7 @@
<div class="mb-5">
<h2>{{ handler.entityTitle(entity_workflow) }}</h2>
{{ macro.breadcrumb({'entity_workflow': entity_workflow}) }}
{{ macro.breadcrumb(entity_workflow) }}
{% if entity_workflow.isOnHoldAtCurrentStep %}
<span class="badge bg-success rounded-pill" title="{{ 'workflow.On hold'|trans|escape('html_attr') }}">{{ 'workflow.On hold'|trans }}</span>
{% endif %}

View File

@@ -68,7 +68,7 @@
</button>
<div>
{{ macro.breadcrumb(l) }}
{{ macro.breadcrumb(l.entity_workflow) }}
{% if l.entity_workflow.isOnHoldAtCurrentStep %}
<span class="badge bg-success rounded-pill" title="{{ 'workflow.On hold'|trans|escape('html_attr') }}">{{ 'workflow.On hold'|trans }}</span>
{% endif %}

View File

@@ -9,11 +9,13 @@
<span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span>
<b>{{ step.previous.transitionAt|format_datetime('short', 'short') }}</b>
</li>
{% if step.destUser|length > 0 %}
{% if step.destUser|length > 0 or step.destUserGroups|length > 0 %}
<li>
<span class="item-key">{{ 'workflow.For'|trans ~ ' : ' }}</span>
<b>
{% for d in step.destUser %}{{ d|chill_entity_render_string({'at_date': step.previous.transitionAt}) }}{% if not loop.last %}, {% endif %}{% endfor %}
{% for d in step.destUser %}<span class="badge-user">{{ d|chill_entity_render_string({'at_date': step.previous.transitionAt}) }}</span>{% if not loop.last %}, {% endif -%}{% endfor -%}
{%- if step.destUser|length > 0 and step.destUserGroups|length > 0 %}, {% endif -%}
{%- for d in step.destUserGroups %}{{ d|chill_entity_render_box }}{% if not loop.last %}, {% endif %}{% endfor -%}
</b>
</li>
{% endif %}
@@ -50,10 +52,10 @@
{% endif %}
{% endmacro %}
{% macro breadcrumb(_ctx) %}
{% macro breadcrumb(entity_workflow) %}
<div class="breadcrumb">
{% for step in _ctx.entity_workflow.stepsChained %}
{% set labels = workflow_metadata(_ctx.entity_workflow, 'label', step.currentStep, _ctx.entity_workflow.workflowName) %}
{% for step in entity_workflow.stepsChained %}
{% set labels = workflow_metadata(entity_workflow, 'label', step.currentStep, entity_workflow.workflowName) %}
{% set label = labels is null ? step.currentStep : labels|localize_translatable_string %}
{% set popTitle = _self.popoverTitle(step) %}
{% set popContent = _self.popoverContent(step) %}

View File

@@ -1,11 +1,14 @@
{{ dest.label }},
Un suivi "{{ workflow.text }}" a atteint une nouvelle étape: {{ workflow.text }}
{%- if is_dest %}
Titre du workflow: "{{ title }}".
{% if is_dest %}
Vous êtes invités à valider cette étape au plus tôt.
{% endif %}
Vous pouvez visualiser le workflow sur cette page:
{{ absolute_url(path('chill_main_workflow_show', {'id': entity_workflow.id, '_locale': 'fr'})) }}

View File

@@ -2,8 +2,14 @@ Chers membres du groupe {{ user_group.label|localize_translatable_string }},
Un suivi "{{ workflow.text }}" a atteint une nouvelle étape: {{ place.text }}
Vous pouvez visualiser le workflow sur cette page:
Titre du workflow: "{{ title }}".
{{ absolute_url(path('chill_main_workflow_show', {'id': entity_workflow.id, '_locale': 'fr'})) }}
Vous êtes invité·e à valider cette étape. Pour obtenir un accès, vous pouvez cliquer sur le lien suivant:
{{ absolute_url(path('chill_main_workflow_grant_access_by_key', {'id': entity_workflow.currentStep.id, '_locale': 'fr', 'accessKey': entity_workflow.currentStep.accessKey})) }}
Dès que vous aurez cliqué une fois sur le lien, vous serez autorisé à valider cette étape.
Notez que vous devez disposer d'un compte utilisateur valide dans Chill.
Cordialement,

View File

@@ -1,5 +1,5 @@
{%- if is_dest -%}
Un suivi {{ workflow.text }} demande votre attention
Un suivi {{ workflow.text }} demande votre attention: {{ title }}
{%- else -%}
Un suivi {{ workflow.text }} a atteint une nouvelle étape: {{ place.text }}
Un suivi {{ workflow.text }} a atteint une nouvelle étape: {{ place.text }}: {{ title }}
{%- endif -%}

View File

@@ -40,4 +40,15 @@ final readonly class ChillUrlGenerator implements ChillUrlGeneratorInterface
return $this->urlGenerator->generate($name, $parameters, $referenceType);
}
public function forwardReturnPath(string $name, array $parameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string
{
$request = $this->requestStack->getCurrentRequest();
if ($request->query->has('returnPath')) {
return $this->urlGenerator->generate($name, [...$parameters, 'returnPath' => $request->query->get('returnPath')], $referenceType);
}
return $this->urlGenerator->generate($name, $parameters, $referenceType);
}
}

View File

@@ -32,4 +32,9 @@ interface ChillUrlGeneratorInterface
* Get the return path or, if any, generate an url.
*/
public function returnPathOr(string $name, array $parameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string;
/**
* Return a new URL, with the same return path as the existing one. If any, no return path is forwarded.
*/
public function forwardReturnPath(string $name, array $parameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string;
}

View File

@@ -62,12 +62,12 @@ abstract class AbstractSearch implements SearchInterface
$recomposed .= ' '.$term.':';
$containsSpace = str_contains((string) $terms[$term], ' ');
if ($containsSpace) {
if ($containsSpace || is_numeric($terms[$term])) {
$recomposed .= '"';
}
$recomposed .= (false === mb_stristr(' ', (string) $terms[$term])) ? $terms[$term] : '('.$terms[$term].')';
if ($containsSpace) {
if ($containsSpace || is_numeric($terms[$term])) {
$recomposed .= '"';
}
}

View File

@@ -17,6 +17,7 @@ use Chill\MainBundle\Workflow\Helper\DuplicateEntityWorkflowFinder;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Workflow\Registry;
class EntityWorkflowVoter extends Voter
{
@@ -32,6 +33,7 @@ class EntityWorkflowVoter extends Voter
private readonly EntityWorkflowManager $manager,
private readonly Security $security,
private readonly DuplicateEntityWorkflowFinder $duplicateEntityWorkflowFinder,
private readonly Registry $registry,
) {}
protected function supports($attribute, $subject)
@@ -83,7 +85,25 @@ class EntityWorkflowVoter extends Voter
return false;
case self::DELETE:
return 'initial' === $subject->getStep();
if ('initial' === $subject->getStep()) {
return true;
}
if (!$subject->isFinal()) {
return false;
}
// the entity workflow is finalized. We check if the final is not positive. If yes, we can
// delete the entity
$workflow = $this->registry->get($subject, $subject->getWorkflowName());
foreach ($workflow->getMarking($subject)->getPlaces() as $place => $key) {
$metadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
if (false === ($metadata['isFinalPositive'] ?? true)) {
return true;
}
}
return false;
case self::SHOW_ENTITY_LINK:
if ('initial' === $subject->getStep()) {

View File

@@ -74,12 +74,14 @@ class DateNormalizer implements ContextAwareNormalizerInterface, DenormalizerInt
$formatterLong = \IntlDateFormatter::create(
$locale,
\IntlDateFormatter::LONG,
$hasTime ? \IntlDateFormatter::SHORT : \IntlDateFormatter::NONE
$hasTime ? \IntlDateFormatter::SHORT : \IntlDateFormatter::NONE,
$date->getTimezone(),
);
$formatterShort = \IntlDateFormatter::create(
$locale,
\IntlDateFormatter::SHORT,
$hasTime ? \IntlDateFormatter::SHORT : \IntlDateFormatter::NONE
$hasTime ? \IntlDateFormatter::SHORT : \IntlDateFormatter::NONE,
$date->getTimezone(),
);
return [

View File

@@ -37,6 +37,10 @@ class CancelStaleWorkflowCronJob implements CronJobInterface
public function canRun(?CronJobExecution $cronJobExecution): bool
{
if (null === $cronJobExecution) {
return true;
}
return $this->clock->now() >= $cronJobExecution->getLastEnd()->add(new \DateInterval('P1D'));
}

View File

@@ -56,9 +56,32 @@ class ChillTwigRoutingHelper extends AbstractExtension
new TwigFunction('chill_return_path_or', $this->getReturnPathOr(...), ['is_safe_callback' => $this->isUrlGenerationSafe(...)]),
new TwigFunction('chill_path_add_return_path', $this->getPathAddReturnPath(...), ['is_safe_callback' => $this->isUrlGenerationSafe(...)]),
new TwigFunction('chill_path_forward_return_path', $this->getPathForwardReturnPath(...), ['is_safe_callback' => $this->isUrlGenerationSafe(...)]),
new TwigFunction('chill_path_force_return_path', $this->getPathForceReturnPath(...), ['is_safe_callback' => $this->isUrlGenerationSafe(...)]),
];
}
/**
* Build a URL with a forced returnPath parameter.
*
* @param string $forcePath the forced path to return to
* @param string $name the name of the route
* @param array $parameters additional parameters for the URL
* @param bool $relative whether the URL should be relative
* @param string|null $label optional label for the return path
*
* @return string the generated URL
*/
public function getPathForceReturnPath(string $forcePath, string $name, array $parameters = [], $relative = false, $label = null): string
{
$params = [...$parameters, 'returnPath' => $forcePath];
if (null !== $label) {
$params['returnPathLabel'] = $label;
}
return $this->originalExtension->getPath($name, $params, $relative);
}
public function getLabelReturnPath($default)
{
$request = $this->requestStack->getCurrentRequest();

View File

@@ -35,7 +35,7 @@ interface ChillEntityRenderInterface
*
* @phpstan-pure
*/
public function renderBox($entity, array $options): string;
public function renderBox(mixed $entity, array $options): string;
/**
* Return the entity as a string.
@@ -46,7 +46,7 @@ interface ChillEntityRenderInterface
*
* @phpstan-pure
*/
public function renderString($entity, array $options): string;
public function renderString(mixed $entity, array $options): string;
/**
* Return true if the class support this object for the given options.

View File

@@ -0,0 +1,35 @@
<?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\Templating\Entity;
use Chill\MainBundle\Entity\GenderIconEnum;
/**
* @implements ChillEntityRenderInterface<GenderIconEnum>
*/
final readonly class ChillGenderIconRender implements ChillEntityRenderInterface
{
public function renderBox($icon, array $options): string
{
return '<i class="'.htmlspecialchars($icon->value, ENT_QUOTES, 'UTF-8').'"></i>';
}
public function renderString($icon, array $options): string
{
return $icon->value;
}
public function supports($icon, array $options): bool
{
return $icon instanceof GenderIconEnum;
}
}

View File

@@ -13,8 +13,6 @@ namespace Chill\MainBundle\Templating\Entity;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use DateTime;
use DateTimeImmutable;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Error\LoaderError;
@@ -26,11 +24,17 @@ use Twig\Error\SyntaxError;
*/
class UserRender implements ChillEntityRenderInterface
{
public const SPLIT_LINE_BEFORE_CHARACTER = 'split_lines_before_characters';
final public const DEFAULT_OPTIONS = [
'main_scope' => true,
'user_job' => true,
'absence' => true,
'at_date' => null, // instanceof DateTimeInterface
/*
* when set, the jobs and service will be splitted in multiple lines. The line will be splitted
* before the given character. Only for renderString, renderBox is not concerned.
*/
self::SPLIT_LINE_BEFORE_CHARACTER => null,
];
public function __construct(
@@ -65,8 +69,6 @@ class UserRender implements ChillEntityRenderInterface
{
$opts = \array_merge(self::DEFAULT_OPTIONS, $options);
// $immutableAtDate = $opts['at_date'] instanceOf DateTime ? DateTimeImmutable::createFromMutable($opts['at_date']) : $opts['at_date'];
if (null === $opts['at_date']) {
$opts['at_date'] = $this->clock->now();
} elseif ($opts['at_date'] instanceof \DateTime) {
@@ -89,6 +91,28 @@ class UserRender implements ChillEntityRenderInterface
$str .= ' ('.$this->translator->trans('absence.Absent').')';
}
if (null !== $opts[self::SPLIT_LINE_BEFORE_CHARACTER]) {
if (!is_int($opts[self::SPLIT_LINE_BEFORE_CHARACTER])) {
throw new \InvalidArgumentException('Only integer for option split_lines_before_characters is allowed');
}
$characterPerLine = $opts[self::SPLIT_LINE_BEFORE_CHARACTER];
$exploded = explode(' ', $str);
$charOnLine = 0;
$str = '';
foreach ($exploded as $word) {
if ($charOnLine + strlen($word) > $characterPerLine) {
$str .= "\n";
$charOnLine = 0;
}
if ($charOnLine > 0) {
$str .= ' ';
}
$str .= $word;
$charOnLine += strlen($word);
}
}
return $str;
}

View File

@@ -0,0 +1,194 @@
<?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\Tests\Security\Authorization;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
use Chill\MainBundle\Workflow\EventSubscriber\EntityWorkflowTransitionEventSubscriber;
use Chill\MainBundle\Workflow\Helper\DuplicateEntityWorkflowFinder;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Workflow\DefinitionBuilder;
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
use Symfony\Component\Workflow\Transition;
use Symfony\Component\Workflow\Workflow;
use Symfony\Component\Workflow\WorkflowInterface;
/**
* @internal
*
* @coversNothing
*/
class EntityWorkflowVoterTest extends TestCase
{
public function testVoteDeleteEntityWorkflowForInitialPlace(): void
{
$entityWorkflow = $this->buildEntityWorkflow();
$voter = $this->buildVoter();
$token = $this->buildToken();
$actual = $voter->vote($token, $entityWorkflow, [EntityWorkflowVoter::DELETE]);
self::assertEquals(Voter::ACCESS_GRANTED, $actual);
}
public function testVoteDeleteEntityWorkflowForSomeOherPlace(): void
{
$entityWorkflow = $this->buildEntityWorkflow();
$registry = $this->buildRegistry();
$workflow = $registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers = [new User()];
$workflow->apply($entityWorkflow, 'move_to_in_between', ['context' => $dto, 'transition' => 'move_to_in_between', 'transitionAt' => new \DateTimeImmutable()]);
assert('in_between' === $entityWorkflow->getStep(), 'we ensure that the workflow is well transitionned');
$voter = $this->buildVoter();
$token = $this->buildToken();
$actual = $voter->vote($token, $entityWorkflow, [EntityWorkflowVoter::DELETE]);
self::assertEquals(Voter::ACCESS_DENIED, $actual);
}
public function testVoteDeleteEntityWorkflowForFinalPositive(): void
{
$entityWorkflow = $this->buildEntityWorkflow();
$registry = $this->buildRegistry();
$workflow = $registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers = [new User()];
$workflow->apply($entityWorkflow, 'move_to_final_positive', ['context' => $dto, 'transition' => 'move_to_final_positive', 'transitionAt' => new \DateTimeImmutable()]);
assert('final_positive' === $entityWorkflow->getStep(), 'we ensure that the workflow is well transitionned');
$voter = $this->buildVoter();
$token = $this->buildToken();
$actual = $voter->vote($token, $entityWorkflow, [EntityWorkflowVoter::DELETE]);
self::assertEquals(Voter::ACCESS_DENIED, $actual);
}
public function testVoteDeleteEntityWorkflowForFinalNegative(): void
{
$entityWorkflow = $this->buildEntityWorkflow();
$registry = $this->buildRegistry();
$workflow = $registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers = [new User()];
$workflow->apply($entityWorkflow, 'move_to_final_negative', ['context' => $dto, 'transition' => 'move_to_final_negative', 'transitionAt' => new \DateTimeImmutable()]);
$voter = $this->buildVoter();
$token = $this->buildToken();
$actual = $voter->vote($token, $entityWorkflow, [EntityWorkflowVoter::DELETE]);
self::assertEquals(Voter::ACCESS_GRANTED, $actual);
}
private function buildToken(): TokenInterface
{
return new UsernamePasswordToken($user = new User(), 'main', $user->getRoles());
}
private function buildVoter(): EntityWorkflowVoter
{
$manager = $this->createMock(EntityWorkflowManager::class);
$security = $this->createMock(Security::class);
$duplicateEntityWorkflowFind = $this->createMock(DuplicateEntityWorkflowFinder::class);
return new EntityWorkflowVoter(
$manager,
$security,
$duplicateEntityWorkflowFind,
$this->buildRegistry()
);
}
private function buildEntityWorkflow(): EntityWorkflow
{
$entityWorkflow = new EntityWorkflow();
$entityWorkflow->setWorkflowName('dummy');
$entityWorkflow->setRelatedEntityId(1)->setRelatedEntityClass(\stdClass::class);
return $entityWorkflow;
}
private function buildRegistry(): Registry
{
$security = $this->createMock(Security::class);
$security->method('getUser')->willReturn(new User());
$builder = new DefinitionBuilder();
$builder->addPlaces(['initial', 'in_between', 'final_positive', 'final_negative']);
$metadataStore = new InMemoryMetadataStore(
placesMetadata: [
'final_positive' => [
'isFinal' => true,
'isFinalPositive' => true,
],
'final_negative' => [
'isFinal' => true,
'isFinalPositive' => false,
],
]
);
$builder->setMetadataStore($metadataStore);
$transitions = [
new Transition('move_to_in_between', 'initial', 'in_between'),
new Transition('move_to_final_positive', 'initial', 'final_positive'),
new Transition('move_to_final_negative', 'initial', 'final_negative'),
];
foreach ($transitions as $transition) {
$builder->addTransition($transition);
}
$definition = $builder->build();
$eventSubscriber = new EventDispatcher();
$eventSubscriber->addSubscriber(
new EntityWorkflowTransitionEventSubscriber(
new NullLogger(),
$security
)
);
$registry = new Registry();
$workflow = new Workflow($definition, new EntityWorkflowMarkingStore(), $eventSubscriber, name: 'dummy');
$registry->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface {
public function supports(WorkflowInterface $workflow, $subject): bool
{
return true;
}
});
return $registry;
}
}

View File

@@ -104,6 +104,11 @@ class CancelStaleWorkflowCronJobTest extends TestCase
(new CronJobExecution('last-canceled-workflow-id'))->setLastEnd(new \DateTimeImmutable('2023-12-31 00:00:01', new \DateTimeZone('+00:00'))),
false,
];
yield [
null,
true,
];
}
private function buildMessageBus(bool $expectDispatchAtLeastOnce = false): MessageBusInterface

View File

@@ -35,7 +35,6 @@ class UserRenderTest extends TestCase
public function testRenderUserWithJobAndScopeAtCertainDate(): void
{
// Create a user with a certain user job
$user = new User();
$userJobA = new UserJob();
$scopeA = new Scope();
@@ -106,4 +105,52 @@ class UserRenderTest extends TestCase
$expectedStringC = 'BOB ISLA (directrice) (service B)';
$this->assertEquals($expectedStringC, $renderer->renderString($user, $optionsNoDate));
}
public function testRenderStringWithSplitLines(): void
{
// Create a user with a certain user job
$user = new User();
$userJobA = new UserJob();
$scopeA = new Scope();
$userJobA->setLabel(['fr' => 'assistant social en maison de service accompagné'])
->setActive(true);
$scopeA->setName(['fr' => 'service de l\'assistant professionnel']);
$user->setLabel('Robert Van Zorrizzeen Gorikke');
$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'));
$user->getUserJobHistories()->add($userJobHistoryA);
$user->getUserScopeHistories()->add($userScopeHistoryA);
// 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-11-15 12:00:00'));
$renderer = new UserRender($translatableStringHelperMock->reveal(), $engineMock, $translatorMock, $clock);
$actual = $renderer->renderString($user, ['split_lines_before_characters' => 30]);
self::assertEquals(<<<'STR'
Robert Van Zorrizzeen Gorikke
(assistant social en maison de
service accompagné) (service de
l'assistant professionnel)
STR, $actual);
}
}

View File

@@ -0,0 +1,135 @@
<?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\Tests\Workflow\EventSubscriber;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
use Chill\MainBundle\Workflow\EventSubscriber\BlockSignatureOnRelatedEntityWithoutAnyStoredObjectGuard;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Workflow\DefinitionBuilder;
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
use Symfony\Component\Workflow\Transition;
use Symfony\Component\Workflow\Workflow;
use Symfony\Component\Workflow\WorkflowInterface;
/**
* @internal
*
* @coversNothing
*/
class BlockSignatureOnRelatedEntityWithoutAnyStoredObjectGuardTest extends TestCase
{
use ProphecyTrait;
public function testAllowedForSignatureOnRelatedEntityWithStoredObject(): void
{
$entityWorkflow = new EntityWorkflow();
$entityWorkflow->setWorkflowName('dummy')->setRelatedEntityClass('with_stored_object');
$registry = $this->buildRegistry();
$workflow = $registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$list = $workflow->buildTransitionBlockerList($entityWorkflow, 'to_signature');
self::assertCount(0, $list);
}
public function testBlockForSignatureOnRelatedEntityWithoutStoredObject(): void
{
$entityWorkflow = new EntityWorkflow();
$entityWorkflow->setWorkflowName('dummy')->setRelatedEntityClass('no_stored_object');
$registry = $this->buildRegistry();
$workflow = $registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$list = $workflow->buildTransitionBlockerList($entityWorkflow, 'to_signature');
self::assertCount(1, $list);
self::assertTrue($list->has('e8e28caa-a106-11ef-97e8-f3919e8b5c8a'));
}
public function testAllowedForNoSignatureOnRelatedEntityWithoutStoredObject(): void
{
$entityWorkflow = new EntityWorkflow();
$entityWorkflow->setWorkflowName('dummy')->setRelatedEntityClass('no_stored_object');
$registry = $this->buildRegistry();
$workflow = $registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$list = $workflow->buildTransitionBlockerList($entityWorkflow, 'to_no_signature');
self::assertTrue($list->isEmpty());
}
public function testAllowedForNoSignatureOnRelatedEntityWithStoredObject(): void
{
$entityWorkflow = new EntityWorkflow();
$entityWorkflow->setWorkflowName('dummy')->setRelatedEntityClass('no_stored_object');
$registry = $this->buildRegistry();
$workflow = $registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$list = $workflow->buildTransitionBlockerList($entityWorkflow, 'to_no_signature');
self::assertTrue($list->isEmpty());
}
private function buildRegistry(): Registry
{
$definitionBuilder = new DefinitionBuilder();
$definitionBuilder
->addPlaces(['initial', 'signature', 'no_signature'])
->addTransition(
new Transition('to_signature', 'initial', 'signature')
)
->addTransition(
new Transition('to_no_signature', 'initial', 'no_signature')
)
->setMetadataStore(
new InMemoryMetadataStore(
placesMetadata: ['signature' => ['isSignature' => ['person']]]
)
);
$workflow = new Workflow($definitionBuilder->build(), new EntityWorkflowMarkingStore(), $eventDispatcher = new EventDispatcher(), name: 'dummy');
$registry = new Registry();
$registry->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface {
public function supports(WorkflowInterface $workflow, object $subject): bool
{
return true;
}
});
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
$entityWorkflowManager->canAssociateStoredObject(Argument::type(EntityWorkflow::class))->will(
function ($args): bool {
/** @var EntityWorkflow $entityWorkflow */
$entityWorkflow = $args[0];
return 'with_stored_object' === $entityWorkflow->getRelatedEntityClass();
}
);
$eventSubscriber = new BlockSignatureOnRelatedEntityWithoutAnyStoredObjectGuard(
$entityWorkflowManager->reveal()
);
$eventDispatcher->addSubscriber($eventSubscriber);
return $registry;
}
}

View File

@@ -16,6 +16,8 @@ use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\EventSubscriber\NotificationOnTransition;
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
use Doctrine\ORM\EntityManagerInterface;
@@ -91,12 +93,18 @@ final class NotificationOnTransitionTest extends TestCase
$security = $this->prophesize(Security::class);
$security->getUser()->willReturn($currentUser);
$entityWorkflowHandler = $this->prophesize(EntityWorkflowHandlerInterface::class);
$entityWorkflowHandler->getEntityTitle($entityWorkflow)->willReturn('workflow title');
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
$entityWorkflowManager->getHandler($entityWorkflow)->willReturn($entityWorkflowHandler->reveal());
$notificationOnTransition = new NotificationOnTransition(
$em->reveal(),
$engine->reveal(),
$extractor->reveal(),
$security->reveal(),
$registry->reveal()
$registry->reveal(),
$entityWorkflowManager->reveal(),
);
$event = new Event($entityWorkflow, new Marking(), new Transition('dummy_transition', ['from_state'], ['to_state']), $workflow);

View File

@@ -14,10 +14,13 @@ namespace Chill\MainBundle\Tests\Workflow\EventSubscriber;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
use Chill\MainBundle\Workflow\EventSubscriber\NotificationToUserGroupsOnTransition;
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use Doctrine\ORM\EntityManagerInterface;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
@@ -45,20 +48,20 @@ class NotificationToUserGroupsOnTransitionTest extends KernelTestCase
use ProphecyTrait;
private Environment $twig;
private BodyRendererInterface $bodyRenderer;
private EntityManagerInterface $em;
protected function setUp(): void
{
self::bootKernel();
$this->twig = self::getContainer()->get('twig');
$this->bodyRenderer = self::getContainer()->get(BodyRendererInterface::class);
$this->em = self::getContainer()->get('doctrine.orm.entity_manager');
}
public function testOnCompletedSendNotificationToUserGroupWithEmailAddress(): void
{
$entityWorkflow = new EntityWorkflow();
$reflection = new \ReflectionClass($entityWorkflow);
$idProperty = $reflection->getProperty('id');
$idProperty->setValue($entityWorkflow, 1);
$this->em->persist($entityWorkflow);
$entityWorkflow->setWorkflowName('dummy');
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
@@ -81,7 +84,12 @@ class NotificationToUserGroupsOnTransitionTest extends KernelTestCase
$metadataExtractor->buildArrayPresentationForWorkflow(Argument::type(Workflow::class))->willReturn(['name' => 'dummy', 'text' => 'Dummy Workflow']);
$metadataExtractor->buildArrayPresentationForPlace($entityWorkflow)->willReturn(['name' => 'to_one', 'text' => 'Dummy Place']);
$registry = $this->buildRegistryWithEventSubscriber($mailer->reveal(), $metadataExtractor->reveal());
$entityWorkflowHandler = $this->prophesize(EntityWorkflowHandlerInterface::class);
$entityWorkflowHandler->getEntityTitle($entityWorkflow)->willReturn('My title');
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
$entityWorkflowManager->getHandler($entityWorkflow)->willReturn($entityWorkflowHandler->reveal());
$registry = $this->buildRegistryWithEventSubscriber($mailer->reveal(), $metadataExtractor->reveal(), $entityWorkflowManager->reveal());
$workflow = $registry->get($entityWorkflow, 'dummy');
$workflow->apply($entityWorkflow, 'to_one', ['context' => $dto, 'transition' => 'to_one', 'transitionAt' => new \DateTimeImmutable(), 'byUser' => new User()]);
@@ -105,13 +113,18 @@ class NotificationToUserGroupsOnTransitionTest extends KernelTestCase
$metadataExtractor->buildArrayPresentationForWorkflow(Argument::type(Workflow::class))->willReturn(['name' => 'dummy', 'text' => 'Dummy Workflow']);
$metadataExtractor->buildArrayPresentationForPlace($entityWorkflow)->willReturn(['name' => 'to_one', 'text' => 'Dummy Place']);
$registry = $this->buildRegistryWithEventSubscriber($mailer->reveal(), $metadataExtractor->reveal());
$entityWorkflowHandler = $this->prophesize(EntityWorkflowHandlerInterface::class);
$entityWorkflowHandler->getEntityTitle($entityWorkflow)->willReturn('My title');
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
$entityWorkflowManager->getHandler($entityWorkflow)->willReturn($entityWorkflowHandler->reveal());
$registry = $this->buildRegistryWithEventSubscriber($mailer->reveal(), $metadataExtractor->reveal(), $entityWorkflowManager->reveal());
$workflow = $registry->get($entityWorkflow, 'dummy');
$workflow->apply($entityWorkflow, 'to_one', ['context' => $dto, 'transition' => 'to_one', 'transitionAt' => new \DateTimeImmutable(), 'byUser' => new User()]);
}
public function buildRegistryWithEventSubscriber(MailerInterface $mailer, MetadataExtractor $metadataExtractor): Registry
private function buildRegistryWithEventSubscriber(MailerInterface $mailer, MetadataExtractor $metadataExtractor, EntityWorkflowManager $entityWorkflowManager): Registry
{
$builder = new DefinitionBuilder();
$builder
@@ -133,7 +146,7 @@ class NotificationToUserGroupsOnTransitionTest extends KernelTestCase
}
});
$notificationEventSubscriber = new NotificationToUserGroupsOnTransition($this->twig, $metadataExtractor, $registry, $mailer);
$notificationEventSubscriber = new NotificationToUserGroupsOnTransition($this->twig, $metadataExtractor, $registry, $mailer, $this->em, $entityWorkflowManager);
$eventDispatcher->addSubscriber($notificationEventSubscriber);
return $registry;

View File

@@ -0,0 +1,291 @@
<?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\Tests\Workflow\Helper;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use Chill\PersonBundle\Entity\Person;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Clock\MockClock;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Workflow\DefinitionBuilder;
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
use Symfony\Component\Workflow\Workflow;
use Symfony\Component\Workflow\WorkflowInterface;
/**
* @internal
*
* @coversNothing
*/
class WorkflowRelatedEntityPermissionHelperTest extends TestCase
{
use ProphecyTrait;
/**
* @dataProvider provideDataAllowedByWorkflowReadOperation
*
* @param list<EntityWorkflow> $entityWorkflows
*/
public function testAllowedByWorkflowRead(
array $entityWorkflows,
User $user,
string $expected,
?\DateTimeImmutable $atDate,
string $message,
): void {
// all entities must have this workflow name, so we are ok to set it here
foreach ($entityWorkflows as $entityWorkflow) {
$entityWorkflow->setWorkflowName('dummy');
}
$helper = $this->buildHelper($entityWorkflows, $user, $atDate);
self::assertEquals($expected, $helper->isAllowedByWorkflowForReadOperation(new \stdClass()), $message);
}
/**
* @dataProvider provideDataAllowedByWorkflowWriteOperation
*
* @param list<EntityWorkflow> $entityWorkflows
*/
public function testAllowedByWorkflowWrite(
array $entityWorkflows,
User $user,
string $expected,
?\DateTimeImmutable $atDate,
string $message,
): void {
// all entities must have this workflow name, so we are ok to set it here
foreach ($entityWorkflows as $entityWorkflow) {
$entityWorkflow->setWorkflowName('dummy');
}
$helper = $this->buildHelper($entityWorkflows, $user, $atDate);
self::assertEquals($expected, $helper->isAllowedByWorkflowForWriteOperation(new \stdClass()), $message);
}
public function testNoWorkflow(): void
{
$helper = $this->buildHelper([], new User(), null);
self::assertEquals(WorkflowRelatedEntityPermissionHelper::ABSTAIN, $helper->isAllowedByWorkflowForWriteOperation(new \stdClass()));
self::assertEquals(WorkflowRelatedEntityPermissionHelper::ABSTAIN, $helper->isAllowedByWorkflowForReadOperation(new \stdClass()));
}
/**
* @param list<EntityWorkflow> $entityWorkflows
*/
private function buildHelper(array $entityWorkflows, User $user, ?\DateTimeImmutable $atDateTime): WorkflowRelatedEntityPermissionHelper
{
$security = $this->prophesize(Security::class);
$security->getUser()->willReturn($user);
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
$entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn($entityWorkflows);
return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable()));
}
public static function provideDataAllowedByWorkflowReadOperation(): iterable
{
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
'abstain because the user is not present as a dest user'];
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = $user = new User();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
'force grant because the user is a current user'];
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = $user = new User();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = new User();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
'force grant because the user was a previous user'];
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futurePersonSignatures[] = new Person();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
'force grant because there is a signature for person'];
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futurePersonSignatures[] = new Person();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
$signature = $entityWorkflow->getCurrentStep()->getSignatures()->first();
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate(new \DateTimeImmutable('2024-01-01T12:00:00'));
yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable('2024-01-10T12:00:00'),
'abstain because there is a signature for person, already signed, and for a long time ago'];
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futurePersonSignatures[] = new Person();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
$signature = $entityWorkflow->getCurrentStep()->getSignatures()->first();
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)
->setStateDate(new \DateTimeImmutable('2024-01-01T12:00:00'));
yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
new \DateTimeImmutable('2024-01-01T12:30:00'),
'force grant because there is a signature for person, already signed, a short time ago'];
}
public static function provideDataAllowedByWorkflowWriteOperation(): iterable
{
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
'abstain because the user is not present as a dest user'];
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = $user = new User();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
'force grant because the user is a current user'];
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = $user = new User();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = new User();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
'force grant because the user was a previous user'];
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = $user = new User();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$entityWorkflow->setStep('final_positive', $dto, 'to_final_positive', new \DateTimeImmutable(), new User());
$entityWorkflow->getCurrentStep()->setIsFinal(true);
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(),
'force denied: user was a previous user, but it is finalized positive'];
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = $user = new User();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable());
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable());
$entityWorkflow->getCurrentStep()->setIsFinal(true);
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
'abstain: user was a previous user, it is finalized, but finalized negative'];
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = $user = new User();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futurePersonSignatures[] = new Person();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
$signature = $entityWorkflow->getCurrentStep()->getSignatures()->first();
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate(new \DateTimeImmutable());
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(),
'force denied because there is a signature'];
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = $user = new User();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futurePersonSignatures[] = new Person();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
'force grant: there is a signature, but still pending'];
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = $user = new User();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futurePersonSignatures[] = new Person();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
$signature = $entityWorkflow->getCurrentStep()->getSignatures()->first();
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate(new \DateTimeImmutable());
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable(), new User());
$entityWorkflow->getCurrentStep()->setIsFinal(true);
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
'abstain: there is a signature on a canceled workflow'];
}
private static function buildRegistry(): Registry
{
$builder = new DefinitionBuilder();
$builder
->setInitialPlaces(['initial'])
->addPlaces(['initial', 'test', 'final_positive', 'final_negative'])
->setMetadataStore(
new InMemoryMetadataStore(
placesMetadata: [
'final_positive' => [
'isFinal' => true,
'isFinalPositive' => true,
],
'final_negative' => [
'isFinal' => true,
'isFinalPositive' => false,
],
]
)
);
$workflow = new Workflow($builder->build(), new EntityWorkflowMarkingStore(), name: 'dummy');
$registry = new Registry();
$registry->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface {
public function supports(WorkflowInterface $workflow, object $subject): bool
{
return true;
}
});
return $registry;
}
}

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