Compare commits

..

5 Commits

Author SHA1 Message Date
68688dd528 Revert "Merge branch 'revert-671bb6d5' into 'master'"
This reverts merge request !732
2024-09-19 13:40:09 +00:00
bfd7dc2270 Merge branch 'revert-671bb6d5' into 'master'
Revert "Merge branch 'signature-app/wp-576-restorestored-object-version' into 'master'"

See merge request Chill-Projet/chill-bundles!732
2024-09-19 13:32:41 +00:00
e4d0705e84 Revert "Merge branch 'signature-app/wp-576-restorestored-object-version' into 'master'"
This reverts merge request !586
2024-09-19 13:26:12 +00:00
671bb6d593 Merge branch 'signature-app/wp-576-restorestored-object-version' into 'master'
See the list of stored object and restore some versions

Closes #307

See merge request Chill-Projet/chill-bundles!730
2024-09-19 11:51:41 +00:00
79e26ffeae Merge branch '309-fix-referrer-scope-aggregator' into 'master'
Fix referrer scope date comparison in aggregator

Closes #309

See merge request Chill-Projet/chill-bundles!728
2024-09-16 13:58:58 +00:00
348 changed files with 1859 additions and 12473 deletions

View File

@@ -0,0 +1,8 @@
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

@@ -0,0 +1,7 @@
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

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

View File

@@ -0,0 +1,6 @@
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"

View File

@@ -0,0 +1,6 @@
kind: Fixed
body: |
Correctly compute the grouping by referrer aggregator
time: 2024-09-16T15:51:50.268336979+02:00
custom:
Issue: "309"

View File

@@ -1,6 +0,0 @@
## 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

View File

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

View File

@@ -1,4 +0,0 @@
## 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.

View File

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

View File

@@ -1,4 +0,0 @@
## 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

View File

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

View File

@@ -1,13 +0,0 @@
## 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

View File

@@ -1,4 +0,0 @@
## 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.

View File

@@ -6,76 +6,22 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## 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-23 & 2024-07-19
## v2.23.0 - 2024-07-19 & 2024-07-23
### 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
@@ -85,8 +31,6 @@ Fix color of Chill footer
* 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
@@ -99,15 +43,25 @@ Fix color of Chill footer
- 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
@@ -150,7 +104,7 @@ Fix color of Chill footer
## 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
@@ -197,96 +151,96 @@ Fix color of Chill footer
## 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
@@ -300,7 +254,7 @@ Fix color of Chill footer
## 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
@@ -331,36 +285,36 @@ Fix color of Chill footer
## 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
@@ -385,11 +339,11 @@ Fix color of Chill footer
## 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
@@ -437,57 +391,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,8 +59,7 @@
"vue-i18n": "^9.1.6",
"vue-multiselect": "3.0.0-alpha.2",
"vue-toast-notification": "^3.1.2",
"vuex": "^4.0.0",
"bootstrap-icons": "^1.11.3"
"vuex": "^4.0.0"
},
"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\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
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,
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
WorkflowStoredObjectPermissionHelper $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 => $key?->getId(),
'choice_label' => static fn (Option $option) => $translatableStringHelper->localize($option->getText()),
'choice_value' => static fn (Option $key): ?int => null === $key ? null : $key->getId(),
'multiple' => false,
'expanded' => false,
'required' => $customField->isRequired(),

View File

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

View File

@@ -1,55 +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\Controller;
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\DocStoreBundle\Workflow\AccompanyingCourseDocumentDuplicator;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
final readonly class DocumentAccompanyingCourseDuplicateController
{
public function __construct(
private Security $security,
private AccompanyingCourseDocumentDuplicator $documentWorkflowDuplicator,
private EntityManagerInterface $entityManager,
private UrlGeneratorInterface $urlGenerator,
) {}
#[Route('/{_locale}/doc-store/accompanying-course-document/{id}/duplicate', name: 'chill_doc_store_accompanying_course_document_duplicate')]
public function __invoke(AccompanyingCourseDocument $document, Request $request, Session $session): Response
{
if (!$this->security->isGranted(AccompanyingCourseDocumentVoter::SEE, $document)) {
throw new AccessDeniedHttpException('not allowed to see this document');
}
if (!$this->security->isGranted(AccompanyingCourseDocumentVoter::CREATE, $document->getCourse())) {
throw new AccessDeniedHttpException('not allowed to create this document');
}
$duplicated = $this->documentWorkflowDuplicator->duplicate($document);
$this->entityManager->persist($duplicated);
$this->entityManager->flush();
return new RedirectResponse(
$this->urlGenerator->generate('accompanying_course_document_edit', ['id' => $duplicated->getId(), 'course' => $duplicated->getCourse()->getId()])
);
}
}

View File

