mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-17 04:05:00 +00:00
Compare commits
177 Commits
295-cancel
...
signature-
Author | SHA1 | Date | |
---|---|---|---|
fd69568842
|
|||
71aaf01687 | |||
a256307b82
|
|||
a6480191e5 | |||
19eb6f7ebb
|
|||
261bc88b5e
|
|||
4f18b1d2b2
|
|||
968835a262
|
|||
85dc9bdb2f
|
|||
c877076429
|
|||
418794e586 | |||
fd66dbf26e
|
|||
fde74b190d
|
|||
527cf23d4f
|
|||
1d708a481d
|
|||
ff5640e193
|
|||
d45de5405b
|
|||
7b322d7bab
|
|||
daef18408a
|
|||
91a4b45607
|
|||
29fa086fde
|
|||
508c4cd674
|
|||
9fe20b5e81
|
|||
d8ded80582
|
|||
d283d62049
|
|||
6cd336922f
|
|||
13dbbb6741
|
|||
1313b6f138
|
|||
3d53e7da65
|
|||
8589bada3f
|
|||
292034d64d
|
|||
3f7c5d23dc
|
|||
78445f0d65
|
|||
c329a1f1f8
|
|||
9d722110a6
|
|||
82e2b9a0f6
|
|||
40b8fae8ba
|
|||
b99ea3b17a
|
|||
3f80d62ca2
|
|||
118ae291e2
|
|||
5c0f3cb317
|
|||
a0b5c208eb
|
|||
7913a377c8
|
|||
7cd638c5fc
|
|||
071c5e3c55
|
|||
da6589ba87
|
|||
a563ba644e
|
|||
2213f6f429
|
|||
9a9d14eb5a
|
|||
4cc001a070
|
|||
6c52ff84a8 | |||
818d800384
|
|||
cef641ee24
|
|||
c4c7280b52
|
|||
d8ad8c3605
|
|||
803332ba5f
|
|||
479651b31e
|
|||
7bedf1b5b8
|
|||
6b764114e4
|
|||
03a150aa16
|
|||
81706a61ef
|
|||
debca1f474
|
|||
2e71808be1
|
|||
0c1d9ee4be
|
|||
87599425df
|
|||
86ec6f82da
|
|||
17f4c85fa5
|
|||
82cd77678b
|
|||
9e69c97250
|
|||
b4fa478177
|
|||
42438d5bb5 | |||
5287824dbe
|
|||
cfce531754
|
|||
83121c2a83
|
|||
5a467ae38d
|
|||
75005b4ed6 | |||
90c5b0341a
|
|||
5a5d259d18
|
|||
758a14366e
|
|||
e91fce524e
|
|||
7b06c80c2a | |||
cf2fe1bba7
|
|||
27df3b2c9b
|
|||
b350c0cfe8 | |||
611a968162
|
|||
ce80207d98
|
|||
5fc5369db6
|
|||
20e8b03588
|
|||
4b65ec9b54
|
|||
a8c5d1f660
|
|||
5f67a7aadc
|
|||
77d06d756a
|
|||
c4c5c860f0 | |||
47f575de92
|
|||
5906171041
|
|||
b0e2e65885
|
|||
dd3f6fb0ab
|
|||
5fa5a2349e
|
|||
48f727dcfd
|
|||
6a0e26ec31
|
|||
943a42cd38
|
|||
d9b36533a2
|
|||
3697aee584 | |||
33cc308e1e
|
|||
7206e13984
|
|||
6f28d154c8
|
|||
4d8de46ac9
|
|||
4696332a46
|
|||
0d54637d35
|
|||
7a7d1d5b16
|
|||
e5737b0c49
|
|||
45323e9136
|
|||
9f1afb8423
|
|||
1494c7ecd7 | |||
911dfc2878
|
|||
8e984f2006
|
|||
f0e8df38af
|
|||
1c0d334b91
|
|||
|
59c34dabd7 | ||
|
119668e415 | ||
|
2b516629f6 | ||
|
092b5c4f90 | ||
|
ae1459cf77 | ||
|
57d2929ecd | ||
bc34d84d63
|
|||
f0f651edea
|
|||
|
3c987e0b8d | ||
f8a986d59b
|
|||
09563979a2
|
|||
|
0ee91800ab | ||
|
d08212df46
|
||
|
4933238f3f
|
||
|
c23568032c
|
||
18af2ca70b
|
|||
f1505a9d15
|
|||
4e588ed0e0
|
|||
70671dadac
|
|||
5dfbdad13d
|
|||
b3e2d4ff9f | |||
01c2848a83
|
|||
d0ee381627
|
|||
8b1b255050
|
|||
f0d581b7f8 | |||
|
1197a46f5f
|
||
00e878892e | |||
941444b7d5
|
|||
a60ea0e066
|
|||
1ddd283f26
|
|||
669b967899
|
|||
d33da6519a
|
|||
f5ba5d574b
|
|||
ccc11b1c1d | |||
|
479a02bbc7 | ||
|
0d62d8d1c6 | ||
|
5b90632231 | ||
5d0b531820 | |||
5be3cae288 | |||
4587f66402 | |||
2bef3c3878
|
|||
cea44d1788
|
|||
84069e03dc
|
|||
ad5e780936
|
|||
19accc4d00
|
|||
6cb085f5f7
|
|||
97239ada84
|
|||
643156f822 | |||
ff0b205591 | |||
2d67843901 | |||
2b09e1459c | |||
029524ba2c | |||
fa91e9494d | |||
4e72d6fea1
|
|||
5666b8b647
|
|||
|
0573f56782 | ||
|
3bee18b0fa | ||
|
843698a1d8 | ||
|
499640e48b |
6
.changes/unreleased/Fixed-20240916-151843.yaml
Normal file
6
.changes/unreleased/Fixed-20240916-151843.yaml
Normal 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"
|
@@ -1,11 +1,30 @@
|
||||
## v2.23.0 - 2024-07-23
|
||||
## v2.23.0 - 2024-07-23 & 2024-07-19
|
||||
### Feature
|
||||
* ([#221](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/221)) [DX] move async-upload-bundle features into chill-bundles
|
||||
* Add job bundle (module emploi)
|
||||
* ([#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
|
||||
|
||||
|
||||
* Handle duplicate reference id in the import of reference addresses
|
||||
* 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
|
||||
* 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
|
||||
- Ajout d'un bouton pour dupliquer les périodes de disponibilités d'une semaine à une autre;
|
||||
- dans l'interface d'administration, filtre sur les utilisateurs actifs. Par défaut, seul les utilisateurs
|
||||
actifs sont affichés;
|
||||
- Nouveau bouton pour indiquer toutes les notifications comme lues;
|
||||
- Améliorations sur l'import des adresses et des codes postaux;
|
||||
- Affiche le nom du fichier déposé quand on téléverse un fichier depuis le poste de travail local;
|
||||
- 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.
|
||||
|
3
.changes/v2.24.0.md
Normal file
3
.changes/v2.24.0.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## 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.
|
3
.changes/v3.1.0.md
Normal file
3
.changes/v3.1.0.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## 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.
|
2
.env
2
.env
@@ -23,7 +23,7 @@ TRUSTED_HOSTS='^(localhost|example\.com|nginx)$'
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
## Wopi server for editing documents online
|
||||
WOPI_SERVER=http://collabora:9980
|
||||
EDITOR_SERVER=http://collabora:9980
|
||||
|
||||
# must be manually set in .env.local
|
||||
# ADMIN_PASSWORD=
|
||||
|
@@ -41,3 +41,5 @@ DATABASE_URL="postgresql://postgres:postgres@db:5432/test?serverVersion=14&chars
|
||||
ASYNC_UPLOAD_TEMP_URL_KEY=
|
||||
ASYNC_UPLOAD_TEMP_URL_BASE_PATH=
|
||||
ASYNC_UPLOAD_TEMP_URL_CONTAINER=
|
||||
|
||||
EDITOR_SERVER=https://localhost:9980
|
||||
|
@@ -122,7 +122,7 @@ unit_tests:
|
||||
- php tests/console chill:db:sync-views --env=test
|
||||
- php -d memory_limit=2G tests/console cache:clear --env=test
|
||||
- php -d memory_limit=3G tests/console doctrine:fixtures:load -n --env=test
|
||||
- php -d memory_limit=4G bin/phpunit --colors=never --exclude-group dbIntensive,openstack-integration
|
||||
- php -d memory_limit=4G bin/phpunit --colors=never --exclude-group dbIntensive,openstack-integration,collabora-integration
|
||||
artifacts:
|
||||
expire_in: 1 day
|
||||
paths:
|
||||
|
221
CHANGELOG.md
221
CHANGELOG.md
@@ -6,33 +6,62 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.23.0 - 2024-07-23
|
||||
## v2.24.0 - 2024-09-11
|
||||
### 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
|
||||
|
||||
* ([#306](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/306)) When a document is converted or downloaded in the browser, this document is removed from the browser memory after 45s. Future click on the button re-download the document.
|
||||
|
||||
## v2.23.0 - 2024-07-19 & 2024-07-23
|
||||
### Feature
|
||||
* ([#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
|
||||
|
||||
|
||||
* Handle duplicate reference id in the import of reference addresses
|
||||
* 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
|
||||
- Ajout d'un bouton pour dupliquer les périodes de disponibilités d'une semaine à une autre;
|
||||
- dans l'interface d'administration, filtre sur les utilisateurs actifs. Par défaut, seul les utilisateurs
|
||||
actifs sont affichés;
|
||||
- Nouveau bouton pour indiquer toutes les notifications comme lues;
|
||||
- Améliorations sur l'import des adresses et des codes postaux;
|
||||
- Affiche le nom du fichier déposé quand on téléverse un fichier depuis le poste de travail local;
|
||||
- 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
|
||||
@@ -75,7 +104,7 @@ and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
|
||||
## v2.20.1 - 2024-06-05
|
||||
### Fixed
|
||||
* Do not allow StoredObjectCreated for edit and convert buttons
|
||||
* Do not allow StoredObjectCreated for edit and convert buttons
|
||||
|
||||
## v2.20.0 - 2024-06-05
|
||||
### Fixed
|
||||
@@ -122,96 +151,96 @@ and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
|
||||
## v2.18.2 - 2024-04-12
|
||||
### Fixed
|
||||
* ([#250](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/250)) Postal codes import : fix the source URL and the keys to handle each record
|
||||
* ([#250](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/250)) Postal codes import : fix the source URL and the keys to handle each record
|
||||
|
||||
## v2.18.1 - 2024-03-26
|
||||
### Fixed
|
||||
* Fix layout issue in document generation for admin (minor)
|
||||
* Fix layout issue in document generation for admin (minor)
|
||||
|
||||
## v2.18.0 - 2024-03-26
|
||||
### Feature
|
||||
* ([#268](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/268)) Improve admin UX to configure document templates for document generation
|
||||
* ([#268](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/268)) Improve admin UX to configure document templates for document generation
|
||||
### Fixed
|
||||
* ([#267](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/267)) Fix the join between job and user in the user list (admin): show only the current user job
|
||||
* ([#267](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/267)) Fix the join between job and user in the user list (admin): show only the current user job
|
||||
|
||||
## v2.17.0 - 2024-03-19
|
||||
### Feature
|
||||
* ([#237](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/237)) New export filter for social actions with an evaluation created between two dates
|
||||
* ([#258](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/258)) In the list of accompangying period, add the list of person's centers and the duration of the course
|
||||
* ([#238](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/238)) Allow to customize list person with new fields
|
||||
* ([#237](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/237)) New export filter for social actions with an evaluation created between two dates
|
||||
* ([#258](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/258)) In the list of accompangying period, add the list of person's centers and the duration of the course
|
||||
* ([#238](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/238)) Allow to customize list person with new fields
|
||||
* ([#159](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/159)) Admin can publish news on the homepage
|
||||
### Fixed
|
||||
* ([#264](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/264)) Fix languages: load the languages in all availables languages configured for Chill
|
||||
* ([#259](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/259)) Keep a consistent behaviour between the filtering of activities within the document generation (model "accompanying period with activities"), and the same filter in the list of activities for an accompanying period
|
||||
* ([#264](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/264)) Fix languages: load the languages in all availables languages configured for Chill
|
||||
* ([#259](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/259)) Keep a consistent behaviour between the filtering of activities within the document generation (model "accompanying period with activities"), and the same filter in the list of activities for an accompanying period
|
||||
|
||||
## v2.16.3 - 2024-02-26
|
||||
### Fixed
|
||||
* ([#236](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/236)) Fix translation of user job -> 'service' must be 'métier'
|
||||
* ([#236](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/236)) Fix translation of user job -> 'service' must be 'métier'
|
||||
### UX
|
||||
* ([#232](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/232)) Order user jobs and services alphabetically in export filters
|
||||
* ([#232](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/232)) Order user jobs and services alphabetically in export filters
|
||||
|
||||
## v2.16.2 - 2024-02-21
|
||||
### Fixed
|
||||
* Check for null values in closing motive of parcours d'accompagnement for correct rendering of template
|
||||
* Check for null values in closing motive of parcours d'accompagnement for correct rendering of template
|
||||
|
||||
## v2.16.1 - 2024-02-09
|
||||
### Fixed
|
||||
* Force bootstrap version to avoid error in builds with newer version
|
||||
* Force bootstrap version to avoid error in builds with newer version
|
||||
|
||||
## v2.16.0 - 2024-02-08
|
||||
### Feature
|
||||
* ([#231](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/231)) Create new filter for persons having a participation in an accompanying period during a certain time span
|
||||
* ([#241](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/241)) [Export][List of accompanyign period] Add two columns: the list of persons participating to the period, and their ids
|
||||
* ([#244](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/244)) Add capability to generate export about change of steps of accompanying period, and generate exports for this
|
||||
* ([#253](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/253)) Export: group accompanying period by person participating
|
||||
* ([#243](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/243)) Export: add filter for courses not linked to a reference address
|
||||
* ([#229](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/229)) Allow to group activities linked with accompanying period by reason
|
||||
* ([#115](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/115)) Prevent social work to be saved when another user edited conccurently the social work
|
||||
* Modernize the event bundle, with some new fields and multiple improvements
|
||||
* ([#231](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/231)) Create new filter for persons having a participation in an accompanying period during a certain time span
|
||||
* ([#241](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/241)) [Export][List of accompanyign period] Add two columns: the list of persons participating to the period, and their ids
|
||||
* ([#244](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/244)) Add capability to generate export about change of steps of accompanying period, and generate exports for this
|
||||
* ([#253](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/253)) Export: group accompanying period by person participating
|
||||
* ([#243](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/243)) Export: add filter for courses not linked to a reference address
|
||||
* ([#229](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/229)) Allow to group activities linked with accompanying period by reason
|
||||
* ([#115](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/115)) Prevent social work to be saved when another user edited conccurently the social work
|
||||
* Modernize the event bundle, with some new fields and multiple improvements
|
||||
### Fixed
|
||||
* ([#220](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/220)) Fix error in logs about wrong typing of eventArgs in onEditNotificationComment method
|
||||
* ([#256](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/256)) Fix the conditions upon which social actions should be optional or required in relation to social issues within the activity creation form
|
||||
* ([#220](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/220)) Fix error in logs about wrong typing of eventArgs in onEditNotificationComment method
|
||||
* ([#256](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/256)) Fix the conditions upon which social actions should be optional or required in relation to social issues within the activity creation form
|
||||
### UX
|
||||
* ([#260](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/260)) Order list of centers alphabetically in dropdown 'user' section admin.
|
||||
* ([#260](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/260)) Order list of centers alphabetically in dropdown 'user' section admin.
|
||||
|
||||
## v2.15.2 - 2024-01-11
|
||||
### Fixed
|
||||
* Fix the id_seq used when creating a new accompanying period participation during fusion of two person files
|
||||
* Fix the id_seq used when creating a new accompanying period participation during fusion of two person files
|
||||
### DX
|
||||
* Set placeholder to False for expanded EntityType form fields where required is set to False.
|
||||
* Set placeholder to False for expanded EntityType form fields where required is set to False.
|
||||
|
||||
## v2.15.1 - 2023-12-20
|
||||
### Fixed
|
||||
* Fix the household export query to exclude accompanying periods that are in draft state.
|
||||
* Fix the household export query to exclude accompanying periods that are in draft state.
|
||||
### DX
|
||||
* ([#167](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/167)) Fixed readthedocs compilation by updating readthedocs config file and requirements for Sphinx
|
||||
* ([#167](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/167)) Fixed readthedocs compilation by updating readthedocs config file and requirements for Sphinx
|
||||
|
||||
## v2.15.0 - 2023-12-11
|
||||
### Feature
|
||||
* ([#191](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/191)) Add export "number of household associate with an exchange"
|
||||
* ([#235](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/235)) Export: add dates on the filter "filter course by activity type"
|
||||
* ([#191](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/191)) Add export "number of household associate with an exchange"
|
||||
* ([#235](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/235)) Export: add dates on the filter "filter course by activity type"
|
||||
### Fixed
|
||||
* ([#214](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/214)) Fix error when posting an empty comment on an accompanying period.
|
||||
* ([#233](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/233)) Fix "filter evaluation by evaluation type" (and add select2 to the list of evaluation types to pick)
|
||||
* ([#214](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/214)) Fix error when posting an empty comment on an accompanying period.
|
||||
* ([#233](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/233)) Fix "filter evaluation by evaluation type" (and add select2 to the list of evaluation types to pick)
|
||||
* ([#234](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/234)) Fix "filter aside activity by date"
|
||||
|
||||
* ([#228](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/228)) Fix export of activity for people created before the introduction of the createdAt column on person (during v1)
|
||||
* ([#246](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/246)) Do not show activities, evaluations and social work when associated to a confidential accompanying period, except for the users which are allowed to see them
|
||||
|
||||
* ([#228](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/228)) Fix export of activity for people created before the introduction of the createdAt column on person (during v1)
|
||||
* ([#246](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/246)) Do not show activities, evaluations and social work when associated to a confidential accompanying period, except for the users which are allowed to see them
|
||||
|
||||
## v2.14.1 - 2023-11-29
|
||||
### Fixed
|
||||
* Export: fix list person with custom fields
|
||||
* ([#100](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/100)) Add a paginator to budget elements (resource and charge types) in the admin
|
||||
* Fix error in ListEvaluation when "handling agents" are alone
|
||||
* Export: fix list person with custom fields
|
||||
* ([#100](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/100)) Add a paginator to budget elements (resource and charge types) in the admin
|
||||
* Fix error in ListEvaluation when "handling agents" are alone
|
||||
|
||||
## v2.14.0 - 2023-11-24
|
||||
### Feature
|
||||
* ([#161](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/161)) Export: in filter "Filter accompanying period work (social action) by type, goal and result", order the items alphabetically or with the defined order
|
||||
* ([#161](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/161)) Export: in filter "Filter accompanying period work (social action) by type, goal and result", order the items alphabetically or with the defined order
|
||||
### Fixed
|
||||
* ([#141](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/141)) Export: on filter "action by type goals, and results", restore the fields when editing a saved export
|
||||
* ([#219](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/219)) Export: fix the list of accompanying period work, when the "calc date" is null
|
||||
* ([#222](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/222)) Fix rendering of custom fields
|
||||
* Fix various errors in custom fields administration
|
||||
* ([#141](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/141)) Export: on filter "action by type goals, and results", restore the fields when editing a saved export
|
||||
* ([#219](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/219)) Export: fix the list of accompanying period work, when the "calc date" is null
|
||||
* ([#222](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/222)) Fix rendering of custom fields
|
||||
* Fix various errors in custom fields administration
|
||||
|
||||
## v2.13.0 - 2023-11-21
|
||||
### Feature
|
||||
@@ -225,7 +254,7 @@ and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
|
||||
## v2.12.1 - 2023-11-16
|
||||
### Fixed
|
||||
* ([#208](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/208)) Export: fix loading of form for "filter action by type, goal and result"
|
||||
* ([#208](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/208)) Export: fix loading of form for "filter action by type, goal and result"
|
||||
|
||||
## v2.12.0 - 2023-11-15
|
||||
### Feature
|
||||
@@ -256,36 +285,36 @@ and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
|
||||
## v2.11.0 - 2023-11-07
|
||||
### Feature
|
||||
* ([#194](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/194)) Export: add a filter "filter activity by creator job"
|
||||
* ([#194](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/194)) Export: add a filter "filter activity by creator job"
|
||||
### Fixed
|
||||
* ([#185](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/185)) Export: fix "group accompanying period by geographical unit": take into account the accompanying periods when the period is not located within an unit
|
||||
* Fix "group activity by creator job" aggregator
|
||||
* ([#185](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/185)) Export: fix "group accompanying period by geographical unit": take into account the accompanying periods when the period is not located within an unit
|
||||
* Fix "group activity by creator job" aggregator
|
||||
|
||||
## v2.10.6 - 2023-11-07
|
||||
### Fixed
|
||||
* ([#182](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/182)) Fix merging of double person files. Adjustement relationship sql statement
|
||||
* ([#185](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/185)) Export: fix aggregator by geographical unit on person: avoid inconsistencies
|
||||
* ([#182](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/182)) Fix merging of double person files. Adjustement relationship sql statement
|
||||
* ([#185](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/185)) Export: fix aggregator by geographical unit on person: avoid inconsistencies
|
||||
|
||||
## v2.10.5 - 2023-11-05
|
||||
### Fixed
|
||||
* ([#183](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/183)) Fix "problem during download" on some filters, which used a wrong data type
|
||||
* ([#184](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/184)) Fix filter "activity by date"
|
||||
* ([#183](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/183)) Fix "problem during download" on some filters, which used a wrong data type
|
||||
* ([#184](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/184)) Fix filter "activity by date"
|
||||
|
||||
## v2.10.4 - 2023-10-26
|
||||
### Fixed
|
||||
* Fix null value constraint errors when merging relationships in doubles
|
||||
* Fix null value constraint errors when merging relationships in doubles
|
||||
|
||||
## v2.10.3 - 2023-10-26
|
||||
### Fixed
|
||||
* ([#175](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/175)) Replace old method of getting translator with injection of translatorInterface
|
||||
* ([#175](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/175)) Replace old method of getting translator with injection of translatorInterface
|
||||
|
||||
## v2.10.2 - 2023-10-26
|
||||
### Fixed
|
||||
* ([#175](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/175)) Use injection of translator instead of ->get().
|
||||
* ([#175](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/175)) Use injection of translator instead of ->get().
|
||||
|
||||
## v2.10.1 - 2023-10-24
|
||||
### Fixed
|
||||
* Fix export controller when generating an export without any data in session
|
||||
* Fix export controller when generating an export without any data in session
|
||||
|
||||
## v2.10.0 - 2023-10-24
|
||||
### Feature
|
||||
@@ -310,11 +339,11 @@ and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
|
||||
## v2.9.2 - 2023-10-17
|
||||
### Fixed
|
||||
* Fix possible null values in string's entities
|
||||
* Fix possible null values in string's entities
|
||||
|
||||
## v2.9.1 - 2023-10-17
|
||||
### Fixed
|
||||
* Fix the handling of activity form when editing or creating an activity in an accompanying period with multiple centers
|
||||
* Fix the handling of activity form when editing or creating an activity in an accompanying period with multiple centers
|
||||
|
||||
## v2.9.0 - 2023-10-17
|
||||
### Feature
|
||||
@@ -362,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
|
||||
|
@@ -43,6 +43,7 @@
|
||||
"symfony/dom-crawler": "^5.4",
|
||||
"symfony/error-handler": "^5.4",
|
||||
"symfony/event-dispatcher": "^5.4",
|
||||
"symfony/event-dispatcher-contracts": "^2.4",
|
||||
"symfony/expression-language": "^5.4",
|
||||
"symfony/filesystem": "^5.4",
|
||||
"symfony/finder": "^5.4",
|
||||
|
@@ -39,9 +39,12 @@ Implements a :code:`Chill\MainBundle\Cron\CronJobInterface`. Here is an example:
|
||||
use Chill\MainBundle\Entity\CronJobExecution;
|
||||
use DateInterval;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
|
||||
class MyCronJob implements CronJobInterface
|
||||
{
|
||||
function __construct(private ClockInterface $clock) {}
|
||||
|
||||
public function canRun(?CronJobExecution $cronJobExecution): bool
|
||||
{
|
||||
// the parameter $cronJobExecution contains data about the last execution of the cronjob
|
||||
@@ -56,7 +59,7 @@ Implements a :code:`Chill\MainBundle\Cron\CronJobInterface`. Here is an example:
|
||||
|
||||
// this cron job should be executed if the last execution is greater than one day, but only during the night
|
||||
|
||||
$now = new DateTimeImmutable('now');
|
||||
$now = $clock->now();
|
||||
|
||||
return $cronJobExecution->getLastStart() < $now->sub(new DateInterval('P1D'))
|
||||
&& in_array($now->format('H'), self::ACCEPTED_HOURS, true)
|
||||
@@ -69,10 +72,15 @@ Implements a :code:`Chill\MainBundle\Cron\CronJobInterface`. Here is an example:
|
||||
return 'arbitrary-and-unique-key';
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
public function run(array $lastExecutionData): void
|
||||
{
|
||||
// here, we execute the command
|
||||
}
|
||||
|
||||
// we return execution data, which will be served for next execution
|
||||
// this data should be easily serializable in a json column: it should contains
|
||||
// only int, string, etc. Avoid storing object
|
||||
return ['last-execution-id' => 0];
|
||||
}
|
||||
}
|
||||
|
||||
How are cron job scheduled ?
|
||||
|
@@ -55,7 +55,7 @@
|
||||
"mime": "^4.0.0",
|
||||
"pdfjs-dist": "^4.3.136",
|
||||
"vis-network": "^9.1.0",
|
||||
"vue": "^3.2.37",
|
||||
"vue": "^3.5.6",
|
||||
"vue-i18n": "^9.1.6",
|
||||
"vue-multiselect": "3.0.0-alpha.2",
|
||||
"vue-toast-notification": "^3.1.2",
|
||||
|
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\ActivityBundle\Export\Aggregator\PersonAggregators;
|
||||
|
||||
use Chill\ActivityBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Export\AggregatorInterface;
|
||||
use Chill\PersonBundle\Entity\Household\Household;
|
||||
use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
||||
use Chill\PersonBundle\Repository\Household\HouseholdRepository;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
final readonly class HouseholdAggregator implements AggregatorInterface
|
||||
{
|
||||
public function __construct(private HouseholdRepository $householdRepository) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
// nothing to add here
|
||||
}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getLabels($key, array $values, mixed $data)
|
||||
{
|
||||
return function (int|string|null $value): string|int {
|
||||
if ('_header' === $value) {
|
||||
return 'export.aggregator.person.by_household.household';
|
||||
}
|
||||
|
||||
if ('' === $value || null === $value || null === $household = $this->householdRepository->find($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $household->getId();
|
||||
};
|
||||
}
|
||||
|
||||
public function getQueryKeys($data)
|
||||
{
|
||||
return ['activity_household_agg'];
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return 'export.aggregator.person.by_household.title';
|
||||
}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data)
|
||||
{
|
||||
$qb->join(
|
||||
HouseholdMember::class,
|
||||
'activity_household_agg_household_member',
|
||||
Join::WITH,
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq('activity_household_agg_household_member.person', 'activity.person'),
|
||||
$qb->expr()->lte('activity_household_agg_household_member.startDate', 'activity.date'),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->gte('activity_household_agg_household_member.endDate', 'activity.date'),
|
||||
$qb->expr()->isNull('activity_household_agg_household_member.endDate')
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
$qb->join(
|
||||
Household::class,
|
||||
'activity_household_agg_household',
|
||||
Join::WITH,
|
||||
$qb->expr()->eq('activity_household_agg_household_member.household', 'activity_household_agg_household')
|
||||
);
|
||||
|
||||
$qb
|
||||
->addSelect('activity_household_agg_household.id AS activity_household_agg')
|
||||
->addGroupBy('activity_household_agg');
|
||||
}
|
||||
|
||||
public function applyOn()
|
||||
{
|
||||
return Declarations::ACTIVITY_PERSON;
|
||||
}
|
||||
}
|
@@ -19,6 +19,7 @@ use Chill\MainBundle\Export\FormatterInterface;
|
||||
use Chill\MainBundle\Export\GroupedExportInterface;
|
||||
use Chill\MainBundle\Export\ListInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
||||
use Chill\PersonBundle\Export\Declarations as PersonDeclarations;
|
||||
use Doctrine\DBAL\Exception\InvalidArgumentException;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
@@ -44,6 +45,7 @@ class ListActivity implements ListInterface, GroupedExportInterface
|
||||
'person_firstname',
|
||||
'person_lastname',
|
||||
'person_id',
|
||||
'household_id',
|
||||
];
|
||||
private readonly bool $filterStatsByCenters;
|
||||
|
||||
@@ -189,19 +191,26 @@ class ListActivity implements ListInterface, GroupedExportInterface
|
||||
{
|
||||
$centers = array_map(static fn ($el) => $el['center'], $acl);
|
||||
|
||||
// throw an error if any fields are present
|
||||
// throw an error if no fields are present
|
||||
if (!\array_key_exists('fields', $data)) {
|
||||
throw new InvalidArgumentException('Any fields have been checked.');
|
||||
throw new InvalidArgumentException('No fields have been checked.');
|
||||
}
|
||||
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
|
||||
$qb
|
||||
->from('ChillActivityBundle:Activity', 'activity')
|
||||
->join('activity.person', 'actperson');
|
||||
->join('activity.person', 'person')
|
||||
->join(
|
||||
HouseholdMember::class,
|
||||
'householdmember',
|
||||
Query\Expr\Join::WITH,
|
||||
'person = householdmember.person AND householdmember.startDate <= activity.date AND (householdmember.endDate IS NULL OR householdmember.endDate > activity.date)'
|
||||
)
|
||||
->join('householdmember.household', 'household');
|
||||
|
||||
if ($this->filterStatsByCenters) {
|
||||
$qb->join('actperson.centerHistory', 'centerHistory');
|
||||
$qb->join('person.centerHistory', 'centerHistory');
|
||||
$qb->where(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->lte('centerHistory.startDate', 'activity.date'),
|
||||
@@ -224,17 +233,22 @@ class ListActivity implements ListInterface, GroupedExportInterface
|
||||
break;
|
||||
|
||||
case 'person_firstname':
|
||||
$qb->addSelect('actperson.firstName AS person_firstname');
|
||||
$qb->addSelect('person.firstName AS person_firstname');
|
||||
|
||||
break;
|
||||
|
||||
case 'person_lastname':
|
||||
$qb->addSelect('actperson.lastName AS person_lastname');
|
||||
$qb->addSelect('person.lastName AS person_lastname');
|
||||
|
||||
break;
|
||||
|
||||
case 'person_id':
|
||||
$qb->addSelect('actperson.id AS person_id');
|
||||
$qb->addSelect('person.id AS person_id');
|
||||
|
||||
break;
|
||||
|
||||
case 'household_id':
|
||||
$qb->addSelect('household.id AS household_id');
|
||||
|
||||
break;
|
||||
|
||||
@@ -284,7 +298,7 @@ class ListActivity implements ListInterface, GroupedExportInterface
|
||||
return ActivityStatsVoter::LISTS;
|
||||
}
|
||||
|
||||
public function supportsModifiers()
|
||||
public function supportsModifiers(): array
|
||||
{
|
||||
return [
|
||||
Declarations::ACTIVITY,
|
||||
|
@@ -73,7 +73,7 @@ final readonly class PeriodHavingActivityBetweenDatesFilter implements FilterInt
|
||||
|
||||
$qb->andWhere(
|
||||
$qb->expr()->exists(
|
||||
'SELECT 1 FROM '.Activity::class." {$alias} WHERE {$alias}.date >= :{$from} AND {$alias}.date < :{$to} AND {$alias}.accompanyingPeriod = activity.accompanyingPeriod"
|
||||
'SELECT 1 FROM '.Activity::class." {$alias} WHERE {$alias}.date >= :{$from} AND {$alias}.date < :{$to} AND {$alias}.accompanyingPeriod = acp"
|
||||
)
|
||||
);
|
||||
|
||||
|
@@ -39,7 +39,7 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
|
||||
return null;
|
||||
}
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data)
|
||||
public function alterQuery(QueryBuilder $qb, $data): void
|
||||
{
|
||||
// create a subquery for activity
|
||||
$sqb = $qb->getEntityManager()->createQueryBuilder();
|
||||
@@ -121,7 +121,7 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
|
||||
];
|
||||
}
|
||||
|
||||
public function describeAction($data, $format = 'string')
|
||||
public function describeAction($data, $format = 'string'): array
|
||||
{
|
||||
return [
|
||||
[] === $data['reasons'] ?
|
||||
@@ -141,7 +141,7 @@ final readonly class PersonHavingActivityBetweenDateFilter implements ExportElem
|
||||
];
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'export.filter.activity.person_between_dates.title';
|
||||
}
|
||||
|
@@ -243,3 +243,7 @@ services:
|
||||
Chill\ActivityBundle\Export\Aggregator\PersonAggregators\PersonAggregator:
|
||||
tags:
|
||||
- { name: chill.export_aggregator, alias: activity_person_agg }
|
||||
|
||||
Chill\ActivityBundle\Export\Aggregator\PersonAggregators\HouseholdAggregator:
|
||||
tags:
|
||||
- { name: chill.export_aggregator, alias: activity_household_agg }
|
||||
|
@@ -428,6 +428,9 @@ export:
|
||||
by_person:
|
||||
title: Grouper les échanges par usager (dossier d'usager dans lequel l'échange est enregistré)
|
||||
person: Usager
|
||||
by_household:
|
||||
title: Grouper les échanges par ménage
|
||||
household: Identifiant ménage
|
||||
acp:
|
||||
by_activity_type:
|
||||
title: Grouper les parcours par type d'échange
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<label class="form-label">{{ $t('created_availabilities') }}</label>
|
||||
<label class="form-label">{{ $t("created_availabilities") }}</label>
|
||||
<vue-multiselect
|
||||
v-model="pickedLocation"
|
||||
:options="locations"
|
||||
@@ -14,10 +14,15 @@
|
||||
></vue-multiselect>
|
||||
</div>
|
||||
</div>
|
||||
<div class="display-options row justify-content-between" style="margin-top: 1rem;">
|
||||
<div
|
||||
class="display-options row justify-content-between"
|
||||
style="margin-top: 1rem"
|
||||
>
|
||||
<div class="col-sm-9 col-xs-12">
|
||||
<div class="input-group mb-3">
|
||||
<label class="input-group-text" for="slotDuration">Durée des créneaux</label>
|
||||
<label class="input-group-text" for="slotDuration"
|
||||
>Durée des créneaux</label
|
||||
>
|
||||
<select v-model="slotDuration" id="slotDuration" class="form-select">
|
||||
<option value="00:05:00">5 minutes</option>
|
||||
<option value="00:10:00">10 minutes</option>
|
||||
@@ -58,13 +63,20 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3 col-xs-12">
|
||||
<div class="col-xs-12 col-sm-3">
|
||||
<div class="float-end">
|
||||
<div class="form-check input-group">
|
||||
<span class="input-group-text">
|
||||
<input id="showHideWE" class="mt-0" type="checkbox" v-model="showWeekends">
|
||||
<input
|
||||
id="showHideWE"
|
||||
class="mt-0"
|
||||
type="checkbox"
|
||||
v-model="showWeekends"
|
||||
/>
|
||||
</span>
|
||||
<label for="showHideWE" class="form-check-label input-group-text">Week-ends</label>
|
||||
<label for="showHideWE" class="form-check-label input-group-text"
|
||||
>Week-ends</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,39 +84,86 @@
|
||||
<FullCalendar :options="calendarOptions" ref="calendarRef">
|
||||
<template v-slot:eventContent="arg: EventApi">
|
||||
<span :class="eventClasses(arg.event)">
|
||||
<b v-if="arg.event.extendedProps.is === 'remote'">{{ arg.event.title}}</b>
|
||||
<b v-else-if="arg.event.extendedProps.is === 'range'">{{ arg.timeText }} - {{ arg.event.extendedProps.locationName }}</b>
|
||||
<b v-else-if="arg.event.extendedProps.is === 'local'">{{ arg.event.title}}</b>
|
||||
<b v-else >no 'is'</b>
|
||||
<a v-if="arg.event.extendedProps.is === 'range'" class="fa fa-fw fa-times delete"
|
||||
@click.prevent="onClickDelete(arg.event)">
|
||||
</a>
|
||||
</span>
|
||||
<b v-if="arg.event.extendedProps.is === 'remote'">{{
|
||||
arg.event.title
|
||||
}}</b>
|
||||
<b v-else-if="arg.event.extendedProps.is === 'range'"
|
||||
>{{ arg.timeText }} - {{ arg.event.extendedProps.locationName }}</b
|
||||
>
|
||||
<b v-else-if="arg.event.extendedProps.is === 'local'">{{
|
||||
arg.event.title
|
||||
}}</b>
|
||||
<b v-else>no 'is'</b>
|
||||
<a
|
||||
v-if="arg.event.extendedProps.is === 'range'"
|
||||
class="fa fa-fw fa-times delete"
|
||||
@click.prevent="onClickDelete(arg.event)"
|
||||
>
|
||||
</a>
|
||||
</span>
|
||||
</template>
|
||||
</FullCalendar>
|
||||
|
||||
<div id="copy-widget">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-sm-4 col-xs-12">
|
||||
<h6 class="chill-red">{{ $t('copy_range_from_to') }}</h6>
|
||||
</div>
|
||||
<div class="col-sm-3 col-xs-12">
|
||||
<input class="form-control" type="date" v-model="copyFrom" />
|
||||
</div>
|
||||
<div class="col-sm-1 col-xs-12" style="text-align: center; font-size: x-large;">
|
||||
<i class="fa fa-angle-double-right"></i>
|
||||
</div>
|
||||
<div class="col-sm-3 col-xs-12" >
|
||||
<input class="form-control" type="date" v-model="copyTo" />
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<button class="btn btn-action" @click="copyDay">
|
||||
{{ $t('copy_range') }}
|
||||
</button>
|
||||
<div class="container mt-2 mb-2">
|
||||
|
||||
<div class="row justify-content-between align-items-center mb-4">
|
||||
<div class="col-xs-12 col-sm-3 col-md-2">
|
||||
<h6 class="chill-red">{{ $t("copy_range_from_to") }}</h6>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-9 col-md-2">
|
||||
<select v-model="dayOrWeek" id="dayOrWeek" class="form-select">
|
||||
<option value="day">{{ $t("from_day_to_day") }}</option>
|
||||
<option value="week">{{ $t("from_week_to_week") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<template v-if="dayOrWeek === 'day'">
|
||||
<div class="col-xs-12 col-sm-3 col-md-3">
|
||||
<input class="form-control" type="date" v-model="copyFrom" />
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-1 col-md-1 copy-chevron">
|
||||
<i class="fa fa-angle-double-right"></i>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-3 col-md-3">
|
||||
<input class="form-control" type="date" v-model="copyTo" />
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-5 col-md-1">
|
||||
<button class="btn btn-action float-end" @click="copyDay">
|
||||
{{ $t("copy_range") }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="col-xs-12 col-sm-3 col-md-3">
|
||||
<select
|
||||
v-model="copyFromWeek"
|
||||
id="copyFromWeek"
|
||||
class="form-select"
|
||||
>
|
||||
<option v-for="w in lastWeeks" :value="w.value" :key="w.value">
|
||||
{{ w.text }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-1 col-md-1 copy-chevron">
|
||||
<i class="fa fa-angle-double-right"></i>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-3 col-md-3">
|
||||
<select v-model="copyToWeek" id="copyToWeek" class="form-select">
|
||||
<option v-for="w in nextWeeks" :value="w.value" :key="w.value">
|
||||
{{ w.text }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-5 col-md-1">
|
||||
<button class="btn btn-action float-end" @click="copyWeek">
|
||||
{{ $t("copy_range") }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- not directly seen, but include in a modal -->
|
||||
@@ -112,42 +171,95 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import type {
|
||||
CalendarOptions,
|
||||
DatesSetArg,
|
||||
EventInput
|
||||
} from '@fullcalendar/core';
|
||||
import {reactive, computed, ref} from "vue";
|
||||
import {useStore} from "vuex";
|
||||
import {key} from './store';
|
||||
import FullCalendar from '@fullcalendar/vue3';
|
||||
import frLocale from '@fullcalendar/core/locales/fr';
|
||||
import interactionPlugin, {DropArg, EventResizeDoneArg} from "@fullcalendar/interaction";
|
||||
EventInput,
|
||||
} from "@fullcalendar/core";
|
||||
import { reactive, computed, ref, onMounted } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { key } from "./store";
|
||||
import FullCalendar from "@fullcalendar/vue3";
|
||||
import frLocale from "@fullcalendar/core/locales/fr";
|
||||
import interactionPlugin, {
|
||||
DropArg,
|
||||
EventResizeDoneArg,
|
||||
} from "@fullcalendar/interaction";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import {EventApi, DateSelectArg, EventDropArg, EventClickArg} from "@fullcalendar/core";
|
||||
import {ISOToDate} from "../../../../../ChillMainBundle/Resources/public/chill/js/date";
|
||||
import {
|
||||
EventApi,
|
||||
DateSelectArg,
|
||||
EventDropArg,
|
||||
EventClickArg,
|
||||
} from "@fullcalendar/core";
|
||||
import {
|
||||
dateToISO,
|
||||
ISOToDate,
|
||||
} from "../../../../../ChillMainBundle/Resources/public/chill/js/date";
|
||||
import VueMultiselect from "vue-multiselect";
|
||||
import {Location} from "../../../../../ChillMainBundle/Resources/public/types";
|
||||
import { Location } from "../../../../../ChillMainBundle/Resources/public/types";
|
||||
import EditLocation from "./Components/EditLocation.vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const store = useStore(key);
|
||||
|
||||
const {t} = useI18n();
|
||||
const { t } = useI18n();
|
||||
|
||||
const showWeekends = ref(false);
|
||||
const slotDuration = ref('00:05:00');
|
||||
const slotMinTime = ref('09:00:00');
|
||||
const slotMaxTime = ref('18:00:00');
|
||||
const slotDuration = ref("00:15:00");
|
||||
const slotMinTime = ref("09:00:00");
|
||||
const slotMaxTime = ref("18:00:00");
|
||||
const copyFrom = ref<string | null>(null);
|
||||
const copyTo = ref<string | null>(null);
|
||||
const editLocation = ref<InstanceType<typeof EditLocation> | null>(null)
|
||||
const editLocation = ref<InstanceType<typeof EditLocation> | null>(null);
|
||||
const dayOrWeek = ref("day");
|
||||
const copyFromWeek = ref<string | null>(null);
|
||||
const copyToWeek = ref<string | null>(null);
|
||||
|
||||
interface Weeks {
|
||||
value: string | null;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const getMonday = (week: number): Date => {
|
||||
const lastMonday = new Date();
|
||||
lastMonday.setDate(
|
||||
lastMonday.getDate() - ((lastMonday.getDay() + 6) % 7) + week * 7
|
||||
);
|
||||
return lastMonday;
|
||||
};
|
||||
|
||||
const dateOptions: Intl.DateTimeFormatOptions = {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
};
|
||||
|
||||
const lastWeeks = computed((): Weeks[] =>
|
||||
Array.from(Array(30).keys()).map((w) => {
|
||||
const lastMonday = getMonday(15-w);
|
||||
return {
|
||||
value: dateToISO(lastMonday),
|
||||
text: `Semaine du ${lastMonday.toLocaleDateString("fr-FR", dateOptions)}`,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const nextWeeks = computed((): Weeks[] =>
|
||||
Array.from(Array(52).keys()).map((w) => {
|
||||
const nextMonday = getMonday(w + 1);
|
||||
return {
|
||||
value: dateToISO(nextMonday),
|
||||
text: `Semaine du ${nextMonday.toLocaleDateString("fr-FR", dateOptions)}`,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const baseOptions = ref<CalendarOptions>({
|
||||
locale: frLocale,
|
||||
plugins: [interactionPlugin, timeGridPlugin],
|
||||
initialView: 'timeGridWeek',
|
||||
initialView: "timeGridWeek",
|
||||
initialDate: new Date(),
|
||||
scrollTimeReset: false,
|
||||
selectable: true,
|
||||
@@ -164,9 +276,9 @@ const baseOptions = ref<CalendarOptions>({
|
||||
selectMirror: false,
|
||||
editable: true,
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'timeGridWeek,timeGridDay'
|
||||
left: "prev,next today",
|
||||
center: "title",
|
||||
right: "timeGridWeek,timeGridDay",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -180,20 +292,23 @@ const locations = computed<Location[]>(() => {
|
||||
|
||||
const pickedLocation = computed<Location | null>({
|
||||
get(): Location | null {
|
||||
return store.state.locations.locationPicked || store.state.locations.currentLocation;
|
||||
return (
|
||||
store.state.locations.locationPicked ||
|
||||
store.state.locations.currentLocation
|
||||
);
|
||||
},
|
||||
set(newLocation: Location | null): void {
|
||||
store.commit('locations/setLocationPicked', newLocation, {root: true});
|
||||
}
|
||||
})
|
||||
store.commit("locations/setLocationPicked", newLocation, { root: true });
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* return the show classes for the event
|
||||
* @param arg
|
||||
*/
|
||||
const eventClasses = function(arg: EventApi): object {
|
||||
return {'calendarRangeItems': true};
|
||||
}
|
||||
const eventClasses = function (arg: EventApi): object {
|
||||
return { calendarRangeItems: true };
|
||||
};
|
||||
|
||||
/*
|
||||
// currently, all events are stored into calendarRanges, due to reactivity bug
|
||||
@@ -230,51 +345,60 @@ const calendarOptions = computed((): CalendarOptions => {
|
||||
* launched when the calendar range date change
|
||||
*/
|
||||
function onDatesSet(event: DatesSetArg): void {
|
||||
store.dispatch('fullCalendar/setCurrentDatesView', {start: event.start, end: event.end});
|
||||
store.dispatch("fullCalendar/setCurrentDatesView", {
|
||||
start: event.start,
|
||||
end: event.end,
|
||||
});
|
||||
}
|
||||
|
||||
function onDateSelect(event: DateSelectArg): void {
|
||||
|
||||
if (null === pickedLocation.value) {
|
||||
window.alert("Indiquez une localisation avant de créer une période de disponibilité.");
|
||||
window.alert(
|
||||
"Indiquez une localisation avant de créer une période de disponibilité."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
store.dispatch('calendarRanges/createRange', {start: event.start, end: event.end, location: pickedLocation.value});
|
||||
store.dispatch("calendarRanges/createRange", {
|
||||
start: event.start,
|
||||
end: event.end,
|
||||
location: pickedLocation.value,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* When a calendar range is deleted
|
||||
*/
|
||||
function onClickDelete(event: EventApi): void {
|
||||
console.log('onClickDelete', event);
|
||||
|
||||
if (event.extendedProps.is !== 'range') {
|
||||
if (event.extendedProps.is !== "range") {
|
||||
return;
|
||||
}
|
||||
|
||||
store.dispatch('calendarRanges/deleteRange', event.extendedProps.calendarRangeId);
|
||||
store.dispatch(
|
||||
"calendarRanges/deleteRange",
|
||||
event.extendedProps.calendarRangeId
|
||||
);
|
||||
}
|
||||
|
||||
function onEventDropOrResize(payload: EventDropArg | EventResizeDoneArg) {
|
||||
if (payload.event.extendedProps.is !== 'range') {
|
||||
if (payload.event.extendedProps.is !== "range") {
|
||||
return;
|
||||
}
|
||||
const changedEvent = payload.event;
|
||||
|
||||
store.dispatch('calendarRanges/patchRangeTime', {
|
||||
store.dispatch("calendarRanges/patchRangeTime", {
|
||||
calendarRangeId: payload.event.extendedProps.calendarRangeId,
|
||||
start: payload.event.start,
|
||||
end: payload.event.end,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function onEventClick(payload: EventClickArg): void {
|
||||
// @ts-ignore TS does not recognize the target. But it does exists.
|
||||
if (payload.jsEvent.target.classList.contains('delete')) {
|
||||
if (payload.jsEvent.target.classList.contains("delete")) {
|
||||
return;
|
||||
}
|
||||
if (payload.event.extendedProps.is !== 'range') {
|
||||
if (payload.event.extendedProps.is !== "range") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -285,10 +409,26 @@ function copyDay() {
|
||||
if (null === copyFrom.value || null === copyTo.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.dispatch('calendarRanges/copyFromDayToAnotherDay', {from: ISOToDate(copyFrom.value), to: ISOToDate(copyTo.value)})
|
||||
store.dispatch("calendarRanges/copyFromDayToAnotherDay", {
|
||||
from: ISOToDate(copyFrom.value),
|
||||
to: ISOToDate(copyTo.value),
|
||||
});
|
||||
}
|
||||
|
||||
function copyWeek() {
|
||||
if (null === copyFromWeek.value || null === copyToWeek.value) {
|
||||
return;
|
||||
}
|
||||
store.dispatch("calendarRanges/copyFromWeekToAnotherWeek", {
|
||||
fromMonday: ISOToDate(copyFromWeek.value),
|
||||
toMonday: ISOToDate(copyToWeek.value),
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
copyFromWeek.value = dateToISO(getMonday(0));
|
||||
copyToWeek.value = dateToISO(getMonday(1));
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -299,4 +439,9 @@ function copyDay() {
|
||||
z-index: 9999999999;
|
||||
padding: 0.25rem 0 0.25rem;
|
||||
}
|
||||
div.copy-chevron {
|
||||
text-align: center;
|
||||
font-size: x-large;
|
||||
width: 2rem;
|
||||
}
|
||||
</style>
|
||||
|
@@ -5,11 +5,9 @@ const appMessages = {
|
||||
show_my_calendar: "Afficher mon calendrier",
|
||||
show_weekends: "Afficher les week-ends",
|
||||
copy_range: "Copier",
|
||||
copy_range_from_to: "Copier les plages d'un jour à l'autre",
|
||||
copy_range_to_next_day: "Copier les plages du jour au jour suivant",
|
||||
copy_range_from_day: "Copier les plages du ",
|
||||
to_the_next_day: " au jour suivant",
|
||||
copy_range_to_next_week: "Copier les plages de la semaine à la semaine suivante",
|
||||
copy_range_from_to: "Copier les plages",
|
||||
from_day_to_day: "d'un jour à l'autre",
|
||||
from_week_to_week: "d'une semaine à l'autre",
|
||||
copy_range_how_to: "Créez les plages de disponibilités durant une journée et copiez-les facilement au jour suivant avec ce bouton. Si les week-ends sont cachés, le jour suivant un vendredi sera le lundi.",
|
||||
new_range_to_save: "Nouvelles plages à enregistrer",
|
||||
update_range_to_save: "Plages à modifier",
|
||||
|
@@ -52,6 +52,23 @@ export default <Module<CalendarRangesState, State>>{
|
||||
}
|
||||
}
|
||||
|
||||
return founds;
|
||||
},
|
||||
getRangesOnWeek: (state: CalendarRangesState) => (mondayDate: Date): EventInputCalendarRange[] => {
|
||||
const founds = [];
|
||||
for (let d of Array.from(Array(7).keys())) {
|
||||
const dateOfWeek = new Date(mondayDate);
|
||||
dateOfWeek.setDate(mondayDate.getDate() + d);
|
||||
const dateStr = <string>dateToISO(dateOfWeek);
|
||||
for (let range of state.ranges) {
|
||||
if (isEventInputCalendarRange(range)
|
||||
&& range.start.startsWith(dateStr)
|
||||
) {
|
||||
founds.push(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return founds;
|
||||
},
|
||||
},
|
||||
@@ -238,7 +255,7 @@ export default <Module<CalendarRangesState, State>>{
|
||||
|
||||
for (let r of rangesToCopy) {
|
||||
let start = new Date(<Date>ISOToDatetime(r.start));
|
||||
start.setFullYear(to.getFullYear(), to.getMonth(), to.getDate())
|
||||
start.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
|
||||
let end = new Date(<Date>ISOToDatetime(r.end));
|
||||
end.setFullYear(to.getFullYear(), to.getMonth(), to.getDate());
|
||||
let location = ctx.rootGetters['locations/getLocationById'](r.locationId);
|
||||
@@ -246,6 +263,23 @@ export default <Module<CalendarRangesState, State>>{
|
||||
promises.push(ctx.dispatch('createRange', {start, end, location}));
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(_ => Promise.resolve(null));
|
||||
},
|
||||
copyFromWeekToAnotherWeek(ctx, {fromMonday, toMonday}: {fromMonday: Date, toMonday: Date}): Promise<null> {
|
||||
|
||||
const rangesToCopy: EventInputCalendarRange[] = ctx.getters['getRangesOnWeek'](fromMonday);
|
||||
const promises = [];
|
||||
const diffTime = toMonday.getTime() - fromMonday.getTime();
|
||||
for (let r of rangesToCopy) {
|
||||
let start = new Date(<Date>ISOToDatetime(r.start));
|
||||
let end = new Date(<Date>ISOToDatetime(r.end));
|
||||
start.setTime(start.getTime() + diffTime);
|
||||
end.setTime(end.getTime() + diffTime);
|
||||
let location = ctx.rootGetters['locations/getLocationById'](r.locationId);
|
||||
|
||||
promises.push(ctx.dispatch('createRange', {start, end, location}));
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(_ => Promise.resolve(null));
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\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()])
|
||||
);
|
||||
}
|
||||
}
|
@@ -201,36 +201,4 @@ class DocumentPersonController extends AbstractController
|
||||
['document' => $document, 'person' => $person]
|
||||
);
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/signature', name: 'person_document_signature', methods: 'GET')]
|
||||
public function signature(Person $person, PersonDocument $document): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person);
|
||||
$this->denyAccessUnlessGranted('CHILL_PERSON_DOCUMENT_SEE', $document);
|
||||
|
||||
$event = new PrivacyEvent($person, [
|
||||
'element_class' => PersonDocument::class,
|
||||
'element_id' => $document->getId(),
|
||||
'action' => 'show',
|
||||
]);
|
||||
$this->eventDispatcher->dispatch($event, PrivacyEvent::PERSON_PRIVACY_EVENT);
|
||||
|
||||
$storedObject = $document->getObject();
|
||||
$content = $this->storedObjectManagerInterface->read($storedObject);
|
||||
$zones = $this->PDFSignatureZoneParser->findSignatureZones($content);
|
||||
|
||||
$signature = [];
|
||||
$signature['id'] = 1;
|
||||
$signature['storedObject'] = [ // TEMP
|
||||
'filename' => $storedObject->getFilename(),
|
||||
'iv' => $storedObject->getIv(),
|
||||
'keyInfos' => $storedObject->getKeyInfos(),
|
||||
];
|
||||
$signature['zones'] = $zones;
|
||||
|
||||
return $this->render(
|
||||
'@ChillDocStore/PersonDocument/signature.html.twig',
|
||||
['document' => $document, 'person' => $person, 'signature' => $signature]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -15,12 +15,19 @@ 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\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\Workflow\EntityWorkflowManager;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
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 Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
class SignatureRequestController
|
||||
{
|
||||
@@ -28,12 +35,24 @@ class SignatureRequestController
|
||||
private readonly MessageBusInterface $messageBus,
|
||||
private readonly StoredObjectManagerInterface $storedObjectManager,
|
||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||
private readonly ChillEntityRenderManagerInterface $entityRender,
|
||||
private readonly NormalizerInterface $normalizer,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
#[Route('/api/1.0/document/workflow/{id}/signature-request', name: 'chill_docstore_signature_request')]
|
||||
public function processSignature(EntityWorkflowStepSignature $signature, Request $request): JsonResponse
|
||||
{
|
||||
if (!$this->security->isGranted(EntityWorkflowStepSignatureVoter::SIGN, $signature)) {
|
||||
throw new AccessDeniedHttpException('not authorized to sign this step');
|
||||
}
|
||||
|
||||
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
|
||||
|
||||
if (EntityWorkflowSignatureStateEnum::PENDING !== $signature->getState()) {
|
||||
return new JsonResponse([], status: Response::HTTP_CONFLICT);
|
||||
}
|
||||
|
||||
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
|
||||
$content = $this->storedObjectManager->read($storedObject);
|
||||
|
||||
@@ -51,8 +70,14 @@ class SignatureRequestController
|
||||
$signature->getId(),
|
||||
$zone,
|
||||
$data['zone']['index'],
|
||||
'test signature', // reason (string)
|
||||
'Mme Caroline Diallo', // signerText (string)
|
||||
'Signed by IP: '.(string) $request->getClientIp().', authenticated user: '.$this->entityRender->renderString($this->security->getUser(), []),
|
||||
$this->entityRender->renderString($signature->getSigner(), [
|
||||
// options for user render
|
||||
'absence' => false,
|
||||
'main_scope' => false,
|
||||
// options for person render
|
||||
'addAge' => false,
|
||||
]),
|
||||
$content
|
||||
));
|
||||
|
||||
@@ -62,6 +87,16 @@ class SignatureRequestController
|
||||
#[Route('/api/1.0/document/workflow/{id}/check-signature', name: 'chill_docstore_check_signature')]
|
||||
public function checkSignature(EntityWorkflowStepSignature $signature): JsonResponse
|
||||
{
|
||||
return new JsonResponse($signature->getState(), JsonResponse::HTTP_OK, []);
|
||||
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
|
||||
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
|
||||
|
||||
return new JsonResponse(
|
||||
[
|
||||
'state' => $signature->getState(),
|
||||
'storedObject' => $this->normalizer->normalize($storedObject, 'json'),
|
||||
],
|
||||
JsonResponse::HTTP_OK,
|
||||
[]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectVersionNormalizer;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectRestoreInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
final readonly class StoredObjectRestoreVersionApiController
|
||||
{
|
||||
public function __construct(private Security $security, private StoredObjectRestoreInterface $storedObjectRestore, private EntityManagerInterface $entityManager, private SerializerInterface $serializer) {}
|
||||
|
||||
#[Route('/api/1.0/doc-store/stored-object/restore-from-version/{id}', methods: ['POST'])]
|
||||
public function restoreStoredObjectVersion(StoredObjectVersion $storedObjectVersion): JsonResponse
|
||||
{
|
||||
if (!$this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObjectVersion->getStoredObject())) {
|
||||
throw new AccessDeniedHttpException('not allowed to edit the stored object');
|
||||
}
|
||||
|
||||
$newVersion = $this->storedObjectRestore->restore($storedObjectVersion);
|
||||
|
||||
$this->entityManager->persist($newVersion);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize($newVersion, 'json', [AbstractNormalizer::GROUPS => ['read', StoredObjectVersionNormalizer::WITH_POINT_IN_TIMES_CONTEXT, StoredObjectVersionNormalizer::WITH_RESTORED_CONTEXT]]),
|
||||
json: true
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectVersionNormalizer;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
|
||||
use Chill\MainBundle\Serializer\Model\Collection;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
final readonly class StoredObjectVersionApiController
|
||||
{
|
||||
public function __construct(
|
||||
private PaginatorFactoryInterface $paginatorFactory,
|
||||
private SerializerInterface $serializer,
|
||||
private Security $security,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Lists the versions of the specified stored object.
|
||||
*
|
||||
* @param StoredObject $storedObject the stored object whose versions are to be listed
|
||||
*
|
||||
* @return JsonResponse a JSON response containing the serialized versions of the stored object, encapsulated in a collection
|
||||
*
|
||||
* @throws AccessDeniedHttpException if the user is not allowed to see the stored object
|
||||
*/
|
||||
#[Route('/api/1.0/doc-store/stored-object/{uuid}/versions', name: 'chill_doc_store_stored_object_versions_list')]
|
||||
public function listVersions(StoredObject $storedObject): JsonResponse
|
||||
{
|
||||
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
||||
throw new AccessDeniedHttpException('not allowed to see this stored object');
|
||||
}
|
||||
|
||||
$total = $storedObject->getVersions()->count();
|
||||
$paginator = $this->paginatorFactory->create($total);
|
||||
|
||||
$criteria = Criteria::create();
|
||||
$criteria->orderBy(['id' => Order::Ascending]);
|
||||
$criteria->setMaxResults($paginator->getItemsPerPage())->setFirstResult($paginator->getCurrentPageFirstItemNumber());
|
||||
$items = $storedObject->getVersions()->matching($criteria);
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize(
|
||||
new Collection($items, $paginator),
|
||||
'json',
|
||||
[AbstractNormalizer::GROUPS => ['read', StoredObjectVersionNormalizer::WITH_POINT_IN_TIMES_CONTEXT, StoredObjectVersionNormalizer::WITH_RESTORED_CONTEXT]]
|
||||
),
|
||||
json: true
|
||||
);
|
||||
}
|
||||
}
|
@@ -18,6 +18,9 @@ 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;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
@@ -89,10 +92,10 @@ class StoredObject implements Document, TrackCreationInterface
|
||||
private string $generationErrors = '';
|
||||
|
||||
/**
|
||||
* @var Collection<int, StoredObjectVersion>
|
||||
* @var Collection<int, StoredObjectVersion>&Selectable<int, StoredObjectVersion>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'storedObject', targetEntity: StoredObjectVersion::class, cascade: ['persist'], orphanRemoval: true)]
|
||||
private Collection $versions;
|
||||
private Collection&Selectable $versions;
|
||||
|
||||
/**
|
||||
* @param StoredObject::STATUS_* $status
|
||||
@@ -256,11 +259,33 @@ class StoredObject implements Document, TrackCreationInterface
|
||||
return $this->template;
|
||||
}
|
||||
|
||||
public function getVersions(): Collection
|
||||
/**
|
||||
* @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();
|
||||
@@ -271,6 +296,47 @@ 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;
|
||||
|
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Entity;
|
||||
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* Represents a snapshot of a stored object at a specific point in time.
|
||||
*
|
||||
* This entity tracks versions of stored objects, reasons for the snapshot,
|
||||
* and the user who initiated the action.
|
||||
*/
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'stored_object_point_in_time', schema: 'chill_doc')]
|
||||
class StoredObjectPointInTime implements TrackCreationInterface
|
||||
{
|
||||
use TrackCreationTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
|
||||
private ?int $id = null;
|
||||
|
||||
public function __construct(
|
||||
#[ORM\ManyToOne(targetEntity: StoredObjectVersion::class, inversedBy: 'pointInTimes')]
|
||||
#[ORM\JoinColumn(name: 'stored_object_version_id', nullable: false)]
|
||||
private StoredObjectVersion $objectVersion,
|
||||
#[ORM\Column(name: 'reason', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, enumType: StoredObjectPointInTimeReasonEnum::class)]
|
||||
private StoredObjectPointInTimeReasonEnum $reason,
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
private ?User $byUser = null,
|
||||
) {
|
||||
$this->objectVersion->addPointInTime($this);
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getByUser(): ?User
|
||||
{
|
||||
return $this->byUser;
|
||||
}
|
||||
|
||||
public function getObjectVersion(): StoredObjectVersion
|
||||
{
|
||||
return $this->objectVersion;
|
||||
}
|
||||
|
||||
public function getReason(): StoredObjectPointInTimeReasonEnum
|
||||
{
|
||||
return $this->reason;
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
<?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\Entity;
|
||||
|
||||
enum StoredObjectPointInTimeReasonEnum: string
|
||||
{
|
||||
case KEEP_BEFORE_CONVERSION = 'keep-before-conversion';
|
||||
case KEEP_BY_USER = 'keep-by-user';
|
||||
}
|
@@ -13,6 +13,9 @@ namespace Chill\DocStoreBundle\Entity;
|
||||
|
||||
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\Selectable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Random\RandomException;
|
||||
|
||||
@@ -39,6 +42,31 @@ class StoredObjectVersion implements TrackCreationInterface
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
|
||||
private string $filename = '';
|
||||
|
||||
/**
|
||||
* @var Collection<int, StoredObjectPointInTime>&Selectable<int, StoredObjectPointInTime>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'objectVersion', targetEntity: StoredObjectPointInTime::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
private Collection&Selectable $pointInTimes;
|
||||
|
||||
/**
|
||||
* Previous storedObjectVersion, from which the current stored object version is created.
|
||||
*
|
||||
* If null, the current stored object version is generated by other means.
|
||||
*
|
||||
* Those version may be associated with the same storedObject, or not. In this last case, that means that
|
||||
* the stored object's current version is created from another stored object version.
|
||||
*/
|
||||
#[ORM\ManyToOne(targetEntity: StoredObjectVersion::class)]
|
||||
private ?StoredObjectVersion $createdFrom = null;
|
||||
|
||||
/**
|
||||
* List of stored object versions created from the current version.
|
||||
*
|
||||
* @var Collection<int, StoredObjectVersion>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'createdFrom', targetEntity: StoredObjectVersion::class)]
|
||||
private Collection $children;
|
||||
|
||||
public function __construct(
|
||||
/**
|
||||
* The stored object associated with this version.
|
||||
@@ -77,6 +105,8 @@ class StoredObjectVersion implements TrackCreationInterface
|
||||
?string $filename = null,
|
||||
) {
|
||||
$this->filename = $filename ?? self::generateFilename($this);
|
||||
$this->pointInTimes = new ArrayCollection();
|
||||
$this->children = new ArrayCollection();
|
||||
}
|
||||
|
||||
public static function generateFilename(StoredObjectVersion $storedObjectVersion): string
|
||||
@@ -124,4 +154,76 @@ class StoredObjectVersion implements TrackCreationInterface
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, StoredObjectPointInTime>&Selectable<int, StoredObjectPointInTime>
|
||||
*/
|
||||
public function getPointInTimes(): Selectable&Collection
|
||||
{
|
||||
return $this->pointInTimes;
|
||||
}
|
||||
|
||||
public function hasPointInTimes(): bool
|
||||
{
|
||||
return $this->pointInTimes->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal use @see{StoredObjectPointInTime} constructor instead
|
||||
*/
|
||||
public function addPointInTime(StoredObjectPointInTime $storedObjectPointInTime): self
|
||||
{
|
||||
if (!$this->pointInTimes->contains($storedObjectPointInTime)) {
|
||||
$this->pointInTimes->add($storedObjectPointInTime);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removePointInTime(StoredObjectPointInTime $storedObjectPointInTime): self
|
||||
{
|
||||
if ($this->pointInTimes->contains($storedObjectPointInTime)) {
|
||||
$this->pointInTimes->removeElement($storedObjectPointInTime);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedFrom(): ?StoredObjectVersion
|
||||
{
|
||||
return $this->createdFrom;
|
||||
}
|
||||
|
||||
public function setCreatedFrom(?StoredObjectVersion $createdFrom): StoredObjectVersion
|
||||
{
|
||||
if (null === $createdFrom && null !== $this->createdFrom) {
|
||||
$this->createdFrom->removeChild($this);
|
||||
}
|
||||
|
||||
$createdFrom?->addChild($this);
|
||||
|
||||
$this->createdFrom = $createdFrom;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addChild(StoredObjectVersion $child): self
|
||||
{
|
||||
if (!$this->children->contains($child)) {
|
||||
$this->children->add($child);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeChild(StoredObjectVersion $child): self
|
||||
{
|
||||
$result = $this->children->removeElement($child);
|
||||
|
||||
if (false === $result) {
|
||||
throw new \UnexpectedValueException('the child is not associated with the current stored object version');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@@ -23,7 +23,7 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository, Associat
|
||||
{
|
||||
private readonly EntityRepository $repository;
|
||||
|
||||
public function __construct(private readonly EntityManagerInterface $em)
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->repository = $em->getRepository(AccompanyingCourseDocument::class);
|
||||
}
|
||||
|
@@ -0,0 +1,27 @@
|
||||
<?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\Repository;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTime;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @template-extends ServiceEntityRepository<StoredObjectPointInTime>
|
||||
*/
|
||||
class StoredObjectPointInTimeRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, StoredObjectPointInTime::class);
|
||||
}
|
||||
}
|
@@ -62,7 +62,7 @@ class StoredObjectVersionRepository implements ObjectRepository
|
||||
*
|
||||
* @return iterable returns an iterable with the IDs of the versions
|
||||
*/
|
||||
public function findIdsByVersionsOlderThanDateAndNotLastVersion(\DateTimeImmutable $beforeDate): iterable
|
||||
public function findIdsByVersionsOlderThanDateAndNotLastVersionAndNotPointInTime(\DateTimeImmutable $beforeDate): iterable
|
||||
{
|
||||
$results = $this->connection->executeQuery(
|
||||
self::QUERY_FIND_IDS_BY_VERSIONS_OLDER_THAN_DATE_AND_NOT_LAST_VERSION,
|
||||
@@ -83,6 +83,8 @@ class StoredObjectVersionRepository implements ObjectRepository
|
||||
sov.createdat < ?::timestamp
|
||||
AND
|
||||
sov.version < (SELECT MAX(sub_sov.version) FROM chill_doc.stored_object_version sub_sov WHERE sub_sov.stored_object_id = sov.stored_object_id)
|
||||
AND
|
||||
NOT EXISTS (SELECT 1 FROM chill_doc.stored_object_point_in_time sub_poi WHERE sub_poi.stored_object_version_id = sov.id)
|
||||
SQL;
|
||||
|
||||
public function getClassName(): string
|
||||
|
@@ -0,0 +1,27 @@
|
||||
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);
|
||||
});
|
||||
});
|
@@ -3,6 +3,7 @@ import DocumentActionButtonsGroup from "../../vuejs/DocumentActionButtonsGroup.v
|
||||
import {createApp} from "vue";
|
||||
import {StoredObject, StoredObjectStatusChange} from "../../types";
|
||||
import {is_object_ready} from "../../vuejs/StoredObjectButton/helpers";
|
||||
import ToastPlugin from "vue-toast-notification";
|
||||
|
||||
const i18n = _createI18n({});
|
||||
|
||||
@@ -48,6 +49,6 @@ window.addEventListener('DOMContentLoaded', function (e) {
|
||||
}
|
||||
});
|
||||
|
||||
app.use(i18n).mount(el);
|
||||
app.use(i18n).use(ToastPlugin).mount(el);
|
||||
})
|
||||
});
|
||||
|
@@ -1,100 +1,132 @@
|
||||
import {DateTime, User} from "../../../ChillMainBundle/Resources/public/types";
|
||||
import {
|
||||
DateTime,
|
||||
User,
|
||||
} from "../../../ChillMainBundle/Resources/public/types";
|
||||
import {SignedUrlGet} from "./vuejs/StoredObjectButton/helpers";
|
||||
|
||||
export type StoredObjectStatus = "empty"|"ready"|"failure"|"pending";
|
||||
export type StoredObjectStatus = "empty" | "ready" | "failure" | "pending";
|
||||
|
||||
export interface StoredObject {
|
||||
id: number,
|
||||
title: string|null,
|
||||
uuid: string,
|
||||
prefix: string,
|
||||
status: StoredObjectStatus,
|
||||
currentVersion: null|StoredObjectVersionCreated|StoredObjectVersionPersisted,
|
||||
totalVersions: number,
|
||||
datas: object,
|
||||
id: number;
|
||||
title: string | null;
|
||||
uuid: string;
|
||||
prefix: string;
|
||||
status: StoredObjectStatus;
|
||||
currentVersion:
|
||||
| null
|
||||
| StoredObjectVersionCreated
|
||||
| StoredObjectVersionPersisted;
|
||||
totalVersions: number;
|
||||
datas: object;
|
||||
/** @deprecated */
|
||||
creationDate: DateTime,
|
||||
createdAt: DateTime|null,
|
||||
createdBy: User|null,
|
||||
creationDate: DateTime;
|
||||
createdAt: DateTime | null;
|
||||
createdBy: User | null;
|
||||
_permissions: {
|
||||
canEdit: boolean,
|
||||
canSee: boolean,
|
||||
},
|
||||
canEdit: boolean;
|
||||
canSee: boolean;
|
||||
};
|
||||
_links?: {
|
||||
dav_link?: {
|
||||
href: string
|
||||
expiration: number
|
||||
},
|
||||
},
|
||||
href: string;
|
||||
expiration: number;
|
||||
};
|
||||
downloadLink?: SignedUrlGet;
|
||||
};
|
||||
}
|
||||
|
||||
export interface StoredObjectVersion {
|
||||
/**
|
||||
* filename of the object in the object storage
|
||||
*/
|
||||
filename: string,
|
||||
iv: number[],
|
||||
keyInfos: JsonWebKey,
|
||||
type: string,
|
||||
filename: string;
|
||||
iv: number[];
|
||||
keyInfos: JsonWebKey;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface StoredObjectVersionCreated extends StoredObjectVersion {
|
||||
persisted: false,
|
||||
persisted: false;
|
||||
}
|
||||
|
||||
export interface StoredObjectVersionPersisted extends StoredObjectVersionCreated {
|
||||
version: number,
|
||||
id: number,
|
||||
createdAt: DateTime|null,
|
||||
createdBy: User|null,
|
||||
export interface StoredObjectVersionPersisted
|
||||
extends StoredObjectVersionCreated {
|
||||
version: number;
|
||||
id: number;
|
||||
createdAt: DateTime | null;
|
||||
createdBy: User | null;
|
||||
}
|
||||
|
||||
export interface StoredObjectStatusChange {
|
||||
id: number,
|
||||
filename: string,
|
||||
status: StoredObjectStatus,
|
||||
type: string,
|
||||
id: number;
|
||||
filename: string;
|
||||
status: StoredObjectStatus;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface StoredObjectVersionWithPointInTime extends StoredObjectVersionPersisted {
|
||||
"point-in-times": StoredObjectPointInTime[];
|
||||
"from-restored": StoredObjectVersionPersisted|null;
|
||||
}
|
||||
|
||||
export interface StoredObjectPointInTime {
|
||||
id: number;
|
||||
byUser: User | null;
|
||||
reason: 'keep-before-conversion'|'keep-by-user';
|
||||
}
|
||||
|
||||
/**
|
||||
* Function executed by the WopiEditButton component.
|
||||
*/
|
||||
export type WopiEditButtonExecutableBeforeLeaveFunction = {
|
||||
(): Promise<void>
|
||||
}
|
||||
(): Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Object containing information for performering a POST request to a swift object store
|
||||
*/
|
||||
export interface PostStoreObjectSignature {
|
||||
method: "POST",
|
||||
max_file_size: number,
|
||||
max_file_count: 1,
|
||||
expires: number,
|
||||
submit_delay: 180,
|
||||
redirect: string,
|
||||
prefix: string,
|
||||
url: string,
|
||||
signature: string,
|
||||
method: "POST";
|
||||
max_file_size: number;
|
||||
max_file_count: 1;
|
||||
expires: number;
|
||||
submit_delay: 180;
|
||||
redirect: string;
|
||||
prefix: string;
|
||||
url: string;
|
||||
signature: string;
|
||||
}
|
||||
|
||||
export interface PDFPage {
|
||||
index: number,
|
||||
width: number,
|
||||
height: number,
|
||||
index: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
export interface SignatureZone {
|
||||
index: number,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
PDFPage: PDFPage,
|
||||
index: number | null;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
PDFPage: PDFPage;
|
||||
}
|
||||
|
||||
export interface Signature {
|
||||
id: number,
|
||||
storedObject: StoredObject,
|
||||
zones: SignatureZone[],
|
||||
id: number;
|
||||
storedObject: StoredObject;
|
||||
zones: SignatureZone[];
|
||||
}
|
||||
|
||||
export type SignedState = 'pending' | 'signed' | 'rejected' | 'canceled' | 'error';
|
||||
export type SignedState =
|
||||
| "pending"
|
||||
| "signed"
|
||||
| "rejected"
|
||||
| "canceled"
|
||||
| "error";
|
||||
|
||||
export interface CheckSignature {
|
||||
state: SignedState;
|
||||
storedObject: StoredObject;
|
||||
}
|
||||
|
||||
export type CanvasEvent = "select" | "add";
|
||||
|
@@ -14,7 +14,10 @@
|
||||
<convert-button :stored-object="props.storedObject" :filename="filename" :classes="{'dropdown-item': true}"></convert-button>
|
||||
</li>
|
||||
<li v-if="isDownloadable">
|
||||
<download-button :stored-object="props.storedObject" :at-version="props.storedObject.currentVersion" :filename="filename" :classes="{'dropdown-item': true}"></download-button>
|
||||
<download-button :stored-object="props.storedObject" :at-version="props.storedObject.currentVersion" :filename="filename" :classes="{'dropdown-item': true}" :display-action-string-in-button="true"></download-button>
|
||||
</li>
|
||||
<li v-if="isHistoryViewable">
|
||||
<history-button :stored-object="props.storedObject" :can-edit="canEdit && props.storedObject._permissions.canEdit"></history-button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -40,6 +43,7 @@ import {
|
||||
WopiEditButtonExecutableBeforeLeaveFunction
|
||||
} from "../types";
|
||||
import DesktopEditButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DesktopEditButton.vue";
|
||||
import HistoryButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton.vue";
|
||||
|
||||
interface DocumentActionButtonsGroupConfig {
|
||||
storedObject: StoredObject,
|
||||
@@ -126,7 +130,11 @@ const isConvertibleToPdf = computed<boolean>(() => {
|
||||
&& is_extension_viewable(props.storedObject.currentVersion.type)
|
||||
&& props.storedObject.currentVersion.type !== 'application/pdf'
|
||||
&& props.storedObject.currentVersion.persisted !== false;
|
||||
})
|
||||
});
|
||||
|
||||
const isHistoryViewable = computed<boolean>(() => {
|
||||
return props.storedObject.status === 'ready';
|
||||
});
|
||||
|
||||
const checkForReady = function(): void {
|
||||
if (
|
||||
|
@@ -26,37 +26,9 @@
|
||||
</template>
|
||||
</modal>
|
||||
</teleport>
|
||||
<div class="col-12">
|
||||
<div
|
||||
class="row justify-content-center mb-2"
|
||||
v-if="signature.zones.length > 1"
|
||||
>
|
||||
<div class="col-4 gap-2 d-grid">
|
||||
<button
|
||||
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
|
||||
class="btn btn-light btn-sm"
|
||||
@click="turnSignature(-1)"
|
||||
>
|
||||
{{ $t("last_sign_zone") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-4 gap-2 d-grid">
|
||||
<button
|
||||
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
|
||||
class="btn btn-light btn-sm"
|
||||
@click="turnSignature(1)"
|
||||
>
|
||||
{{ $t("next_sign_zone") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="turn-page"
|
||||
class="row justify-content-center mb-2"
|
||||
v-if="pageCount > 1"
|
||||
>
|
||||
<div class="col-6-sm col-3-md text-center">
|
||||
<div class="col-12 m-auto">
|
||||
<div class="row justify-content-center border-bottom pdf-tools d-md-none">
|
||||
<div v-if="pageCount > 1" class="col text-center turn-page">
|
||||
<button
|
||||
class="btn btn-light btn-sm"
|
||||
:disabled="page <= 1"
|
||||
@@ -64,7 +36,7 @@
|
||||
>
|
||||
❮
|
||||
</button>
|
||||
<span>page {{ page }} / {{ pageCount }}</span>
|
||||
<span>{{ page }}/{{ pageCount }}</span>
|
||||
<button
|
||||
class="btn btn-light btn-sm"
|
||||
:disabled="page >= pageCount"
|
||||
@@ -73,15 +45,161 @@
|
||||
❯
|
||||
</button>
|
||||
</div>
|
||||
<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"
|
||||
@click="turnSignature(-1)"
|
||||
>
|
||||
{{ $t("last_zone") }}
|
||||
</button>
|
||||
</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"
|
||||
@click="turnSignature(1)"
|
||||
>
|
||||
{{ $t("next_zone") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col text-end p-0">
|
||||
<button
|
||||
class="btn btn-misc btn-sm"
|
||||
:hidden="!userSignatureZone"
|
||||
@click="undoSign"
|
||||
v-if="signature.zones.length > 1"
|
||||
:title="$t('choose_another_signature')"
|
||||
>
|
||||
{{ $t("another_zone") }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-misc btn-sm"
|
||||
:hidden="!userSignatureZone"
|
||||
@click="undoSign"
|
||||
v-else
|
||||
>
|
||||
{{ $t("cancel") }}
|
||||
</button>
|
||||
</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')"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="row justify-content-center border-bottom pdf-tools d-none d-md-flex"
|
||||
>
|
||||
<div v-if="pageCount > 1" class="col-2 text-center turn-page p-0">
|
||||
<button
|
||||
class="btn btn-light btn-sm"
|
||||
:disabled="page <= 1"
|
||||
@click="turnPage(-1)"
|
||||
>
|
||||
❮
|
||||
</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 text-end d-xl-none"
|
||||
>
|
||||
<button
|
||||
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
|
||||
class="btn btn-light btn-sm"
|
||||
@click="turnSignature(-1)"
|
||||
>
|
||||
{{ $t("last_zone") }}
|
||||
</button>
|
||||
</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"
|
||||
@click="turnSignature(1)"
|
||||
>
|
||||
{{ $t("next_zone") }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="signature.zones.length > 1 && signedState !== 'signed'"
|
||||
class="col text-end d-none d-xl-flex p-0"
|
||||
>
|
||||
<button
|
||||
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
|
||||
class="btn btn-light btn-sm"
|
||||
@click="turnSignature(-1)"
|
||||
>
|
||||
{{ $t("last_sign_zone") }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="signature.zones.length > 1 && signedState !== 'signed'"
|
||||
class="col text-start d-none d-xl-flex p-0"
|
||||
>
|
||||
<button
|
||||
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
|
||||
class="btn btn-light btn-sm"
|
||||
@click="turnSignature(1)"
|
||||
>
|
||||
{{ $t("next_sign_zone") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col text-end p-0" v-if="signedState !== 'signed'">
|
||||
<button
|
||||
class="btn btn-misc btn-sm"
|
||||
:hidden="!userSignatureZone"
|
||||
@click="undoSign"
|
||||
v-if="signature.zones.length > 1"
|
||||
>
|
||||
{{ $t("choose_another_signature") }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-misc btn-sm"
|
||||
:hidden="!userSignatureZone"
|
||||
@click="undoSign"
|
||||
v-else
|
||||
>
|
||||
{{ $t("cancel") }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="col text-end p-0 pe-2 pe-xxl-4"
|
||||
v-if="signedState !== 'signed'"
|
||||
>
|
||||
<button
|
||||
class="btn btn-create btn-sm"
|
||||
:class="{ active: canvasEvent === 'add' }"
|
||||
@click="toggleAddZone()"
|
||||
:title="$t('add_sign_zone')"
|
||||
>
|
||||
{{ $t("add_zone") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 text-center">
|
||||
<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-12 p-4" id="action-buttons" v-if="signedState !== 'signed'">
|
||||
<div class="col-xs-12 col-md-12 col-lg-9 m-auto p-4" id="action-buttons">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="col-4" v-if="signedState !== 'signed'">
|
||||
<button
|
||||
class="btn btn-action me-2"
|
||||
:disabled="!userSignatureZone"
|
||||
@@ -90,26 +208,18 @@
|
||||
{{ $t("sign") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-6 d-flex justify-content-end">
|
||||
<button
|
||||
class="btn btn-misc me-2"
|
||||
:hidden="!userSignatureZone"
|
||||
@click="undoSign"
|
||||
v-if="signature.zones.length > 1"
|
||||
<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("choose_another_signature") }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-misc me-2"
|
||||
:hidden="!userSignatureZone"
|
||||
@click="undoSign"
|
||||
v-else
|
||||
>
|
||||
{{ $t("cancel") }}
|
||||
</button>
|
||||
<button class="btn btn-delete" @click="undoSign">
|
||||
{{ $t("cancel_signing") }}
|
||||
</button>
|
||||
</a>
|
||||
<a class="btn btn-misc" v-else :href="getReturnPath()">
|
||||
{{ $t("return") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -119,7 +229,13 @@
|
||||
import { ref, Ref, reactive } from "vue";
|
||||
import { useToast } from "vue-toast-notification";
|
||||
import "vue-toast-notification/dist/theme-sugar.css";
|
||||
import { Signature, SignatureZone, SignedState } from "../../types";
|
||||
import {
|
||||
CanvasEvent,
|
||||
CheckSignature,
|
||||
Signature,
|
||||
SignatureZone,
|
||||
SignedState,
|
||||
} from "../../types";
|
||||
import { makeFetch } from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
||||
import * as pdfjsLib from "pdfjs-dist";
|
||||
import {
|
||||
@@ -135,19 +251,18 @@ console.log(PdfWorker); // incredible but this is needed
|
||||
// pdfjsLib.GlobalWorkerOptions.workerSrc = PdfWorker;
|
||||
|
||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
||||
import {
|
||||
download_and_decrypt_doc,
|
||||
} from "../StoredObjectButton/helpers";
|
||||
import { download_and_decrypt_doc } from "../StoredObjectButton/helpers";
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = "pdfjs-dist/build/pdf.worker.mjs";
|
||||
|
||||
const modalOpen: Ref<boolean> = ref(false);
|
||||
const loading: Ref<boolean> = ref(false);
|
||||
const adding: Ref<boolean> = ref(false);
|
||||
const canvasEvent: Ref<CanvasEvent> = ref("select");
|
||||
const signedState: Ref<SignedState> = ref("pending");
|
||||
const page: Ref<number> = ref(1);
|
||||
const pageCount: Ref<number> = ref(0);
|
||||
let userSignatureZone: Ref<null | SignatureZone> = ref(null);
|
||||
let pdfSource: Ref<string> = ref("");
|
||||
let pdf = {} as PDFDocumentProxy;
|
||||
|
||||
declare global {
|
||||
@@ -160,11 +275,13 @@ const $toast = useToast();
|
||||
|
||||
const signature = window.signature;
|
||||
|
||||
console.log(signature);
|
||||
|
||||
const mountPdf = async (url: string) => {
|
||||
const loadingTask = pdfjsLib.getDocument(url);
|
||||
pdf = await loadingTask.promise;
|
||||
pageCount.value = pdf.numPages;
|
||||
await setPage(1);
|
||||
await setPage(page.value);
|
||||
};
|
||||
|
||||
const getRenderContext = (pdfPage: PDFPageProxy) => {
|
||||
@@ -187,59 +304,61 @@ const setPage = async (page: number) => {
|
||||
await pdfPage.render(renderContext);
|
||||
};
|
||||
|
||||
const init = () => downloadAndOpen().then(initPdf);
|
||||
|
||||
async function downloadAndOpen(): Promise<Blob> {
|
||||
let raw;
|
||||
try {
|
||||
raw = await download_and_decrypt_doc(signature.storedObject, signature.storedObject.currentVersion);
|
||||
raw = await download_and_decrypt_doc(
|
||||
signature.storedObject,
|
||||
signature.storedObject.currentVersion
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("error while downloading and decrypting document", e);
|
||||
throw e;
|
||||
}
|
||||
await mountPdf(URL.createObjectURL(raw));
|
||||
initPdf();
|
||||
return raw;
|
||||
}
|
||||
|
||||
const initPdf = () => {
|
||||
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
||||
canvas.addEventListener(
|
||||
"pointerup",
|
||||
(e: PointerEvent) => canvasClick(e, canvas),
|
||||
false
|
||||
);
|
||||
setTimeout(() => addZones(page.value), 800);
|
||||
canvas.addEventListener("pointerup", canvasClick, false);
|
||||
setTimeout(() => drawAllZones(page.value), 800);
|
||||
};
|
||||
|
||||
const scaleXToCanvas = (x: number, canvasWidth: number, PDFWidth: number) =>
|
||||
Math.round((x * canvasWidth) / PDFWidth);
|
||||
|
||||
const scaleYToCanvas = (h: number, canvasHeight: number, PDFHeight: number) =>
|
||||
Math.round((h * canvasHeight) / PDFHeight);
|
||||
|
||||
const hitSignature = (
|
||||
zone: SignatureZone,
|
||||
xy: number[],
|
||||
canvasWidth: number,
|
||||
canvasHeight: number
|
||||
) => {
|
||||
const scaleXToCanvas = (x: number) =>
|
||||
Math.round((x * canvasWidth) / zone.PDFPage.width);
|
||||
const scaleHeightToCanvas = (h: number) =>
|
||||
Math.round((h * canvasHeight) / zone.PDFPage.height);
|
||||
const scaleYToCanvas = (y: number) =>
|
||||
Math.round(zone.PDFPage.height - scaleHeightToCanvas(y));
|
||||
return (
|
||||
scaleXToCanvas(zone.x) < xy[0] &&
|
||||
xy[0] < scaleXToCanvas(zone.x + zone.width) &&
|
||||
scaleYToCanvas(zone.y) < xy[1] &&
|
||||
xy[1] < scaleYToCanvas(zone.y) + scaleHeightToCanvas(zone.height)
|
||||
);
|
||||
};
|
||||
) =>
|
||||
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) < xy[0] &&
|
||||
xy[0] <
|
||||
scaleXToCanvas(zone.x + zone.width, canvasWidth, zone.PDFPage.width) &&
|
||||
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;
|
||||
|
||||
const selectZone = (z: SignatureZone, canvas: HTMLCanvasElement) => {
|
||||
userSignatureZone.value = z;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx) {
|
||||
setPage(page.value);
|
||||
setTimeout(() => drawZone(z, ctx, canvas.width, canvas.height), 200);
|
||||
setTimeout(() => drawAllZones(page.value), 200);
|
||||
}
|
||||
};
|
||||
|
||||
const canvasClick = (e: PointerEvent, canvas: HTMLCanvasElement) =>
|
||||
const selectZoneEvent = (e: PointerEvent, canvas: HTMLCanvasElement) =>
|
||||
signature.zones
|
||||
.filter((z) => z.PDFPage.index + 1 === page.value)
|
||||
.map((z) => {
|
||||
@@ -256,11 +375,18 @@ const canvasClick = (e: PointerEvent, canvas: HTMLCanvasElement) =>
|
||||
}
|
||||
});
|
||||
|
||||
const canvasClick = (e: PointerEvent) => {
|
||||
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
||||
canvasEvent.value === "select"
|
||||
? selectZoneEvent(e, canvas)
|
||||
: addZoneEvent(e, canvas);
|
||||
};
|
||||
|
||||
const turnPage = async (upOrDown: number) => {
|
||||
userSignatureZone.value = null;
|
||||
//userSignatureZone.value = null; // desactivate the reset of the zone when turning page
|
||||
page.value = page.value + upOrDown;
|
||||
await setPage(page.value);
|
||||
setTimeout(() => addZones(page.value), 200);
|
||||
setTimeout(() => drawAllZones(page.value), 200);
|
||||
};
|
||||
|
||||
const turnSignature = async (upOrDown: number) => {
|
||||
@@ -290,12 +416,6 @@ const drawZone = (
|
||||
) => {
|
||||
const unselectedBlue = "#007bff";
|
||||
const selectedBlue = "#034286";
|
||||
const scaleXToCanvas = (x: number) =>
|
||||
Math.round((x * canvasWidth) / zone.PDFPage.width);
|
||||
const scaleHeightToCanvas = (h: number) =>
|
||||
Math.round((h * canvasHeight) / zone.PDFPage.height);
|
||||
const scaleYToCanvas = (y: number) =>
|
||||
Math.round(zone.PDFPage.height - scaleHeightToCanvas(y));
|
||||
ctx.strokeStyle =
|
||||
userSignatureZone.value?.index === zone.index
|
||||
? selectedBlue
|
||||
@@ -303,16 +423,22 @@ const drawZone = (
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineJoin = "bevel";
|
||||
ctx.strokeRect(
|
||||
scaleXToCanvas(zone.x),
|
||||
scaleYToCanvas(zone.y),
|
||||
scaleXToCanvas(zone.width),
|
||||
scaleHeightToCanvas(zone.height)
|
||||
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width),
|
||||
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 16px serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillStyle = "black";
|
||||
const xText = scaleXToCanvas(zone.x) + scaleXToCanvas(zone.width) / 2;
|
||||
const yText = scaleYToCanvas(zone.y) + scaleHeightToCanvas(zone.height) / 2;
|
||||
const xText =
|
||||
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) +
|
||||
scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width) / 2;
|
||||
const yText =
|
||||
zone.PDFPage.height -
|
||||
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) +
|
||||
scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height) / 2;
|
||||
if (userSignatureZone.value?.index === zone.index) {
|
||||
ctx.fillStyle = selectedBlue;
|
||||
ctx.fillText("Signer ici", xText, yText);
|
||||
@@ -320,27 +446,33 @@ const drawZone = (
|
||||
ctx.fillStyle = unselectedBlue;
|
||||
ctx.fillText("Choisir cette", xText, yText - 12);
|
||||
ctx.fillText("zone de signature", xText, yText + 12);
|
||||
// ctx.strokeStyle = "#c6c6c6"; // halo
|
||||
// ctx.strokeText("Choisir cette", xText, yText - 12);
|
||||
// ctx.strokeText("zone de signature", xText, yText + 12);
|
||||
}
|
||||
};
|
||||
|
||||
const addZones = (page: number) => {
|
||||
const drawAllZones = (page: number) => {
|
||||
const canvas = document.querySelectorAll("canvas")[0];
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx) {
|
||||
if (ctx && signedState.value !== "signed") {
|
||||
signature.zones
|
||||
.filter((z) => z.PDFPage.index + 1 === page)
|
||||
.map((z) => drawZone(z, ctx, canvas.width, canvas.height));
|
||||
.map((z) => {
|
||||
if (userSignatureZone.value) {
|
||||
if (userSignatureZone.value?.index === z.index) {
|
||||
drawZone(z, ctx, canvas.width, canvas.height);
|
||||
}
|
||||
} else {
|
||||
drawZone(z, ctx, canvas.width, canvas.height);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const checkSignature = () => {
|
||||
const url = `/api/1.0/document/workflow/${signature.id}/check-signature`;
|
||||
return makeFetch("GET", url)
|
||||
return makeFetch<null, CheckSignature>("GET", url)
|
||||
.then((r) => {
|
||||
signedState.value = r as SignedState;
|
||||
signedState.value = r.state;
|
||||
signature.storedObject = r.storedObject;
|
||||
checkForReady();
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -414,22 +546,66 @@ const confirmSign = () => {
|
||||
};
|
||||
|
||||
const undoSign = async () => {
|
||||
// const canvas = document.querySelectorAll("canvas")[0];
|
||||
// const ctx = canvas.getContext("2d");
|
||||
// if (ctx && userSignatureZone.value) {
|
||||
// //drawZone(userSignatureZone.value, ctx, canvas.width, canvas.height);
|
||||
// }
|
||||
signature.zones = signature.zones.filter((z) => z.index !== null);
|
||||
await setPage(page.value);
|
||||
setTimeout(() => addZones(page.value), 200);
|
||||
setTimeout(() => drawAllZones(page.value), 200);
|
||||
userSignatureZone.value = null;
|
||||
adding.value = false;
|
||||
canvasEvent.value = "select";
|
||||
};
|
||||
|
||||
downloadAndOpen();
|
||||
const toggleAddZone = () => {
|
||||
canvasEvent.value === "select"
|
||||
? (canvasEvent.value = "add")
|
||||
: (canvasEvent.value = "select");
|
||||
};
|
||||
|
||||
const addZoneEvent = async (e: PointerEvent, canvas: HTMLCanvasElement) => {
|
||||
const BOX_WIDTH = 180;
|
||||
const BOX_HEIGHT = 90;
|
||||
const PDFPageHeight = canvas.height;
|
||||
const PDFPageWidth = canvas.width;
|
||||
|
||||
const x = e.offsetX;
|
||||
const y = e.offsetY;
|
||||
const newZone: SignatureZone = {
|
||||
index: null,
|
||||
x:
|
||||
scaleXToCanvas(x, canvas.width, PDFPageWidth) -
|
||||
scaleXToCanvas(BOX_WIDTH / 2, canvas.width, PDFPageWidth),
|
||||
y:
|
||||
PDFPageHeight -
|
||||
scaleYToCanvas(y, canvas.height, PDFPageHeight) +
|
||||
scaleYToCanvas(BOX_HEIGHT / 2, canvas.height, PDFPageHeight),
|
||||
width: BOX_WIDTH,
|
||||
height: BOX_HEIGHT,
|
||||
PDFPage: {
|
||||
index: page.value - 1,
|
||||
width: PDFPageWidth,
|
||||
height: PDFPageHeight,
|
||||
},
|
||||
};
|
||||
signature.zones.push(newZone);
|
||||
userSignatureZone.value = newZone;
|
||||
|
||||
await setPage(page.value);
|
||||
setTimeout(() => drawAllZones(page.value), 200);
|
||||
canvasEvent.value = "select";
|
||||
adding.value = true;
|
||||
};
|
||||
|
||||
const getReturnPath = () =>
|
||||
window.location.search
|
||||
? window.location.search.split("?returnPath=")[1] ??
|
||||
window.location.pathname
|
||||
: window.location.pathname;
|
||||
|
||||
init();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
#canvas {
|
||||
box-shadow: 0 20px 20px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 20px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
div#action-buttons {
|
||||
position: sticky;
|
||||
@@ -437,7 +613,15 @@ div#action-buttons {
|
||||
background-color: white;
|
||||
z-index: 100;
|
||||
}
|
||||
div#turn-page {
|
||||
div.pdf-tools {
|
||||
background-color: #f3f3f3;
|
||||
font-size: 0.8rem;
|
||||
@media (min-width: 1400px) {
|
||||
// background: none;
|
||||
// border: none !important;
|
||||
}
|
||||
}
|
||||
div.turn-page {
|
||||
span {
|
||||
font-size: 0.8rem;
|
||||
margin: 0 0.4rem;
|
||||
|
@@ -10,13 +10,20 @@ const appMessages = {
|
||||
you_are_going_to_sign: 'Vous allez signer le document',
|
||||
signature_confirmation: 'Confirmation de la signature',
|
||||
sign: 'Signer',
|
||||
choose_another_signature: 'Choisir une autre zone de signature',
|
||||
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',
|
||||
last_zone: 'Zone précédente',
|
||||
next_zone: 'Zone suivante',
|
||||
add_zone: 'Ajouter une zone',
|
||||
another_zone: 'Autre zone',
|
||||
electronic_signature_in_progress: 'Signature électronique en cours...',
|
||||
loading: 'Chargement...'
|
||||
loading: 'Chargement...',
|
||||
remove_sign_zone: 'Enlever la zone',
|
||||
return: 'Retour',
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -3,6 +3,7 @@
|
||||
import {StoredObject, StoredObjectVersionCreated} from "../../types";
|
||||
import {encryptFile, fetchNewStoredObject, uploadVersion} from "../../js/async-upload/uploader";
|
||||
import {computed, ref, Ref} from "vue";
|
||||
import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue";
|
||||
|
||||
interface DropFileConfig {
|
||||
existingDoc?: StoredObject,
|
||||
@@ -16,6 +17,7 @@ const emit = defineEmits<{
|
||||
|
||||
const is_dragging: Ref<boolean> = ref(false);
|
||||
const uploading: Ref<boolean> = ref(false);
|
||||
const display_filename: Ref<string|null> = ref(null);
|
||||
|
||||
const has_existing_doc = computed<boolean>(() => {
|
||||
return props.existingDoc !== undefined && props.existingDoc !== null;
|
||||
@@ -77,6 +79,7 @@ const onFileChange = async (event: Event): Promise<void> => {
|
||||
|
||||
const handleFile = async (file: File): Promise<void> => {
|
||||
uploading.value = true;
|
||||
display_filename.value = file.name;
|
||||
const type = file.type;
|
||||
|
||||
// create a stored_object if not exists
|
||||
@@ -108,18 +111,11 @@ const handleFile = async (file: File): Promise<void> => {
|
||||
<template>
|
||||
<div class="drop-file">
|
||||
<div v-if="!uploading" :class="{ area: true, dragging: is_dragging}" @click="onZoneClick" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
|
||||
<p v-if="has_existing_doc">
|
||||
<i class="fa fa-file-pdf-o" v-if="props.existingDoc?.type === 'application/pdf'"></i>
|
||||
<i class="fa fa-file-word-o" v-else-if="props.existingDoc?.type === 'application/vnd.oasis.opendocument.text'"></i>
|
||||
<i class="fa fa-file-word-o" v-else-if="props.existingDoc?.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'"></i>
|
||||
<i class="fa fa-file-word-o" v-else-if="props.existingDoc?.type === 'application/msword'"></i>
|
||||
<i class="fa fa-file-excel-o" v-else-if="props.existingDoc?.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'"></i>
|
||||
<i class="fa fa-file-excel-o" v-else-if="props.existingDoc?.type === 'application/vnd.ms-excel'"></i>
|
||||
<i class="fa fa-file-image-o" v-else-if="props.existingDoc?.type === 'image/jpeg'"></i>
|
||||
<i class="fa fa-file-image-o" v-else-if="props.existingDoc?.type === 'image/png'"></i>
|
||||
<i class="fa fa-file-archive-o" v-else-if="props.existingDoc?.type === 'application/x-zip-compressed'"></i>
|
||||
<i class="fa fa-file-code-o" v-else ></i>
|
||||
<p v-if="has_existing_doc" class="file-icon">
|
||||
<file-icon :type="props.existingDoc?.type"></file-icon>
|
||||
</p>
|
||||
|
||||
<p v-if="display_filename !== null" class="display-filename">{{ display_filename }}</p>
|
||||
<!-- todo i18n -->
|
||||
<p v-if="has_existing_doc">Déposez un document ou cliquez ici pour remplacer le document existant</p>
|
||||
<p v-else>Déposez un document ou cliquez ici pour ouvrir le navigateur de fichier</p>
|
||||
@@ -135,9 +131,18 @@ const handleFile = async (file: File): Promise<void> => {
|
||||
.drop-file {
|
||||
width: 100%;
|
||||
|
||||
.file-icon {
|
||||
font-size: xx-large;
|
||||
}
|
||||
|
||||
.display-filename {
|
||||
font-variant: small-caps;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
& > .area, & > .waiting {
|
||||
width: 100%;
|
||||
height: 8rem;
|
||||
height: 10rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -158,4 +163,5 @@ const handleFile = async (file: File): Promise<void> => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
interface FileIconConfig {
|
||||
type: string;
|
||||
}
|
||||
|
||||
const props = defineProps<FileIconConfig>();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<i class="fa fa-file-pdf-o" v-if="props.type === 'application/pdf'"></i>
|
||||
<i class="fa fa-file-word-o" v-else-if="props.type === 'application/vnd.oasis.opendocument.text'"></i>
|
||||
<i class="fa fa-file-word-o" v-else-if="props.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'"></i>
|
||||
<i class="fa fa-file-word-o" v-else-if="props.type === 'application/msword'"></i>
|
||||
<i class="fa fa-file-excel-o" v-else-if="props.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'"></i>
|
||||
<i class="fa fa-file-excel-o" v-else-if="props.type === 'application/vnd.ms-excel'"></i>
|
||||
<i class="fa fa-file-image-o" v-else-if="props.type === 'image/jpeg'"></i>
|
||||
<i class="fa fa-file-image-o" v-else-if="props.type === 'image/png'"></i>
|
||||
<i class="fa fa-file-archive-o" v-else-if="props.type === 'application/x-zip-compressed'"></i>
|
||||
<i class="fa fa-file-code-o" v-else ></i>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<a :class="props.classes" @click="download_and_open($event)">
|
||||
<a :class="props.classes" @click="download_and_open($event)" ref="btn">
|
||||
<i class="fa fa-file-pdf-o"></i>
|
||||
Télécharger en pdf
|
||||
</a>
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
import {build_convert_link, download_and_decrypt_doc, download_doc} from "./helpers";
|
||||
import mime from "mime";
|
||||
import {reactive} from "vue";
|
||||
import {reactive, ref} from "vue";
|
||||
import {StoredObject} from "../../types";
|
||||
|
||||
interface ConvertButtonConfig {
|
||||
@@ -24,6 +24,7 @@ interface DownloadButtonState {
|
||||
|
||||
const props = defineProps<ConvertButtonConfig>();
|
||||
const state: DownloadButtonState = reactive({content: null});
|
||||
const btn = ref<HTMLAnchorElement | null>(null);
|
||||
|
||||
async function download_and_open(event: Event): Promise<void> {
|
||||
const button = event.target as HTMLAnchorElement;
|
||||
@@ -41,6 +42,14 @@ async function download_and_open(event: Event): Promise<void> {
|
||||
}
|
||||
|
||||
button.click();
|
||||
const reset_pending = setTimeout(reset_state, 45000);
|
||||
}
|
||||
|
||||
function reset_state(): void {
|
||||
state.content = null;
|
||||
btn.value?.removeAttribute('download');
|
||||
btn.value?.removeAttribute('href');
|
||||
btn.value?.removeAttribute('type');
|
||||
}
|
||||
|
||||
</script>
|
||||
|
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<a v-if="!state.is_ready" :class="props.classes" @click="download_and_open($event)">
|
||||
<a v-if="!state.is_ready" :class="props.classes" @click="download_and_open()" title="Télécharger">
|
||||
<i class="fa fa-download"></i>
|
||||
Télécharger
|
||||
<template v-if="displayActionStringInButton">Télécharger</template>
|
||||
</a>
|
||||
<a v-else :class="props.classes" target="_blank" :type="props.storedObject.type" :download="buildDocumentName()" :href="state.href_url" ref="open_button">
|
||||
<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>
|
||||
Ouvrir
|
||||
<template v-if="displayActionStringInButton">Ouvrir</template>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
@@ -20,6 +20,15 @@ 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,
|
||||
}
|
||||
|
||||
interface DownloadButtonState {
|
||||
@@ -28,13 +37,17 @@ interface DownloadButtonState {
|
||||
href_url: string,
|
||||
}
|
||||
|
||||
const props = defineProps<DownloadButtonConfig>();
|
||||
const props = withDefaults(defineProps<DownloadButtonConfig>(), {displayActionStringInButton: true, directDownload: false});
|
||||
const state: DownloadButtonState = reactive({is_ready: false, is_running: false, href_url: "#"});
|
||||
|
||||
const open_button = ref<HTMLAnchorElement | null>(null);
|
||||
|
||||
function buildDocumentName(): string {
|
||||
const document_name = props.filename ?? props.storedObject.title ?? 'document';
|
||||
let document_name = props.filename ?? props.storedObject.title;
|
||||
|
||||
if ('' === document_name) {
|
||||
document_name = 'document';
|
||||
}
|
||||
|
||||
const ext = mime.getExtension(props.atVersion.type);
|
||||
|
||||
@@ -45,9 +58,7 @@ function buildDocumentName(): string {
|
||||
return document_name;
|
||||
}
|
||||
|
||||
async function download_and_open(event: Event): Promise<void> {
|
||||
const button = event.target as HTMLAnchorElement;
|
||||
|
||||
async function download_and_open(): Promise<void> {
|
||||
if (state.is_running) {
|
||||
console.log('state is running, aborting');
|
||||
return;
|
||||
@@ -74,13 +85,33 @@ async function download_and_open(event: Event): Promise<void> {
|
||||
state.is_running = false;
|
||||
state.is_ready = true;
|
||||
|
||||
await nextTick();
|
||||
open_button.value?.click();
|
||||
if (!props.directDownload) {
|
||||
await nextTick();
|
||||
open_button.value?.click();
|
||||
|
||||
console.log('open button should have been clicked');
|
||||
setTimeout(reset_state, 45000);
|
||||
}
|
||||
}
|
||||
|
||||
function reset_state(): void {
|
||||
state.href_url = '#';
|
||||
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>
|
||||
|
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import HistoryButtonModal from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonModal.vue";
|
||||
import {StoredObject, StoredObjectVersionWithPointInTime} from "./../../types";
|
||||
import {computed, reactive, ref, useTemplateRef} from "vue";
|
||||
import {get_versions} from "./HistoryButton/api";
|
||||
|
||||
interface HistoryButtonConfig {
|
||||
storedObject: StoredObject;
|
||||
canEdit: boolean;
|
||||
}
|
||||
|
||||
interface HistoryButtonState {
|
||||
versions: StoredObjectVersionWithPointInTime[];
|
||||
loaded: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<HistoryButtonConfig>();
|
||||
const state = reactive<HistoryButtonState>({versions: [], loaded: false});
|
||||
const modal = useTemplateRef<typeof HistoryButtonModal>('modal');
|
||||
|
||||
const download_version_and_open_modal = async function (): Promise<void> {
|
||||
if (null !== modal.value) {
|
||||
modal.value.open();
|
||||
} else {
|
||||
console.log("modal is null");
|
||||
}
|
||||
|
||||
if (!state.loaded) {
|
||||
const versions = await get_versions(props.storedObject);
|
||||
|
||||
for (const version of versions) {
|
||||
state.versions.push(version);
|
||||
}
|
||||
state.loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
const onRestoreVersion = ({newVersion}: {newVersion: StoredObjectVersionWithPointInTime}) => {
|
||||
state.versions.unshift(newVersion);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a @click="download_version_and_open_modal" class="dropdown-item">
|
||||
<history-button-modal ref="modal" :versions="state.versions" :stored-object="storedObject" :can-edit="canEdit" @restore-version="onRestoreVersion"></history-button-modal>
|
||||
<i class="fa fa-history"></i>
|
||||
Historique
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
i.fa::before {
|
||||
color: var(--bs-dropdown-link-hover-color);
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import {StoredObject, StoredObjectVersionWithPointInTime} from "./../../../types";
|
||||
import HistoryButtonListItem from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonListItem.vue";
|
||||
import {computed, reactive} from "vue";
|
||||
|
||||
interface HistoryButtonListConfig {
|
||||
versions: StoredObjectVersionWithPointInTime[];
|
||||
storedObject: StoredObject;
|
||||
canEdit: boolean;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]
|
||||
}>()
|
||||
|
||||
interface HistoryButtonListState {
|
||||
/**
|
||||
* Contains the number of the newly created version when a version is restored.
|
||||
*/
|
||||
restored: number;
|
||||
}
|
||||
|
||||
const props = defineProps<HistoryButtonListConfig>();
|
||||
|
||||
const state = reactive<HistoryButtonListState>({restored: -1})
|
||||
|
||||
const higher_version = computed<number>(() => props.versions.reduce(
|
||||
(accumulator: number, version: StoredObjectVersionWithPointInTime) => Math.max(accumulator, version.version),
|
||||
-1
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Executed when a version in child component is restored.
|
||||
*
|
||||
* internally, keep track of the newly restored version
|
||||
*/
|
||||
const onRestored = ({newVersion}: {newVersion: StoredObjectVersionWithPointInTime}) => {
|
||||
state.restored = newVersion.version;
|
||||
emit('restoreVersion', {newVersion});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="props.versions.length > 0">
|
||||
<div class="container">
|
||||
<template v-for="v in props.versions">
|
||||
<history-button-list-item
|
||||
:version="v"
|
||||
:can-edit="canEdit"
|
||||
:is-current="higher_version === v.version"
|
||||
:stored-object="storedObject"
|
||||
@restore-version="onRestored"
|
||||
></history-button-list-item>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>Chargement des versions</p>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
@@ -0,0 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
import {StoredObject, StoredObjectPointInTime, StoredObjectVersionWithPointInTime} from "./../../../types";
|
||||
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
|
||||
import {ISOToDatetime} from "./../../../../../../ChillMainBundle/Resources/public/chill/js/date";
|
||||
import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue";
|
||||
import RestoreVersionButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/RestoreVersionButton.vue";
|
||||
import DownloadButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DownloadButton.vue";
|
||||
import {computed} from "vue";
|
||||
|
||||
interface HistoryButtonListItemConfig {
|
||||
version: StoredObjectVersionWithPointInTime;
|
||||
storedObject: StoredObject;
|
||||
canEdit: boolean;
|
||||
isCurrent: boolean;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]
|
||||
}>()
|
||||
|
||||
const props = defineProps<HistoryButtonListItemConfig>();
|
||||
|
||||
const onRestore = ({newVersion}: {newVersion: StoredObjectVersionWithPointInTime}) => {
|
||||
emit('restoreVersion', {newVersion});
|
||||
}
|
||||
|
||||
const isKeptBeforeConversion = computed<boolean>(() => props.version["point-in-times"].reduce(
|
||||
(accumulator: boolean, pit: StoredObjectPointInTime) => accumulator || "keep-before-conversion" === pit.reason,
|
||||
false
|
||||
),
|
||||
);
|
||||
|
||||
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 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}));
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="classes">
|
||||
<div class="col-12 tags" v-if="isCurrent || isKeptBeforeConversion || isRestored || isDuplicated">
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<ul class="record_actions small slim on-version-actions">
|
||||
<li v-if="canEdit && !isCurrent">
|
||||
<restore-version-button :stored-object-version="props.version" @restore-version="onRestore"></restore-version-button>
|
||||
</li>
|
||||
<li>
|
||||
<download-button :stored-object="storedObject" :at-version="version" :classes="{btn: true, 'btn-outline-primary': true}" :display-action-string-in-button="false"></download-button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
div.tags {
|
||||
span.badge:not(:last-child) {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
// to make the animation restart, we have the same animation twice,
|
||||
// and alternate between both
|
||||
.blinking-1 {
|
||||
animation-name: backgroundColorPalette-1;
|
||||
animation-duration: 8s;
|
||||
animation-iteration-count: 1;
|
||||
animation-direction: normal;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
@keyframes backgroundColorPalette-1 {
|
||||
0% {
|
||||
background: var(--bs-chill-green-dark);
|
||||
}
|
||||
25% {
|
||||
background: var(--bs-chill-green);
|
||||
}
|
||||
65% {
|
||||
background: var(--bs-chill-beige);
|
||||
}
|
||||
100% {
|
||||
background: unset;
|
||||
}
|
||||
}
|
||||
.blinking-2 {
|
||||
animation-name: backgroundColorPalette-2;
|
||||
animation-duration: 8s;
|
||||
animation-iteration-count: 1;
|
||||
animation-direction: normal;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
@keyframes backgroundColorPalette-2 {
|
||||
0% {
|
||||
background: var(--bs-chill-green-dark);
|
||||
}
|
||||
25% {
|
||||
background: var(--bs-chill-green);
|
||||
}
|
||||
65% {
|
||||
background: var(--bs-chill-beige);
|
||||
}
|
||||
100% {
|
||||
background: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
||||
import {reactive} from "vue";
|
||||
import HistoryButtonList from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonList.vue";
|
||||
import {StoredObject, StoredObjectVersionWithPointInTime} from "./../../../types";
|
||||
|
||||
interface HistoryButtonListConfig {
|
||||
versions: StoredObjectVersionWithPointInTime[];
|
||||
storedObject: StoredObject;
|
||||
canEdit: boolean;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]
|
||||
}>()
|
||||
|
||||
interface HistoryButtonModalState {
|
||||
opened: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<HistoryButtonListConfig>();
|
||||
const state = reactive<HistoryButtonModalState>({opened: false});
|
||||
|
||||
const open = () => {
|
||||
state.opened = true;
|
||||
}
|
||||
|
||||
defineExpose({open});
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<modal v-if="state.opened" @close="state.opened = false">
|
||||
<template v-slot:header>
|
||||
<h3>Historique des versions du document</h3>
|
||||
</template>
|
||||
<template v-slot:body>
|
||||
<p>Les versions sont conservées pendant 90 jours.</p>
|
||||
<history-button-list :versions="props.versions" :can-edit="canEdit" :stored-object="storedObject" @restore-version="(payload) => emit('restoreVersion', payload)"></history-button-list>
|
||||
</template>
|
||||
</modal>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import {StoredObjectVersionPersisted, StoredObjectVersionWithPointInTime} from "../../../types";
|
||||
import {useToast} from "vue-toast-notification";
|
||||
import {restore_version} from "./api";
|
||||
|
||||
interface RestoreVersionButtonProps {
|
||||
storedObjectVersion: StoredObjectVersionPersisted,
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]
|
||||
}>()
|
||||
|
||||
const props = defineProps<RestoreVersionButtonProps>()
|
||||
|
||||
const $toast = useToast();
|
||||
|
||||
const restore_version_fn = async () => {
|
||||
const newVersion = await restore_version(props.storedObjectVersion);
|
||||
|
||||
$toast.success("Version restaurée");
|
||||
emit('restoreVersion', {newVersion});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button class="btn btn-outline-action" @click="restore_version_fn" title="Restaurer"><i class="fa fa-rotate-left"></i> Restaurer</button>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
@@ -0,0 +1,12 @@
|
||||
import {StoredObject, StoredObjectVersionPersisted, StoredObjectVersionWithPointInTime} from "../../../types";
|
||||
import {fetchResults, makeFetch} from "../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
||||
|
||||
export const get_versions = async (storedObject: StoredObject): Promise<StoredObjectVersionWithPointInTime[]> => {
|
||||
const versions = await fetchResults<StoredObjectVersionWithPointInTime>(`/api/1.0/doc-store/stored-object/${storedObject.uuid}/versions`);
|
||||
|
||||
return versions.sort((a: StoredObjectVersionWithPointInTime, b: StoredObjectVersionWithPointInTime) => b.version - a.version);
|
||||
}
|
||||
|
||||
export const restore_version = async (version: StoredObjectVersionPersisted): Promise<StoredObjectVersionWithPointInTime> => {
|
||||
return await makeFetch<null, StoredObjectVersionWithPointInTime>("POST", `/api/1.0/doc-store/stored-object/restore-from-version/${version.id}`);
|
||||
}
|
@@ -161,7 +161,14 @@ async function download_and_decrypt_doc(storedObject: StoredObject, atVersion: n
|
||||
throw new Error("no version associated to stored object");
|
||||
}
|
||||
|
||||
const downloadInfo= await download_info_link(storedObject, atVersionToDownload);
|
||||
// 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 rawResponse = await window.fetch(downloadInfo.url);
|
||||
|
||||
|
@@ -0,0 +1 @@
|
||||
<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>
|
@@ -73,8 +73,15 @@
|
||||
<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('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a>
|
||||
<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>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document) %}
|
||||
@@ -82,9 +89,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_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>
|
||||
{% 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>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
|
@@ -0,0 +1,43 @@
|
||||
{% 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 %}
|
@@ -11,6 +11,7 @@ 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;
|
||||
@@ -30,10 +31,17 @@ 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 = [])
|
||||
@@ -55,6 +63,24 @@ 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);
|
||||
|
||||
|
@@ -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\Serializer\Normalizer;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTime;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
class StoredObjectPointInTimeNormalizer implements NormalizerInterface, NormalizerAwareInterface
|
||||
{
|
||||
use NormalizerAwareTrait;
|
||||
|
||||
public function normalize($object, ?string $format = null, array $context = [])
|
||||
{
|
||||
/* @var StoredObjectPointInTime $object */
|
||||
return [
|
||||
'id' => $object->getId(),
|
||||
'reason' => $object->getReason()->value,
|
||||
'byUser' => $this->normalizer->normalize($object->getByUser(), $format, [AbstractNormalizer::GROUPS => 'read']),
|
||||
];
|
||||
}
|
||||
|
||||
public function supportsNormalization($data, ?string $format = null)
|
||||
{
|
||||
return $data instanceof StoredObjectPointInTime;
|
||||
}
|
||||
}
|
@@ -12,6 +12,8 @@ declare(strict_types=1);
|
||||
namespace Chill\DocStoreBundle\Serializer\Normalizer;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Chill\MainBundle\Serializer\Normalizer\UserNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
@@ -20,13 +22,17 @@ class StoredObjectVersionNormalizer implements NormalizerInterface, NormalizerAw
|
||||
{
|
||||
use NormalizerAwareTrait;
|
||||
|
||||
final public const WITH_POINT_IN_TIMES_CONTEXT = 'with-point-in-times';
|
||||
|
||||
final public const WITH_RESTORED_CONTEXT = 'with-restored';
|
||||
|
||||
public function normalize($object, ?string $format = null, array $context = [])
|
||||
{
|
||||
if (!$object instanceof StoredObjectVersion) {
|
||||
throw new \InvalidArgumentException('The object must be an instance of '.StoredObjectVersion::class);
|
||||
}
|
||||
|
||||
return [
|
||||
$data = [
|
||||
'id' => $object->getId(),
|
||||
'filename' => $object->getFilename(),
|
||||
'version' => $object->getVersion(),
|
||||
@@ -34,8 +40,18 @@ class StoredObjectVersionNormalizer implements NormalizerInterface, NormalizerAw
|
||||
'keyInfos' => $object->getKeyInfos(),
|
||||
'type' => $object->getType(),
|
||||
'createdAt' => $this->normalizer->normalize($object->getCreatedAt(), $format, $context),
|
||||
'createdBy' => $this->normalizer->normalize($object->getCreatedBy(), $format, $context),
|
||||
'createdBy' => $this->normalizer->normalize($object->getCreatedBy(), $format, [...$context, UserNormalizer::AT_DATE => $object->getCreatedAt()]),
|
||||
];
|
||||
|
||||
if (in_array(self::WITH_POINT_IN_TIMES_CONTEXT, $context[AbstractNormalizer::GROUPS] ?? [], true)) {
|
||||
$data['point-in-times'] = $this->normalizer->normalize($object->getPointInTimes(), $format, $context);
|
||||
}
|
||||
|
||||
if (in_array(self::WITH_RESTORED_CONTEXT, $context[AbstractNormalizer::GROUPS] ?? [], true)) {
|
||||
$data['from-restored'] = $this->normalizer->normalize($object->getCreatedFrom(), $format, [AbstractNormalizer::GROUPS => ['read']]);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function supportsNormalization($data, ?string $format = null, array $context = [])
|
||||
|
@@ -18,7 +18,7 @@ final readonly class PdfSignedMessage
|
||||
{
|
||||
public function __construct(
|
||||
public readonly int $signatureId,
|
||||
public readonly int $signatureZoneIndex,
|
||||
public readonly ?int $signatureZoneIndex,
|
||||
public readonly string $content,
|
||||
) {}
|
||||
}
|
||||
|
@@ -12,12 +12,11 @@ declare(strict_types=1);
|
||||
namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner;
|
||||
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepSignatureRepository;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\SignatureStepStateChanger;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
|
||||
final readonly class PdfSignedMessageHandler implements MessageHandlerInterface
|
||||
@@ -33,7 +32,7 @@ final readonly class PdfSignedMessageHandler implements MessageHandlerInterface
|
||||
private StoredObjectManagerInterface $storedObjectManager,
|
||||
private EntityWorkflowStepSignatureRepository $entityWorkflowStepSignatureRepository,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private ClockInterface $clock,
|
||||
private SignatureStepStateChanger $signatureStepStateChanger,
|
||||
) {}
|
||||
|
||||
public function __invoke(PdfSignedMessage $message): void
|
||||
@@ -54,8 +53,8 @@ final readonly class PdfSignedMessageHandler implements MessageHandlerInterface
|
||||
|
||||
$this->storedObjectManager->write($storedObject, $message->content);
|
||||
|
||||
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate($this->clock->now());
|
||||
$signature->setZoneSignatureIndex($message->signatureZoneIndex);
|
||||
$this->signatureStepStateChanger->markSignatureAsSigned($signature, $message->signatureZoneIndex);
|
||||
|
||||
$this->entityManager->flush();
|
||||
$this->entityManager->clear();
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ final readonly class RequestPdfSignMessage
|
||||
public function __construct(
|
||||
public int $signatureId,
|
||||
public PDFSignatureZone $PDFSignatureZone,
|
||||
public int $signatureZoneIndex,
|
||||
public ?int $signatureZoneIndex,
|
||||
public string $reason,
|
||||
public string $signerText,
|
||||
public string $content,
|
||||
|
@@ -17,7 +17,7 @@ final readonly class PDFSignatureZone
|
||||
{
|
||||
public function __construct(
|
||||
#[Groups(['read'])]
|
||||
public int $index,
|
||||
public ?int $index,
|
||||
#[Groups(['read'])]
|
||||
public float $x,
|
||||
#[Groups(['read'])]
|
||||
|
@@ -50,7 +50,7 @@ final readonly class RemoveOldVersionCronJob implements CronJobInterface
|
||||
$deleteBeforeDate = $this->clock->now()->sub(new \DateInterval(self::KEEP_INTERVAL));
|
||||
$maxDeleted = $lastExecutionData[self::LAST_DELETED_KEY] ?? 0;
|
||||
|
||||
foreach ($this->storedObjectVersionRepository->findIdsByVersionsOlderThanDateAndNotLastVersion($deleteBeforeDate) as $id) {
|
||||
foreach ($this->storedObjectVersionRepository->findIdsByVersionsOlderThanDateAndNotLastVersionAndNotPointInTime($deleteBeforeDate) as $id) {
|
||||
$this->messageBus->dispatch(new RemoveOldVersionMessage($id));
|
||||
$maxDeleted = max($maxDeleted, $id);
|
||||
}
|
||||
|
@@ -18,6 +18,7 @@ use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
|
||||
/**
|
||||
@@ -49,13 +50,18 @@ final readonly class RemoveOldVersionMessageHandler implements MessageHandlerInt
|
||||
$this->logger->info(self::LOG_PREFIX.'Received one message', ['storedObjectVersionId' => $message->storedObjectVersionId]);
|
||||
|
||||
$storedObjectVersion = $this->storedObjectVersionRepository->find($message->storedObjectVersionId);
|
||||
$storedObject = $storedObjectVersion->getStoredObject();
|
||||
|
||||
if (null === $storedObjectVersion) {
|
||||
$this->logger->error(self::LOG_PREFIX.'StoredObjectVersion not found in database', ['storedObjectVersionId' => $message->storedObjectVersionId]);
|
||||
throw new \RuntimeException('StoredObjectVersion not found with id '.$message->storedObjectVersionId);
|
||||
}
|
||||
|
||||
if ($storedObjectVersion->hasPointInTimes()) {
|
||||
throw new UnrecoverableMessageHandlingException('the stored object version is now associated with a point in time');
|
||||
}
|
||||
|
||||
$storedObject = $storedObjectVersion->getStoredObject();
|
||||
|
||||
$this->storedObjectManager->delete($storedObjectVersion);
|
||||
// to ensure an immediate deletion
|
||||
$this->entityManager->remove($storedObjectVersion);
|
||||
|
@@ -0,0 +1,56 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\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) {}
|
||||
|
||||
public function restore(StoredObjectVersion $storedObjectVersion): StoredObjectVersion
|
||||
{
|
||||
$oldContent = $this->storedObjectManager->read($storedObjectVersion);
|
||||
|
||||
$newVersion = $this->storedObjectManager->write($storedObjectVersion->getStoredObject(), $oldContent, $storedObjectVersion->getType());
|
||||
|
||||
$newVersion->setCreatedFrom($storedObjectVersion);
|
||||
|
||||
$this->logger->info('[StoredObjectRestore] Restore stored object version', [
|
||||
'stored_object_uuid' => $storedObjectVersion->getStoredObject()->getUuid(),
|
||||
'old_version_id' => $storedObjectVersion->getId(),
|
||||
'old_version_version' => $storedObjectVersion->getVersion(),
|
||||
'new_version_id' => $newVersion->getVersion(),
|
||||
]);
|
||||
|
||||
return $newVersion;
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Service;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
|
||||
/**
|
||||
* Restore an old version of the stored object as the current one.
|
||||
*/
|
||||
interface StoredObjectRestoreInterface
|
||||
{
|
||||
public function restore(StoredObjectVersion $storedObjectVersion): StoredObjectVersion;
|
||||
}
|
@@ -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\Service;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTime;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||
use Chill\WopiBundle\Service\WopiConverter;
|
||||
use Symfony\Component\Mime\MimeTypesInterface;
|
||||
|
||||
/**
|
||||
* Class StoredObjectToPdfConverter.
|
||||
*
|
||||
* Converts stored objects to PDF or other specified formats using WopiConverter.
|
||||
*/
|
||||
class StoredObjectToPdfConverter
|
||||
{
|
||||
public function __construct(
|
||||
private readonly StoredObjectManagerInterface $storedObjectManager,
|
||||
private readonly WopiConverter $wopiConverter,
|
||||
private readonly MimeTypesInterface $mimeTypes,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Converts the given stored object to a specified format and stores the new version.
|
||||
*
|
||||
* @param StoredObject $storedObject the stored object to be converted
|
||||
* @param string $lang the language for the conversion context
|
||||
* @param string $convertTo The target format for the conversion. Default is 'pdf'.
|
||||
*
|
||||
* @return array{0: StoredObjectPointInTime, 1: StoredObjectVersion} contains the point in time before conversion and the new version of the stored object
|
||||
*
|
||||
* @throws \UnexpectedValueException if the preferred mime type for the conversion is not found
|
||||
* @throws \RuntimeException if the conversion or storage of the new version fails
|
||||
* @throws StoredObjectManagerException
|
||||
*/
|
||||
public function addConvertedVersion(StoredObject $storedObject, string $lang, $convertTo = 'pdf'): array
|
||||
{
|
||||
$newMimeType = $this->mimeTypes->getMimeTypes($convertTo)[0] ?? null;
|
||||
|
||||
if (null === $newMimeType) {
|
||||
throw new \UnexpectedValueException(sprintf('could not find a preferred mime type for conversion to %s', $convertTo));
|
||||
}
|
||||
|
||||
$currentVersion = $storedObject->getCurrentVersion();
|
||||
|
||||
if ($currentVersion->getType() === $newMimeType) {
|
||||
throw new \UnexpectedValueException('Already at the same mime type');
|
||||
}
|
||||
|
||||
$content = $this->storedObjectManager->read($currentVersion);
|
||||
|
||||
try {
|
||||
$converted = $this->wopiConverter->convert($lang, $content, $newMimeType, $convertTo);
|
||||
} catch (\RuntimeException $e) {
|
||||
throw new \RuntimeException('could not store a new version for document', previous: $e);
|
||||
}
|
||||
|
||||
$pointInTime = new StoredObjectPointInTime($currentVersion, StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION);
|
||||
$version = $this->storedObjectManager->write($storedObject, $converted, $newMimeType);
|
||||
|
||||
return [$pointInTime, $version];
|
||||
}
|
||||
}
|
@@ -11,25 +11,49 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocStoreBundle\Service;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
|
||||
class WorkflowStoredObjectPermissionHelper
|
||||
{
|
||||
public function __construct(private readonly Security $security, private readonly EntityWorkflowManager $entityWorkflowManager) {}
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||
private readonly Registry $registry,
|
||||
) {}
|
||||
|
||||
public function notBlockedByWorkflow(object $entity): bool
|
||||
{
|
||||
$workflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
|
||||
$entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
|
||||
$currentUser = $this->security->getUser();
|
||||
|
||||
foreach ($workflows as $workflow) {
|
||||
if ($workflow->isFinal()) {
|
||||
return false;
|
||||
foreach ($entityWorkflows as $entityWorkflow) {
|
||||
if ($entityWorkflow->isFinal()) {
|
||||
|
||||
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||
$marking = $workflow->getMarkingStore()->getMarking($entityWorkflow);
|
||||
foreach ($marking->getPlaces() as $place => $active) {
|
||||
$metadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
|
||||
if ($metadata['isFinalPositive'] ?? true) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!$entityWorkflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$workflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) {
|
||||
return false;
|
||||
// as soon as there is one signatured applyied, we are not able to
|
||||
// edit the document any more
|
||||
foreach ($entityWorkflow->getSteps() as $step) {
|
||||
foreach ($step->getSignatures() as $signature) {
|
||||
if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -28,6 +28,10 @@ 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'],
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -15,6 +15,7 @@ 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;
|
||||
@@ -177,6 +178,17 @@ 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, [
|
||||
|
@@ -0,0 +1,74 @@
|
||||
<?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 App\Tests\Chill\DocStoreBundle\Tests\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectRestoreInterface;
|
||||
use Chill\DocStoreBundle\Controller\StoredObjectRestoreVersionApiController;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class StoredObjectRestoreVersionApiControllerTest extends TestCase
|
||||
{
|
||||
public function testRestoreStoredObjectVersion(): void
|
||||
{
|
||||
$security = $this->createMock(Security::class);
|
||||
$storedObjectRestore = $this->createMock(StoredObjectRestoreInterface::class);
|
||||
$entityManager = $this->createMock(EntityManagerInterface::class);
|
||||
$serializer = $this->createMock(SerializerInterface::class);
|
||||
$storedObjectVersion = $this->createMock(StoredObjectVersion::class);
|
||||
$controller = new StoredObjectRestoreVersionApiController($security, $storedObjectRestore, $entityManager, $serializer);
|
||||
|
||||
$security->expects($this->once())
|
||||
->method('isGranted')
|
||||
->willReturn(true);
|
||||
$storedObjectRestore->expects($this->once())
|
||||
->method('restore')
|
||||
->willReturn($storedObjectVersion);
|
||||
$entityManager->expects($this->once())
|
||||
->method('persist');
|
||||
$entityManager->expects($this->once())
|
||||
->method('flush');
|
||||
|
||||
$serializer->expects($this->once())
|
||||
->method('serialize')
|
||||
->willReturn('test');
|
||||
|
||||
$response = $controller->restoreStoredObjectVersion($storedObjectVersion);
|
||||
|
||||
self::assertEquals(200, $response->getStatusCode());
|
||||
self::assertEquals('test', $response->getContent());
|
||||
}
|
||||
|
||||
public function testRestoreStoredObjectVersionAccessDenied(): void
|
||||
{
|
||||
$security = $this->createMock(Security::class);
|
||||
$storedObjectRestore = $this->createMock(StoredObjectRestoreInterface::class);
|
||||
$entityManager = $this->createMock(EntityManagerInterface::class);
|
||||
$serializer = $this->createMock(SerializerInterface::class);
|
||||
$storedObjectVersion = $this->createMock(StoredObjectVersion::class);
|
||||
$controller = new StoredObjectRestoreVersionApiController($security, $storedObjectRestore, $entityManager, $serializer);
|
||||
|
||||
self::expectException(\Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException::class);
|
||||
$security->expects($this->once())
|
||||
->method('isGranted')
|
||||
->willReturn(false);
|
||||
$controller->restoreStoredObjectVersion($storedObjectVersion);
|
||||
}
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Controller\StoredObjectVersionApiController;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectVersionNormalizer;
|
||||
use Chill\MainBundle\Pagination\Paginator;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
|
||||
use Chill\MainBundle\Serializer\Normalizer\CollectionNormalizer;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Encoder\JsonEncoder;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class StoredObjectVersionApiControllerTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testListVersion(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
for ($i = 0; $i < 15; ++$i) {
|
||||
$storedObject->registerVersion();
|
||||
}
|
||||
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)
|
||||
->willReturn(true)
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$controller = $this->buildController($security->reveal());
|
||||
|
||||
$response = $controller->listVersions($storedObject);
|
||||
$body = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
self::assertEquals($response->getStatusCode(), 200);
|
||||
self::assertIsArray($body);
|
||||
self::assertArrayHasKey('results', $body);
|
||||
self::assertCount(10, $body['results']);
|
||||
}
|
||||
|
||||
private function buildController(Security $security): StoredObjectVersionApiController
|
||||
{
|
||||
$paginator = $this->prophesize(Paginator::class);
|
||||
$paginator->getCurrentPageFirstItemNumber()->willReturn(0);
|
||||
$paginator->getItemsPerPage()->willReturn(10);
|
||||
$paginator->getTotalItems()->willReturn(15);
|
||||
$paginator->hasNextPage()->willReturn(false);
|
||||
$paginator->hasPreviousPage()->willReturn(false);
|
||||
|
||||
$paginatorFactory = $this->prophesize(PaginatorFactoryInterface::class);
|
||||
$paginatorFactory->create(Argument::type('int'))->willReturn($paginator);
|
||||
|
||||
$serializer = new Serializer([
|
||||
new StoredObjectVersionNormalizer(), new CollectionNormalizer(),
|
||||
], [new JsonEncoder()]);
|
||||
|
||||
return new StoredObjectVersionApiController($paginatorFactory->reveal(), $serializer, $security);
|
||||
}
|
||||
}
|
@@ -12,6 +12,8 @@ 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;
|
||||
|
||||
/**
|
||||
@@ -54,4 +56,27 @@ 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());
|
||||
}
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ 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;
|
||||
@@ -132,7 +133,8 @@ class StoredObjectTypeTest extends TypeTestCase
|
||||
new StoredObjectNormalizer(
|
||||
$jwtTokenProvider->reveal(),
|
||||
$urlGenerator->reveal(),
|
||||
$security->reveal()
|
||||
$security->reveal(),
|
||||
$this->createMock(TempUrlGeneratorInterface::class)
|
||||
),
|
||||
new StoredObjectDenormalizer($storedObjectRepository->reveal()),
|
||||
new StoredObjectVersionNormalizer(),
|
||||
|
@@ -11,6 +11,8 @@ 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;
|
||||
@@ -70,7 +72,9 @@ class StoredObjectNormalizerTest extends TestCase
|
||||
return ['sub' => 'sub'];
|
||||
});
|
||||
|
||||
$normalizer = new StoredObjectNormalizer($jwtProvider, $urlGenerator, $security);
|
||||
$tempUrlGenerator = $this->createMock(TempUrlGeneratorInterface::class);
|
||||
|
||||
$normalizer = new StoredObjectNormalizer($jwtProvider, $urlGenerator, $security, $tempUrlGenerator);
|
||||
$normalizer->setNormalizer($globalNormalizer);
|
||||
|
||||
$actual = $normalizer->normalize($storedObject, 'json');
|
||||
@@ -95,4 +99,48 @@ 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']);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Serializer\Normalizer;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTime;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum;
|
||||
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectPointInTimeNormalizer;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Serializer\Normalizer\UserNormalizer;
|
||||
use Chill\MainBundle\Templating\Entity\UserRender;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class StoredObjectPointInTimeNormalizerTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testNormalize(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
$version = $storedObject->registerVersion();
|
||||
$storedObjectPointInTime = new StoredObjectPointInTime($version, StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION, new User());
|
||||
|
||||
$normalizer = new StoredObjectPointInTimeNormalizer();
|
||||
$normalizer->setNormalizer($this->buildNormalizer());
|
||||
|
||||
$actual = $normalizer->normalize($storedObjectPointInTime, 'json', ['read']);
|
||||
|
||||
self::assertIsArray($actual);
|
||||
self::assertArrayHasKey('id', $actual);
|
||||
self::assertArrayHasKey('byUser', $actual);
|
||||
self::assertArrayHasKey('reason', $actual);
|
||||
self::assertEquals(StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION->value, $actual['reason']);
|
||||
}
|
||||
|
||||
public function buildNormalizer(): NormalizerInterface
|
||||
{
|
||||
$userRender = $this->prophesize(UserRender::class);
|
||||
$userRender->renderString(Argument::type(User::class), Argument::type('array'))->willReturn('username');
|
||||
|
||||
return new Serializer(
|
||||
[new UserNormalizer($userRender->reveal(), new MockClock())]
|
||||
);
|
||||
}
|
||||
}
|
@@ -35,7 +35,7 @@ class StoredObjectVersionRepositoryTest extends KernelTestCase
|
||||
$repository = new StoredObjectVersionRepository($this->entityManager);
|
||||
|
||||
// get old version, to get a chance to get one
|
||||
$actual = $repository->findIdsByVersionsOlderThanDateAndNotLastVersion(new \DateTimeImmutable('1970-01-01'));
|
||||
$actual = $repository->findIdsByVersionsOlderThanDateAndNotLastVersionAndNotPointInTime(new \DateTimeImmutable('1970-01-01'));
|
||||
|
||||
self::assertIsIterable($actual);
|
||||
self::assertContainsOnly('int', $actual);
|
||||
|
@@ -20,12 +20,12 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepSignatureRepository;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\SignatureStepStateChanger;
|
||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -45,6 +45,9 @@ class PdfSignedMessageHandlerTest extends TestCase
|
||||
$entityWorkflow->setStep('new_step', $dto, 'new_transition', new \DateTimeImmutable(), new User());
|
||||
$step = $entityWorkflow->getCurrentStep();
|
||||
$signature = $step->getSignatures()->first();
|
||||
$stateChanger = $this->createMock(SignatureStepStateChanger::class);
|
||||
$stateChanger->expects(self::once())->method('markSignatureAsSigned')
|
||||
->with($signature, 99);
|
||||
|
||||
$handler = new PdfSignedMessageHandler(
|
||||
new NullLogger(),
|
||||
@@ -52,15 +55,12 @@ class PdfSignedMessageHandlerTest extends TestCase
|
||||
$this->buildStoredObjectManager($storedObject, $expectedContent = '1234'),
|
||||
$this->buildSignatureRepository($signature),
|
||||
$this->buildEntityManager(true),
|
||||
new MockClock('now'),
|
||||
$stateChanger,
|
||||
);
|
||||
|
||||
// we simply call the handler. The mocked StoredObjectManager will check that the "write" method is invoked once
|
||||
// with the content "1234"
|
||||
$handler(new PdfSignedMessage(10, 99, $expectedContent));
|
||||
|
||||
self::assertEquals('signed', $signature->getState()->value);
|
||||
self::assertEquals(99, $signature->getZoneSignatureIndex());
|
||||
}
|
||||
|
||||
private function buildSignatureRepository(EntityWorkflowStepSignature $signature): EntityWorkflowStepSignatureRepository
|
||||
|
@@ -46,7 +46,7 @@ class RemoveOldVersionCronJobTest extends KernelTestCase
|
||||
$clock = new MockClock(new \DateTimeImmutable('2024-01-01 00:00:00', new \DateTimeZone('+00:00')));
|
||||
$repository = $this->createMock(StoredObjectVersionRepository::class);
|
||||
$repository->expects($this->once())
|
||||
->method('findIdsByVersionsOlderThanDateAndNotLastVersion')
|
||||
->method('findIdsByVersionsOlderThanDateAndNotLastVersionAndNotPointInTime')
|
||||
->with(new \DateTime('2023-10-03 00:00:00', new \DateTimeZone('+00:00')))
|
||||
->willReturnCallback(function ($arg) {
|
||||
yield 1;
|
||||
|
@@ -0,0 +1,130 @@
|
||||
<?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());
|
||||
}
|
||||
}
|
@@ -0,0 +1,53 @@
|
||||
<?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\Service\StoredObjectManagerInterface;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectRestore;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class StoredObjectRestoreTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testRestore(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
$version = $storedObject->registerVersion(type: 'application/test');
|
||||
|
||||
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
|
||||
$storedObjectManager->read($version)->willReturn('1234')->shouldBeCalledOnce();
|
||||
$storedObjectManager->write($storedObject, '1234', 'application/test')->shouldBeCalledOnce()
|
||||
->will(function ($args) {
|
||||
/** @var StoredObject $object */
|
||||
$object = $args[0];
|
||||
|
||||
return $object->registerVersion();
|
||||
})
|
||||
;
|
||||
|
||||
$restore = new StoredObjectRestore($storedObjectManager->reveal(), new NullLogger());
|
||||
|
||||
$newVersion = $restore->restore($version);
|
||||
|
||||
self::assertNotSame($version, $newVersion);
|
||||
self::assertSame($version, $newVersion->getCreatedFrom());
|
||||
}
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
<?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\StoredObjectVersion;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectToPdfConverter;
|
||||
use Chill\WopiBundle\Service\WopiConverter;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\Mime\MimeTypes;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class StoredObjectToPdfConverterTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testAddConvertedVersion(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
$currentVersion = $storedObject->registerVersion(type: 'text/html');
|
||||
|
||||
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
|
||||
$storedObjectManager->read($currentVersion)->willReturn('1234');
|
||||
$storedObjectManager->write($storedObject, '5678', 'application/pdf')->shouldBeCalled()
|
||||
->will(function ($args) {
|
||||
/** @var StoredObject $storedObject */
|
||||
$storedObject = $args[0];
|
||||
|
||||
return $storedObject->registerVersion(type: $args[2]);
|
||||
});
|
||||
|
||||
$converter = $this->prophesize(WopiConverter::class);
|
||||
$converter->convert('fr', '1234', 'application/pdf', 'pdf')->shouldBeCalled()
|
||||
->willReturn('5678');
|
||||
|
||||
$converter = new StoredObjectToPdfConverter($storedObjectManager->reveal(), $converter->reveal(), MimeTypes::getDefault());
|
||||
|
||||
$actual = $converter->addConvertedVersion($storedObject, 'fr');
|
||||
|
||||
self::assertIsArray($actual);
|
||||
self::assertInstanceOf(StoredObjectPointInTime::class, $actual[0]);
|
||||
self::assertSame($currentVersion, $actual[0]->getObjectVersion());
|
||||
self::assertInstanceOf(StoredObjectVersion::class, $actual[1]);
|
||||
}
|
||||
}
|
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Service;
|
||||
|
||||
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
|
||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Workflow\DefinitionBuilder;
|
||||
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
|
||||
use Symfony\Component\Workflow\Workflow;
|
||||
use Symfony\Component\Workflow\WorkflowInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class WorkflowStoredObjectPermissionHelperTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
/**
|
||||
* @dataProvider provideDataNotBlockByWorkflow
|
||||
*/
|
||||
public function testNotBlockByWorkflow(EntityWorkflow $entityWorkflow, User $user, bool $expected, string $message): void
|
||||
{
|
||||
// all entities must have this workflow name, so we are ok to set it here
|
||||
$entityWorkflow->setWorkflowName('dummy');
|
||||
$object = new \stdClass();
|
||||
$helper = $this->buildHelper($object, $entityWorkflow, $user);
|
||||
|
||||
self::assertEquals($expected, $helper->notBlockedByWorkflow($entityWorkflow), $message);
|
||||
}
|
||||
|
||||
private function buildHelper(object $relatedEntity, EntityWorkflow $entityWorkflow, User $user): WorkflowStoredObjectPermissionHelper
|
||||
{
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->getUser()->willReturn($user);
|
||||
|
||||
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
||||
$entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn([$entityWorkflow]);
|
||||
|
||||
return new WorkflowStoredObjectPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $this->buildRegistry());
|
||||
}
|
||||
|
||||
public static function provideDataNotBlockByWorkflow(): iterable
|
||||
{
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable());
|
||||
|
||||
yield [$entityWorkflow, new User(), false, 'blocked because the user is not present as a dest user'];
|
||||
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$dto->futureDestUsers[] = $user = new User();
|
||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user);
|
||||
|
||||
yield [$entityWorkflow, $user, true, 'allowed because the user is present as a dest user'];
|
||||
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$dto->futureDestUsers[] = $user = new User();
|
||||
$entityWorkflow->setStep('final_positive', $dto, 'to_final_positive', new \DateTimeImmutable(), $user);
|
||||
$entityWorkflow->getCurrentStep()->setIsFinal(true);
|
||||
|
||||
yield [$entityWorkflow, $user, false, 'blocked because the step is final, and final positive'];
|
||||
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$dto->futureDestUsers[] = $user = new User();
|
||||
$entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable(), $user);
|
||||
$entityWorkflow->getCurrentStep()->setIsFinal(true);
|
||||
|
||||
yield [$entityWorkflow, $user, true, 'allowed because the step is final, and final negative'];
|
||||
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$dto->futureDestUsers[] = $user = new User();
|
||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user);
|
||||
$step = $entityWorkflow->getCurrentStep();
|
||||
new EntityWorkflowStepSignature($step, new Person());
|
||||
|
||||
yield [$entityWorkflow, $user, true, 'allow, a signature is present but still pending'];
|
||||
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$dto->futureDestUsers[] = $user = new User();
|
||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user);
|
||||
$step = $entityWorkflow->getCurrentStep();
|
||||
$signature = new EntityWorkflowStepSignature($step, new Person());
|
||||
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED);
|
||||
|
||||
yield [$entityWorkflow, $user, false, 'blocked, a signature is present and signed'];
|
||||
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$dto->futureDestUsers[] = $user = new User();
|
||||
$entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable(), $user);
|
||||
$step = $entityWorkflow->getCurrentStep();
|
||||
$signature = new EntityWorkflowStepSignature($step, new Person());
|
||||
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED);
|
||||
|
||||
yield [$entityWorkflow, $user, false, 'blocked, a signature is present and signed, although the workflow is final negative'];
|
||||
|
||||
}
|
||||
|
||||
private static function buildRegistry(): Registry
|
||||
{
|
||||
$builder = new DefinitionBuilder();
|
||||
$builder
|
||||
->setInitialPlaces(['initial'])
|
||||
->addPlaces(['initial', 'test', 'final_positive', 'final_negative'])
|
||||
->setMetadataStore(
|
||||
new InMemoryMetadataStore(
|
||||
placesMetadata: [
|
||||
'final_positive' => [
|
||||
'isFinal' => true,
|
||||
'isFinalPositive' => true,
|
||||
],
|
||||
'final_negative' => [
|
||||
'isFinal' => true,
|
||||
'isFinalPositive' => false,
|
||||
],
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
$workflow = new Workflow($builder->build(), new EntityWorkflowMarkingStore(), name: 'dummy');
|
||||
$registry = new Registry();
|
||||
$registry->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface {
|
||||
public function supports(WorkflowInterface $workflow, object $subject): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return $registry;
|
||||
}
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
<?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
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,94 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
@@ -16,20 +16,28 @@ 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>
|
||||
*/
|
||||
readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowWithStoredObjectHandlerInterface
|
||||
final readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowWithStoredObjectHandlerInterface, EntityWorkflowWithPublicViewInterface
|
||||
{
|
||||
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
|
||||
@@ -87,12 +95,28 @@ readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkfl
|
||||
|
||||
public function getSuggestedUsers(EntityWorkflow $entityWorkflow): array
|
||||
{
|
||||
$suggestedUsers = $entityWorkflow->getUsersInvolved();
|
||||
$related = $this->getRelatedEntity($entityWorkflow);
|
||||
|
||||
$referrer = $this->getRelatedEntity($entityWorkflow)->getCourse()->getUser();
|
||||
$suggestedUsers[spl_object_hash($referrer)] = $referrer;
|
||||
if (null === $related) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $suggestedUsers;
|
||||
$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
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function getTemplate(EntityWorkflow $entityWorkflow, array $options = []): string
|
||||
@@ -136,4 +160,31 @@ readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkfl
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
@@ -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');
|
||||
}
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
<?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,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
@@ -105,3 +105,50 @@ paths:
|
||||
404:
|
||||
description: "Not found"
|
||||
|
||||
/1.0/doc-store/stored-object/{uuid}/versions:
|
||||
get:
|
||||
tags:
|
||||
- storedobject
|
||||
summary: Get a signed route to post stored object
|
||||
parameters:
|
||||
- in: path
|
||||
name: uuid
|
||||
required: true
|
||||
allowEmptyValue: false
|
||||
description: The UUID of the storedObjeect
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
200:
|
||||
description: "OK"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
403:
|
||||
description: "Unauthorized"
|
||||
404:
|
||||
description: "Not found"
|
||||
|
||||
/1.0/doc-store/stored-object/restore-from-version/{id}:
|
||||
post:
|
||||
tags:
|
||||
- storedobject
|
||||
summary: Restore an old version of a stored object
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
allowEmptyValue: false
|
||||
description: The id of the stored object version
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
200:
|
||||
description: "OK"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
|
||||
|
@@ -5,5 +5,6 @@ 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('vue_document_signature', __dirname + '/Resources/public/vuejs/DocumentSignature/index.ts');
|
||||
encore.addEntry('mod_document_download_button', __dirname + '/Resources/public/module/button_download/index');
|
||||
encore.addEntry('vue_document_signature', __dirname + '/Resources/public/vuejs/DocumentSignature/index');
|
||||
};
|
||||
|
@@ -0,0 +1,49 @@
|
||||
<?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 Version20240910093735 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add point in time for stored object version';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE SEQUENCE chill_doc.stored_object_point_in_time_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||
$this->addSql('CREATE TABLE chill_doc.stored_object_point_in_time (id INT NOT NULL, stored_object_version_id INT NOT NULL, reason TEXT NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, byUser_id INT DEFAULT NULL, createdBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE INDEX IDX_CC83C7B81D0AB8B9 ON chill_doc.stored_object_point_in_time (stored_object_version_id)');
|
||||
$this->addSql('CREATE INDEX IDX_CC83C7B8D23C0240 ON chill_doc.stored_object_point_in_time (byUser_id)');
|
||||
$this->addSql('CREATE INDEX IDX_CC83C7B83174800F ON chill_doc.stored_object_point_in_time (createdBy_id)');
|
||||
$this->addSql('COMMENT ON COLUMN chill_doc.stored_object_point_in_time.createdAt IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object_point_in_time ADD CONSTRAINT FK_CC83C7B81D0AB8B9 FOREIGN KEY (stored_object_version_id) REFERENCES chill_doc.stored_object_version (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object_point_in_time ADD CONSTRAINT FK_CC83C7B8D23C0240 FOREIGN KEY (byUser_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object_point_in_time ADD CONSTRAINT FK_CC83C7B83174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object ALTER prefix SET DEFAULT \'\'');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object_version ALTER filename SET DEFAULT \'\'');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP SEQUENCE chill_doc.stored_object_point_in_time_id_seq CASCADE');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object_point_in_time DROP CONSTRAINT FK_CC83C7B81D0AB8B9');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object_point_in_time DROP CONSTRAINT FK_CC83C7B8D23C0240');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object_point_in_time DROP CONSTRAINT FK_CC83C7B83174800F');
|
||||
$this->addSql('DROP TABLE chill_doc.stored_object_point_in_time');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object ALTER prefix DROP DEFAULT');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object_version ALTER filename DROP DEFAULT');
|
||||
}
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
<?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 Version20240918073234 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add a relation between stored object version when a version is restored';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object_version ADD createdFrom_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object_version ADD CONSTRAINT FK_C1D553024DEC38BB FOREIGN KEY (createdFrom_id) REFERENCES chill_doc.stored_object_version (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('CREATE INDEX IDX_C1D553024DEC38BB ON chill_doc.stored_object_version (createdFrom_id)');
|
||||
$this->addSql('ALTER INDEX chill_doc.idx_c1d55302232d562b RENAME TO IDX_C1D553024B136083');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object_version DROP createdFrom_id');
|
||||
$this->addSql('ALTER INDEX chill_doc.idx_c1d553024b136083 RENAME TO idx_c1d55302232d562b');
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
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}
|
||||
|
@@ -80,6 +80,10 @@ 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
|
||||
|
@@ -94,4 +94,38 @@ class NotificationApiController
|
||||
|
||||
return new JsonResponse(null, JsonResponse::HTTP_ACCEPTED, [], false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/mark/allread", name="chill_api_main_notification_mark_allread", methods={"POST"})
|
||||
*/
|
||||
public function markAllRead(): JsonResponse
|
||||
{
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
throw new \RuntimeException('Invalid user');
|
||||
}
|
||||
|
||||
$modifiedNotificationIds = $this->notificationRepository->markAllNotificationAsReadForUser($user);
|
||||
|
||||
return new JsonResponse($modifiedNotificationIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/mark/undoallread", name="chill_api_main_notification_mark_undoallread", methods={"POST"})
|
||||
*/
|
||||
public function undoAllRead(Request $request): JsonResponse
|
||||
{
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
throw new \RuntimeException('Invalid user');
|
||||
}
|
||||
|
||||
$ids = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
$touchedIds = $this->notificationRepository->markAllNotificationAsUnreadForUser($user, $ids);
|
||||
|
||||
return new JsonResponse($touchedIds);
|
||||
}
|
||||
}
|
||||
|
@@ -169,7 +169,7 @@ class NotificationController extends AbstractController
|
||||
#[Route(path: '/inbox', name: 'chill_main_notification_my')]
|
||||
public function inboxAction(): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
|
||||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||
$currentUser = $this->security->getUser();
|
||||
|
||||
$notificationsNbr = $this->notificationRepository->countAllForAttendee($currentUser);
|
||||
@@ -177,8 +177,8 @@ class NotificationController extends AbstractController
|
||||
|
||||
$notifications = $this->notificationRepository->findAllForAttendee(
|
||||
$currentUser,
|
||||
$limit = $paginator->getItemsPerPage(),
|
||||
$offset = $paginator->getCurrentPage()->getFirstItemNumber()
|
||||
$paginator->getItemsPerPage(),
|
||||
$paginator->getCurrentPage()->getFirstItemNumber()
|
||||
);
|
||||
|
||||
return $this->render('@ChillMain/Notification/list.html.twig', [
|
||||
|
@@ -278,7 +278,7 @@ final class PasswordController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Symfony\Component\Form\Form
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
private function passwordForm(User $user)
|
||||
{
|
||||
|
@@ -214,7 +214,7 @@ class UserController extends CRUDController
|
||||
|
||||
return $this->redirect(
|
||||
$request->query->has('returnPath') ? $request->query->get('returnPath') :
|
||||
$this->generateUrl('chill_main_homepage')
|
||||
$this->generateUrl('chill_main_homepage')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ class UserController extends CRUDController
|
||||
|
||||
return $this->redirect(
|
||||
$request->query->has('returnPath') ? $request->query->get('returnPath') :
|
||||
$this->generateUrl('chill_crud_admin_user_edit', ['id' => $user->getId()])
|
||||
$this->generateUrl('chill_crud_admin_user_edit', ['id' => $user->getId()])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -264,6 +264,7 @@ class UserController extends CRUDController
|
||||
return $this->getFilterOrderHelperFactory()
|
||||
->create(self::class)
|
||||
->addSearchBox(['label'])
|
||||
->addCheckbox('activeFilter', [true => 'Active', false => 'Inactive'], ['Active'])
|
||||
->build();
|
||||
}
|
||||
|
||||
@@ -273,11 +274,7 @@ class UserController extends CRUDController
|
||||
return parent::countEntities($action, $request, $filterOrder);
|
||||
}
|
||||
|
||||
if (null === $filterOrder->getQueryString()) {
|
||||
return parent::countEntities($action, $request, $filterOrder);
|
||||
}
|
||||
|
||||
return $this->userRepository->countByUsernameOrEmail($filterOrder->getQueryString());
|
||||
return $this->userRepository->countFilteredUsers($filterOrder->getQueryString(), $filterOrder->getCheckboxData('activeFilter'));
|
||||
}
|
||||
|
||||
protected function createFormFor(string $action, $entity, ?string $formClass = null, array $formOptions = []): FormInterface
|
||||
@@ -334,16 +331,13 @@ class UserController extends CRUDController
|
||||
return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder);
|
||||
}
|
||||
|
||||
if (null === $filterOrder->getQueryString()) {
|
||||
return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder);
|
||||
}
|
||||
$queryString = $filterOrder->getQueryString();
|
||||
$activeFilter = $filterOrder->getCheckboxData('activeFilter');
|
||||
$nb = $this->userRepository->countFilteredUsers($queryString, $activeFilter);
|
||||
|
||||
return $this->userRepository->findByUsernameOrEmail(
|
||||
$filterOrder->getQueryString(),
|
||||
['usernameCanonical' => 'ASC'],
|
||||
$paginator->getItemsPerPage(),
|
||||
$paginator->getCurrentPageFirstItemNumber()
|
||||
);
|
||||
$paginator = $this->getPaginatorFactory()->create($nb);
|
||||
|
||||
return $this->userRepository->findFilteredUsers($queryString, $activeFilter, $paginator->getCurrentPageFirstItemNumber(), $paginator->getItemsPerPage());
|
||||
}
|
||||
|
||||
protected function onPrePersist(string $action, $entity, FormInterface $form, Request $request)
|
||||
@@ -374,10 +368,12 @@ class UserController extends CRUDController
|
||||
$returnPathParams = $request->query->has('returnPath') ? ['returnPath' => $request->query->get('returnPath')] : [];
|
||||
|
||||
return $this->createFormBuilder()
|
||||
->setAction($this->generateUrl(
|
||||
'admin_user_add_groupcenter',
|
||||
array_merge($returnPathParams, ['uid' => $user->getId()])
|
||||
))
|
||||
->setAction(
|
||||
$this->generateUrl(
|
||||
'admin_user_add_groupcenter',
|
||||
array_merge($returnPathParams, ['uid' => $user->getId()])
|
||||
)
|
||||
)
|
||||
->setMethod('POST')
|
||||
->add(self::FORM_GROUP_CENTER_COMPOSED, ComposedGroupCenterType::class)
|
||||
->add('submit', SubmitType::class, ['label' => 'Add a new groupCenter'])
|
||||
@@ -392,10 +388,12 @@ class UserController extends CRUDController
|
||||
$returnPathParams = $request->query->has('returnPath') ? ['returnPath' => $request->query->get('returnPath')] : [];
|
||||
|
||||
return $this->createFormBuilder()
|
||||
->setAction($this->generateUrl(
|
||||
'admin_user_delete_groupcenter',
|
||||
array_merge($returnPathParams, ['uid' => $user->getId(), 'gcid' => $groupCenter->getId()])
|
||||
))
|
||||
->setAction(
|
||||
$this->generateUrl(
|
||||
'admin_user_delete_groupcenter',
|
||||
array_merge($returnPathParams, ['uid' => $user->getId(), 'gcid' => $groupCenter->getId()])
|
||||
)
|
||||
)
|
||||
->setMethod('DELETE')
|
||||
->add('submit', SubmitType::class, ['label' => 'Delete'])
|
||||
->getForm();
|
||||
|
@@ -0,0 +1,28 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
<?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 {}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user