@@ -15,14 +15,11 @@ 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;
@@ -41,8 +38,6 @@ 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')]
@@ -57,17 +52,11 @@ class SignatureRequestController
if (EntityWorkflowSignatureStateEnum::PENDING !== $signature->getState()) {
return new JsonResponse([], status: Response::HTTP_CONFLICT);
}
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
$content = $this->storedObjectManager->read($storedObject);
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);
$data = \json_decode((string) $request->getContent(), true, 512, JSON_THROW_ON_ERROR); // TODO parse payload: json_decode ou, mieux, dataTransfertObject
$zone = new PDFSignatureZone(
$data['zone']['index'],
$data['zone']['x'],
@@ -86,7 +75,6 @@ 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,7 +18,6 @@ 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,7 +40,6 @@ 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,7 +19,6 @@ 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

@@ -18,8 +18,6 @@ use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Order;
use Doctrine\Common\Collections\ReadableCollection;
use Doctrine\Common\Collections\Selectable;
use Doctrine\ORM\Mapping as ORM;
use Ramsey\Uuid\Uuid;
@@ -259,33 +257,11 @@ class StoredObject implements Document, TrackCreationInterface
return $this->template;
}
/**
* @return Selectable<int, StoredObjectVersion>&Collection<int, StoredObjectVersion>
*/
public function getVersions(): Collection&Selectable
{
return $this->versions;
}
/**
* Retrieves versions sorted by a given order.
*
* @param 'ASC'|'DESC' $order the sorting order, default is Order::Ascending
*
* @return readableCollection&Selectable The ordered collection of versions
*/
public function getVersionsOrdered(string $order = 'ASC'): ReadableCollection&Selectable
{
$versions = $this->getVersions()->toArray();
match ($order) {
'ASC' => usort($versions, static fn (StoredObjectVersion $a, StoredObjectVersion $b) => $a->getVersion() <=> $b->getVersion()),
'DESC' => usort($versions, static fn (StoredObjectVersion $a, StoredObjectVersion $b) => $b->getVersion() <=> $a->getVersion()),
};
return new ArrayCollection($versions);
}
public function hasCurrentVersion(): bool
{
return null !== $this->getCurrentVersion();
@@ -296,47 +272,6 @@ class StoredObject implements Document, TrackCreationInterface
return null !== $this->template;
}
/**
* Checks if there is a version kept before conversion.
*
* @return bool true if a version is kept before conversion, false otherwise
*/
public function hasKeptBeforeConversionVersion(): bool
{
foreach ($this->getVersions() as $version) {
foreach ($version->getPointInTimes() as $pointInTime) {
if (StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION === $pointInTime->getReason()) {
return true;
}
}
}
return false;
}
/**
* Retrieves the last version of the stored object that was kept before conversion.
*
* This method iterates through the ordered versions and their respective points
* in time to find the most recent version that has a point in time with the reason
* 'KEEP_BEFORE_CONVERSION'.
*
* @return StoredObjectVersion|null the version that was kept before conversion,
* or null if not found
*/
public function getLastKeptBeforeConversionVersion(): ?StoredObjectVersion
{
foreach ($this->getVersionsOrdered('DESC') as $version) {
foreach ($version->getPointInTimes() as $pointInTime) {
if (StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION === $pointInTime->getReason()) {
return $version;
}
}
}
return null;
}
public function setTemplate(?DocGeneratorTemplate $template): StoredObject
{
$this->template = $template;

View File

@@ -17,23 +17,15 @@ 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,
@@ -42,10 +34,7 @@ class DocumentCategoryType extends AbstractType
->add('idInsideBundle', null, [
'disabled' => true,
])
->add('documentClass', ChoiceType::class, [
'choices' => $documentClasses,
'expanded' => false,
'required' => true,
->add('documentClass', null, [
'disabled' => false,
])
->add('name', TranslatableStringFormType::class);

View File

@@ -23,7 +23,7 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository, Associat
{
private readonly EntityRepository $repository;
public function __construct(EntityManagerInterface $em)
public function __construct(private readonly EntityManagerInterface $em)
{
$this->repository = $em->getRepository(AccompanyingCourseDocument::class);
}

View File

@@ -1,27 +0,0 @@
import {_createI18n} from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
import {createApp} from "vue";
import DocumentActionButtonsGroup from "../../vuejs/DocumentActionButtonsGroup.vue";
import {StoredObject, StoredObjectStatusChange} from "../../types";
import {defineComponent} from "vue";
import DownloadButton from "../../vuejs/StoredObjectButton/DownloadButton.vue";
import ToastPlugin from "vue-toast-notification";
const i18n = _createI18n({});
window.addEventListener('DOMContentLoaded', function (e) {
document.querySelectorAll<HTMLDivElement>('div[data-download-button-single]').forEach((el) => {
const storedObject = JSON.parse(el.dataset.storedObject as string) as StoredObject;
const title = el.dataset.title as string;
const app = createApp({
components: {DownloadButton},
data() {
return {storedObject, title, classes: {btn: true, "btn-outline-primary": true}};
},
template: '<download-button :stored-object="storedObject" :at-version="storedObject.currentVersion" :classes="classes" :filename="title" :direct-download="true"></download-button>',
});
app.use(i18n).use(ToastPlugin).mount(el);
});
});

View File

@@ -2,7 +2,6 @@ import {
DateTime,
User,
} from "../../../ChillMainBundle/Resources/public/types";
import {SignedUrlGet} from "./vuejs/StoredObjectButton/helpers";
export type StoredObjectStatus = "empty" | "ready" | "failure" | "pending";
@@ -31,7 +30,6 @@ export interface StoredObject {
href: string;
expiration: number;
};
downloadLink?: SignedUrlGet;
};
}
@@ -130,12 +128,3 @@ export interface CheckSignature {
}
export type CanvasEvent = "select" | "add";
export interface ZoomLevel {
id: number;
zoom: number;
label: {
fr?: string,
nl?: string
};
}

View File

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

@@ -1,9 +1,9 @@
<template>
<a v-if="!state.is_ready" :class="props.classes" @click="download_and_open()" title="T&#233;l&#233;charger">
<a v-if="!state.is_ready" :class="props.classes" @click="download_and_open($event)" title="T&#233;l&#233;charger">
<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.atVersion.type" :download="buildDocumentName()" :href="state.href_url" ref="open_button" title="Ouvrir">
<a v-else :class="props.classes" target="_blank" :type="props.storedObject.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>
@@ -20,15 +20,7 @@ interface DownloadButtonConfig {
atVersion: StoredObjectVersion,
classes: { [k: string]: boolean },
filename?: string,
/**
* if true, display the action string into the button. If false, displays only
* the icon
*/
displayActionStringInButton?: boolean,
/**
* if true, will download directly the file on load
*/
directDownload?: boolean,
displayActionStringInButton: boolean,
}
interface DownloadButtonState {
@@ -37,17 +29,13 @@ interface DownloadButtonState {
href_url: string,
}
const props = withDefaults(defineProps<DownloadButtonConfig>(), {displayActionStringInButton: true, directDownload: false});
const props = withDefaults(defineProps<DownloadButtonConfig>(), {displayActionStringInButton: true});
const state: DownloadButtonState = reactive({is_ready: false, is_running: false, href_url: "#"});
const open_button = ref<HTMLAnchorElement | null>(null);
function buildDocumentName(): string {
let document_name = props.filename ?? props.storedObject.title;
if ('' === document_name) {
document_name = 'document';
}
const document_name = props.filename ?? props.storedObject.title ?? 'document';
const ext = mime.getExtension(props.atVersion.type);
@@ -58,7 +46,9 @@ function buildDocumentName(): string {
return document_name;
}
async function download_and_open(): Promise<void> {
async function download_and_open(event: Event): Promise<void> {
const button = event.target as HTMLAnchorElement;
if (state.is_running) {
console.log('state is running, aborting');
return;
@@ -85,13 +75,11 @@ async function download_and_open(): Promise<void> {
state.is_running = false;
state.is_ready = true;
if (!props.directDownload) {
await nextTick();
open_button.value?.click();
await nextTick();
open_button.value?.click();
console.log('open button should have been clicked');
console.log('open button should have been clicked');
setTimeout(reset_state, 45000);
}
const timer = setTimeout(reset_state, 45000);
}
function reset_state(): void {
@@ -99,19 +87,10 @@ function reset_state(): void {
state.is_ready = false;
state.is_running = false;
}
onMounted(() => {
if (props.directDownload) {
download_and_open();
}
});
</script>
<style scoped lang="scss">
i.fa::before {
color: var(--bs-dropdown-link-hover-color);
}
i.fa {
margin-right: 0.5rem;
}
</style>

View File

@@ -50,6 +50,7 @@ const onRestored = ({newVersion}: {newVersion: StoredObjectVersionWithPointInTim
:version="v"
:can-edit="canEdit"
:is-current="higher_version === v.version"
:is-restored="v.version === state.restored"
:stored-object="storedObject"
@restore-version="onRestored"
></history-button-list-item>

View File

@@ -12,6 +12,7 @@ interface HistoryButtonListItemConfig {
storedObject: StoredObject;
canEdit: boolean;
isCurrent: boolean;
isRestored: boolean;
}
const emit = defineEmits<{
@@ -30,9 +31,7 @@ const isKeptBeforeConversion = computed<boolean>(() => props.version["point-in-t
),
);
const isRestored = computed<boolean>(() => props.version.version > 0 && null !== props.version["from-restored"]);
const isDuplicated = computed<boolean>(() => props.version.version === 0 && null !== props.version["from-restored"]);
const isRestored = computed<boolean>(() => null !== props.version["from-restored"]);
const classes = computed<{row: true, 'row-hover': true, 'blinking-1': boolean, 'blinking-2': boolean}>(() => ({row: true, 'row-hover': true, 'blinking-1': props.isRestored && 0 === props.version.version % 2, 'blinking-2': props.isRestored && 1 === props.version.version % 2}));
@@ -40,14 +39,13 @@ const classes = computed<{row: true, 'row-hover': true, 'blinking-1': boolean, '
<template>
<div :class="classes">
<div class="col-12 tags" v-if="isCurrent || isKeptBeforeConversion || isRestored || isDuplicated">
<div class="col-12 tags" v-if="isCurrent || isKeptBeforeConversion || isRestored">
<span class="badge bg-success" v-if="isCurrent">Version actuelle</span>
<span class="badge bg-info" v-if="isKeptBeforeConversion">Conservée avant conversion dans un autre format</span>
<span class="badge bg-info" v-if="isRestored">Restaurée depuis la version {{ version["from-restored"]?.version + 1 }}</span>
<span class="badge bg-info" v-if="isDuplicated">Dupliqué depuis un autre document</span>
<span class="badge bg-info" v-if="isRestored">Restaurée depuis la version {{ version["from-restored"]?.version }}</span>
</div>
<div class="col-12">
<file-icon :type="version.type"></file-icon> <span><strong>#{{ version.version + 1 }}</strong></span> <template v-if="version.createdBy !== null && version.createdAt !== null"><strong v-if="version.version == 0">Créé par</strong><strong v-else>modifié par</strong> <span class="badge-user"><UserRenderBoxBadge :user="version.createdBy"></UserRenderBoxBadge></span> <strong>à</strong> {{ $d(ISOToDatetime(version.createdAt.datetime8601), 'long') }}</template><template v-if="version.createdBy === null && version.createdAt !== null"><strong v-if="version.version == 0">Créé le</strong><strong v-else>modifié le</strong> {{ $d(ISOToDatetime(version.createdAt.datetime8601), 'long') }}</template>
<file-icon :type="version.type"></file-icon> <span><strong>#{{ version.version + 1 }}</strong></span> <strong v-if="version.version == 0">Créé par</strong><strong v-else>modifié par</strong> <span class="badge-user"><UserRenderBoxBadge :user="version.createdBy"></UserRenderBoxBadge></span> <strong>à</strong> {{ $d(ISOToDatetime(version.createdAt.datetime8601), 'long') }}
</div>
<div class="col-12">
<ul class="record_actions small slim on-version-actions">
@@ -55,7 +53,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, 'btn-sm': true}" :display-action-string-in-button="false"></download-button>
<download-button :stored-object="storedObject" :at-version="version" :classes="{btn: true, 'btn-outline-primary': true}" :display-action-string-in-button="false"></download-button>
</li>
</ul>
</div>

View File

@@ -22,6 +22,7 @@ const props = defineProps<HistoryButtonListConfig>();
const state = reactive<HistoryButtonModalState>({opened: false});
const open = () => {
console.log('open');
state.opened = true;
}

View File

@@ -161,14 +161,7 @@ async function download_and_decrypt_doc(storedObject: StoredObject, atVersion: n
throw new Error("no version associated to stored object");
}
// sometimes, the downloadInfo may be embedded into the storedObject
console.log('storedObject', storedObject);
let downloadInfo;
if (typeof storedObject._links !== 'undefined' && typeof storedObject._links.downloadLink !== 'undefined') {
downloadInfo = storedObject._links.downloadLink;
} else {
downloadInfo = await download_info_link(storedObject, atVersionToDownload);
}
const downloadInfo= await download_info_link(storedObject, atVersionToDownload);
const rawResponse = await window.fetch(downloadInfo.url);
@@ -197,32 +190,6 @@ 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
@@ -240,7 +207,6 @@ 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

@@ -1 +0,0 @@
<div data-download-button-single="data-download-button-single" data-stored-object="{{ document_json|json_encode|escape('html_attr') }}" data-title="{{ title|escape('html_attr') }}"></div>

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

@@ -73,15 +73,8 @@
<li>
{{ document.object|chill_document_button_group(document.title) }}
</li>
{% endif %}
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_DELETE', document) %}
<li class="delete">
<a href="{{ chill_return_path_or('chill_docstore_accompanying_course_document_delete', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-delete"></a>
</li>
{% endif %}
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE', document.course) %}
<li>
<a href="{{ chill_path_add_return_path('chill_doc_store_accompanying_course_document_duplicate', {'id': document.id}) }}" class="btn btn-duplicate" title="{{ 'Duplicate'|trans|e('html_attr') }}"></a>
<a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a>
</li>
{% endif %}
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document) %}
@@ -89,9 +82,9 @@
<a href="{{ path('accompanying_course_document_edit', {'course': accompanyingCourse.id, 'id': document.id }) }}" class="btn btn-update"></a>
</li>
{% endif %}
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
<li>
<a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a>
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_DELETE', document) %}
<li class="delete">
<a href="{{ chill_return_path_or('chill_docstore_accompanying_course_document_delete', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-delete"></a>
</li>
{% endif %}
{% else %}

View File

@@ -1,43 +0,0 @@
{% extends '@ChillMain/Workflow/workflow_view_send_public_layout.html.twig' %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('mod_document_download_button') }}
{% endblock %}
{% block js %}
{{ parent() }}
{{ encore_entry_script_tags('mod_document_download_button') }}
{% endblock %}
{% block title %}{{ 'workflow.public_link.title'|trans }} - {{ title }}{% endblock %}
{% block public_content %}
<h1>{{ 'workflow.public_link.shared_doc'|trans }}</h1>
{% set previous = send.entityWorkflowStepChained.previous %}
{% if previous is not null %}
{% if previous.transitionBy is not null %}
<p>{{ 'workflow.public_link.doc_shared_by_at_explanation'|trans({'byUser': previous.transitionBy|chill_entity_render_string( { 'at_date': previous.transitionAt } ), 'at': previous.transitionAt }) }}</p>
{% else %}
<p>{{ 'workflow.public_link.doc_shared_automatically_at_explanation'|trans({'at': previous.transitionAt}) }}</p>
{% endif %}
{% endif %}
<div class="row">
<div class="col-xs-12 col-sm-6 col-md-4">
<div class="card"">
<div class="card-body">
<h2 class="card-title">{{ title }}</h2>
<h3>{{ 'workflow.public_link.main_document'|trans }}</h3>
<ul class="record_actions slim small">
<li>
{{ storedObject|chill_document_download_only_button(storedObject.title(), false) }}
</li>
</ul>
</div>
</div>
</div>
</div>
{% endblock %}

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

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\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
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,
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
WorkflowStoredObjectPermissionHelper $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\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
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,
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
WorkflowStoredObjectPermissionHelper $workflowDocumentService,
) {
parent::__construct($security, $workflowDocumentService);
}

View File

@@ -11,7 +11,6 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Serializer\Normalizer;
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
@@ -31,17 +30,10 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa
{
use NormalizerAwareTrait;
/**
* when added to the groups, a download link is included in the normalization,
* and no webdav links are generated.
*/
public const DOWNLOAD_LINK_ONLY = 'read:download-link-only';
public function __construct(
private readonly JWTDavTokenProviderInterface $JWTDavTokenProvider,
private readonly UrlGeneratorInterface $urlGenerator,
private readonly Security $security,
private readonly TempUrlGeneratorInterface $tempUrlGenerator,
) {}
public function normalize($object, ?string $format = null, array $context = [])
@@ -63,24 +55,6 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa
// deprecated property
$datas['creationDate'] = $datas['createdAt'];
if (array_key_exists(AbstractNormalizer::GROUPS, $context)) {
$groupsNormalized = is_array($context[AbstractNormalizer::GROUPS]) ? $context[AbstractNormalizer::GROUPS] : [$context[AbstractNormalizer::GROUPS]];
} else {
$groupsNormalized = [];
}
if (in_array(self::DOWNLOAD_LINK_ONLY, $groupsNormalized, true)) {
$datas['_permissions'] = [
'canSee' => true,
'canEdit' => false,
];
$datas['_links'] = [
'downloadLink' => $this->normalizer->normalize($this->tempUrlGenerator->generate('GET', $object->getCurrentVersion()->getFilename(), 180), $format, [AbstractNormalizer::GROUPS => ['read']]),
];
return $datas;
}
$canSee = $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $object);
$canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $object);

View File

@@ -17,30 +17,15 @@ 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 implements LocaleAwareInterface
class PDFSignatureZoneAvailable
{
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>
*/
@@ -53,16 +38,10 @@ class PDFSignatureZoneAvailable implements LocaleAwareInterface
}
if ('application/pdf' !== $storedObject->getType()) {
$content = $this->converter->convert($this->getLocale(), $this->storedObjectManager->read($storedObject), $storedObject->getType());
} else {
$content = $this->storedObjectManager->read($storedObject);
throw new \RuntimeException('Only PDF documents are supported');
}
$zones = $this->pdfSignatureZoneParser->findSignatureZones($content);
// free some memory as soon as possible...
unset($content);
$zones = $this->pdfSignatureZoneParser->findSignatureZones($this->storedObjectManager->read($storedObject));
$signatureZonesIndexes = array_map(
fn (EntityWorkflowStepSignature $step) => $step->getZoneSignatureIndex(),
$this->collectSignaturesInUse($entityWorkflow)

View File

@@ -1,56 +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\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
use Psr\Log\LoggerInterface;
/**
* Class which duplicate a stored object into a new one, recreating a stored object.
*/
class StoredObjectDuplicate
{
public function __construct(private readonly StoredObjectManagerInterface $storedObjectManager, private readonly LoggerInterface $logger) {}
public function duplicate(StoredObject|StoredObjectVersion $from, bool $onlyLastKeptBeforeConversionVersion = true): StoredObject
{
$storedObject = $from instanceof StoredObjectVersion ? $from->getStoredObject() : $from;
$fromVersion = match ($storedObject->hasKeptBeforeConversionVersion() && $onlyLastKeptBeforeConversionVersion) {
true => $from->getLastKeptBeforeConversionVersion(),
false => $storedObject->getCurrentVersion(),
};
if (null === $fromVersion) {
throw new \UnexpectedValueException('could not find a version to restore');
}
$oldContent = $this->storedObjectManager->read($fromVersion);
$storedObject = new StoredObject();
$newVersion = $this->storedObjectManager->write($storedObject, $oldContent, $fromVersion->getType());
$newVersion->setCreatedFrom($fromVersion);
$this->logger->info('[StoredObjectDuplicate] Duplicated stored object from a version of a previous stored object', [
'from_stored_object_uuid' => $fromVersion->getStoredObject()->getUuid(),
'to_stored_object_uuid' => $storedObject->getUuid(),
'old_version_id' => $fromVersion->getId(),
'old_version_version' => $fromVersion->getVersion(),
'new_version_id' => $newVersion->getVersion(),
]);
return $storedObject;
}
}

View File

@@ -14,9 +14,6 @@ namespace Chill\DocStoreBundle\Service;
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
use Psr\Log\LoggerInterface;
/**
* Class responsible for restoring stored object versions into the same stored object.
*/
final readonly class StoredObjectRestore implements StoredObjectRestoreInterface
{
public function __construct(private readonly StoredObjectManagerInterface $storedObjectManager, private readonly LoggerInterface $logger) {}

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, 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
* @return array{0: StoredObjectPointInTime, 1: StoredObjectVersion} contains the point in time before conversion and the new version of the stored object
*
* @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', bool $includeConvertedContent = false): array
public function addConvertedVersion(StoredObject $storedObject, string $lang, $convertTo = 'pdf'): array
{
$newMimeType = $this->mimeTypes->getMimeTypes($convertTo)[0] ?? null;
@@ -70,11 +70,6 @@ class StoredObjectToPdfConverter
$pointInTime = new StoredObjectPointInTime($currentVersion, StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION);
$version = $this->storedObjectManager->write($storedObject, $converted, $newMimeType);
if (!$includeConvertedContent) {
return [$pointInTime, $version];
}
return [$pointInTime, $version, $converted];
return [$pointInTime, $version];
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Service;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Symfony\Component\Security\Core\Security;
class WorkflowStoredObjectPermissionHelper
{
public function __construct(private readonly Security $security, private readonly EntityWorkflowManager $entityWorkflowManager) {}
public function notBlockedByWorkflow(object $entity): bool
{
$workflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
$currentUser = $this->security->getUser();
foreach ($workflows as $workflow) {
if ($workflow->isFinal()) {
return false;
}
if (!$workflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) {
return false;
}
}
return true;
}
}

View File

@@ -28,10 +28,6 @@ class WopiEditTwigExtension extends AbstractExtension
'needs_environment' => true,
'is_safe' => ['html'],
]),
new TwigFilter('chill_document_download_only_button', [WopiEditTwigExtensionRuntime::class, 'renderDownloadButton'], [
'needs_environment' => true,
'is_safe' => ['html'],
]),
];
}

View File

@@ -15,7 +15,6 @@ use ChampsLibres\WopiLib\Contract\Service\Discovery\DiscoveryInterface;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectNormalizer;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
@@ -178,17 +177,6 @@ final readonly class WopiEditTwigExtensionRuntime implements RuntimeExtensionInt
]);
}
public function renderDownloadButton(Environment $environment, StoredObject $storedObject, string $title = ''): string
{
return $environment->render(
'@ChillDocStore/Button/button_download.html.twig',
[
'document_json' => $this->normalizer->normalize($storedObject, 'json', [AbstractNormalizer::GROUPS => ['read', StoredObjectNormalizer::DOWNLOAD_LINK_ONLY]]),
'title' => $title,
]
);
}
public function renderEditButton(Environment $environment, StoredObject $document, ?array $options = null): string
{
return $environment->render(self::TEMPLATE, [

View File

@@ -12,8 +12,6 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Tests\Entity;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Entity\StoredObjectPointInTime;
use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
@@ -56,27 +54,4 @@ class StoredObjectTest extends KernelTestCase
self::assertNotSame($firstVersion, $version);
}
public function testHasKeptBeforeConversionVersion(): void
{
$storedObject = new StoredObject();
$version1 = $storedObject->registerVersion();
self::assertFalse($storedObject->hasKeptBeforeConversionVersion());
// add a point in time without the correct version
new StoredObjectPointInTime($version1, StoredObjectPointInTimeReasonEnum::KEEP_BY_USER);
self::assertFalse($storedObject->hasKeptBeforeConversionVersion());
self::assertNull($storedObject->getLastKeptBeforeConversionVersion());
new StoredObjectPointInTime($version1, StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION);
self::assertTrue($storedObject->hasKeptBeforeConversionVersion());
// add a second version
$version2 = $storedObject->registerVersion();
new StoredObjectPointInTime($version2, StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION);
self::assertSame($version2, $storedObject->getLastKeptBeforeConversionVersion());
}
}

View File

@@ -11,7 +11,6 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Tests\Form;
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Form\DataMapper\StoredObjectDataMapper;
use Chill\DocStoreBundle\Form\DataTransformer\StoredObjectDataTransformer;
@@ -133,8 +132,7 @@ class StoredObjectTypeTest extends TypeTestCase
new StoredObjectNormalizer(
$jwtTokenProvider->reveal(),
$urlGenerator->reveal(),
$security->reveal(),
$this->createMock(TempUrlGeneratorInterface::class)
$security->reveal()
),
new StoredObjectDenormalizer($storedObjectRepository->reveal()),
new StoredObjectVersionNormalizer(),

View File

@@ -15,11 +15,10 @@ 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 Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Security;
/**
@@ -29,21 +28,26 @@ use Symfony\Component\Security\Core\Security;
*/
class AbstractStoredObjectVoterTest extends TestCase
{
use ProphecyTrait;
private AssociatedEntityToStoredObjectInterface $repository;
private Security $security;
private WorkflowStoredObjectPermissionHelper $workflowDocumentService;
private function buildStoredObjectVoter(
bool $canBeAssociatedWithWorkflow,
AssociatedEntityToStoredObjectInterface $repository,
Security $security,
?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
): AbstractStoredObjectVoter {
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
{
// 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,
?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null,
) {
parent::__construct($security, $workflowDocumentService);
}
@@ -70,89 +74,95 @@ 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
{
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(new \stdClass()), $this->prophesize(Security::class)->reveal(), null);
[$user, $token, $subject, $entity] = $this->setupMockObjects();
self::assertTrue($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject()));
// Setup mocks for voteOnAttribute method
$this->setupMocksForVoteOnAttribute($user, $token, true, $entity, true);
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
$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()));
self::assertTrue($voter->supports(StoredObjectRoleEnum::SEE, $subject));
}
/**
* @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');
$security = $this->prophesize(Security::class);
$security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
$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 static function dataProviderVoteOnAttribute(): iterable
public function testVoteOnAttributeAllowedAndWorkflowAllowed(): void
{
// 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'];
[$user, $token, $subject, $entity] = $this->setupMockObjects();
// 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'];
// Setup mocks for voteOnAttribute method
$this->setupMocksForVoteOnAttribute($user, $token, true, $entity, true);
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
// 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;
// The voteOnAttribute method should return True when workflow is allowed
self::assertTrue($voter->voteOnAttribute(StoredObjectRoleEnum::SEE, $subject, $token));
}
public function testVoteOnAttributeNotAllowed(): void
{
[$user, $token, $subject, $entity] = $this->setupMockObjects();
// 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);
// 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);
}
}

View File

@@ -11,8 +11,6 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Tests\Serializer\Normalizer;
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
@@ -72,9 +70,7 @@ class StoredObjectNormalizerTest extends TestCase
return ['sub' => 'sub'];
});
$tempUrlGenerator = $this->createMock(TempUrlGeneratorInterface::class);
$normalizer = new StoredObjectNormalizer($jwtProvider, $urlGenerator, $security, $tempUrlGenerator);
$normalizer = new StoredObjectNormalizer($jwtProvider, $urlGenerator, $security);
$normalizer->setNormalizer($globalNormalizer);
$actual = $normalizer->normalize($storedObject, 'json');
@@ -99,48 +95,4 @@ class StoredObjectNormalizerTest extends TestCase
self::assertArrayHasKey('dav_link', $actual['_links']);
self::assertEqualsCanonicalizing(['href' => $davLink, 'expiration' => $d->getTimestamp()], $actual['_links']['dav_link']);
}
public function testWithDownloadLinkOnly(): void
{
$storedObject = new StoredObject();
$storedObject->registerVersion();
$storedObject->setTitle('test');
$reflection = new \ReflectionClass(StoredObject::class);
$idProperty = $reflection->getProperty('id');
$idProperty->setValue($storedObject, 1);
$jwtProvider = $this->createMock(JWTDavTokenProviderInterface::class);
$jwtProvider->expects($this->never())->method('createToken')->withAnyParameters();
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
$urlGenerator->expects($this->never())->method('generate');
$security = $this->createMock(Security::class);
$security->expects($this->never())->method('isGranted');
$globalNormalizer = $this->createMock(NormalizerInterface::class);
$globalNormalizer->expects($this->exactly(4))->method('normalize')
->withAnyParameters()
->willReturnCallback(function (?object $object, string $format, array $context) {
if (null === $object) {
return null;
}
return ['sub' => 'sub'];
});
$tempUrlGenerator = $this->createMock(TempUrlGeneratorInterface::class);
$tempUrlGenerator->expects($this->once())->method('generate')->with('GET', $storedObject->getCurrentVersion()->getFilename(), $this->isType('int'))
->willReturn(new SignedUrl('GET', 'https://some-link/test', new \DateTimeImmutable('300 seconds'), $storedObject->getCurrentVersion()->getFilename()));
$normalizer = new StoredObjectNormalizer($jwtProvider, $urlGenerator, $security, $tempUrlGenerator);
$normalizer->setNormalizer($globalNormalizer);
$actual = $normalizer->normalize($storedObject, 'json', ['groups' => ['read', 'read:download-link-only']]);
self::assertIsArray($actual);
self::assertArrayHasKey('_links', $actual);
self::assertArrayHasKey('downloadLink', $actual['_links']);
self::assertEquals(['sub' => 'sub'], $actual['_links']['downloadLink']);
}
}

View File

@@ -22,7 +22,6 @@ 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;
@@ -66,7 +65,6 @@ class PDFSignatureZoneAvailableTest extends TestCase
$entityWorkflowManager->reveal(),
$parser->reveal(),
$storedObjectManager->reveal(),
$this->prophesize(WopiConverter::class)->reveal(),
);
$actual = $filter->getAvailableSignatureZones($entityWorkflow);

View File

@@ -1,130 +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\Entity\StoredObject;
use Chill\DocStoreBundle\Entity\StoredObjectPointInTime;
use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum;
use Chill\DocStoreBundle\Service\StoredObjectDuplicate;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\NullLogger;
/**
* @internal
*
* @coversNothing
*/
class StoredObjectDuplicateTest extends TestCase
{
use ProphecyTrait;
public function testDuplicateHappyScenario(): void
{
$storedObject = new StoredObject();
// we create multiple version, we want the last to be duplicated
$storedObject->registerVersion(type: 'application/test');
$version = $storedObject->registerVersion(type: $type = 'application/test');
$manager = $this->createMock(StoredObjectManagerInterface::class);
$manager->method('read')->with($version)->willReturn('1234');
$manager
->expects($this->once())
->method('write')
->with($this->isInstanceOf(StoredObject::class), '1234', 'application/test')
->willReturnCallback(fn (StoredObject $so, $content, $type) => $so->registerVersion(type: $type));
$storedObjectDuplicate = new StoredObjectDuplicate($manager, new NullLogger());
$actual = $storedObjectDuplicate->duplicate($storedObject);
self::assertNotNull($actual->getCurrentVersion());
self::assertNotNull($actual->getCurrentVersion()->getCreatedFrom());
self::assertSame($version, $actual->getCurrentVersion()->getCreatedFrom());
}
public function testDuplicateWithKeptVersion(): void
{
$storedObject = new StoredObject();
// we create two versions for stored object
// the first one is "kept before conversion", and that one should
// be duplicated, not the second one
$version1 = $storedObject->registerVersion(type: $type = 'application/test');
new StoredObjectPointInTime($version1, StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION);
$version2 = $storedObject->registerVersion(type: $type = 'application/test');
$manager = $this->prophesize(StoredObjectManagerInterface::class);
// we create both possibilities for the method "read"
$manager->read($version1)->willReturn('1234');
$manager->read($version2)->willReturn('4567');
// we create the write method, and check that it is called with the content from version1, not version2
$manager->write(Argument::type(StoredObject::class), '1234', 'application/test')
->shouldBeCalled()
->will(function ($args) {
/** @var StoredObject $storedObject */
$storedObject = $args[0]; // args are ordered by key, so the first one is the stored object...
$type = $args[2]; // and the last one is the string $type
return $storedObject->registerVersion(type: $type);
});
// we create the service which will duplicate things
$storedObjectDuplicate = new StoredObjectDuplicate($manager->reveal(), new NullLogger());
$actual = $storedObjectDuplicate->duplicate($storedObject);
self::assertNotNull($actual->getCurrentVersion());
self::assertNotNull($actual->getCurrentVersion()->getCreatedFrom());
self::assertSame($version1, $actual->getCurrentVersion()->getCreatedFrom());
}
public function testDuplicateWithKeptVersionButWeWantToDuplicateTheLastOne(): void
{
$storedObject = new StoredObject();
// we create two versions for stored object
// the first one is "kept before conversion", and that one should
// be duplicated, not the second one
$version1 = $storedObject->registerVersion(type: $type = 'application/test');
new StoredObjectPointInTime($version1, StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION);
$version2 = $storedObject->registerVersion(type: $type = 'application/test');
$manager = $this->prophesize(StoredObjectManagerInterface::class);
// we create both possibilities for the method "read"
$manager->read($version1)->willReturn('1234');
$manager->read($version2)->willReturn('4567');
// we create the write method, and check that it is called with the content from version1, not version2
$manager->write(Argument::type(StoredObject::class), '4567', 'application/test')
->shouldBeCalled()
->will(function ($args) {
/** @var StoredObject $storedObject */
$storedObject = $args[0]; // args are ordered by key, so the first one is the stored object...
$type = $args[2]; // and the last one is the string $type
return $storedObject->registerVersion(type: $type);
});
// we create the service which will duplicate things
$storedObjectDuplicate = new StoredObjectDuplicate($manager->reveal(), new NullLogger());
$actual = $storedObjectDuplicate->duplicate($storedObject, false);
self::assertNotNull($actual->getCurrentVersion());
self::assertNotNull($actual->getCurrentVersion()->getCreatedFrom());
self::assertSame($version2, $actual->getCurrentVersion()->getCreatedFrom());
}
}

View File

@@ -1,70 +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\AccompanyingCourseDocument;
use Chill\DocStoreBundle\Entity\DocumentCategory;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Service\StoredObjectDuplicate;
use Chill\DocStoreBundle\Workflow\AccompanyingCourseDocumentDuplicator;
use Chill\MainBundle\Entity\User;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Clock\MockClock;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @internal
*
* @coversNothing
*/
class AccompanyingCourseDocumentDuplicatorTest extends TestCase
{
public function testDuplicate(): void
{
$object = new StoredObject();
$document = new AccompanyingCourseDocument();
$document
->setDate($date = new \DateTimeImmutable())
->setObject($object)
->setTitle('Title')
->setUser($user = new User())
->setCategory($category = new DocumentCategory('bundle', 10))
->setDescription($description = 'Description');
$actual = $this->buildDuplicator()->duplicate($document);
self::assertSame($date, $actual->getDate());
// FYI, the duplication of object is checked by the mock
self::assertNotNull($actual->getObject());
self::assertStringStartsWith('Title', $actual->getTitle());
self::assertSame($user, $actual->getUser());
self::assertSame($category, $actual->getCategory());
self::assertEquals($description, $actual->getDescription());
}
private function buildDuplicator(): AccompanyingCourseDocumentDuplicator
{
$storedObjectDuplicate = $this->createMock(StoredObjectDuplicate::class);
$storedObjectDuplicate->expects($this->once())->method('duplicate')
->with($this->isInstanceOf(StoredObject::class))->willReturn(new StoredObject());
$translator = $this->createMock(TranslatorInterface::class);
$translator->method('trans')->withAnyParameters()->willReturn('duplicated');
$clock = new MockClock();
return new AccompanyingCourseDocumentDuplicator(
$storedObjectDuplicate,
$translator,
$clock
);
}
}

View File

@@ -1,94 +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\AccompanyingCourseDocument;
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
use Chill\DocStoreBundle\Workflow\AccompanyingCourseDocumentWorkflowHandler;
use Chill\DocStoreBundle\Workflow\WorkflowWithPublicViewDocumentHelper;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Service\AccompanyingPeriod\ProvidePersonsAssociated;
use Chill\PersonBundle\Service\AccompanyingPeriod\ProvideThirdPartiesAssociated;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment;
/**
* @internal
*
* @coversNothing
*/
class AccompanyingCourseDocumentWorkflowHandlerTest extends TestCase
{
use ProphecyTrait;
public function testGetSuggestedUsers()
{
$accompanyingPeriod = new AccompanyingPeriod();
$document = new AccompanyingCourseDocument();
$document->setCourse($accompanyingPeriod)->setUser($user1 = new User());
$accompanyingPeriod->setUser($user = new User());
$entityWorkflow = new EntityWorkflow();
$entityWorkflow->setRelatedEntityId(1);
$handler = new AccompanyingCourseDocumentWorkflowHandler(
$this->prophesize(TranslatorInterface::class)->reveal(),
$this->prophesize(EntityWorkflowRepository::class)->reveal(),
$this->buildRepository($document, 1),
new WorkflowWithPublicViewDocumentHelper($this->prophesize(Environment::class)->reveal()),
$this->prophesize(ProvideThirdPartiesAssociated::class)->reveal(),
$this->prophesize(ProvidePersonsAssociated::class)->reveal(),
);
$users = $handler->getSuggestedUsers($entityWorkflow);
self::assertCount(2, $users);
self::assertContains($user, $users);
self::assertContains($user1, $users);
}
public function testGetSuggestedUsersWithDuplicates()
{
$accompanyingPeriod = new AccompanyingPeriod();
$document = new AccompanyingCourseDocument();
$document->setCourse($accompanyingPeriod)->setUser($user1 = new User());
$accompanyingPeriod->setUser($user1);
$entityWorkflow = new EntityWorkflow();
$entityWorkflow->setRelatedEntityId(1);
$handler = new AccompanyingCourseDocumentWorkflowHandler(
$this->prophesize(TranslatorInterface::class)->reveal(),
$this->prophesize(EntityWorkflowRepository::class)->reveal(),
$this->buildRepository($document, 1),
new WorkflowWithPublicViewDocumentHelper($this->prophesize(Environment::class)->reveal()),
$this->prophesize(ProvideThirdPartiesAssociated::class)->reveal(),
$this->prophesize(ProvidePersonsAssociated::class)->reveal(),
);
$users = $handler->getSuggestedUsers($entityWorkflow);
self::assertCount(1, $users);
self::assertContains($user1, $users);
}
private function buildRepository(AccompanyingCourseDocument $document, int $id): AccompanyingCourseDocumentRepository
{
$repository = $this->prophesize(AccompanyingCourseDocumentRepository::class);
$repository->find($id)->willReturn($document);
return $repository->reveal();
}
}

View File

@@ -0,0 +1,208 @@
<?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,45 +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\Entity\AccompanyingCourseDocument;
use Chill\DocStoreBundle\Service\StoredObjectDuplicate;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Stores the logic to duplicate an AccompanyingCourseDocument associated to a workflow.
*/
class AccompanyingCourseDocumentDuplicator
{
public function __construct(
private readonly StoredObjectDuplicate $storedObjectDuplicate,
private readonly TranslatorInterface $translator,
private readonly ClockInterface $clock,
) {}
public function duplicate(AccompanyingCourseDocument $document): AccompanyingCourseDocument
{
$newDoc = new AccompanyingCourseDocument();
$newDoc
->setCourse($document->getCourse())
->setTitle($document->getTitle().' ('.$this->translator->trans('acc_course_document.duplicated_at', ['at' => $this->clock->now()]).')')
->setDate($document->getDate())
->setDescription($document->getDescription())
->setCategory($document->getCategory())
->setUser($document->getUser())
->setObject($this->storedObjectDuplicate->duplicate($document->getObject()))
;
return $newDoc;
}
}

View File

@@ -16,28 +16,20 @@ use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Workflow\EntityWorkflowWithPublicViewInterface;
use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface;
use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Chill\PersonBundle\Service\AccompanyingPeriod\ProvideThirdPartiesAssociated;
use Chill\PersonBundle\Service\AccompanyingPeriod\ProvidePersonsAssociated;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @implements EntityWorkflowWithStoredObjectHandlerInterface<AccompanyingCourseDocument>
*/
final readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowWithStoredObjectHandlerInterface, EntityWorkflowWithPublicViewInterface
readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowWithStoredObjectHandlerInterface
{
public function __construct(
private TranslatorInterface $translator,
private EntityWorkflowRepository $workflowRepository,
private AccompanyingCourseDocumentRepository $repository,
private WorkflowWithPublicViewDocumentHelper $publicViewDocumentHelper,
private ProvideThirdPartiesAssociated $thirdPartiesAssociated,
private ProvidePersonsAssociated $providePersonsAssociated,
) {}
public function getDeletionRoles(): array
@@ -95,28 +87,12 @@ final readonly class AccompanyingCourseDocumentWorkflowHandler implements Entity
public function getSuggestedUsers(EntityWorkflow $entityWorkflow): array
{
$related = $this->getRelatedEntity($entityWorkflow);
$suggestedUsers = $entityWorkflow->getUsersInvolved();
if (null === $related) {
return [];
}
$referrer = $this->getRelatedEntity($entityWorkflow)->getCourse()->getUser();
$suggestedUsers[spl_object_hash($referrer)] = $referrer;
$users = [];
if (null !== $user = $related->getUser()) {
$users[] = $user;
}
if (null !== $user = $related->getCourse()->getUser()) {
$users[] = $user;
}
return array_values(
// filter objects to remove duplicates
array_filter(
$users,
fn ($o, $k) => array_search($o, $users, true) === $k,
ARRAY_FILTER_USE_BOTH
)
);
return $suggestedUsers;
}
public function getTemplate(EntityWorkflow $entityWorkflow, array $options = []): string
@@ -160,31 +136,4 @@ final readonly class AccompanyingCourseDocumentWorkflowHandler implements Entity
return $this->workflowRepository->findByRelatedEntity(AccompanyingCourseDocument::class, $object->getId());
}
public function renderPublicView(EntityWorkflowSend $entityWorkflowSend, EntityWorkflowViewMetadataDTO $metadata): string
{
return $this->publicViewDocumentHelper->render($entityWorkflowSend, $metadata, $this);
}
public function getSuggestedPersons(EntityWorkflow $entityWorkflow): array
{
$related = $this->getRelatedEntity($entityWorkflow);
if (null === $related) {
return [];
}
return $this->providePersonsAssociated->getPersonsAssociated($related->getCourse());
}
public function getSuggestedThirdParties(EntityWorkflow $entityWorkflow): array
{
$related = $this->getRelatedEntity($entityWorkflow);
if (null === $related) {
return [];
}
return $this->thirdPartiesAssociated->getThirdPartiesAssociated($related->getCourse());
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\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

@@ -1,45 +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\MainBundle\Entity\Workflow\EntityWorkflowSend;
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface;
use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO;
use Twig\Environment;
class WorkflowWithPublicViewDocumentHelper
{
public function __construct(private readonly Environment $twig) {}
public function render(EntityWorkflowSend $send, EntityWorkflowViewMetadataDTO $metadata, EntityWorkflowHandlerInterface&EntityWorkflowWithStoredObjectHandlerInterface $handler): string
{
$entityWorkflow = $send->getEntityWorkflowStep()->getEntityWorkflow();
$storedObject = $handler->getAssociatedStoredObject($entityWorkflow);
if (null === $storedObject) {
return 'document removed';
}
$title = $handler->getEntityTitle($entityWorkflow);
return $this->twig->render(
'@ChillDocStore/Workflow/public_view_with_document_render.html.twig',
[
'title' => $title,
'storedObject' => $storedObject,
'send' => $send,
'metadata' => $metadata,
]
);
}
}

View File

@@ -5,6 +5,5 @@ module.exports = function(encore)
});
encore.addEntry('mod_async_upload', __dirname + '/Resources/public/module/async_upload/index.ts');
encore.addEntry('mod_document_action_buttons_group', __dirname + '/Resources/public/module/document_action_buttons_group/index');
encore.addEntry('mod_document_download_button', __dirname + '/Resources/public/module/button_download/index');
encore.addEntry('vue_document_signature', __dirname + '/Resources/public/vuejs/DocumentSignature/index');
encore.addEntry('vue_document_signature', __dirname + '/Resources/public/vuejs/DocumentSignature/index.ts');
};

View File

@@ -1,41 +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\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

@@ -1,11 +0,0 @@
acc_course_document:
duplicated_at: >-
Dupliqué le {at, date, long} à {at, time, short}
workflow:
public_link:
doc_shared_by_at_explanation: >-
Le document a été partagé avec vous par {byUser}, le {at, date, long} à {at, time, short}.
doc_shared_automatically_at_explanation: >-
Le document a été partagé avec vous le {at, date, long} à {at, time, short}

View File

@@ -74,18 +74,12 @@ 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
workflow:
Document deleted: Document supprimé
public_link:
shared_doc: Document partagé
title: Document partagé
main_document: Document principal
# ROLES
accompanyingCourseDocument: Documents dans les parcours d'accompagnement

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\EventBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Class Status.
@@ -37,7 +36,6 @@ 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 '@ChillMain/Admin/layoutWithVerticalMenu.html.twig' %}
{% extends "@ChillEvent/Admin/index.html.twig" %}
{% block admin_content -%}
<h1>{{ 'EventType list'|trans }}</h1>
<table class="table table-bordered border-dark align-middle">
<table class="records_list">
<thead>
<tr>
<th>{{ 'Id'|trans }}</th>

View File

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

View File

@@ -1,10 +1,10 @@
{% extends '@ChillMain/Admin/layoutWithVerticalMenu.html.twig' %}
{% extends "@ChillEvent/Admin/index.html.twig" %}
{% block admin_content -%}
<h1>{{ 'Status list'|trans }}</h1>
<table class="table table-bordered border-dark align-middle">
<table class="records_list">
<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\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
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,
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
WorkflowStoredObjectPermissionHelper $workflowDocumentService,
) {
parent::__construct($security, $workflowDocumentService);
}

View File

@@ -0,0 +1,19 @@
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

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

View File

@@ -5,7 +5,8 @@ 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'
images = __dirname + '/Resources/public/images/index.js',
sass_styles = __dirname + '/Resources/public/sass/index.js'
;
encore.addEntry('dispositifs_edit', dispositif_edit_file);
@@ -14,4 +15,6 @@ module.exports = function(encore, chillEntries)
encore.addEntry('images', images);
encore.addEntry('cs_cv', cv_edit_file);
chillEntries.push(sass_styles);
};

View File

@@ -1,32 +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\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

@@ -1,26 +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\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,9 +12,7 @@ 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;
@@ -66,14 +64,10 @@ class UserController extends CRUDController
$form->handleRequest($request);
if ($form->isValid()) {
$formData = $form[self::FORM_GROUP_CENTER_COMPOSED]->getData();
$selectedCenters = $formData['center'];
foreach ($selectedCenters as $center) {
$groupCenter = $this->getPersistedGroupCenter($center, $formData['permissionsgroup']);
$user->addGroupCenter($groupCenter);
}
$groupCenter = $this->getPersistedGroupCenter(
$form[self::FORM_GROUP_CENTER_COMPOSED]->getData()
);
$user->addGroupCenter($groupCenter);
if (0 === $this->validator->validate($user)->count()) {
$em->flush();
@@ -425,21 +419,17 @@ class UserController extends CRUDController
}
}
private function getPersistedGroupCenter(Center $center, PermissionsGroup $permissionsGroup)
private function getPersistedGroupCenter(GroupCenter $groupCenter)
{
$em = $this->managerRegistry->getManager();
$groupCenterManaged = $em->getRepository(GroupCenter::class)
->findOneBy([
'center' => $center,
'permissionsGroup' => $permissionsGroup,
'center' => $groupCenter->getCenter(),
'permissionsGroup' => $groupCenter->getPermissionsGroup(),
]);
if (!$groupCenterManaged) {
$groupCenter = new GroupCenter();
$groupCenter->setCenter($center);
$groupCenter->setPermissionsGroup($permissionsGroup);
$em->persist($groupCenter);
return $groupCenter;

View File

@@ -1,28 +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\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\CRUDController;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Symfony\Component\HttpFoundation\Request;
class UserGroupAdminController extends CRUDController
{
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
{
$query->addSelect('JSON_EXTRACT(e.label, :lang) AS HIDDEN labeli18n')
->setParameter('lang', $request->getLocale());
$query->addOrderBy('labeli18n', 'ASC');
return $query;
}
}

View File

@@ -1,16 +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\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
class UserGroupApiController extends ApiController {}

View File

@@ -1,177 +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\MainBundle\Controller;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
use Chill\MainBundle\Repository\UserGroupRepositoryInterface;
use Chill\MainBundle\Routing\ChillUrlGeneratorInterface;
use Chill\MainBundle\Security\Authorization\UserGroupVoter;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderManagerInterface;
use Doctrine\ORM\EntityManagerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Translation\TranslatableMessage;
use Twig\Environment;
/**
* Controller to see and manage user groups.
*/
final readonly class UserGroupController
{
public function __construct(
private UserGroupRepositoryInterface $userGroupRepository,
private Security $security,
private PaginatorFactoryInterface $paginatorFactory,
private Environment $twig,
private FormFactoryInterface $formFactory,
private ChillUrlGeneratorInterface $chillUrlGenerator,
private EntityManagerInterface $objectManager,
private ChillEntityRenderManagerInterface $chillEntityRenderManager,
) {}
#[Route('/{_locale}/main/user-groups/my', name: 'chill_main_user_groups_my')]
public function myUserGroups(): Response
{
if (!$this->security->isGranted('ROLE_USER')) {
throw new AccessDeniedHttpException();
}
$user = $this->security->getUser();
if (!$user instanceof User) {
throw new AccessDeniedHttpException();
}
$nb = $this->userGroupRepository->countByUser($user);
$paginator = $this->paginatorFactory->create($nb);
$groups = $this->userGroupRepository->findByUser($user, true, $paginator->getItemsPerPage(), $paginator->getCurrentPageFirstItemNumber());
$forms = new \SplObjectStorage();
foreach ($groups as $group) {
$forms->attach($group, $this->createFormAppendUserForGroup($group)?->createView());
}
return new Response($this->twig->render('@ChillMain/UserGroup/my_user_groups.html.twig', [
'groups' => $groups,
'paginator' => $paginator,
'forms' => $forms,
]));
}
#[Route('/{_locale}/main/user-groups/{id}/append', name: 'chill_main_user_groups_append_users')]
public function appendUsersToGroup(UserGroup $userGroup, Request $request, Session $session): Response
{
if (!$this->security->isGranted(UserGroupVoter::APPEND_TO_GROUP, $userGroup)) {
throw new AccessDeniedHttpException();
}
$form = $this->createFormAppendUserForGroup($userGroup);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
foreach ($form['users']->getData() as $user) {
$userGroup->addUser($user);
$session->getFlashBag()->add(
'success',
new TranslatableMessage(
'user_group.user_added',
[
'user_group' => $this->chillEntityRenderManager->renderString($userGroup, []),
'user' => $this->chillEntityRenderManager->renderString($user, []),
]
)
);
}
$this->objectManager->flush();
return new RedirectResponse(
$this->chillUrlGenerator->returnPathOr('chill_main_user_groups_my')
);
}
if ($form->isSubmitted()) {
$errors = [];
foreach ($form->getErrors() as $error) {
$errors[] = $error->getMessage();
}
return new Response(implode(', ', $errors));
}
return new RedirectResponse(
$this->chillUrlGenerator->returnPathOr('chill_main_user_groups_my')
);
}
/**
* @ParamConverter("user", class=User::class, options={"id" = "userId"})
*/
#[Route('/{_locale}/main/user-group/{id}/user/{userId}/remove', name: 'chill_main_user_groups_remove_user')]
public function removeUserToGroup(UserGroup $userGroup, User $user, Session $session): Response
{
if (!$this->security->isGranted(UserGroupVoter::APPEND_TO_GROUP, $userGroup)) {
throw new AccessDeniedHttpException();
}
$userGroup->removeUser($user);
$this->objectManager->flush();
$session->getFlashBag()->add(
'success',
new TranslatableMessage(
'user_group.user_removed',
[
'user_group' => $this->chillEntityRenderManager->renderString($userGroup, []),
'user' => $this->chillEntityRenderManager->renderString($user, []),
]
)
);
return new RedirectResponse(
$this->chillUrlGenerator->returnPathOr('chill_main_user_groups_my')
);
}
private function createFormAppendUserForGroup(UserGroup $group): ?FormInterface
{
if (!$this->security->isGranted(UserGroupVoter::APPEND_TO_GROUP, $group)) {
return null;
}
$builder = $this->formFactory->createBuilder(FormType::class, ['users' => []], [
'action' => $this->chillUrlGenerator->generateWithReturnPath('chill_main_user_groups_append_users', ['id' => $group->getId()]),
]);
$builder->add('users', PickUserDynamicType::class, [
'submit_on_adding_new_entity' => true,
'label' => 'user_group.append_users',
'mapped' => false,
'multiple' => true,
]);
return $builder->getForm();
}
}

View File

@@ -71,7 +71,7 @@ final readonly class WorkflowAddSignatureController
return new Response(
$this->twig->render(
'@ChillMain/Workflow/signature_sign.html.twig',
'@ChillMain/Workflow/_signature_sign.html.twig',
['signature' => $signatureClient]
)
);

View File

@@ -300,12 +300,19 @@ class WorkflowController extends AbstractController
if (\count($workflow->getEnabledTransitions($entityWorkflow)) > 0) {
// possible transition
$stepDTO = new WorkflowTransitionContextDTO($entityWorkflow);
$usersInvolved = $entityWorkflow->getUsersInvolved();
$currentUserFound = array_search($this->security->getUser(), $usersInvolved, true);
if (false !== $currentUserFound) {
unset($usersInvolved[$currentUserFound]);
}
$transitionForm = $this->createForm(
WorkflowStepType::class,
$stepDTO,
[
'entity_workflow' => $entityWorkflow,
'suggested_users' => $usersInvolved,
]
);
@@ -423,7 +430,7 @@ class WorkflowController extends AbstractController
}
return $this->render(
'@ChillMain/Workflow/signature_metadata.html.twig',
'@ChillMain/Workflow/_signature_metadata.html.twig',
[
'metadata_form' => $metadataForm->createView(),
'person' => $signature->getSigner(),

View File

@@ -1,98 +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\MainBundle\Controller;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
use Chill\MainBundle\Routing\ChillUrlGeneratorInterface;
use Chill\MainBundle\Security\Authorization\EntityWorkflowStepSignatureVoter;
use Chill\MainBundle\Workflow\SignatureStepStateChanger;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Twig\Environment;
final readonly class WorkflowSignatureCancelController
{
public function __construct(
private EntityManagerInterface $entityManager,
private Security $security,
private FormFactoryInterface $formFactory,
private Environment $twig,
private SignatureStepStateChanger $signatureStepStateChanger,
private ChillUrlGeneratorInterface $chillUrlGenerator,
) {}
#[Route('/{_locale}/main/workflow/signature/{id}/cancel', name: 'chill_main_workflow_signature_cancel')]
public function cancelSignature(EntityWorkflowStepSignature $signature, Request $request): Response
{
return $this->markSignatureAction(
$signature,
$request,
EntityWorkflowStepSignatureVoter::CANCEL,
function (EntityWorkflowStepSignature $signature) {$this->signatureStepStateChanger->markSignatureAsCanceled($signature); },
'@ChillMain/WorkflowSignature/cancel.html.twig',
);
}
#[Route('/{_locale}/main/workflow/signature/{id}/reject', name: 'chill_main_workflow_signature_reject')]
public function rejectSignature(EntityWorkflowStepSignature $signature, Request $request): Response
{
return $this->markSignatureAction(
$signature,
$request,
EntityWorkflowStepSignatureVoter::REJECT,
function (EntityWorkflowStepSignature $signature) {$this->signatureStepStateChanger->markSignatureAsRejected($signature); },
'@ChillMain/WorkflowSignature/reject.html.twig',
);
}
private function markSignatureAction(
EntityWorkflowStepSignature $signature,
Request $request,
string $permissionAttribute,
callable $markSignature,
string $template,
): Response {
if (!$this->security->isGranted($permissionAttribute, $signature)) {
throw new AccessDeniedHttpException('not allowed to cancel this signature');
}
$form = $this->formFactory->create();
$form->add('confirm', SubmitType::class, ['label' => 'Confirm']);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$markSignature($signature);
$this->entityManager->flush();
return new RedirectResponse(
$this->chillUrlGenerator->returnPathOr('chill_main_workflow_show', ['id' => $signature->getStep()->getEntityWorkflow()->getId()])
);
}
return
new Response(
$this->twig->render(
$template,
['form' => $form->createView(), 'signature' => $signature]
)
);
}
}

View File

@@ -1,89 +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\MainBundle\Controller;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSendView;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\Exception\HandlerWithPublicViewNotFoundException;
use Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage;
use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Annotation\Route;
use Twig\Environment;
final readonly class WorkflowViewSendPublicController
{
public const LOG_PREFIX = '[workflow-view-send-public-controller] ';
public function __construct(
private EntityManagerInterface $entityManager,
private LoggerInterface $chillLogger,
private EntityWorkflowManager $entityWorkflowManager,
private ClockInterface $clock,
private Environment $environment,
private MessageBusInterface $messageBus,
) {}
#[Route('/public/main/workflow/send/{uuid}/view/{verificationKey}', name: 'chill_main_workflow_send_view_public', methods: ['GET'])]
public function __invoke(EntityWorkflowSend $workflowSend, string $verificationKey, Request $request): Response
{
if (50 < $workflowSend->getNumberOfErrorTrials()) {
throw new AccessDeniedHttpException('number of trials exceeded, no more access allowed');
}
if ($verificationKey !== $workflowSend->getPrivateToken()) {
$this->chillLogger->info(self::LOG_PREFIX.'Invalid trial for this send', ['client_ip' => $request->getClientIp()]);
$workflowSend->increaseErrorTrials();
$this->entityManager->flush();
throw new AccessDeniedHttpException('invalid verification key');
}
if ($this->clock->now() > $workflowSend->getExpireAt()) {
return new Response(
$this->environment->render('@ChillMain/Workflow/workflow_view_send_public_expired.html.twig'),
409
);
}
if (100 < $workflowSend->getViews()->count()) {
$this->chillLogger->info(self::LOG_PREFIX.'100 view reached, not allowed to see it again');
throw new AccessDeniedHttpException('100 views reached, not allowed to see it again');
}
try {
$metadata = new EntityWorkflowViewMetadataDTO(
$workflowSend->getViews()->count(),
100 - $workflowSend->getViews()->count(),
);
$response = new Response(
$this->entityWorkflowManager->renderPublicView($workflowSend, $metadata),
);
$view = new EntityWorkflowSendView($workflowSend, $this->clock->now(), $request->getClientIp());
$this->entityManager->persist($view);
$this->messageBus->dispatch(new PostPublicViewMessage($view->getId()));
$this->entityManager->flush();
return $response;
} catch (HandlerWithPublicViewNotFoundException $e) {
throw new \RuntimeException('Could not render the public view', previous: $e);
}
}
}

View File

@@ -1,63 +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\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

@@ -1,68 +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\MainBundle\DataFixtures\ORM;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
use Doctrine\Persistence\ObjectManager;
class LoadUserGroup extends Fixture implements FixtureGroupInterface
{
public static function getGroups(): array
{
return ['user-group'];
}
public function load(ObjectManager $manager)
{
$centerASocial = $manager->getRepository(User::class)->findOneBy(['username' => 'center a_social']);
$centerBSocial = $manager->getRepository(User::class)->findOneBy(['username' => 'center b_social']);
$multiCenter = $manager->getRepository(User::class)->findOneBy(['username' => 'multi_center']);
$administrativeA = $manager->getRepository(User::class)->findOneBy(['username' => 'center a_administrative']);
$administrativeB = $manager->getRepository(User::class)->findOneBy(['username' => 'center b_administrative']);
$level1 = $this->generateLevelGroup('Niveau 1', '#eec84aff', '#000000ff', 'level');
$level1->addUser($centerASocial)->addUser($centerBSocial);
$manager->persist($level1);
$level2 = $this->generateLevelGroup('Niveau 2', ' #e2793dff', '#000000ff', 'level');
$level2->addUser($multiCenter);
$manager->persist($level2);
$level3 = $this->generateLevelGroup('Niveau 3', ' #df4949ff', '#000000ff', 'level');
$level3->addUser($multiCenter);
$manager->persist($level3);
$tss = $this->generateLevelGroup('Travailleur sociaux', '#43b29dff', '#000000ff', '');
$tss->addUser($multiCenter)->addUser($centerASocial)->addUser($centerBSocial);
$manager->persist($tss);
$admins = $this->generateLevelGroup('Administratif', '#334d5cff', '#000000ff', '');
$admins->addUser($administrativeA)->addUser($administrativeB);
$manager->persist($admins);
$manager->flush();
}
private function generateLevelGroup(string $title, string $backgroundColor, string $foregroundColor, string $excludeKey): UserGroup
{
$userGroup = new UserGroup();
return $userGroup
->setLabel(['fr' => $title])
->setBackgroundColor($backgroundColor)
->setForegroundColor($foregroundColor)
->setExcludeKey($excludeKey)
;
}
}

View File

@@ -17,8 +17,6 @@ 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;
@@ -26,8 +24,6 @@ use Chill\MainBundle\Controller\LocationTypeController;
use Chill\MainBundle\Controller\NewsItemController;
use Chill\MainBundle\Controller\RegroupmentController;
use Chill\MainBundle\Controller\UserController;
use Chill\MainBundle\Controller\UserGroupAdminController;
use Chill\MainBundle\Controller\UserGroupApiController;
use Chill\MainBundle\Controller\UserJobApiController;
use Chill\MainBundle\Controller\UserJobController;
use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface;
@@ -56,7 +52,6 @@ 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;
@@ -64,18 +59,15 @@ use Chill\MainBundle\Entity\LocationType;
use Chill\MainBundle\Entity\NewsItem;
use Chill\MainBundle\Entity\Regroupment;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
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;
use Chill\MainBundle\Form\NewsItemType;
use Chill\MainBundle\Form\RegroupmentType;
use Chill\MainBundle\Form\UserGroupType;
use Chill\MainBundle\Form\UserJobType;
use Chill\MainBundle\Form\UserType;
use Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType;
@@ -361,28 +353,6 @@ class ChillMainExtension extends Extension implements
{
$container->prependExtensionConfig('chill_main', [
'cruds' => [
[
'class' => UserGroup::class,
'controller' => UserGroupAdminController::class,
'name' => 'admin_user_group',
'base_path' => '/admin/main/user-group',
'base_role' => 'ROLE_ADMIN',
'form_class' => UserGroupType::class,
'actions' => [
'index' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillMain/UserGroup/index.html.twig',
],
'new' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillMain/UserGroup/new.html.twig',
],
'edit' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillMain/UserGroup/edit.html.twig',
],
],
],
[
'class' => UserJob::class,
'controller' => UserJobController::class,
@@ -515,28 +485,6 @@ 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',
@@ -840,21 +788,6 @@ 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,
@@ -870,21 +803,6 @@ class ChillMainExtension extends Extension implements
],
],
],
[
'class' => UserGroup::class,
'controller' => UserGroupApiController::class,
'name' => 'user-group',
'base_path' => '/api/1.0/main/user-group',
'base_role' => 'ROLE_USER',
'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
],
],
],
],
],
]);
}

View File

@@ -1,104 +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\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

@@ -1,20 +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\MainBundle\Entity;
enum GenderEnum: string
{
case MALE = 'man';
case FEMALE = 'woman';
case NEUTRAL = 'neutral';
case UNKNOWN = 'unknown';
}

View File

@@ -1,22 +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\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

@@ -1,244 +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\MainBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
use Doctrine\Common\Collections\ReadableCollection;
use Doctrine\Common\Collections\Selectable;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity]
#[ORM\Table(name: 'chill_main_user_group')]
// this discriminator key is required for automated denormalization
#[DiscriminatorMap('type', mapping: ['user_group' => UserGroup::class])]
class UserGroup
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)]
private ?int $id = null;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: false, options: ['default' => true])]
private bool $active = true;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
private array $label = [];
/**
* @var Collection<int, User>&Selectable<int, User>
*/
#[ORM\ManyToMany(targetEntity: User::class)]
#[ORM\JoinTable(name: 'chill_main_user_group_user')]
private Collection&Selectable $users;
/**
* @var Collection<int, User>&Selectable<int, User>
*/
#[ORM\ManyToMany(targetEntity: User::class)]
#[ORM\JoinTable(name: 'chill_main_user_group_user_admin')]
private Collection&Selectable $adminUsers;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => '#ffffffff'])]
private string $backgroundColor = '#ffffffff';
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => '#000000ff'])]
private string $foregroundColor = '#000000ff';
/**
* Groups with same exclude key are mutually exclusive: adding one in a many-to-one relationship
* will exclude others.
*
* An empty string means "no exclusion"
*/
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
private string $excludeKey = '';
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
#[Assert\Email]
private string $email = '';
public function __construct()
{
$this->adminUsers = new ArrayCollection();
$this->users = new ArrayCollection();
}
public function isActive(): bool
{
return $this->active;
}
public function setActive(bool $active): self
{
$this->active = $active;
return $this;
}
public function addAdminUser(User $user): self
{
if (!$this->adminUsers->contains($user)) {
$this->adminUsers[] = $user;
}
return $this;
}
public function removeAdminUser(User $user): self
{
$this->adminUsers->removeElement($user);
return $this;
}
public function addUser(User $user): self
{
if (!$this->users->contains($user)) {
$this->users[] = $user;
}
return $this;
}
public function removeUser(User $user): self
{
if ($this->users->contains($user)) {
$this->users->removeElement($user);
}
return $this;
}
public function getId(): ?int
{
return $this->id;
}
public function getLabel(): array
{
return $this->label;
}
/**
* @return Selectable<int, User>&Collection<int, User>
*/
public function getUsers(): Collection&Selectable
{
return $this->users;
}
/**
* @return Selectable<int, User>&Collection<int, User>
*/
public function getAdminUsers(): Collection&Selectable
{
return $this->adminUsers;
}
public function getForegroundColor(): string
{
return $this->foregroundColor;
}
public function getExcludeKey(): string
{
return $this->excludeKey;
}
public function getBackgroundColor(): string
{
return $this->backgroundColor;
}
public function setForegroundColor(string $foregroundColor): self
{
$this->foregroundColor = $foregroundColor;
return $this;
}
public function setBackgroundColor(string $backgroundColor): self
{
$this->backgroundColor = $backgroundColor;
return $this;
}
public function setExcludeKey(string $excludeKey): self
{
$this->excludeKey = $excludeKey;
return $this;
}
public function setLabel(array $label): self
{
$this->label = $label;
return $this;
}
public function getEmail(): string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
public function hasEmail(): bool
{
return '' !== $this->email;
}
/**
* Checks if the current object is an instance of the UserGroup class.
*
* In use in twig template, to discriminate when there an object can be polymorphic.
*
* @return bool returns true if the current object is an instance of UserGroup, false otherwise
*/
public function isUserGroup(): bool
{
return true;
}
public function contains(User $user): bool
{
return $this->users->contains($user);
}
public function getUserListByLabelAscending(): ReadableCollection
{
$criteria = Criteria::create();
$criteria->orderBy(['label' => Order::Ascending]);
return $this->getUsers()->matching($criteria);
}
public function getAdminUserListByLabelAscending(): ReadableCollection
{
$criteria = Criteria::create();
$criteria->orderBy(['label' => Order::Ascending]);
return $this->getAdminUsers()->matching($criteria);
}
}

View File

@@ -243,9 +243,6 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
throw new \RuntimeException();
}
/**
* @return Selectable<int, EntityWorkflowStep>&Collection<int, EntityWorkflowStep>
*/
public function getSteps(): Collection&Selectable
{
return $this->steps;
@@ -434,7 +431,6 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
$previousStep = $this->getCurrentStep();
$previousStep
->setComment($transitionContextDTO->comment)
->setTransitionAfter($transition)
->setTransitionAt($transitionAt)
->setTransitionBy($byUser);
@@ -446,18 +442,18 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
$newStep->addCcUser($user);
}
foreach ($transitionContextDTO->getFutureDestUsers() as $user) {
foreach ($transitionContextDTO->futureDestUsers as $user) {
$newStep->addDestUser($user);
}
foreach ($transitionContextDTO->getFutureDestUserGroups() as $userGroup) {
$newStep->addDestUserGroup($userGroup);
}
if (null !== $transitionContextDTO->futureUserSignature) {
$newStep->addDestUser($transitionContextDTO->futureUserSignature);
}
foreach ($transitionContextDTO->futureDestEmails as $email) {
$newStep->addDestEmail($email);
}
if (null !== $transitionContextDTO->futureUserSignature) {
new EntityWorkflowStepSignature($newStep, $transitionContextDTO->futureUserSignature);
} else {
@@ -466,13 +462,6 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
}
}
foreach ($transitionContextDTO->futureDestineeThirdParties as $thirdParty) {
new EntityWorkflowSend($newStep, $thirdParty, $transitionAt->add(new \DateInterval('P30D')));
}
foreach ($transitionContextDTO->futureDestineeEmails as $email) {
new EntityWorkflowSend($newStep, $email, $transitionAt->add(new \DateInterval('P30D')));
}
// copy the freeze
if ($this->isFreeze()) {
$newStep->setFreezeAfter(true);

View File

@@ -1,227 +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\MainBundle\Entity\Workflow;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
use Random\Randomizer;
/**
* An entity which stores then sending of a workflow's content to
* some external entity.
*/
#[ORM\Entity]
#[ORM\Table(name: 'chill_main_workflow_entity_send')]
class EntityWorkflowSend implements TrackCreationInterface
{
use TrackCreationTrait;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: Types::INTEGER)]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: ThirdParty::class)]
#[ORM\JoinColumn(nullable: true)]
private ?ThirdParty $destineeThirdParty = null;
#[ORM\Column(type: Types::TEXT, nullable: false, options: ['default' => ''])]
private string $destineeEmail = '';
#[ORM\Column(type: 'uuid', unique: true, nullable: false)]
private UuidInterface $uuid;
#[ORM\Column(type: Types::STRING, length: 255, nullable: false)]
private string $privateToken;
#[ORM\Column(type: Types::INTEGER, nullable: false, options: ['default' => 0])]
private int $numberOfErrorTrials = 0;
/**
* @var Collection<int, EntityWorkflowSendView>
*/
#[ORM\OneToMany(mappedBy: 'send', targetEntity: EntityWorkflowSendView::class, cascade: ['remove'])]
private Collection $views;
public function __construct(
#[ORM\ManyToOne(targetEntity: EntityWorkflowStep::class, inversedBy: 'sends')]
#[ORM\JoinColumn(nullable: false)]
private EntityWorkflowStep $entityWorkflowStep,
string|ThirdParty $destinee,
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: false)]
private \DateTimeImmutable $expireAt,
) {
$this->uuid = Uuid::uuid4();
$random = new Randomizer();
$this->privateToken = bin2hex($random->getBytes(48));
$this->entityWorkflowStep->addSend($this);
if ($destinee instanceof ThirdParty) {
$this->destineeThirdParty = $destinee;
} else {
$this->destineeEmail = $destinee;
}
$this->views = new ArrayCollection();
}
/**
* @internal use the @see{EntityWorkflowSendView}'s constructor instead
*/
public function addView(EntityWorkflowSendView $view): self
{
if (!$this->views->contains($view)) {
$this->views->add($view);
}
return $this;
}
public function getDestineeEmail(): string
{
return $this->destineeEmail;
}
public function getDestineeThirdParty(): ?ThirdParty
{
return $this->destineeThirdParty;
}
public function getId(): ?int
{
return $this->id;
}
public function getNumberOfErrorTrials(): int
{
return $this->numberOfErrorTrials;
}
public function getPrivateToken(): string
{
return $this->privateToken;
}
public function getEntityWorkflowStep(): EntityWorkflowStep
{
return $this->entityWorkflowStep;
}
public function getEntityWorkflowStepChained(): ?EntityWorkflowStep
{
foreach ($this->getEntityWorkflowStep()->getEntityWorkflow()->getStepsChained() as $step) {
if ($this->getEntityWorkflowStep() === $step) {
return $step;
}
}
return null;
}
public function getUuid(): UuidInterface
{
return $this->uuid;
}
public function getExpireAt(): \DateTimeImmutable
{
return $this->expireAt;
}
public function getViews(): Collection
{
return $this->views;
}
public function increaseErrorTrials(): void
{
$this->numberOfErrorTrials = $this->numberOfErrorTrials + 1;
}
public function getDestinee(): string|ThirdParty
{
if (null !== $this->getDestineeThirdParty()) {
return $this->getDestineeThirdParty();
}
return $this->getDestineeEmail();
}
/**
* Determines the kind of destinee based on whether the destinee is a thirdParty or an emailAddress.
*
* @return 'thirdParty'|'email' 'thirdParty' if the destinee is a third party, 'email' otherwise
*/
public function getDestineeKind(): string
{
if (null !== $this->getDestineeThirdParty()) {
return 'thirdParty';
}
return 'email';
}
public function isViewed(): bool
{
return $this->views->count() > 0;
}
public function isExpired(?\DateTimeImmutable $now = null): bool
{
return ($now ?? new \DateTimeImmutable('now')) >= $this->expireAt;
}
/**
* Retrieves the most recent view.
*
* @return EntityWorkflowSendView|null returns the last view or null if there are no views
*/
public function getLastView(): ?EntityWorkflowSendView
{
$last = null;
foreach ($this->views as $view) {
if (null === $last) {
$last = $view;
} else {
if ($view->getViewAt() > $last->getViewAt()) {
$last = $view;
}
}
}
return $last;
}
/**
* Retrieves an array of views grouped by their remote IP address.
*
* @return array<string, list<EntityWorkflowSendView>> an associative array where the keys are IP addresses and the values are arrays of views associated with those IPs
*/
public function getViewsByIp(): array
{
$views = [];
foreach ($this->getViews() as $view) {
$views[$view->getRemoteIp()][] = $view;
}
return $views;
}
}

View File

@@ -1,60 +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\MainBundle\Entity\Workflow;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
/**
* Register the viewing action from an external destinee.
*/
#[ORM\Entity(readOnly: true)]
#[ORM\Table(name: 'chill_main_workflow_entity_send_views')]
class EntityWorkflowSendView
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: Types::INTEGER)]
private ?int $id = null;
public function __construct(
#[ORM\ManyToOne(targetEntity: EntityWorkflowSend::class, inversedBy: 'views')]
#[ORM\JoinColumn(nullable: false)]
private EntityWorkflowSend $send,
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
private \DateTimeInterface $viewAt,
#[ORM\Column(type: Types::TEXT)]
private string $remoteIp = '',
) {
$this->send->addView($this);
}
public function getId(): ?int
{
return $this->id;
}
public function getRemoteIp(): string
{
return $this->remoteIp;
}
public function getSend(): EntityWorkflowSend
{
return $this->send;
}
public function getViewAt(): \DateTimeInterface
{
return $this->viewAt;
}
}

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Entity\Workflow;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
@@ -49,13 +48,6 @@ class EntityWorkflowStep
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user')]
private Collection $destUser;
/**
* @var Collection<int, UserGroup>
*/
#[ORM\ManyToMany(targetEntity: UserGroup::class)]
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user_group')]
private Collection $destUserGroups;
/**
* @var Collection<int, User>
*/
@@ -66,7 +58,7 @@ class EntityWorkflowStep
/**
* @var Collection <int, EntityWorkflowStepSignature>
*/
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepSignature::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepSignature::class, cascade: ['persist'], orphanRemoval: true)]
private Collection $signatures;
#[ORM\ManyToOne(targetEntity: EntityWorkflow::class, inversedBy: 'steps')]
@@ -112,21 +104,13 @@ class EntityWorkflowStep
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepHold::class)]
private Collection $holdsOnStep;
/**
* @var Collection<int, EntityWorkflowSend>
*/
#[ORM\OneToMany(mappedBy: 'entityWorkflowStep', targetEntity: EntityWorkflowSend::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
private Collection $sends;
public function __construct()
{
$this->ccUser = new ArrayCollection();
$this->destUser = new ArrayCollection();
$this->destUserGroups = new ArrayCollection();
$this->destUserByAccessKey = new ArrayCollection();
$this->signatures = new ArrayCollection();
$this->holdsOnStep = new ArrayCollection();
$this->sends = new ArrayCollection();
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(32));
}
@@ -139,9 +123,6 @@ class EntityWorkflowStep
return $this;
}
/**
* @deprecated
*/
public function addDestEmail(string $email): self
{
if (!\in_array($email, $this->destEmail, true)) {
@@ -160,22 +141,6 @@ class EntityWorkflowStep
return $this;
}
public function addDestUserGroup(UserGroup $userGroup): self
{
if (!$this->destUserGroups->contains($userGroup)) {
$this->destUserGroups[] = $userGroup;
}
return $this;
}
public function removeDestUserGroup(UserGroup $userGroup): self
{
$this->destUserGroups->removeElement($userGroup);
return $this;
}
public function addDestUserByAccessKey(User $user): self
{
if (!$this->destUserByAccessKey->contains($user) && !$this->destUser->contains($user)) {
@@ -197,18 +162,6 @@ class EntityWorkflowStep
return $this;
}
/**
* @internal use @see{EntityWorkflowSend}'s constructor instead
*/
public function addSend(EntityWorkflowSend $send): self
{
if (!$this->sends->contains($send)) {
$this->sends[] = $send;
}
return $this;
}
public function removeSignature(EntityWorkflowStepSignature $signature): self
{
if ($this->signatures->contains($signature)) {
@@ -225,9 +178,7 @@ class EntityWorkflowStep
/**
* get all the users which are allowed to apply a transition: those added manually, and
* those added automatically by using an access key.
*
* This method exclude the users associated with user groups
* those added automatically bu using an access key.
*
* @psalm-suppress DuplicateArrayKey
*/
@@ -241,14 +192,6 @@ class EntityWorkflowStep
);
}
/**
* @return Collection<int, UserGroup>
*/
public function getDestUserGroups(): Collection
{
return $this->destUserGroups;
}
public function getCcUser(): Collection
{
return $this->ccUser;
@@ -264,11 +207,6 @@ class EntityWorkflowStep
return $this->currentStep;
}
/**
* @return array<string>
*
* @deprecated
*/
public function getDestEmail(): array
{
return $this->destEmail;
@@ -303,14 +241,6 @@ class EntityWorkflowStep
return $this->signatures;
}
/**
* @return Collection<int, EntityWorkflowSend>
*/
public function getSends(): Collection
{
return $this->sends;
}
public function getId(): ?int
{
return $this->id;

View File

@@ -161,16 +161,6 @@ class EntityWorkflowStepSignature implements TrackCreationInterface, TrackUpdate
return EntityWorkflowSignatureStateEnum::PENDING == $this->getState();
}
public function isCanceled(): bool
{
return EntityWorkflowSignatureStateEnum::CANCELED === $this->getState();
}
public function isRejected(): bool
{
return EntityWorkflowSignatureStateEnum::REJECTED === $this->getState();
}
/**
* Checks whether all signatures associated with a given workflow step are not pending.
*

View File

@@ -1,64 +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\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

@@ -36,7 +36,6 @@ class ChillCollectionType extends AbstractType
$view->vars['identifier'] = $options['identifier'];
$view->vars['empty_collection_explain'] = $options['empty_collection_explain'];
$view->vars['js_caller'] = $options['js_caller'];
$view->vars['uniqid'] = uniqid();
}
public function configureOptions(OptionsResolver $resolver)

View File

@@ -13,30 +13,36 @@ namespace Chill\MainBundle\Form\Type;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\PermissionsGroup;
use Chill\MainBundle\Repository\CenterRepository;
use Doctrine\ORM\EntityRepository;
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', ChoiceType::class, [
'choices' => $centers,
'choice_label' => fn (Center $center) => $center->getName(),
'multiple' => true,
])->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;
},
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('data_class', \Chill\MainBundle\Entity\GroupCenter::class);
}
public function getBlockPrefix()
{
return 'composed_groupcenter';

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