mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-19 21:24:59 +00:00
Compare commits
433 Commits
signature-
...
signature-
Author | SHA1 | Date | |
---|---|---|---|
|
bf61324c1d | ||
|
abf20b0cf2 | ||
|
86896a12e6 | ||
|
3a959b7044 | ||
|
f8d95384ea | ||
8a374864fa
|
|||
bb848746d5
|
|||
3738c110f8
|
|||
f57fdb2b4c
|
|||
b57824fc7e
|
|||
6b4e1ed2d3
|
|||
b0485dbcc8
|
|||
c16219dc6d
|
|||
ad47804c91 | |||
85e2466611 | |||
94d6b5eff8 | |||
d87f380f16 | |||
58bf722fae | |||
50fb79ebbf | |||
58912f1d98 | |||
9604ba5f4b | |||
b689a51a48 | |||
8c0d2f58ba | |||
212230448b | |||
2bfb8fe387 | |||
6362b98a00 | |||
6e2a08cae8 | |||
305105faae | |||
85811cc6ae | |||
7eee995627 | |||
c0c448fb39 | |||
6445342136 | |||
d52e54fd2a | |||
547a9d1369 | |||
288a02f5b7 | |||
2f9884072c | |||
ee45ff61a6 | |||
5dfd8daf3a | |||
564813ef3d
|
|||
5fed42a623
|
|||
a46e987f81 | |||
81220b5b22 | |||
5b0019cde7 | |||
b19dd4fc11 | |||
44226d6f7f
|
|||
d75607a1d2 | |||
bf66af0f25 | |||
15f3e474a0 | |||
5623cf946e | |||
0a6f3a99da | |||
b42473b01d | |||
be19d09bad | |||
50bd9f32c3 | |||
1396304af5 | |||
c33e4adeec | |||
7351a35c42 | |||
72e3325626 | |||
0a46b5304d | |||
|
e57d52d00e | ||
64e527672d | |||
123168a5ee
|
|||
3836d0dc9b
|
|||
51ab4bef38
|
|||
|
567ca8a26f | ||
|
111305d09c | ||
|
67395f52b5 | ||
|
421226c0dc | ||
|
77da2c1ac6 | ||
|
39d3ba2f40 | ||
|
fb62e54d63 | ||
|
c968d6c541 | ||
|
c428e6665f | ||
|
5b7e3f0336 | ||
|
0c8ef37860 | ||
|
794c479b9e | ||
|
1bee3114ac | ||
|
1344b65dd4 | ||
|
68dcf4dd28 | ||
|
b0a8fd54a8 | ||
|
0f589ec57e | ||
|
2d4fc45a0c | ||
|
c80f23f0db | ||
|
c950400fe2 | ||
|
21c1e77d36 | ||
|
bbfd0caf10 | ||
|
9192883217 | ||
3836622d27 | |||
cc2c4be1b0 | |||
873940786f | |||
c82991674e | |||
3fc3f32c5f | |||
20af766cdf | |||
681f637d13 | |||
fb8a6d960e | |||
a2310a662f | |||
dd7d126bec | |||
29f6a43288 | |||
74be6460d4 | |||
c8e87ced35 | |||
b8002d56ec | |||
a80b36bb31 | |||
116fe35ad2 | |||
5b95336bac | |||
f9d5ba7778 | |||
f76379551c | |||
15094d5a91 | |||
db73dcffc7 | |||
8aec69f0f9
|
|||
9f88eef249
|
|||
d689ce9aef
|
|||
d5e4991982
|
|||
1079c7e394 | |||
ca68b58246
|
|||
747a1de321
|
|||
9e92ede16f
|
|||
31f842471a
|
|||
7d0f9175be
|
|||
e83307ca6d
|
|||
bc2dfd159c | |||
b100792a34 | |||
00ceee1fd5 | |||
215eba41b7 | |||
52a3d1be1b | |||
8d543be5cc | |||
0474b25859 | |||
db94af0958
|
|||
3e8805bdda | |||
a887602f4f
|
|||
c1cf27c42d
|
|||
fe6b4848e6
|
|||
b5af9f7b63
|
|||
7f3de62b2c
|
|||
cfa51cd659 | |||
facc4affed | |||
f9122341d1 | |||
7dd5f542a6 | |||
3b80d9a93b | |||
790576863f | |||
25e89571f7 | |||
435836c7d1 | |||
af4db22184 | |||
724b98e8c5 | |||
2adc8b3bf6 | |||
21b79c1981 | |||
428494ca1f | |||
5d57ec8a3b | |||
719fabc878 | |||
e9a9a3430f | |||
ead1abb825
|
|||
54d045f261
|
|||
702a5a27d2 | |||
|
41dd4d89f7 | ||
|
3a7ed7ef8f | ||
c648a560cc | |||
3d7c8596ee | |||
345f379650 | |||
3262a1dd02 | |||
a9f4f8c973 | |||
c19c597ba0 | |||
03800029c9 | |||
064dfc5a56 | |||
ba95687f46 | |||
a309cc0774
|
|||
5b0babb9b0 | |||
ac2f314395 | |||
8c92d11722 | |||
3db4fff80d
|
|||
fb743b522d | |||
d1653a074b | |||
254122d125 | |||
c9d2e37cee | |||
2c99ea17d4 | |||
18df08e8c3 | |||
db3961275b
|
|||
cd488d7576 | |||
436661d952 | |||
c3b8d42047 | |||
c9d54a5fea
|
|||
|
9c28df25a1 | ||
86c862e69d
|
|||
d88b5a0098
|
|||
|
c5a24e8ac5 | ||
9bc6fe6aff
|
|||
18a03fd740
|
|||
e9d4b9e2ab | |||
efaad1981d | |||
742f2540f6 | |||
bab6528ed6 | |||
a25f2c7539 | |||
c06e76a0ee | |||
4607c36b57 | |||
7c03a25f1a | |||
cce04ee490 | |||
e54633d14d | |||
d9892f6822 | |||
f75c7a0232 | |||
062afd6695 | |||
830dace1ba | |||
2ce9810243 | |||
26b3d84d62 | |||
30078db841 | |||
aaac80be84 | |||
a0fead48e1 | |||
2d09efb2e0 | |||
3a87513a11 | |||
|
d9c50cffb7 | ||
|
25ccb16308 | ||
|
ba25c181f5 | ||
d3956319ca | |||
bd36735cb1 | |||
1310d53589 | |||
e38d47ec5e | |||
36f2275a56 | |||
9a34064b23 | |||
4c3bfc90b5 | |||
1812e84c92 | |||
dfa7de4f38 | |||
145419a76b | |||
b1ba5cc608 | |||
87a6757e5e | |||
bd41308bbd
|
|||
f8fa96d836
|
|||
d28cec3786
|
|||
7cd36cd483
|
|||
d3d98cdec2
|
|||
49dd7f94fa
|
|||
916724c0c5
|
|||
|
102d0dad94 | ||
|
8d225dd68c | ||
|
61d0005be8 | ||
73797b98f6 | |||
3d40db7493 | |||
760d65b972 | |||
d26fa6bde6 | |||
427f232ab8 | |||
482f279dc5 | |||
e0828b1f0f | |||
e015f71bb0 | |||
04a48f22ad | |||
ad4fe80240 | |||
4b82e67952 | |||
47f4cfddbb
|
|||
e95f9e9846 | |||
1f4bef754d
|
|||
19e34d5dc0
|
|||
fab00f679c
|
|||
791b3776c5
|
|||
6bd38f1a58
|
|||
68d21c9267
|
|||
e7ca89e0c1
|
|||
fc8bc33ba9
|
|||
cbd9489810
|
|||
e9a9262fae | |||
d9e37d0958 | |||
65c41e6fa9 | |||
7923b5a1ef | |||
90b615c5b2
|
|||
5ca222b501 | |||
3e4495dd6e
|
|||
bca0d04201
|
|||
f66ac50571 | |||
b454774836
|
|||
008f344e49
|
|||
90bfd87ec6
|
|||
cc0030c1cd
|
|||
d60ba3ecb2
|
|||
cd5001ac74 | |||
98f47ac512
|
|||
31b541d12f
|
|||
72045ce082
|
|||
0bfb3de465
|
|||
9ec4c77fb7
|
|||
77c53972c8
|
|||
350d991a85
|
|||
0ce9cdd07a
|
|||
1993fac1c4
|
|||
83883567a2
|
|||
29d57934a1
|
|||
f43d79c940
|
|||
be730679c8
|
|||
f62f1891d8
|
|||
ebb856fe85
|
|||
61877e0157
|
|||
4c3f082163
|
|||
635b1ee537 | |||
35109133f6
|
|||
a220dad83b
|
|||
9eb571549b
|
|||
db8257d230 | |||
bce93efe83 | |||
06401af801 | |||
ea1d4c48f2
|
|||
|
33cba27dd4 | ||
3fd6e52e9d | |||
f60a595ab6 | |||
436ef33dbc | |||
405aad7333 | |||
a5c2576124 | |||
33a6f9996e | |||
b96cbc5594 | |||
854d72fa42 | |||
cd6fd091dc | |||
cb5ade3d14 | |||
dddb6d66bc | |||
d34f9450b8 | |||
9ce1788a14 | |||
2895638f3b | |||
2708bafb1f | |||
7d309136b1 | |||
82d3ec4d6f | |||
cad2dea148 | |||
bff14aa700 | |||
66570cd430 | |||
53df2ec9ba | |||
068503a830 | |||
c07a728f1d | |||
b7e61c6747 | |||
97846a5877 | |||
4ed9d3d8e2 | |||
a7ec7c9f37 | |||
d82d534a4c | |||
684f28291a | |||
526882a5b6 | |||
422b6b99eb | |||
02b150b0a5 | |||
20c27c100c | |||
12a22bcc13 | |||
cba8a342d5 | |||
6f55ba15d6 | |||
454ab73303 | |||
800942bc92 | |||
c4e7683e48 | |||
28c986fddf | |||
adca4f0d6a | |||
7b25c8e390 | |||
0b5be0419b | |||
650e85c481 | |||
56d5d08ed3 | |||
d3390ca334 | |||
511c0af5fa | |||
4c354c47c9 | |||
d8b6cef7b4 | |||
e312929d86 | |||
20b38af812 | |||
2f07be0843 | |||
11c069a2ff | |||
3929602f59 | |||
38fcccfd83 | |||
deaab80270 | |||
a71573136a | |||
b1082f6a55 | |||
dcc285e976 | |||
ed3e0f889e | |||
63fe8070c4 | |||
8e3322f578 | |||
00756a3bde | |||
2c68224e9c | |||
c9e13be736 | |||
b9b342fe44 | |||
31f29f0bc5 | |||
0bc9fff825 | |||
25f93e8a89 | |||
4e0d8e4def | |||
1ecc825945 | |||
addc623add | |||
1b96deb4ee | |||
f510acd170 | |||
835409cb94 | |||
2121b3ef28 | |||
6c9101c167 | |||
b46883fe36 | |||
8d58805abd | |||
c3a799cb7d | |||
bc683b28d6 | |||
d91b1a70bf | |||
853014d8d2 | |||
ad6154a1e4 | |||
50c04382ef | |||
d62e9ce269 | |||
2149ef1cb4 | |||
d15fbadd27 | |||
fbbf421d8b | |||
fe695f1a14 | |||
d0ec6f9819 | |||
166d7fe0b0
|
|||
ab9d5439c1
|
|||
0737838dd6
|
|||
88bac5b5d8
|
|||
0b739fda34 | |||
9b8e143855 | |||
a533ab77ed | |||
087032881b | |||
82667a1c0f | |||
db6408926b | |||
f5c7ab6ef0 | |||
a13ada2937 | |||
3be8a39a1a | |||
d7eb1e01da | |||
bd62202d22 | |||
0e3de2ec8a | |||
aa2a398f9e | |||
33187448a0 | |||
a4482ad28b | |||
8ed5a023e8 | |||
cf1df462dc
|
|||
a0328b9d68
|
|||
813a80d6f9
|
|||
ab95bb157e
|
|||
18fd1dbc4a
|
|||
a35f7656cb
|
|||
ff05f9f48a
|
|||
482c494034
|
|||
653ac1d62b | |||
499009ac43 | |||
192b161e78 | |||
1b1f355123 | |||
39a863448c | |||
0c1a4a5f59 | |||
6f358ee1a9 | |||
0f36b9349b | |||
d18cc29acf | |||
4220d1a2d3 | |||
1ae27152c2 | |||
b946f8c10a | |||
62d6106801 | |||
89fb87f71f | |||
1337360690 | |||
9324c33caf | |||
c2dd9ef676 | |||
a42d7231d9 | |||
38deaf6f36 | |||
04fc5b6614 | |||
384b2be577 |
@@ -1,5 +0,0 @@
|
||||
kind: Feature
|
||||
body: '[DX] move async-upload-bundle features into chill-bundles'
|
||||
time: 2023-12-12T15:48:41.954970271+01:00
|
||||
custom:
|
||||
Issue: "221"
|
@@ -1,6 +0,0 @@
|
||||
kind: Feature
|
||||
body: |
|
||||
Upgrade import of address list to the last version of compiled addresses of belgian-best-address
|
||||
time: 2024-05-30T16:00:03.440767606+02:00
|
||||
custom:
|
||||
Issue: ""
|
@@ -1,6 +0,0 @@
|
||||
kind: Feature
|
||||
body: |
|
||||
Upgrade CKEditor and refactor configuration with use of typescript
|
||||
time: 2024-05-31T19:02:42.776662753+02:00
|
||||
custom:
|
||||
Issue: ""
|
7
.changes/unreleased/Feature-20240614-153537.yaml
Normal file
7
.changes/unreleased/Feature-20240614-153537.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
kind: Feature
|
||||
body: The behavoir of the voters for stored objects is adjusted so as to limit edit
|
||||
and delete possibilities to users related to the activity, social action or workflow
|
||||
entity.
|
||||
time: 2024-06-14T15:35:37.582159301+02:00
|
||||
custom:
|
||||
Issue: "286"
|
5
.changes/unreleased/Feature-20240718-151233.yaml
Normal file
5
.changes/unreleased/Feature-20240718-151233.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
kind: Feature
|
||||
body: Metadata form added for person signatures
|
||||
time: 2024-07-18T15:12:33.8134266+02:00
|
||||
custom:
|
||||
Issue: "288"
|
@@ -1,6 +0,0 @@
|
||||
kind: Fixed
|
||||
body: Fix resolving of centers for an household, which will fix in turn the access
|
||||
control
|
||||
time: 2024-04-10T10:37:36.462484988+02:00
|
||||
custom:
|
||||
Issue: ""
|
21
.changes/v2.20.0.md
Normal file
21
.changes/v2.20.0.md
Normal file
@@ -0,0 +1,21 @@
|
||||
## v2.20.0 - 2024-06-05
|
||||
### Fixed
|
||||
* ([#170](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/170)) Display agents traitants instead of accompanying period referrer in export list social actions.
|
||||
* Added translations for choices of durations (> 5 hours)
|
||||
### Feature
|
||||
* ([#145](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/145)) Allow to open documents in LibreOffice locally (need configuration within security);
|
||||
|
||||
This endpoint should be added to make the endpoint works properly:
|
||||
|
||||
```yaml
|
||||
security:
|
||||
firewalls:
|
||||
dav:
|
||||
pattern: ^/dav
|
||||
provider: chain_provider
|
||||
stateless: true
|
||||
guard:
|
||||
authenticators:
|
||||
- Chill\DocStoreBundle\Security\Guard\JWTOnDavUrlAuthenticator
|
||||
|
||||
```
|
3
.changes/v2.20.1.md
Normal file
3
.changes/v2.20.1.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## v2.20.1 - 2024-06-05
|
||||
### Fixed
|
||||
* Do not allow StoredObjectCreated for edit and convert buttons
|
31
.changes/v2.21.0.md
Normal file
31
.changes/v2.21.0.md
Normal file
@@ -0,0 +1,31 @@
|
||||
## v2.21.0 - 2024-06-18
|
||||
### Feature
|
||||
* Add flash menu buttons in search results, to open directly a new calendar, or a new activity in an accompanying period
|
||||
* ([#122](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/122)) Improve the list of calendar in the search results: make all calendar clicable, and display a list of calendars
|
||||
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add start date and end date on filters "filter course by referrer job" and "filter course by referrer scope"
|
||||
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] the aggregator "Group by referrer" now accept a date range.
|
||||
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's scope"
|
||||
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's jobs"
|
||||
* ([#168](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/168) In the UX, display user job and service at the time when he performs an action:
|
||||
now, the job and service is shown:
|
||||
* at the activity's date,
|
||||
* at the appointment's date,
|
||||
* when the user is marked as referrer for an accompanying period work,
|
||||
* when the user apply a transition in a workflow,
|
||||
* when the user updates or creates "something" ("created/updated by ... at ..."),
|
||||
* or when he wrote a comment,
|
||||
* …
|
||||
|
||||
### Traduction francophone
|
||||
* Ajout d'un menu "flash" dans les résultats de recherche, pour créer un rendez-vous ou un échange dans un parcours depuis les résultats de recherche;
|
||||
* Améliore la liste des rendez-vous dans les résultats de recherche: les rendez-vous sont cliquables;
|
||||
* [exports] Ajout d'intervalles de dates pour des filtres et regroupements des parcours par référent, métier du référent, service du référent;
|
||||
* Affiche le métier et le service des utilisateurs à la date à laquelle il a exécuté une action. Le métier et le service est affiché:
|
||||
* à la date d'un échange,
|
||||
* au jour d'un rendez-vous,
|
||||
* quand l'utilisateur est devenu référent d'un parcours d'accompagnement,
|
||||
* quand il a appliqué une transition sur un workflow,
|
||||
* quand il a mise à jour ou créé une fiche, dans les mentions "créé / mise à jour par ..., le ...",
|
||||
* quand il a mis à jour un commentaire,
|
||||
* …
|
||||
|
6
.changes/v2.22.0.md
Normal file
6
.changes/v2.22.0.md
Normal file
@@ -0,0 +1,6 @@
|
||||
## v2.22.0 - 2024-06-25
|
||||
### Feature
|
||||
* ([#216](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/216)) [event bundle] exports added for the event module
|
||||
|
||||
### Traduction francophone
|
||||
* Exports sont ajoutés pour la module événement.
|
5
.changes/v2.22.1.md
Normal file
5
.changes/v2.22.1.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## v2.22.1 - 2024-07-01
|
||||
### Fixed
|
||||
* Remove debug word
|
||||
### DX
|
||||
* Add a command for reading official address DB from Luxembourg and update chill addresses
|
3
.changes/v2.22.2.md
Normal file
3
.changes/v2.22.2.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## v2.22.2 - 2024-07-03
|
||||
### Fixed
|
||||
* Remove scope required for event participation stats
|
11
.changes/v2.23.0.md
Normal file
11
.changes/v2.23.0.md
Normal file
@@ -0,0 +1,11 @@
|
||||
## v2.23.0 - 2024-07-23
|
||||
### Feature
|
||||
* ([#221](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/221)) [DX] move async-upload-bundle features into chill-bundles
|
||||
* Add job bundle (module emploi)
|
||||
* Upgrade import of address list to the last version of compiled addresses of belgian-best-address
|
||||
|
||||
* Upgrade CKEditor and refactor configuration with use of typescript
|
||||
|
||||
### Fixed
|
||||
* Fix resolving of centers for an household, which will fix in turn the access control
|
||||
* Resolved type hinting error in activity list export
|
5
.changes/v3.0.0.md
Normal file
5
.changes/v3.0.0.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## 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
|
@@ -138,4 +138,4 @@ release:
|
||||
- echo "running release_job"
|
||||
release:
|
||||
tag_name: '$CI_COMMIT_TAG'
|
||||
description: "./.changes/v$CI_COMMIT_TAG.md"
|
||||
description: "./.changes/$CI_COMMIT_TAG.md"
|
||||
|
93
CHANGELOG.md
93
CHANGELOG.md
@@ -6,6 +6,99 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
## v2.23.0 - 2024-07-23
|
||||
### Feature
|
||||
* ([#221](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/221)) [DX] move async-upload-bundle features into chill-bundles
|
||||
* Add job bundle (module emploi)
|
||||
* Upgrade import of address list to the last version of compiled addresses of belgian-best-address
|
||||
|
||||
* Upgrade CKEditor and refactor configuration with use of typescript
|
||||
|
||||
### 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
|
||||
|
||||
## v2.22.1 - 2024-07-01
|
||||
### Fixed
|
||||
* Remove debug word
|
||||
### DX
|
||||
* Add a command for reading official address DB from Luxembourg and update chill addresses
|
||||
|
||||
## v2.22.0 - 2024-06-25
|
||||
### Feature
|
||||
* ([#216](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/216)) [event bundle] exports added for the event module
|
||||
|
||||
### Traduction francophone
|
||||
* Exports sont ajoutés pour la module événement.
|
||||
|
||||
## v2.21.0 - 2024-06-18
|
||||
### Feature
|
||||
* Add flash menu buttons in search results, to open directly a new calendar, or a new activity in an accompanying period
|
||||
* ([#122](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/122)) Improve the list of calendar in the search results: make all calendar clicable, and display a list of calendars
|
||||
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add start date and end date on filters "filter course by referrer job" and "filter course by referrer scope"
|
||||
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] the aggregator "Group by referrer" now accept a date range.
|
||||
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's scope"
|
||||
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's jobs"
|
||||
* ([#168](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/168) In the UX, display user job and service at the time when he performs an action:
|
||||
now, the job and service is shown:
|
||||
* at the activity's date,
|
||||
* at the appointment's date,
|
||||
* when the user is marked as referrer for an accompanying period work,
|
||||
* when the user apply a transition in a workflow,
|
||||
* when the user updates or creates "something" ("created/updated by ... at ..."),
|
||||
* or when he wrote a comment,
|
||||
* …
|
||||
|
||||
### Traduction francophone
|
||||
* Ajout d'un menu "flash" dans les résultats de recherche, pour créer un rendez-vous ou un échange dans un parcours depuis les résultats de recherche;
|
||||
* Améliore la liste des rendez-vous dans les résultats de recherche: les rendez-vous sont cliquables;
|
||||
* [exports] Ajout d'intervalles de dates pour des filtres et regroupements des parcours par référent, métier du référent, service du référent;
|
||||
* Affiche le métier et le service des utilisateurs à la date à laquelle il a exécuté une action. Le métier et le service est affiché:
|
||||
* à la date d'un échange,
|
||||
* au jour d'un rendez-vous,
|
||||
* quand l'utilisateur est devenu référent d'un parcours d'accompagnement,
|
||||
* quand il a appliqué une transition sur un workflow,
|
||||
* quand il a mise à jour ou créé une fiche, dans les mentions "créé / mise à jour par ..., le ...",
|
||||
* quand il a mis à jour un commentaire,
|
||||
* …
|
||||
|
||||
|
||||
## v2.20.1 - 2024-06-05
|
||||
### Fixed
|
||||
* Do not allow StoredObjectCreated for edit and convert buttons
|
||||
|
||||
## v2.20.0 - 2024-06-05
|
||||
### Fixed
|
||||
* ([#170](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/170)) Display agents traitants instead of accompanying period referrer in export list social actions.
|
||||
* Added translations for choices of durations (> 5 hours)
|
||||
### Feature
|
||||
* ([#145](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/145)) Allow to open documents in LibreOffice locally (need configuration within security);
|
||||
|
||||
This endpoint should be added to make the endpoint works properly:
|
||||
|
||||
```yaml
|
||||
security:
|
||||
firewalls:
|
||||
dav:
|
||||
pattern: ^/dav
|
||||
provider: chain_provider
|
||||
stateless: true
|
||||
guard:
|
||||
authenticators:
|
||||
- Chill\DocStoreBundle\Security\Guard\JWTOnDavUrlAuthenticator
|
||||
|
||||
```
|
||||
|
||||
## v2.19.0 - 2024-05-14
|
||||
### Feature
|
||||
* ([#197](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/197)) Make the script which subscribe to microsoft calendars changes more tolerant to errors or missing configuration on the microsoft side
|
||||
|
@@ -19,7 +19,6 @@
|
||||
"doctrine/doctrine-migrations-bundle": "^3.0",
|
||||
"doctrine/orm": "^2.13.0",
|
||||
"erusev/parsedown": "^1.7",
|
||||
"graylog2/gelf-php": "^1.5",
|
||||
"knplabs/knp-menu-bundle": "^3.0",
|
||||
"knplabs/knp-time-bundle": "^1.12",
|
||||
"knpuniversity/oauth2-client-bundle": "^2.10",
|
||||
@@ -93,11 +92,12 @@
|
||||
"phpstan/phpstan": "^1.9",
|
||||
"phpstan/phpstan-deprecation-rules": "^1.1",
|
||||
"phpstan/phpstan-strict-rules": "^1.0",
|
||||
"phpunit/phpunit": ">= 7.5",
|
||||
"phpunit/phpunit": "^10.5.24",
|
||||
"rector/rector": "^1.1.0",
|
||||
"symfony/debug-bundle": "^5.4",
|
||||
"symfony/dotenv": "^5.4",
|
||||
"symfony/maker-bundle": "^1.20",
|
||||
"symfony/phpunit-bridge": "^7.1",
|
||||
"symfony/runtime": "^5.4",
|
||||
"symfony/stopwatch": "^5.4",
|
||||
"symfony/var-dumper": "^5.4"
|
||||
@@ -115,6 +115,8 @@
|
||||
"Chill\\DocGeneratorBundle\\": "src/Bundle/ChillDocGeneratorBundle",
|
||||
"Chill\\DocStoreBundle\\": "src/Bundle/ChillDocStoreBundle",
|
||||
"Chill\\EventBundle\\": "src/Bundle/ChillEventBundle",
|
||||
"Chill\\FranceTravailApiBundle\\": "src/Bundle/ChillFranceTravailApiBundle/src",
|
||||
"Chill\\JobBundle\\": "src/Bundle/ChillJobBundle/src",
|
||||
"Chill\\MainBundle\\": "src/Bundle/ChillMainBundle",
|
||||
"Chill\\PersonBundle\\": "src/Bundle/ChillPersonBundle",
|
||||
"Chill\\ReportBundle\\": "src/Bundle/ChillReportBundle",
|
||||
|
@@ -21,7 +21,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
class BirthdateFilter implements ExportElementValidatedInterface, FilterInterface
|
||||
{
|
||||
// add specific role for this filter
|
||||
public function addRole()
|
||||
public function addRole(): ?string
|
||||
{
|
||||
// we do not need any new role for this filter, so we return null
|
||||
return null;
|
||||
|
@@ -56,7 +56,7 @@ We strongly encourage you to initialize a git repository at this step, to track
|
||||
cat <<< "$(jq '.extra.symfony += {"endpoint": ["flex://defaults", "https://gitlab.com/api/v4/projects/57371968/repository/files/index.json/raw?ref=main"]}' composer.json)" > composer.json
|
||||
# install chill and some dependencies
|
||||
# TODO fix the suffix "alpha1" and replace by ^3.0.0 when version 3.0.0 will be released
|
||||
symfony composer require chill-project/chill-bundles v3.0.0-alpha1 champs-libres/wopi-lib dev-master@dev champs-libres/wopi-bundle dev-master@dev
|
||||
symfony composer require chill-project/chill-bundles v3.0.0-RC3 champs-libres/wopi-lib dev-master@dev champs-libres/wopi-bundle dev-master@dev
|
||||
|
||||
We encourage you to accept the inclusion of the "Docker configuration from recipes": this is the documented way to run the database.
|
||||
You must also accept to configure recipes from the contrib repository, unless you want to configure the bundles manually).
|
||||
@@ -95,7 +95,7 @@ custom developments. But most of the time, this should be fine.
|
||||
|
||||
You have to configure some local variables, which are described in the :code:`.env` file. The secrets should not be stored
|
||||
in this :code:`.env` file, but instead using the `secrets management tool <https://symfony.com/doc/current/configuration/secrets.html>`_
|
||||
or in the :code:`.env.local` file, which should not be commited to the git repository.
|
||||
or in the :code:`.env.local` file, which should not be committed to the git repository.
|
||||
|
||||
You do not need to set variables for the smtp server, redis server and relatorio server, as they are generated automatically
|
||||
by the symfony server, from the docker compose services.
|
||||
@@ -110,10 +110,15 @@ you can either:
|
||||
.. code-block:: env
|
||||
|
||||
ADMIN_PASSWORD=\$2y\$13\$iyvJLuT4YEa6iWXyQV4/N.hNHpNG8kXlYDkkt5MkYy4FXcSwYAwmm
|
||||
# note: if you copy-paste the line above, the password will be "admin".
|
||||
|
||||
- add the generated password to the secrets manager (**note**: you must add the generated hashed password to the secrets env,
|
||||
not the password in clear text).
|
||||
|
||||
- set up the jwt authentication bundle
|
||||
|
||||
Some environment variables are available for the JWT authentication bundle in the :code:`.env` file.
|
||||
|
||||
Prepare migrations and other tools
|
||||
**********************************
|
||||
|
||||
@@ -130,6 +135,8 @@ To continue the installation process, you will have to run migrations:
|
||||
symfony console messenger:setup-transports
|
||||
# prepare some views
|
||||
symfony console chill:db:sync-views
|
||||
# generate jwt token, required for some api features (webdav access, ...)
|
||||
symfony console lexik:jwt:generate-keypair
|
||||
|
||||
.. warning::
|
||||
|
||||
@@ -164,7 +171,7 @@ can rely on the whole chill framework, meaning there is no need to add them to t
|
||||
You will require some bundles to have the following development tools:
|
||||
|
||||
- add fixtures
|
||||
- add profiler and var-dumper to debug
|
||||
- add profiler and debug bundle
|
||||
|
||||
Install fixtures
|
||||
****************
|
||||
@@ -179,7 +186,7 @@ Install fixtures
|
||||
This will generate user accounts, centers, and some basic configuration.
|
||||
|
||||
The accounts created are: :code:`center a_social`, :code:`center b_social`, :code:`center a_direction`, ... The full list is
|
||||
visibile in the "users" table: :code:`docker compose exec database psql -U app -c "SELECT username FROM users"`.
|
||||
visible in the "users" table: :code:`docker compose exec database psql -U app -c "SELECT username FROM users"`.
|
||||
|
||||
The password is always :code:`password`.
|
||||
|
||||
@@ -192,7 +199,7 @@ Add web profiler and debugger
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
symfony composer require --dev symfony/web-profiler-bundle symfony/var-dumper
|
||||
symfony composer require --dev symfony/web-profiler-bundle symfony/debug-bundle
|
||||
|
||||
Working on chill bundles
|
||||
************************
|
||||
|
@@ -27,7 +27,7 @@
|
||||
"popper.js": "^1.16.1",
|
||||
"postcss-loader": "^7.0.2",
|
||||
"raw-loader": "^4.0.2",
|
||||
"sass-loader": "^13.0.0",
|
||||
"sass-loader": "^14.0.0",
|
||||
"select2": "^4.0.13",
|
||||
"select2-bootstrap-theme": "0.1.0-beta.10",
|
||||
"style-loader": "^3.3.1",
|
||||
@@ -46,17 +46,20 @@
|
||||
"@fullcalendar/vue3": "^6.1.4",
|
||||
"@popperjs/core": "^2.9.2",
|
||||
"@types/leaflet": "^1.9.3",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"dropzone": "^5.7.6",
|
||||
"es6-promise": "^4.2.8",
|
||||
"leaflet": "^1.7.1",
|
||||
"marked": "^12.0.2",
|
||||
"masonry-layout": "^4.2.2",
|
||||
"mime": "^4.0.0",
|
||||
"pdfjs-dist": "^4.3.136",
|
||||
"swagger-ui": "^4.15.5",
|
||||
"vis-network": "^9.1.0",
|
||||
"vue": "^3.2.37",
|
||||
"vue-i18n": "^9.1.6",
|
||||
"vue-multiselect": "3.0.0-alpha.2",
|
||||
"vue-toast-notification": "^2.0",
|
||||
"vue-toast-notification": "^3.1.2",
|
||||
"vuex": "^4.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
|
@@ -1,34 +1,29 @@
|
||||
parameters:
|
||||
ignoreErrors:
|
||||
-
|
||||
message: "#^Foreach overwrites \\$key with its key variable\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillCustomFieldsBundle/Controller/CustomFieldsGroupController.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillCustomFieldsBundle/Entity/CustomField.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
|
||||
message: "#^Property Chill\\\\CustomFieldsBundle\\\\Entity\\\\CustomField\\:\\:\\$required \\(false\\) does not accept bool\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillPersonBundle/Form/PersonType.php
|
||||
path: src/Bundle/ChillCustomFieldsBundle/Entity/CustomField.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillMainBundle/Templating/ChillTwigRoutingHelper.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillCustomFieldsBundle/Entity/CustomFieldsGroup.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
|
||||
message: "#^Parameter \\#1 \\$user of method Chill\\\\DocStoreBundle\\\\Entity\\\\Document\\:\\:setUser\\(\\) expects Chill\\\\MainBundle\\\\Entity\\\\User\\|null, Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface\\|null given\\.$#"
|
||||
count: 2
|
||||
path: src/Bundle/ChillMainBundle/Repository/NotificationRepository.php
|
||||
path: src/Bundle/ChillDocStoreBundle/Controller/DocumentAccompanyingCourseController.php
|
||||
|
||||
-
|
||||
message: "#^Foreach overwrites \\$key with its key variable\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillCustomFieldsBundle/Controller/CustomFieldsGroupController.php
|
||||
message: "#^Parameter \\#1 \\$user of method Chill\\\\DocStoreBundle\\\\Entity\\\\Document\\:\\:setUser\\(\\) expects Chill\\\\MainBundle\\\\Entity\\\\User\\|null, Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface\\|null given\\.$#"
|
||||
count: 2
|
||||
path: src/Bundle/ChillDocStoreBundle/Controller/DocumentPersonController.php
|
||||
|
||||
-
|
||||
message: "#^Variable \\$participation might not be defined\\.$#"
|
||||
@@ -40,6 +35,106 @@ parameters:
|
||||
count: 1
|
||||
path: src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php
|
||||
|
||||
-
|
||||
message: "#^Comparison operation \"\\>\" between \\(bool\\|int\\|Redis\\) and 0 results in an error\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillFranceTravailApiBundle/src/ApiHelper/ApiWrapper.php
|
||||
|
||||
-
|
||||
message: "#^Variable \\$response might not be defined\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillFranceTravailApiBundle/src/ApiHelper/ApiWrapper.php
|
||||
|
||||
-
|
||||
message: "#^Function GuzzleHttp\\\\Psr7\\\\get not found\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillFranceTravailApiBundle/src/ApiHelper/PartenaireRomeAppellation.php
|
||||
|
||||
-
|
||||
message: "#^Function GuzzleHttp\\\\Psr7\\\\str not found\\.$#"
|
||||
count: 2
|
||||
path: src/Bundle/ChillFranceTravailApiBundle/src/ApiHelper/PartenaireRomeAppellation.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$seconds of function sleep expects int, string given\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillFranceTravailApiBundle/src/ApiHelper/PartenaireRomeAppellation.php
|
||||
|
||||
-
|
||||
message: "#^Unreachable statement \\- code above always terminates\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Controller/CSPersonController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$interval of method DateTimeImmutable\\:\\:add\\(\\) expects DateInterval, string\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Entity/Immersion.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$object of static method DateTimeImmutable\\:\\:createFromMutable\\(\\) expects DateTime, DateTimeInterface given\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Entity/Immersion.php
|
||||
|
||||
-
|
||||
message: "#^Property Chill\\\\JobBundle\\\\Entity\\\\Rome\\\\Metier\\:\\:\\$appellations is never read, only written\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Entity/Rome/Metier.php
|
||||
|
||||
-
|
||||
message: "#^Method Chill\\\\JobBundle\\\\Export\\\\ListCSPerson\\:\\:splitArrayToColumns\\(\\) never returns Closure so it can be removed from the return type\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Export/ListCSPerson.php
|
||||
|
||||
-
|
||||
message: "#^Variable \\$f might not be defined\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Export/ListCSPerson.php
|
||||
|
||||
-
|
||||
message: "#^Method Chill\\\\JobBundle\\\\Export\\\\ListFrein\\:\\:splitArrayToColumns\\(\\) never returns Closure so it can be removed from the return type\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Export/ListFrein.php
|
||||
|
||||
-
|
||||
message: "#^Method Chill\\\\JobBundle\\\\Export\\\\ListProjetProfessionnel\\:\\:splitArrayToColumns\\(\\) never returns Closure so it can be removed from the return type\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Export/ListProjetProfessionnel.php
|
||||
|
||||
-
|
||||
message: "#^Property Chill\\\\JobBundle\\\\Form\\\\ChoiceLoader\\\\RomeAppellationChoiceLoader\\:\\:\\$appellationRepository \\(Chill\\\\JobBundle\\\\Repository\\\\Rome\\\\AppellationRepository\\) does not accept Doctrine\\\\ORM\\\\EntityRepository\\<Chill\\\\JobBundle\\\\Entity\\\\Rome\\\\Appellation\\>\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Form/ChoiceLoader/RomeAppellationChoiceLoader.php
|
||||
|
||||
-
|
||||
message: "#^Result of && is always false\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Form/ChoiceLoader/RomeAppellationChoiceLoader.php
|
||||
|
||||
-
|
||||
message: "#^Strict comparison using \\=\\=\\= between array\\{\\} and Symfony\\\\Component\\\\Validator\\\\ConstraintViolationListInterface will always evaluate to false\\.$#"
|
||||
count: 2
|
||||
path: src/Bundle/ChillJobBundle/src/Form/ChoiceLoader/RomeAppellationChoiceLoader.php
|
||||
|
||||
-
|
||||
message: "#^Strict comparison using \\=\\=\\= between null and string will always evaluate to false\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Form/ChoiceLoader/RomeAppellationChoiceLoader.php
|
||||
|
||||
-
|
||||
message: "#^Variable \\$metier might not be defined\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Form/ChoiceLoader/RomeAppellationChoiceLoader.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$interval of method DateTimeImmutable\\:\\:add\\(\\) expects DateInterval, string\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Security/Authorization/CSConnectesVoter.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$object of static method DateTimeImmutable\\:\\:createFromMutable\\(\\) expects DateTime, DateTimeInterface given\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillJobBundle/src/Security/Authorization/CSConnectesVoter.php
|
||||
|
||||
-
|
||||
message: "#^Cannot unset offset '_token' on array\\{formatter\\: mixed, export\\: mixed, centers\\: mixed, alias\\: string\\}\\.$#"
|
||||
count: 1
|
||||
@@ -65,11 +160,31 @@ parameters:
|
||||
count: 1
|
||||
path: src/Bundle/ChillMainBundle/Form/ChoiceLoader/PostalCodeChoiceLoader.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
|
||||
count: 2
|
||||
path: src/Bundle/ChillMainBundle/Repository/NotificationRepository.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$user of method Chill\\\\MainBundle\\\\Security\\\\Authorization\\\\AuthorizationHelper\\:\\:userHasAccessForCenter\\(\\) expects Chill\\\\MainBundle\\\\Entity\\\\User, Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface given\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillMainBundle/Templating/ChillTwigRoutingHelper.php
|
||||
|
||||
-
|
||||
message: "#^Foreach overwrites \\$value with its value variable\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillPersonBundle/Form/ChoiceLoader/PersonChoiceLoader.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
|
||||
count: 1
|
||||
path: src/Bundle/ChillPersonBundle/Form/PersonType.php
|
||||
|
||||
-
|
||||
message: "#^Foreach overwrites \\$value with its value variable\\.$#"
|
||||
count: 1
|
||||
|
22
rector.php
22
rector.php
@@ -28,6 +28,9 @@ return static function (RectorConfig $rectorConfig): void {
|
||||
|
||||
// register a single rule
|
||||
$rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class);
|
||||
$rectorConfig->rule(Rector\TypeDeclaration\Rector\ClassMethod\AddParamTypeFromPropertyTypeRector::class);
|
||||
$rectorConfig->rule(Rector\TypeDeclaration\Rector\Class_\MergeDateTimePropertyTypeDeclarationRector::class);
|
||||
$rectorConfig->rule(Rector\TypeDeclaration\Rector\ClassMethod\AddReturnTypeDeclarationBasedOnParentClassMethodRector::class);
|
||||
|
||||
// part of the symfony 54 rules
|
||||
$rectorConfig->rule(\Rector\Symfony\Symfony53\Rector\StaticPropertyFetch\KernelTestCaseContainerPropertyDeprecationRector::class);
|
||||
@@ -36,14 +39,14 @@ return static function (RectorConfig $rectorConfig): void {
|
||||
|
||||
//define sets of rules
|
||||
$rectorConfig->sets([
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_50,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_50_TYPES,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_51,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_52,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_53,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_54,
|
||||
LevelSetList::UP_TO_PHP_82,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_40,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_41,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_42,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_43,
|
||||
\Rector\Symfony\Set\SymfonySetList::SYMFONY_44,
|
||||
\Rector\Doctrine\Set\DoctrineSetList::DOCTRINE_CODE_QUALITY,
|
||||
\Rector\Doctrine\Set\DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES,
|
||||
\Rector\PHPUnit\Set\PHPUnitSetList::PHPUNIT_90,
|
||||
]);
|
||||
|
||||
$rectorConfig->ruleWithConfiguration(\Rector\Php80\Rector\Class_\AnnotationToAttributeRector::class, [
|
||||
@@ -66,9 +69,8 @@ return static function (RectorConfig $rectorConfig): void {
|
||||
|
||||
// skip some path...
|
||||
$rectorConfig->skip([
|
||||
// we must adapt service definition
|
||||
\Rector\Symfony\Symfony28\Rector\MethodCall\GetToConstructorInjectionRector::class,
|
||||
\Rector\Symfony\Symfony34\Rector\Closure\ContainerGetNameToTypeInTestsRector::class,
|
||||
// waiting for fixing this bug: https://github.com/rectorphp/rector-doctrine/issues/342
|
||||
\Rector\Doctrine\CodeQuality\Rector\Property\ImproveDoctrineCollectionDocTypeInEntityRector::class,
|
||||
]);
|
||||
|
||||
$rectorConfig->ruleWithConfiguration(AnnotationToAttributeRector::class, [
|
||||
|
@@ -99,10 +99,10 @@ final class ActivityController extends AbstractController
|
||||
|
||||
$form = $this->createDeleteForm($activity->getId(), $person, $accompanyingPeriod);
|
||||
|
||||
if (Request::METHOD_DELETE === $request->getMethod()) {
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isValid()) {
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->logger->notice('An activity has been removed', [
|
||||
'by_user' => $this->getUser()->getUsername(),
|
||||
'activity_id' => $activity->getId(),
|
||||
@@ -640,7 +640,6 @@ final class ActivityController extends AbstractController
|
||||
|
||||
return $this->createFormBuilder()
|
||||
->setAction($this->generateUrl('chill_activity_activity_delete', $params))
|
||||
->setMethod('DELETE')
|
||||
->add('submit', SubmitType::class, ['label' => 'Delete'])
|
||||
->getForm();
|
||||
}
|
||||
|
@@ -80,7 +80,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
private \DateTime $date;
|
||||
|
||||
/**
|
||||
* @var Collection<StoredObject>
|
||||
* @var Collection<int, StoredObject>
|
||||
*/
|
||||
#[Assert\Valid(traverse: true)]
|
||||
#[ORM\ManyToMany(targetEntity: StoredObject::class, cascade: ['persist'])]
|
||||
@@ -107,7 +107,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
private ?Person $person = null;
|
||||
|
||||
/**
|
||||
* @var Collection<Person>
|
||||
* @var Collection<int, \Chill\PersonBundle\Entity\Person>
|
||||
*/
|
||||
#[Groups(['read', 'docgen:read'])]
|
||||
#[ORM\ManyToMany(targetEntity: Person::class)]
|
||||
@@ -117,7 +117,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
private PrivateCommentEmbeddable $privateComment;
|
||||
|
||||
/**
|
||||
* @var Collection<ActivityReason>
|
||||
* @var Collection<int, ActivityReason>
|
||||
*/
|
||||
#[Groups(['docgen:read'])]
|
||||
#[ORM\ManyToMany(targetEntity: ActivityReason::class)]
|
||||
@@ -132,7 +132,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
private string $sentReceived = '';
|
||||
|
||||
/**
|
||||
* @var Collection<SocialAction>
|
||||
* @var Collection<int, \Chill\PersonBundle\Entity\SocialWork\SocialAction>
|
||||
*/
|
||||
#[Groups(['read', 'docgen:read'])]
|
||||
#[ORM\ManyToMany(targetEntity: SocialAction::class)]
|
||||
@@ -140,7 +140,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
private Collection $socialActions;
|
||||
|
||||
/**
|
||||
* @var Collection<SocialIssue>
|
||||
* @var Collection<int, SocialIssue>
|
||||
*/
|
||||
#[Groups(['read', 'docgen:read'])]
|
||||
#[ORM\ManyToMany(targetEntity: SocialIssue::class)]
|
||||
@@ -148,7 +148,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
private Collection $socialIssues;
|
||||
|
||||
/**
|
||||
* @var Collection<ThirdParty>
|
||||
* @var Collection<int, ThirdParty>
|
||||
*/
|
||||
#[Groups(['read', 'docgen:read'])]
|
||||
#[ORM\ManyToMany(targetEntity: ThirdParty::class)]
|
||||
@@ -162,7 +162,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
private ?User $user = null;
|
||||
|
||||
/**
|
||||
* @var Collection<User>
|
||||
* @var Collection<int, User>
|
||||
*/
|
||||
#[Groups(['read', 'docgen:read'])]
|
||||
#[ORM\ManyToMany(targetEntity: User::class)]
|
||||
|
@@ -79,11 +79,9 @@ class ActivityReason
|
||||
/**
|
||||
* Set active.
|
||||
*
|
||||
* @param bool $active
|
||||
*
|
||||
* @return ActivityReason
|
||||
*/
|
||||
public function setActive($active)
|
||||
public function setActive(bool $active)
|
||||
{
|
||||
$this->active = $active;
|
||||
|
||||
@@ -110,11 +108,9 @@ class ActivityReason
|
||||
/**
|
||||
* Set name.
|
||||
*
|
||||
* @param array $name
|
||||
*
|
||||
* @return ActivityReason
|
||||
*/
|
||||
public function setName($name)
|
||||
public function setName(array $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
|
@@ -40,9 +40,9 @@ class ActivityReasonCategory implements \Stringable
|
||||
/**
|
||||
* Array of ActivityReason.
|
||||
*
|
||||
* @var Collection<ActivityReason>
|
||||
* @var Collection<int, ActivityReason>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: ActivityReason::class, mappedBy: 'category')]
|
||||
#[ORM\OneToMany(mappedBy: 'category', targetEntity: ActivityReason::class)]
|
||||
private Collection $reasons;
|
||||
|
||||
/**
|
||||
|
@@ -152,7 +152,7 @@ class ListActivityHelper
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->translator->trans($value);
|
||||
return $this->translator->trans((string) $value);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@@ -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 = acp"
|
||||
'SELECT 1 FROM '.Activity::class." {$alias} WHERE {$alias}.date >= :{$from} AND {$alias}.date < :{$to} AND {$alias}.accompanyingPeriod = activity.accompanyingPeriod"
|
||||
)
|
||||
);
|
||||
|
||||
|
@@ -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\ActivityBundle\Menu;
|
||||
|
||||
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
|
||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||
use Knp\Menu\MenuItem;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
final readonly class AccompanyingCourseQuickMenuBuilder implements LocalMenuBuilderInterface
|
||||
{
|
||||
public function __construct(private Security $security) {}
|
||||
|
||||
public static function getMenuIds(): array
|
||||
{
|
||||
return ['accompanying_course_quick_menu'];
|
||||
}
|
||||
|
||||
public function buildMenu($menuId, MenuItem $menu, array $parameters)
|
||||
{
|
||||
/** @var \Chill\PersonBundle\Entity\AccompanyingPeriod $accompanyingCourse */
|
||||
$accompanyingCourse = $parameters['accompanying-course'];
|
||||
|
||||
if ($this->security->isGranted(ActivityVoter::CREATE, $accompanyingCourse)) {
|
||||
$menu
|
||||
->addChild('Create a new activity in accompanying course', [
|
||||
'route' => 'chill_activity_activity_new',
|
||||
'routeParameters' => [
|
||||
// 'activityType_id' => '',
|
||||
'accompanying_period_id' => $accompanyingCourse->getId(),
|
||||
],
|
||||
])
|
||||
->setExtras([
|
||||
'order' => 10,
|
||||
'icon' => 'plus',
|
||||
])
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
@@ -12,6 +12,8 @@ declare(strict_types=1);
|
||||
namespace Chill\ActivityBundle\Repository;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
@@ -23,7 +25,7 @@ use Doctrine\Persistence\ManagerRegistry;
|
||||
* @method Activity[] findAll()
|
||||
* @method Activity[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class ActivityRepository extends ServiceEntityRepository
|
||||
class ActivityRepository extends ServiceEntityRepository implements AssociatedEntityToStoredObjectInterface
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
@@ -97,4 +99,16 @@ class ActivityRepository extends ServiceEntityRepository
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?Activity
|
||||
{
|
||||
$qb = $this->createQueryBuilder('a');
|
||||
$query = $qb
|
||||
->leftJoin('a.documents', 'ad')
|
||||
->where('ad.id = :storedObjectId')
|
||||
->setParameter('storedObjectId', $storedObject->getId())
|
||||
->getQuery();
|
||||
|
||||
return $query->getOneOrNullResult();
|
||||
}
|
||||
}
|
||||
|
@@ -15,9 +15,9 @@ use Chill\ActivityBundle\Entity\ActivityType;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
|
||||
final class ActivityTypeRepository implements ActivityTypeRepositoryInterface
|
||||
final readonly class ActivityTypeRepository implements ActivityTypeRepositoryInterface
|
||||
{
|
||||
private readonly EntityRepository $repository;
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
|
@@ -68,7 +68,7 @@
|
||||
<div class="wl-col title"><h3>{{ 'Referrer'|trans }}</h3></div>
|
||||
<div class="wl-col list">
|
||||
<p class="wl-item">
|
||||
<span class="badge-user">{{ activity.user|chill_entity_render_box }}</span>
|
||||
<span class="badge-user">{{ activity.user|chill_entity_render_box({'at_date': activity.date}) }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -87,7 +87,7 @@
|
||||
<li>
|
||||
{% if bloc.type == 'user' %}
|
||||
<span class="badge-user">
|
||||
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false }) }}
|
||||
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }}
|
||||
</span>
|
||||
{% else %}
|
||||
{{ _self.insert_onthefly(bloc.type, item) }}
|
||||
@@ -114,7 +114,7 @@
|
||||
<li>
|
||||
{% if bloc.type == 'user' %}
|
||||
<span class="badge-user">
|
||||
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false }) }}
|
||||
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }}
|
||||
</span>
|
||||
{% else %}
|
||||
{{ _self.insert_onthefly(bloc.type, item) }}
|
||||
@@ -142,7 +142,7 @@
|
||||
<span class="wl-item">
|
||||
{% if bloc.type == 'user' %}
|
||||
<span class="badge-user">
|
||||
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false }) }}
|
||||
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }}
|
||||
{%- if context == 'calendar_accompanyingCourse' or context == 'calendar_person' %}
|
||||
{% set invite = entity.inviteForUser(item) %}
|
||||
{% if invite is not null %}
|
||||
|
@@ -41,7 +41,7 @@
|
||||
{% if activity.user and t.userVisible %}
|
||||
<li>
|
||||
<span class="item-key">{{ 'Referrer'|trans ~ ': ' }}</span>
|
||||
<span class="badge-user">{{ activity.user|chill_entity_render_box }}</span>
|
||||
<span class="badge-user">{{ activity.user|chill_entity_render_box({'at_date': activity.date}) }}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
|
@@ -37,7 +37,7 @@
|
||||
{%- if entity.user is not null %}
|
||||
<dt class="inline">{{ 'Referrer'|trans|capitalize }}</dt>
|
||||
<dd>
|
||||
<span class="badge-user">{{ entity.user|chill_entity_render_box }}</span>
|
||||
<span class="badge-user">{{ entity.user|chill_entity_render_box({'at_date': entity.date}) }}</span>
|
||||
</dd>
|
||||
{% endif %}
|
||||
|
||||
|
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\ActivityBundle\Security\Authorization;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\ActivityBundle\Repository\ActivityRepository;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
|
||||
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ActivityRepository $repository,
|
||||
Security $security,
|
||||
WorkflowStoredObjectPermissionHelper $workflowDocumentService
|
||||
) {
|
||||
parent::__construct($security, $workflowDocumentService);
|
||||
}
|
||||
|
||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||
{
|
||||
return $this->repository;
|
||||
}
|
||||
|
||||
protected function getClass(): string
|
||||
{
|
||||
return Activity::class;
|
||||
}
|
||||
|
||||
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
|
||||
{
|
||||
return match ($attribute) {
|
||||
StoredObjectRoleEnum::EDIT => ActivityVoter::UPDATE,
|
||||
StoredObjectRoleEnum::SEE => ActivityVoter::SEE_DETAILS,
|
||||
};
|
||||
}
|
||||
|
||||
protected function canBeAssociatedWithWorkflow(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
@@ -145,7 +145,7 @@ class ActivityVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn
|
||||
throw new \RuntimeException('Could not determine context of activity.');
|
||||
}
|
||||
} elseif ($subject instanceof AccompanyingPeriod) {
|
||||
if (AccompanyingPeriod::STEP_CLOSED === $subject->getStep()) {
|
||||
if (AccompanyingPeriod::STEP_CLOSED === $subject->getStep() || AccompanyingPeriod::STEP_DRAFT === $subject->getStep()) {
|
||||
if (\in_array($attribute, [self::UPDATE, self::CREATE, self::DELETE], true)) {
|
||||
return false;
|
||||
}
|
||||
|
@@ -77,6 +77,18 @@ Choose a type: Choisir un type
|
||||
4 hours: 4 heures
|
||||
4 hours 30: 4 heures 30
|
||||
5 hours: 5 heures
|
||||
5 hours 30: 5 heure 30
|
||||
6 hours: 6 heures
|
||||
6 hours 30: 6 heure 30
|
||||
7 hours: 7 heures
|
||||
7 hours 30: 7 heure 30
|
||||
8 hours: 8 heures
|
||||
8 hours 30: 8 heure 30
|
||||
9 hours: 9 heures
|
||||
9 hours 30: 9 heure 30
|
||||
10 hours: 10 heures
|
||||
11 hours: 11 heures
|
||||
12 hours: 12 heures
|
||||
Concerned groups: Parties concernées par l'échange
|
||||
Persons in accompanying course: Usagers du parcours
|
||||
Third persons: Tiers non-pro.
|
||||
@@ -210,6 +222,7 @@ Documents label: Libellé du champ Documents
|
||||
# activity type category admin
|
||||
ActivityTypeCategory list: Liste des catégories des types d'échange
|
||||
Create a new activity type category: Créer une nouvelle catégorie de type d'échange
|
||||
Create a new activity in accompanying course: Créer un échange dans le parcours
|
||||
|
||||
# activity delete
|
||||
Remove activity: Supprimer un échange
|
||||
|
@@ -22,9 +22,9 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
class AsideActivityCategory
|
||||
{
|
||||
/**
|
||||
* @var Collection<AsideActivityCategory>
|
||||
* @var Collection<int, AsideActivityCategory>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: AsideActivityCategory::class, mappedBy: 'parent')]
|
||||
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: AsideActivityCategory::class)]
|
||||
private Collection $children;
|
||||
|
||||
#[ORM\Id]
|
||||
|
@@ -49,13 +49,13 @@
|
||||
<li>
|
||||
<span>
|
||||
<abbr class="referrer" title={{ 'Created by'|trans }}>{{ 'By'|trans }}:</abbr>
|
||||
<b>{{ entity.createdBy|chill_entity_render_box }}</b>
|
||||
<b>{{ entity.createdBy|chill_entity_render_box({'at_date': entity.date}) }}</b>
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>
|
||||
<abbr class="referrer" title={{ 'Created for'|trans }}>{{ 'For'|trans }}:</abbr>
|
||||
<b>{{ entity.agent|chill_entity_render_box }}</b>
|
||||
<b>{{ entity.agent|chill_entity_render_box({'at_date': entity.date}) }}</b>
|
||||
|
||||
</span>
|
||||
</li>
|
||||
|
@@ -18,11 +18,11 @@
|
||||
<dd>{{ entity.type|chill_entity_render_box }}</dd>
|
||||
|
||||
<dt class="inline">{{ 'Created by'|trans }}</dt>
|
||||
<dd>{{ entity.createdBy }}</dd>
|
||||
<dd>{{ entity.createdBy|chill_entity_render_box({'at_date': entity.date}) }}</dd>
|
||||
|
||||
<dt class="inline">{{ 'Created for'|trans }}</dt>
|
||||
<dd>{{ entity.agent }}</dd>
|
||||
|
||||
<dd>{{ entity.agent|chill_entity_render_box({'at_date': entity.date}) }}</dd>
|
||||
|
||||
<dt class="inline">{{ 'Asideactivity location'|trans }}</dt>
|
||||
{%- if entity.location.name is defined -%}
|
||||
<dd>{{ entity.location.name }}</dd>
|
||||
|
@@ -72,21 +72,21 @@ days: jours
|
||||
1 hour 30: 1 heure 30
|
||||
1 hour 45: 1 heure 45
|
||||
2 hours: 2 heures
|
||||
2 hours 30: 2 heure 30
|
||||
2 hours 30: 2 heures 30
|
||||
3 hours: 3 heures
|
||||
3 hours 30: 3 heure 30
|
||||
3 hours 30: 3 heures 30
|
||||
4 hours: 4 heures
|
||||
4 hours 30: 4 heure 30
|
||||
4 hours 30: 4 heures 30
|
||||
5 hours: 5 heures
|
||||
5 hours 30: 5 heure 30
|
||||
5 hours 30: 5 heures 30
|
||||
6 hours: 6 heures
|
||||
6 hours 30: 6 heure 30
|
||||
6 hours 30: 6 heures 30
|
||||
7 hours: 7 heures
|
||||
7 hours 30: 7 heure 30
|
||||
7 hours 30: 7 heures 30
|
||||
8 hours: 8 heures
|
||||
8 hours 30: 8 heure 30
|
||||
8 hours 30: 8 heures 30
|
||||
9 hours: 9 heures
|
||||
9 hours 30: 9 heure 30
|
||||
9 hours 30: 9 heures 30
|
||||
10 hours: 10 heures
|
||||
1/2 day: 1/2 jour
|
||||
1 day: 1 jour
|
||||
|
@@ -54,7 +54,7 @@ abstract class AbstractElementController extends AbstractController
|
||||
$indexPage = 'chill_budget_elements_household_index';
|
||||
}
|
||||
|
||||
if (Request::METHOD_DELETE === $request->getMethod()) {
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
@@ -198,10 +198,9 @@ abstract class AbstractElementController extends AbstractController
|
||||
/**
|
||||
* Creates a form to delete a help request entity by id.
|
||||
*/
|
||||
private function createDeleteForm(): Form
|
||||
private function createDeleteForm(): \Symfony\Component\Form\FormInterface
|
||||
{
|
||||
return $this->createFormBuilder()
|
||||
->setMethod(Request::METHOD_DELETE)
|
||||
->add('submit', SubmitType::class, ['label' => 'Delete'])
|
||||
->getForm();
|
||||
}
|
||||
|
@@ -100,7 +100,7 @@ class Charge extends AbstractElement implements HasCentersInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setHelp($help)
|
||||
public function setHelp(?string $help)
|
||||
{
|
||||
$this->help = $help;
|
||||
|
||||
|
@@ -15,9 +15,9 @@ use Chill\BudgetBundle\Entity\ChargeKind;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
|
||||
final class ChargeKindRepository implements ChargeKindRepositoryInterface
|
||||
final readonly class ChargeKindRepository implements ChargeKindRepositoryInterface
|
||||
{
|
||||
private readonly EntityRepository $repository;
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
|
@@ -15,9 +15,9 @@ use Chill\BudgetBundle\Entity\ResourceKind;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
|
||||
final class ResourceKindRepository implements ResourceKindRepositoryInterface
|
||||
final readonly class ResourceKindRepository implements ResourceKindRepositoryInterface
|
||||
{
|
||||
private readonly EntityRepository $repository;
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
|
@@ -84,7 +84,7 @@ class CalendarController extends AbstractController
|
||||
|
||||
$form = $this->createDeleteForm($entity);
|
||||
|
||||
if (Request::METHOD_DELETE === $request->getMethod()) {
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isValid()) {
|
||||
@@ -512,7 +512,6 @@ class CalendarController extends AbstractController
|
||||
{
|
||||
return $this->createFormBuilder()
|
||||
->setAction($this->generateUrl('chill_calendar_calendar_delete', ['id' => $calendar->getId()]))
|
||||
->setMethod('DELETE')
|
||||
->add('submit', SubmitType::class, ['label' => 'Delete'])
|
||||
->getForm();
|
||||
}
|
||||
|
@@ -103,7 +103,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
|
||||
private int $dateTimeVersion = 0;
|
||||
|
||||
/**
|
||||
* @var Collection<CalendarDoc>
|
||||
* @var Collection<int, \Chill\CalendarBundle\Entity\CalendarDoc>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'calendar', targetEntity: CalendarDoc::class, orphanRemoval: true)]
|
||||
private Collection $documents;
|
||||
@@ -120,7 +120,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* @var Collection&Selectable<int, Invite>
|
||||
* @var \Doctrine\Common\Collections\Collection<int, \Chill\CalendarBundle\Entity\Invite>&Selectable
|
||||
*/
|
||||
#[Serializer\Groups(['read', 'docgen:read'])]
|
||||
#[ORM\OneToMany(mappedBy: 'calendar', targetEntity: Invite::class, cascade: ['persist', 'remove', 'merge', 'detach'], orphanRemoval: true)]
|
||||
@@ -143,7 +143,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
|
||||
private ?Person $person = null;
|
||||
|
||||
/**
|
||||
* @var Collection<Person>
|
||||
* @var Collection<int, Person>
|
||||
*/
|
||||
#[Serializer\Groups(['calendar:read', 'read', 'calendar:light', 'docgen:read'])]
|
||||
#[Assert\Count(min: 1, minMessage: 'calendar.At least {{ limit }} person is required.')]
|
||||
@@ -157,7 +157,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
|
||||
private PrivateCommentEmbeddable $privateComment;
|
||||
|
||||
/**
|
||||
* @var Collection<ThirdParty>
|
||||
* @var Collection<int, ThirdParty>
|
||||
*/
|
||||
#[Serializer\Groups(['calendar:read', 'read', 'calendar:light', 'docgen:read'])]
|
||||
#[ORM\ManyToMany(targetEntity: ThirdParty::class)]
|
||||
@@ -440,6 +440,16 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
|
||||
return $this->startDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the date of the calendar.
|
||||
*
|
||||
* Useful for showing the date of the calendar event, required by twig in some places.
|
||||
*/
|
||||
public function getDate(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->getStartDate();
|
||||
}
|
||||
|
||||
public function getStatus(): ?string
|
||||
{
|
||||
return $this->status;
|
||||
|
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\CalendarBundle\Menu;
|
||||
|
||||
use Chill\CalendarBundle\Security\Voter\CalendarVoter;
|
||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||
use Knp\Menu\MenuItem;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
final readonly class AccompanyingCourseQuickMenuBuilder implements LocalMenuBuilderInterface
|
||||
{
|
||||
public function __construct(private Security $security) {}
|
||||
|
||||
public static function getMenuIds(): array
|
||||
{
|
||||
return ['accompanying_course_quick_menu'];
|
||||
}
|
||||
|
||||
public function buildMenu($menuId, MenuItem $menu, array $parameters)
|
||||
{
|
||||
/** @var \Chill\PersonBundle\Entity\AccompanyingPeriod $accompanyingCourse */
|
||||
$accompanyingCourse = $parameters['accompanying-course'];
|
||||
|
||||
if ($this->security->isGranted(CalendarVoter::CREATE, $accompanyingCourse)) {
|
||||
$menu
|
||||
->addChild('Create a new calendar in accompanying course', [
|
||||
'route' => 'chill_calendar_calendar_new',
|
||||
'routeParameters' => [
|
||||
'accompanying_period_id' => $accompanyingCourse->getId(),
|
||||
],
|
||||
])
|
||||
->setExtras([
|
||||
'order' => 20,
|
||||
'icon' => 'plus',
|
||||
])
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1 +1,2 @@
|
||||
import './scss/badge.scss';
|
||||
import './scss/calendar-list.scss';
|
||||
|
@@ -0,0 +1,26 @@
|
||||
ul.calendar-list {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
& > li {
|
||||
display: inline-block;
|
||||
}
|
||||
& > li:nth-child(n+2) {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
div.calendar-list {
|
||||
|
||||
ul.calendar-list {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
& > a.calendar-list__global {
|
||||
display: inline-block;;
|
||||
padding: 0.2rem;
|
||||
min-width: 2rem;
|
||||
border: 1px solid var(--bs-chill-blue);
|
||||
border-radius: 0.25rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
@@ -55,7 +55,7 @@
|
||||
<div class="item-col">
|
||||
<ul class="list-content">
|
||||
{% if calendar.mainUser is not empty %}
|
||||
<span class="badge-user">{{ calendar.mainUser|chill_entity_render_box }}</span>
|
||||
<span class="badge-user">{{ calendar.mainUser|chill_entity_render_box({'at_date': calendar.startDate}) }}</span>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -132,7 +132,7 @@
|
||||
<li class="cancel">
|
||||
<span class="createdBy">
|
||||
{{ 'Created by'|trans }}
|
||||
<b>{{ calendar.activity.createdBy|chill_entity_render_string }}</b>, {{ 'on'|trans }} {{ calendar.activity.createdAt|format_datetime('short', 'short') }}
|
||||
<b>{{ calendar.activity.createdBy|chill_entity_render_string({'at_date': calendar.activity.createdAt}) }}</b>, {{ 'on'|trans }} {{ calendar.activity.createdAt|format_datetime('short', 'short') }}
|
||||
</span>
|
||||
</li>
|
||||
{% if is_granted('CHILL_ACTIVITY_SEE', calendar.activity) %}
|
||||
|
@@ -89,7 +89,7 @@ class CalendarVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn
|
||||
switch ($attribute) {
|
||||
case self::SEE:
|
||||
case self::CREATE:
|
||||
if (AccompanyingPeriod::STEP_DRAFT === $subject->getStep()) {
|
||||
if (AccompanyingPeriod::STEP_DRAFT === $subject->getStep() || AccompanyingPeriod::STEP_CLOSED === $subject->getStep()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@@ -47,7 +47,7 @@ final class CalendarContextTest extends TestCase
|
||||
{
|
||||
$expected =
|
||||
[
|
||||
'track_datetime' => true,
|
||||
'trackDatetime' => true,
|
||||
'askMainPerson' => true,
|
||||
'mainPersonLabel' => 'docgen.calendar.Destinee',
|
||||
'askThirdParty' => false,
|
||||
@@ -61,7 +61,7 @@ final class CalendarContextTest extends TestCase
|
||||
{
|
||||
$expected =
|
||||
[
|
||||
'track_datetime' => true,
|
||||
'trackDatetime' => true,
|
||||
'askMainPerson' => true,
|
||||
'mainPersonLabel' => 'docgen.calendar.Destinee',
|
||||
'askThirdParty' => false,
|
||||
|
@@ -26,6 +26,7 @@ The calendar item has been successfully removed.: Le rendez-vous a été supprim
|
||||
From the day: Du
|
||||
to the day: au
|
||||
Transform to activity: Transformer en échange
|
||||
Create a new calendar in accompanying course: Créer un rendez-vous dans le parcours
|
||||
Will send SMS: Un SMS de rappel sera envoyé
|
||||
Will not send SMS: Aucun SMS de rappel ne sera envoyé
|
||||
SMS already sent: Un SMS a été envoyé
|
||||
|
@@ -172,11 +172,9 @@ class CustomField
|
||||
/**
|
||||
* Set active.
|
||||
*
|
||||
* @param bool $active
|
||||
*
|
||||
* @return CustomField
|
||||
*/
|
||||
public function setActive($active)
|
||||
public function setActive(bool $active)
|
||||
{
|
||||
$this->active = $active;
|
||||
|
||||
@@ -224,18 +222,16 @@ class CustomField
|
||||
/**
|
||||
* Set order.
|
||||
*
|
||||
* @param float $order
|
||||
*
|
||||
* @return CustomField
|
||||
*/
|
||||
public function setOrdering($order)
|
||||
public function setOrdering(?float $order)
|
||||
{
|
||||
$this->ordering = $order;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setRequired($required)
|
||||
public function setRequired(bool $required)
|
||||
{
|
||||
$this->required = $required;
|
||||
|
||||
@@ -245,7 +241,7 @@ class CustomField
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setSlug($slug)
|
||||
public function setSlug(?string $slug)
|
||||
{
|
||||
$this->slug = $slug;
|
||||
|
||||
@@ -255,11 +251,9 @@ class CustomField
|
||||
/**
|
||||
* Set type.
|
||||
*
|
||||
* @param string $type
|
||||
*
|
||||
* @return CustomField
|
||||
*/
|
||||
public function setType($type)
|
||||
public function setType(?string $type)
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
|
@@ -23,9 +23,9 @@ class Option
|
||||
private bool $active = true;
|
||||
|
||||
/**
|
||||
* @var Collection<Option>
|
||||
* @var Collection<int, Option>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: Option::class, mappedBy: 'parent')]
|
||||
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: Option::class)]
|
||||
private Collection $children;
|
||||
|
||||
#[ORM\Id]
|
||||
@@ -129,7 +129,7 @@ class Option
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setActive($active)
|
||||
public function setActive(bool $active)
|
||||
{
|
||||
$this->active = $active;
|
||||
|
||||
@@ -139,7 +139,7 @@ class Option
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setInternalKey($internal_key)
|
||||
public function setInternalKey(string $internal_key)
|
||||
{
|
||||
$this->internalKey = $internal_key;
|
||||
|
||||
@@ -149,7 +149,7 @@ class Option
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setKey($key)
|
||||
public function setKey(?string $key)
|
||||
{
|
||||
$this->key = $key;
|
||||
|
||||
|
@@ -69,7 +69,7 @@ class CustomFieldsDefaultGroup
|
||||
*
|
||||
* @return CustomFieldsDefaultGroup
|
||||
*/
|
||||
public function setCustomFieldsGroup($customFieldsGroup)
|
||||
public function setCustomFieldsGroup(?CustomFieldsGroup $customFieldsGroup)
|
||||
{
|
||||
$this->customFieldsGroup = $customFieldsGroup;
|
||||
|
||||
@@ -79,11 +79,9 @@ class CustomFieldsDefaultGroup
|
||||
/**
|
||||
* Set entity.
|
||||
*
|
||||
* @param string $entity
|
||||
*
|
||||
* @return CustomFieldsDefaultGroup
|
||||
*/
|
||||
public function setEntity($entity)
|
||||
public function setEntity(?string $entity)
|
||||
{
|
||||
$this->entity = $entity;
|
||||
|
||||
|
@@ -32,9 +32,9 @@ class CustomFieldsGroup
|
||||
* The custom fields of the group.
|
||||
* The custom fields are asc-ordered regarding to their property "ordering".
|
||||
*
|
||||
* @var Collection<CustomField>
|
||||
* @var Collection<int, CustomField>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: CustomField::class, mappedBy: 'customFieldGroup')]
|
||||
#[ORM\OneToMany(mappedBy: 'customFieldGroup', targetEntity: CustomField::class)]
|
||||
#[ORM\OrderBy(['ordering' => \Doctrine\Common\Collections\Criteria::ASC])]
|
||||
private Collection $customFields;
|
||||
|
||||
@@ -165,11 +165,9 @@ class CustomFieldsGroup
|
||||
/**
|
||||
* Set entity.
|
||||
*
|
||||
* @param string $entity
|
||||
*
|
||||
* @return CustomFieldsGroup
|
||||
*/
|
||||
public function setEntity($entity)
|
||||
public function setEntity(?string $entity)
|
||||
{
|
||||
$this->entity = $entity;
|
||||
|
||||
|
@@ -21,14 +21,14 @@ use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
final class RelatorioDriver implements DriverInterface
|
||||
final readonly class RelatorioDriver implements DriverInterface
|
||||
{
|
||||
private readonly string $url;
|
||||
private string $url;
|
||||
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $client,
|
||||
private HttpClientInterface $client,
|
||||
ParameterBagInterface $parameterBag,
|
||||
private readonly LoggerInterface $logger
|
||||
private LoggerInterface $logger
|
||||
) {
|
||||
$this->url = $parameterBag->get('chill_doc_generator')['driver']['relatorio']['url'];
|
||||
}
|
||||
|
@@ -16,11 +16,11 @@ use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
final class DocGeneratorTemplateRepository implements DocGeneratorTemplateRepositoryInterface
|
||||
final readonly class DocGeneratorTemplateRepository implements DocGeneratorTemplateRepositoryInterface
|
||||
{
|
||||
private readonly EntityRepository $repository;
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager, private readonly RequestStack $requestStack)
|
||||
public function __construct(EntityManagerInterface $entityManager, private RequestStack $requestStack)
|
||||
{
|
||||
$this->repository = $entityManager->getRepository(DocGeneratorTemplate::class);
|
||||
}
|
||||
|
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocGeneratorBundle\Test;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
/**
|
||||
* @template T of object
|
||||
*/
|
||||
abstract class DocGenNormalizerTestAbstract extends KernelTestCase
|
||||
{
|
||||
public function testNullValueHasSameKeysAsNull(): void
|
||||
{
|
||||
$normalizedObject = $this->getNormalizer()->normalize($this->provideNotNullObject(), 'docgen', [
|
||||
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => $this->provideDocGenExpectClass(),
|
||||
]);
|
||||
$nullNormalizedObject = $this->getNormalizer()->normalize(null, 'docgen', [
|
||||
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => $this->provideDocGenExpectClass(),
|
||||
]);
|
||||
|
||||
self::assertEqualsCanonicalizing(array_keys($normalizedObject), array_keys($nullNormalizedObject));
|
||||
self::assertArrayHasKey('isNull', $nullNormalizedObject, 'each object must have an "isNull" key');
|
||||
self::assertTrue($nullNormalizedObject['isNull'], 'isNull key must be true for null objects');
|
||||
self::assertFalse($normalizedObject['isNull'], 'isNull key must be false for null objects');
|
||||
|
||||
foreach ($normalizedObject as $key => $value) {
|
||||
if (in_array($key, ['isNull', 'type'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
if (array_is_list($value)) {
|
||||
self::assertEquals([], $nullNormalizedObject[$key], "list must be serialized as an empty array, in {$key}");
|
||||
} else {
|
||||
self::assertEqualsCanonicalizing(array_keys($value), array_keys($nullNormalizedObject[$key]), "sub-object must have the same keys, in {$key}");
|
||||
}
|
||||
} elseif (is_string($value)) {
|
||||
self::assertEquals('', $nullNormalizedObject[$key], 'strings must be ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return T
|
||||
*/
|
||||
abstract public function provideNotNullObject(): object;
|
||||
|
||||
/**
|
||||
* @return class-string<T>
|
||||
*/
|
||||
abstract public function provideDocGenExpectClass(): string;
|
||||
|
||||
abstract public function getNormalizer(): NormalizerInterface;
|
||||
}
|
@@ -89,6 +89,7 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf
|
||||
$g = new SignedUrlPost(
|
||||
$url = $this->generateUrl($object_name),
|
||||
$expires,
|
||||
$object_name,
|
||||
$this->max_post_file_size,
|
||||
$max_file_count,
|
||||
$submit_delay,
|
||||
@@ -127,7 +128,7 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf
|
||||
];
|
||||
$url = $url.'?'.\http_build_query($args);
|
||||
|
||||
$signature = new SignedUrl(strtoupper($method), $url, $expires);
|
||||
$signature = new SignedUrl(strtoupper($method), $url, $expires, $object_name);
|
||||
|
||||
$this->event_dispatcher->dispatch(
|
||||
new TempUrlGenerateEvent($signature)
|
||||
|
@@ -21,6 +21,8 @@ readonly class SignedUrl
|
||||
#[Serializer\Groups(['read'])]
|
||||
public string $url,
|
||||
public \DateTimeImmutable $expires,
|
||||
#[Serializer\Groups(['read'])]
|
||||
public string $object_name,
|
||||
) {}
|
||||
|
||||
#[Serializer\Groups(['read'])]
|
||||
|
@@ -18,6 +18,7 @@ readonly class SignedUrlPost extends SignedUrl
|
||||
public function __construct(
|
||||
string $url,
|
||||
\DateTimeImmutable $expires,
|
||||
string $object_name,
|
||||
#[Serializer\Groups(['read'])]
|
||||
public int $max_file_size,
|
||||
#[Serializer\Groups(['read'])]
|
||||
@@ -31,6 +32,6 @@ readonly class SignedUrlPost extends SignedUrl
|
||||
#[Serializer\Groups(['read'])]
|
||||
public string $signature,
|
||||
) {
|
||||
parent::__construct('POST', $url, $expires);
|
||||
parent::__construct('POST', $url, $expires, $object_name);
|
||||
}
|
||||
}
|
||||
|
@@ -26,6 +26,8 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneParser;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
|
||||
/**
|
||||
* Class DocumentPersonController.
|
||||
@@ -40,6 +42,8 @@ class DocumentPersonController extends AbstractController
|
||||
protected TranslatorInterface $translator,
|
||||
protected EventDispatcherInterface $eventDispatcher,
|
||||
protected AuthorizationHelper $authorizationHelper,
|
||||
protected PDFSignatureZoneParser $PDFSignatureZoneParser,
|
||||
protected StoredObjectManagerInterface $storedObjectManagerInterface,
|
||||
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry
|
||||
) {}
|
||||
|
||||
@@ -197,4 +201,36 @@ 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]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessage;
|
||||
use Chill\DocStoreBundle\Service\Signature\PDFPage;
|
||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
class SignatureRequestController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly MessageBusInterface $messageBus,
|
||||
private readonly StoredObjectManagerInterface $storedObjectManager,
|
||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||
) {}
|
||||
|
||||
#[Route('/api/1.0/document/workflow/{id}/signature-request', name: 'chill_docstore_signature_request')]
|
||||
public function processSignature(EntityWorkflowStepSignature $signature, Request $request): JsonResponse
|
||||
{
|
||||
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
|
||||
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
|
||||
$content = $this->storedObjectManager->read($storedObject);
|
||||
|
||||
$data = \json_decode((string) $request->getContent(), true, 512, JSON_THROW_ON_ERROR); // TODO parse payload: json_decode ou, mieux, dataTransfertObject
|
||||
$zone = new PDFSignatureZone(
|
||||
$data['zone']['index'],
|
||||
$data['zone']['x'],
|
||||
$data['zone']['y'],
|
||||
$data['zone']['height'],
|
||||
$data['zone']['width'],
|
||||
new PDFPage($data['zone']['PDFPage']['index'], $data['zone']['PDFPage']['width'], $data['zone']['PDFPage']['height'])
|
||||
);
|
||||
|
||||
$this->messageBus->dispatch(new RequestPdfSignMessage(
|
||||
$signature->getId(),
|
||||
$zone,
|
||||
$data['zone']['index'],
|
||||
'test signature', // reason (string)
|
||||
'Mme Caroline Diallo', // signerText (string)
|
||||
$content
|
||||
));
|
||||
|
||||
return new JsonResponse(null, JsonResponse::HTTP_OK, []);
|
||||
}
|
||||
|
||||
#[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, []);
|
||||
}
|
||||
}
|
@@ -14,6 +14,7 @@ namespace Chill\DocStoreBundle\DependencyInjection;
|
||||
use Chill\DocStoreBundle\Controller\StoredObjectApiController;
|
||||
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
|
||||
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
|
||||
@@ -35,6 +36,8 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
|
||||
|
||||
$container->setParameter('chill_doc_store', $config);
|
||||
|
||||
$container->registerForAutoconfiguration(StoredObjectVoterInterface::class)->addTag('stored_object_voter');
|
||||
|
||||
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
|
||||
$loader->load('services.yaml');
|
||||
$loader->load('services/controller.yaml');
|
||||
@@ -42,6 +45,7 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
|
||||
$loader->load('services/fixtures.yaml');
|
||||
$loader->load('services/form.yaml');
|
||||
$loader->load('services/templating.yaml');
|
||||
$loader->load('services/security.yaml');
|
||||
}
|
||||
|
||||
public function prepend(ContainerBuilder $container)
|
||||
|
@@ -129,7 +129,7 @@ class Document implements TrackCreationInterface, TrackUpdateInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUser($user): self
|
||||
public function setUser(?\Chill\MainBundle\Entity\User $user): self
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
|
@@ -86,7 +86,7 @@ class DocumentCategory
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setDocumentClass($documentClass): self
|
||||
public function setDocumentClass(?string $documentClass): self
|
||||
{
|
||||
$this->documentClass = $documentClass;
|
||||
|
||||
|
@@ -55,14 +55,14 @@ class PersonDocument extends Document implements HasCenterInterface, HasScopeInt
|
||||
return $this->scope;
|
||||
}
|
||||
|
||||
public function setPerson($person): self
|
||||
public function setPerson(Person $person): self
|
||||
{
|
||||
$this->person = $person;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setScope($scope): self
|
||||
public function setScope(?Scope $scope): self
|
||||
{
|
||||
$this->scope = $scope;
|
||||
|
||||
|
@@ -313,4 +313,19 @@ class StoredObject implements Document, TrackCreationInterface
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function saveHistory(): void
|
||||
{
|
||||
if ('' === $this->getFilename()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->datas['history'][] = [
|
||||
'filename' => $this->getFilename(),
|
||||
'iv' => $this->getIv(),
|
||||
'key_infos' => $this->getKeyInfos(),
|
||||
'type' => $this->getType(),
|
||||
'before' => (new \DateTimeImmutable('now'))->getTimestamp(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@@ -57,8 +57,8 @@ class StoredObjectDataMapper implements DataMapperInterface
|
||||
|
||||
/** @var StoredObject $viewData */
|
||||
if ($viewData->getFilename() !== $forms['stored_object']->getData()['filename']) {
|
||||
// we do not want to erase the previous object
|
||||
$viewData = new StoredObject();
|
||||
// we want to keep the previous history
|
||||
$viewData->saveHistory();
|
||||
}
|
||||
|
||||
$viewData->setFilename($forms['stored_object']->getData()['filename']);
|
||||
|
@@ -12,13 +12,14 @@ declare(strict_types=1);
|
||||
namespace Chill\DocStoreBundle\Repository;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
class AccompanyingCourseDocumentRepository implements ObjectRepository
|
||||
class AccompanyingCourseDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface
|
||||
{
|
||||
private readonly EntityRepository $repository;
|
||||
|
||||
@@ -45,6 +46,16 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository
|
||||
return $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('d');
|
||||
$query = $qb->where('d.object = :storedObject')
|
||||
->setParameter('storedObject', $storedObject)
|
||||
->getQuery();
|
||||
|
||||
return $query->getOneOrNullResult();
|
||||
}
|
||||
|
||||
public function find($id): ?AccompanyingCourseDocument
|
||||
{
|
||||
return $this->repository->find($id);
|
||||
@@ -55,7 +66,7 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository
|
||||
return $this->repository->findAll();
|
||||
}
|
||||
|
||||
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null)
|
||||
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
|
||||
{
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
@@ -65,7 +76,7 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository
|
||||
return $this->findOneBy($criteria);
|
||||
}
|
||||
|
||||
public function getClassName()
|
||||
public function getClassName(): string
|
||||
{
|
||||
return AccompanyingCourseDocument::class;
|
||||
}
|
||||
|
@@ -0,0 +1,19 @@
|
||||
<?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\StoredObject;
|
||||
|
||||
interface AssociatedEntityToStoredObjectInterface
|
||||
{
|
||||
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object;
|
||||
}
|
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\DocStoreBundle\Repository;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\PersonDocument;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
@@ -19,7 +20,7 @@ use Doctrine\Persistence\ObjectRepository;
|
||||
/**
|
||||
* @template ObjectRepository<PersonDocument::class>
|
||||
*/
|
||||
readonly class PersonDocumentRepository implements ObjectRepository
|
||||
readonly class PersonDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface
|
||||
{
|
||||
private EntityRepository $repository;
|
||||
|
||||
@@ -53,4 +54,14 @@ readonly class PersonDocumentRepository implements ObjectRepository
|
||||
{
|
||||
return PersonDocument::class;
|
||||
}
|
||||
|
||||
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('d');
|
||||
$query = $qb->where('d.object = :storedObject')
|
||||
->setParameter('storedObject', $storedObject)
|
||||
->getQuery();
|
||||
|
||||
return $query->getOneOrNullResult();
|
||||
}
|
||||
}
|
||||
|
@@ -26,11 +26,11 @@ export interface StoredObject {
|
||||
}
|
||||
|
||||
export interface StoredObjectCreated {
|
||||
status: "stored_object_created",
|
||||
filename: string,
|
||||
iv: Uint8Array,
|
||||
keyInfos: object,
|
||||
type: string,
|
||||
status: "stored_object_created",
|
||||
filename: string,
|
||||
iv: Uint8Array,
|
||||
keyInfos: object,
|
||||
type: string,
|
||||
}
|
||||
|
||||
export interface StoredObjectStatusChange {
|
||||
@@ -51,14 +51,37 @@ export type WopiEditButtonExecutableBeforeLeaveFunction = {
|
||||
* 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,
|
||||
}
|
||||
export interface SignatureZone {
|
||||
index: number | null,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
PDFPage: PDFPage,
|
||||
}
|
||||
|
||||
export interface Signature {
|
||||
id: number,
|
||||
storedObject: StoredObject,
|
||||
zones: SignatureZone[],
|
||||
}
|
||||
|
||||
export type SignedState = 'pending' | 'signed' | 'rejected' | 'canceled' | 'error';
|
||||
|
||||
export type CanvasEvent = 'select' | 'add';
|
@@ -4,13 +4,13 @@
|
||||
Actions
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li v-if="props.canEdit && is_extension_editable(props.storedObject.type)">
|
||||
<li v-if="props.canEdit && is_extension_editable(props.storedObject.type) && props.storedObject.status !== 'stored_object_created'">
|
||||
<wopi-edit-button :stored-object="props.storedObject" :classes="{'dropdown-item': true}" :execute-before-leave="props.executeBeforeLeave"></wopi-edit-button>
|
||||
</li>
|
||||
<li v-if="props.canEdit && is_extension_editable(props.storedObject.type) && props.davLink !== undefined && props.davLinkExpiration !== undefined">
|
||||
<desktop-edit-button :classes="{'dropdown-item': true}" :edit-link="props.davLink" :expiration-link="props.davLinkExpiration"></desktop-edit-button>
|
||||
</li>
|
||||
<li v-if="props.storedObject.type != 'application/pdf' && is_extension_viewable(props.storedObject.type) && props.canConvertPdf">
|
||||
<li v-if="props.storedObject.type != 'application/pdf' && is_extension_viewable(props.storedObject.type) && props.canConvertPdf && props.storedObject.status !== 'stored_object_created'">
|
||||
<convert-button :stored-object="props.storedObject" :filename="filename" :classes="{'dropdown-item': true}"></convert-button>
|
||||
</li>
|
||||
<li v-if="props.canDownload">
|
||||
|
@@ -0,0 +1,559 @@
|
||||
<template>
|
||||
<teleport to="body">
|
||||
<modal v-if="modalOpen" @close="modalOpen = false">
|
||||
<template v-slot:header>
|
||||
<h2>{{ $t("signature_confirmation") }}</h2>
|
||||
</template>
|
||||
<template v-slot:body>
|
||||
<div class="signature-modal-body text-center" v-if="loading">
|
||||
<p>{{ $t("electronic_signature_in_progress") }}</p>
|
||||
<div class="loading">
|
||||
<i
|
||||
class="fa fa-circle-o-notch fa-spin fa-3x"
|
||||
:title="$t('loading')"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="signature-modal-body text-center" v-else>
|
||||
<p>{{ $t("you_are_going_to_sign") }}</p>
|
||||
<p>{{ $t("are_you_sure") }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<button class="btn btn-action" @click.prevent="confirmSign">
|
||||
{{ $t("yes") }}
|
||||
</button>
|
||||
</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">
|
||||
<button
|
||||
class="btn btn-light btn-sm"
|
||||
:disabled="page <= 1"
|
||||
@click="turnPage(-1)"
|
||||
>
|
||||
❮
|
||||
</button>
|
||||
<span>page {{ page }} / {{ pageCount }}</span>
|
||||
<button
|
||||
class="btn btn-light btn-sm"
|
||||
:disabled="page >= pageCount"
|
||||
@click="turnPage(1)"
|
||||
>
|
||||
❯
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="multiPage" class="col-12 text-center">
|
||||
<canvas
|
||||
v-for="p in pageCount"
|
||||
:key="p"
|
||||
class="m-auto"
|
||||
:id="`canvas-${p}`"
|
||||
></canvas>
|
||||
</div>
|
||||
<div v-else class="col-12 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="row mb-3">
|
||||
<div class="col-12 d-flex justify-content-end">
|
||||
<div class="col-4 col-xl-3 gap-2 d-grid">
|
||||
<button
|
||||
v-if="adding"
|
||||
class="btn btn-misc btn-cancel me-2 btn-sm"
|
||||
@click="removeNewZone()"
|
||||
>
|
||||
{{ $t("remove_sign_zone") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-4 gap-2 d-grid">
|
||||
<button
|
||||
class="btn btn-create btn-sm"
|
||||
:class="{ active: canvasEvent === 'add' }"
|
||||
@click="toggleAddZone()"
|
||||
>
|
||||
{{ $t("add_sign_zone") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<button
|
||||
class="btn btn-action me-2"
|
||||
:disabled="!userSignatureZone"
|
||||
@click="sign"
|
||||
>
|
||||
{{ $t("sign") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-8 d-flex justify-content-end">
|
||||
<button
|
||||
class="btn btn-misc me-2"
|
||||
:hidden="!userSignatureZone"
|
||||
@click="undoSign"
|
||||
v-if="signature.zones.length > 1"
|
||||
>
|
||||
{{ $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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, Ref, reactive } from "vue";
|
||||
import { useToast } from "vue-toast-notification";
|
||||
import "vue-toast-notification/dist/theme-sugar.css";
|
||||
import {
|
||||
CanvasEvent,
|
||||
Signature,
|
||||
SignatureZone,
|
||||
SignedState,
|
||||
} from "../../types";
|
||||
import { makeFetch } from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
||||
import * as pdfjsLib from "pdfjs-dist";
|
||||
import {
|
||||
PDFDocumentProxy,
|
||||
PDFPageProxy,
|
||||
} from "pdfjs-dist/types/src/display/api";
|
||||
|
||||
// @ts-ignore
|
||||
import * as PdfWorker from "pdfjs-dist/build/pdf.worker.mjs";
|
||||
console.log(PdfWorker); // incredible but this is needed
|
||||
|
||||
// import { PdfWorker } from 'pdfjs-dist/build/pdf.worker.mjs'
|
||||
// pdfjsLib.GlobalWorkerOptions.workerSrc = PdfWorker;
|
||||
|
||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
||||
import {
|
||||
build_download_info_link,
|
||||
download_and_decrypt_doc,
|
||||
} from "../StoredObjectButton/helpers";
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = "pdfjs-dist/build/pdf.worker.mjs";
|
||||
|
||||
const multiPage: Ref<boolean> = ref(true);
|
||||
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 pdf = {} as PDFDocumentProxy;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
signature: Signature;
|
||||
}
|
||||
}
|
||||
|
||||
const $toast = useToast();
|
||||
|
||||
const signature = window.signature;
|
||||
const urlInfo = build_download_info_link(signature.storedObject.filename);
|
||||
|
||||
console.log(signature);
|
||||
|
||||
const mountPdf = async (url: string) => {
|
||||
const loadingTask = pdfjsLib.getDocument(url);
|
||||
pdf = await loadingTask.promise;
|
||||
pageCount.value = pdf.numPages;
|
||||
if (multiPage.value) {
|
||||
await setAllPages();
|
||||
} else {
|
||||
await setPage(1);
|
||||
}
|
||||
};
|
||||
|
||||
const getRenderContext = (pdfPage: PDFPageProxy) => {
|
||||
const scale = 1;
|
||||
const viewport = pdfPage.getViewport({ scale });
|
||||
let canvas;
|
||||
if (multiPage.value) {
|
||||
canvas = document.getElementById(
|
||||
`canvas-${pdfPage.pageNumber}`
|
||||
) as HTMLCanvasElement;
|
||||
} else {
|
||||
canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
||||
}
|
||||
const context = canvas.getContext("2d") as CanvasRenderingContext2D;
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
return {
|
||||
canvasContext: context,
|
||||
viewport: viewport,
|
||||
};
|
||||
};
|
||||
|
||||
const setAllPages = async () =>
|
||||
Array.from(Array(pageCount.value).keys()).map((p) => setPage(p + 1));
|
||||
|
||||
const setPage = async (page: number) => {
|
||||
const pdfPage = await pdf.getPage(page);
|
||||
const renderContext = getRenderContext(pdfPage);
|
||||
await pdfPage.render(renderContext);
|
||||
};
|
||||
|
||||
async function downloadAndOpen(): Promise<Blob> {
|
||||
let raw;
|
||||
try {
|
||||
raw = await download_and_decrypt_doc(
|
||||
urlInfo,
|
||||
signature.storedObject.keyInfos,
|
||||
new Uint8Array(signature.storedObject.iv)
|
||||
);
|
||||
} 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", canvasClick, false);
|
||||
setTimeout(() => addZones(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
|
||||
) =>
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
const selectZoneOnCanvas = (e: PointerEvent, canvas: HTMLCanvasElement) =>
|
||||
signature.zones
|
||||
.filter((z) => z.PDFPage.index + 1 === page.value)
|
||||
.map((z) => {
|
||||
if (
|
||||
hitSignature(z, [e.offsetX, e.offsetY], canvas.width, canvas.height)
|
||||
) {
|
||||
if (userSignatureZone.value === null) {
|
||||
selectZone(z, canvas);
|
||||
} else {
|
||||
if (userSignatureZone.value.index === z.index) {
|
||||
sign();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const canvasClick = (e: PointerEvent) => {
|
||||
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
||||
canvasEvent.value === "select"
|
||||
? selectZoneOnCanvas(e, canvas)
|
||||
: addZoneOnCanvas(e, canvas);
|
||||
};
|
||||
|
||||
const turnPage = async (upOrDown: number) => {
|
||||
userSignatureZone.value = null;
|
||||
page.value = page.value + upOrDown;
|
||||
await setPage(page.value);
|
||||
setTimeout(() => addZones(page.value), 200);
|
||||
};
|
||||
|
||||
const turnSignature = async (upOrDown: number) => {
|
||||
let zoneIndex = userSignatureZone.value?.index ?? -1;
|
||||
if (zoneIndex < -1) {
|
||||
zoneIndex = -1;
|
||||
}
|
||||
if (zoneIndex < signature.zones.length) {
|
||||
zoneIndex = zoneIndex + upOrDown;
|
||||
} else {
|
||||
zoneIndex = 0;
|
||||
}
|
||||
let currentZone = signature.zones[zoneIndex];
|
||||
if (currentZone) {
|
||||
page.value = currentZone.PDFPage.index + 1;
|
||||
userSignatureZone.value = currentZone;
|
||||
const canvas = document.querySelectorAll("canvas")[0];
|
||||
selectZone(currentZone, canvas);
|
||||
}
|
||||
};
|
||||
|
||||
const drawZone = (
|
||||
zone: SignatureZone,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
canvasWidth: number,
|
||||
canvasHeight: number
|
||||
) => {
|
||||
const unselectedBlue = "#007bff";
|
||||
const selectedBlue = "#034286";
|
||||
ctx.strokeStyle =
|
||||
userSignatureZone.value?.index === zone.index
|
||||
? selectedBlue
|
||||
: unselectedBlue;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineJoin = "bevel";
|
||||
ctx.strokeRect(
|
||||
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, 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);
|
||||
} else {
|
||||
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 canvas = document.querySelectorAll("canvas")[0];
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx) {
|
||||
signature.zones
|
||||
.filter((z) => z.PDFPage.index + 1 === page)
|
||||
.map((z) => drawZone(z, ctx, canvas.width, canvas.height));
|
||||
}
|
||||
};
|
||||
|
||||
const checkSignature = () => {
|
||||
const url = `/api/1.0/document/workflow/${signature.id}/check-signature`;
|
||||
return makeFetch("GET", url)
|
||||
.then((r) => {
|
||||
signedState.value = r as SignedState;
|
||||
checkForReady();
|
||||
})
|
||||
.catch((error) => {
|
||||
signedState.value = "error";
|
||||
console.log("Error while checking the signature", error);
|
||||
$toast.error(
|
||||
`Erreur lors de la vérification de la signature: ${error.txt}`
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const maxTryForReady = 60; //2 minutes for trying to sign
|
||||
let tryForReady = 0;
|
||||
|
||||
const stopTrySigning = () => {
|
||||
loading.value = false;
|
||||
modalOpen.value = false;
|
||||
};
|
||||
|
||||
const checkForReady = () => {
|
||||
if (tryForReady > maxTryForReady) {
|
||||
stopTrySigning();
|
||||
tryForReady = 0;
|
||||
console.log("Reached the maximum number of tentative to try signing");
|
||||
$toast.error(
|
||||
"Le nombre maximum de tentatives pour essayer de signer est atteint"
|
||||
);
|
||||
}
|
||||
if (signedState.value === "rejected") {
|
||||
stopTrySigning();
|
||||
console.log("Signature rejected by the server");
|
||||
$toast.error("Signature rejetée par le serveur");
|
||||
}
|
||||
if (signedState.value === "canceled") {
|
||||
stopTrySigning();
|
||||
console.log("Signature canceled");
|
||||
$toast.error("Signature annulée");
|
||||
}
|
||||
if (signedState.value === "pending") {
|
||||
tryForReady = tryForReady + 1;
|
||||
setTimeout(() => checkSignature(), 2000);
|
||||
} else {
|
||||
stopTrySigning();
|
||||
if (signedState.value === "signed") {
|
||||
userSignatureZone.value = null;
|
||||
downloadAndOpen();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const sign = () => (modalOpen.value = true);
|
||||
|
||||
const confirmSign = () => {
|
||||
loading.value = true;
|
||||
const url = `/api/1.0/document/workflow/${signature.id}/signature-request`;
|
||||
const body = {
|
||||
storedObject: signature.storedObject,
|
||||
zone: userSignatureZone.value,
|
||||
};
|
||||
makeFetch("POST", url, body)
|
||||
.then((r) => {
|
||||
checkForReady();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Error while posting the signature", error);
|
||||
stopTrySigning();
|
||||
$toast.error(
|
||||
`Erreur lors de la soumission de la signature: ${error.txt}`
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const undoSign = async () => {
|
||||
signature.zones = signature.zones.filter((z) => z.index !== null);
|
||||
await setPage(page.value);
|
||||
setTimeout(() => addZones(page.value), 200);
|
||||
userSignatureZone.value = null;
|
||||
adding.value = false;
|
||||
};
|
||||
|
||||
const toggleAddZone = () => {
|
||||
canvasEvent.value === "select"
|
||||
? (canvasEvent.value = "add")
|
||||
: (canvasEvent.value = "select");
|
||||
};
|
||||
|
||||
const addZoneOnCanvas = (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);
|
||||
|
||||
setTimeout(() => addZones(page.value), 200);
|
||||
canvasEvent.value = "select";
|
||||
adding.value = true;
|
||||
};
|
||||
|
||||
const removeNewZone = async () => {
|
||||
signature.zones = signature.zones.filter((z) => z.index !== null);
|
||||
userSignatureZone.value = null;
|
||||
await setPage(page.value);
|
||||
setTimeout(() => addZones(page.value), 200);
|
||||
canvasEvent.value = "select";
|
||||
adding.value = false;
|
||||
};
|
||||
|
||||
downloadAndOpen();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
canvas {
|
||||
box-shadow: 0 20px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
div#action-buttons {
|
||||
position: sticky;
|
||||
bottom: 0px;
|
||||
background-color: white;
|
||||
z-index: 100;
|
||||
}
|
||||
div#turn-page {
|
||||
span {
|
||||
font-size: 0.8rem;
|
||||
margin: 0 0.4rem;
|
||||
}
|
||||
}
|
||||
div.signature-modal-body {
|
||||
height: 8rem;
|
||||
}
|
||||
</style>
|
||||
|
@@ -0,0 +1,32 @@
|
||||
import { createApp } from "vue";
|
||||
// @ts-ignore
|
||||
import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
|
||||
import App from "./App.vue";
|
||||
|
||||
const appMessages = {
|
||||
fr: {
|
||||
yes: 'Oui',
|
||||
are_you_sure: 'Êtes-vous sûr·e?',
|
||||
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',
|
||||
cancel: 'Annuler',
|
||||
cancel_signing: 'Refuser de signer',
|
||||
last_sign_zone: 'Zone de signature précédente',
|
||||
next_sign_zone: 'Zone de signature suivante',
|
||||
electronic_signature_in_progress: 'Signature électronique en cours...',
|
||||
loading: 'Chargement...',
|
||||
add_sign_zone: 'Ajouter une zone de signature',
|
||||
remove_sign_zone: 'Enlever la zone',
|
||||
}
|
||||
}
|
||||
|
||||
const i18n = _createI18n(appMessages);
|
||||
|
||||
const app = createApp({
|
||||
template: `<app></app>`,
|
||||
})
|
||||
.use(i18n)
|
||||
.component("app", App)
|
||||
.mount("#document-signature");
|
@@ -13,7 +13,7 @@ import {reactive} from "vue";
|
||||
import {StoredObject, StoredObjectCreated} from "../../types";
|
||||
|
||||
interface ConvertButtonConfig {
|
||||
storedObject: StoredObject|StoredObjectCreated,
|
||||
storedObject: StoredObject,
|
||||
classes: { [key: string]: boolean},
|
||||
filename?: string,
|
||||
};
|
||||
|
@@ -11,7 +11,7 @@ import {build_wopi_editor_link} from "./helpers";
|
||||
import {StoredObject, StoredObjectCreated, WopiEditButtonExecutableBeforeLeaveFunction} from "../../types";
|
||||
|
||||
interface WopiEditButtonConfig {
|
||||
storedObject: StoredObject|StoredObjectCreated,
|
||||
storedObject: StoredObject,
|
||||
returnPath?: string,
|
||||
classes: {[k: string] : boolean},
|
||||
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction,
|
||||
|
@@ -71,7 +71,7 @@
|
||||
</li>
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
|
||||
<li>
|
||||
{{ document.object|chill_document_button_group(document.title, is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document)) }}
|
||||
{{ document.object|chill_document_button_group(document.title) }}
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a>
|
||||
@@ -90,7 +90,7 @@
|
||||
{% else %}
|
||||
{% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %}
|
||||
<li>
|
||||
{{ document.object|chill_document_button_group(document.title, is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document)) }}
|
||||
{{ document.object|chill_document_button_group(document.title) }}
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ path('person_document_show', {'person': person.id, 'id': document.id}) }}" class="btn btn-show"></a>
|
||||
|
@@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="shortcut icon" href="{{ asset('build/images/favicon.ico') }}" type="image/x-icon">
|
||||
<title>Signature</title>
|
||||
|
||||
{{ encore_entry_link_tags('mod_bootstrap') }}
|
||||
{{ encore_entry_link_tags('mod_forkawesome') }}
|
||||
{{ encore_entry_link_tags('chill') }}
|
||||
{{ encore_entry_link_tags('vue_document_signature') }}
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
{% block js %}
|
||||
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
|
||||
<script type="text/javascript">
|
||||
window.signature = {{ signature|json_encode|raw }};
|
||||
</script>
|
||||
{{ encore_entry_script_tags('vue_document_signature') }}
|
||||
{% endblock %}
|
||||
|
||||
<div class="content" id="content">
|
||||
<div class="container-xxl">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-12 col-lg-9 my-5 m-auto">
|
||||
<h4>{{ 'Document %title%' | trans({ '%title%': document.title }) }}</h4>
|
||||
<div class="row" id="document-signature"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@@ -70,12 +70,12 @@ class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements Prov
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function supports($attribute, $subject): bool
|
||||
public function supports($attribute, $subject): bool
|
||||
{
|
||||
return $this->voterHelper->supports($attribute, $subject);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||
public function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||
{
|
||||
if (!$token->getUser() instanceof User) {
|
||||
return false;
|
||||
|
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\DocStoreBundle\Security\Authorization;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
@@ -22,6 +23,7 @@ final class AsyncUploadVoter extends Voter
|
||||
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly StoredObjectRepository $storedObjectRepository
|
||||
) {}
|
||||
|
||||
protected function supports($attribute, $subject): bool
|
||||
@@ -32,10 +34,16 @@ final class AsyncUploadVoter extends Voter
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||
{
|
||||
/** @var SignedUrl $subject */
|
||||
if (!in_array($subject->method, ['POST', 'GET', 'HEAD'], true)) {
|
||||
if (!in_array($subject->method, ['POST', 'GET', 'HEAD', 'PUT'], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN');
|
||||
$storedObject = $this->storedObjectRepository->findOneBy(['filename' => $subject->object_name]);
|
||||
|
||||
return match ($subject->method) {
|
||||
'GET', 'HEAD' => $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject),
|
||||
'PUT' => $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject),
|
||||
'POST' => $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -12,9 +12,10 @@ declare(strict_types=1);
|
||||
namespace Chill\DocStoreBundle\Security\Authorization;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Security\Guard\DavTokenAuthenticationEventSubscriber;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
/**
|
||||
* Voter for the content of a stored object.
|
||||
@@ -23,6 +24,10 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
*/
|
||||
class StoredObjectVoter extends Voter
|
||||
{
|
||||
public const LOG_PREFIX = '[stored object voter] ';
|
||||
|
||||
public function __construct(private readonly Security $security, private readonly iterable $storedObjectVoters, private readonly LoggerInterface $logger) {}
|
||||
|
||||
protected function supports($attribute, $subject): bool
|
||||
{
|
||||
return StoredObjectRoleEnum::tryFrom($attribute) instanceof StoredObjectRoleEnum
|
||||
@@ -32,24 +37,28 @@ class StoredObjectVoter extends Voter
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||
{
|
||||
/** @var StoredObject $subject */
|
||||
if (
|
||||
!$token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)
|
||||
|| $subject->getUuid()->toString() !== $token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)
|
||||
) {
|
||||
return false;
|
||||
$attributeAsEnum = StoredObjectRoleEnum::from($attribute);
|
||||
|
||||
// Loop through context-specific voters
|
||||
foreach ($this->storedObjectVoters as $storedObjectVoter) {
|
||||
if ($storedObjectVoter->supports($attributeAsEnum, $subject)) {
|
||||
$grant = $storedObjectVoter->voteOnAttribute($attributeAsEnum, $subject, $token);
|
||||
|
||||
if (false === $grant) {
|
||||
$this->logger->debug(self::LOG_PREFIX.'deny access by storedObjectVoter', ['stored_object_voter' => $storedObjectVoter::class]);
|
||||
}
|
||||
|
||||
return $grant;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)) {
|
||||
return false;
|
||||
// User role-based fallback
|
||||
if ($this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN')) {
|
||||
// TODO: this maybe considered as a security issue, as all authenticated users can reach a stored object which
|
||||
// is potentially detached from an existing entity.
|
||||
return true;
|
||||
}
|
||||
|
||||
$askedRole = StoredObjectRoleEnum::from($attribute);
|
||||
$tokenRoleAuthorization =
|
||||
$token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS);
|
||||
|
||||
return match ($askedRole) {
|
||||
StoredObjectRoleEnum::SEE => StoredObjectRoleEnum::EDIT === $tokenRoleAuthorization || StoredObjectRoleEnum::SEE === $tokenRoleAuthorization,
|
||||
StoredObjectRoleEnum::EDIT => StoredObjectRoleEnum::EDIT === $tokenRoleAuthorization
|
||||
};
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@@ -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\Security\Authorization\StoredObjectVoter;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
|
||||
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
|
||||
{
|
||||
abstract protected function getRepository(): AssociatedEntityToStoredObjectInterface;
|
||||
|
||||
/**
|
||||
* @return class-string
|
||||
*/
|
||||
abstract protected function getClass(): string;
|
||||
|
||||
abstract protected function attributeToRole(StoredObjectRoleEnum $attribute): string;
|
||||
|
||||
abstract protected function canBeAssociatedWithWorkflow(): bool;
|
||||
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly ?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null,
|
||||
) {}
|
||||
|
||||
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool
|
||||
{
|
||||
$class = $this->getClass();
|
||||
|
||||
return $this->getRepository()->findAssociatedEntityToStoredObject($subject) instanceof $class;
|
||||
}
|
||||
|
||||
public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool
|
||||
{
|
||||
// Retrieve the related accompanying course document
|
||||
$entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);
|
||||
|
||||
// Determine the attribute to pass to AccompanyingCourseDocumentVoter
|
||||
$voterAttribute = $this->attributeToRole($attribute);
|
||||
|
||||
if (false === $this->security->isGranted($voterAttribute, $entity)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (StoredObjectRoleEnum::SEE !== $attribute && $this->canBeAssociatedWithWorkflow()) {
|
||||
if (null === $this->workflowDocumentService) {
|
||||
throw new \LogicException('Provide a workflow document service');
|
||||
}
|
||||
|
||||
return $this->workflowDocumentService->notBlockedByWorkflow($entity);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
||||
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredObjectVoter
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AccompanyingCourseDocumentRepository $repository,
|
||||
Security $security,
|
||||
WorkflowStoredObjectPermissionHelper $workflowDocumentService
|
||||
) {
|
||||
parent::__construct($security, $workflowDocumentService);
|
||||
}
|
||||
|
||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||
{
|
||||
return $this->repository;
|
||||
}
|
||||
|
||||
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
|
||||
{
|
||||
return match ($attribute) {
|
||||
StoredObjectRoleEnum::EDIT => AccompanyingCourseDocumentVoter::UPDATE,
|
||||
StoredObjectRoleEnum::SEE => AccompanyingCourseDocumentVoter::SEE_DETAILS,
|
||||
};
|
||||
}
|
||||
|
||||
protected function getClass(): string
|
||||
{
|
||||
return AccompanyingCourseDocument::class;
|
||||
}
|
||||
|
||||
protected function canBeAssociatedWithWorkflow(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\PersonDocument;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
|
||||
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PersonDocumentRepository $repository,
|
||||
Security $security,
|
||||
WorkflowStoredObjectPermissionHelper $workflowDocumentService
|
||||
) {
|
||||
parent::__construct($security, $workflowDocumentService);
|
||||
}
|
||||
|
||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
||||
{
|
||||
return $this->repository;
|
||||
}
|
||||
|
||||
protected function getClass(): string
|
||||
{
|
||||
return PersonDocument::class;
|
||||
}
|
||||
|
||||
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
|
||||
{
|
||||
return match ($attribute) {
|
||||
StoredObjectRoleEnum::EDIT => PersonDocumentVoter::UPDATE,
|
||||
StoredObjectRoleEnum::SEE => PersonDocumentVoter::SEE_DETAILS,
|
||||
};
|
||||
}
|
||||
|
||||
protected function canBeAssociatedWithWorkflow(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -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\Security\Authorization;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
|
||||
interface StoredObjectVoterInterface
|
||||
{
|
||||
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool;
|
||||
|
||||
public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool;
|
||||
}
|
@@ -15,6 +15,7 @@ use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
@@ -32,7 +33,8 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa
|
||||
|
||||
public function __construct(
|
||||
private readonly JWTDavTokenProviderInterface $JWTDavTokenProvider,
|
||||
private readonly UrlGeneratorInterface $urlGenerator
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
private readonly Security $security
|
||||
) {}
|
||||
|
||||
public function normalize($object, ?string $format = null, array $context = [])
|
||||
@@ -55,13 +57,13 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa
|
||||
// deprecated property
|
||||
$datas['creationDate'] = $datas['createdAt'];
|
||||
|
||||
$canDavSee = in_array(self::ADD_DAV_SEE_LINK_CONTEXT, $context['groups'] ?? [], true);
|
||||
$canDavEdit = in_array(self::ADD_DAV_EDIT_LINK_CONTEXT, $context['groups'] ?? [], true);
|
||||
$canSee = $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $object);
|
||||
$canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $object);
|
||||
|
||||
if ($canDavSee || $canDavEdit) {
|
||||
if ($canSee || $canEdit) {
|
||||
$accessToken = $this->JWTDavTokenProvider->createToken(
|
||||
$object,
|
||||
$canDavEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE
|
||||
$canEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE
|
||||
);
|
||||
|
||||
$datas['_links'] = [
|
||||
|
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner;
|
||||
|
||||
/**
|
||||
* Message which is received when a pdf is signed.
|
||||
*/
|
||||
final readonly class PdfSignedMessage
|
||||
{
|
||||
public function __construct(
|
||||
public readonly int $signatureId,
|
||||
public readonly string $content
|
||||
) {}
|
||||
}
|
@@ -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\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 Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
|
||||
final readonly class PdfSignedMessageHandler implements MessageHandlerInterface
|
||||
{
|
||||
/**
|
||||
* log prefix.
|
||||
*/
|
||||
private const P = '[pdf signed message] ';
|
||||
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
private EntityWorkflowManager $entityWorkflowManager,
|
||||
private StoredObjectManagerInterface $storedObjectManager,
|
||||
private EntityWorkflowStepSignatureRepository $entityWorkflowStepSignatureRepository,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private ClockInterface $clock,
|
||||
) {}
|
||||
|
||||
public function __invoke(PdfSignedMessage $message): void
|
||||
{
|
||||
$this->logger->info(self::P.'a message is received', ['signaturedId' => $message->signatureId]);
|
||||
|
||||
$signature = $this->entityWorkflowStepSignatureRepository->find($message->signatureId);
|
||||
|
||||
if (null === $signature) {
|
||||
throw new \RuntimeException('no signature found');
|
||||
}
|
||||
|
||||
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($signature->getStep()->getEntityWorkflow());
|
||||
|
||||
if (null === $storedObject) {
|
||||
throw new \RuntimeException('no stored object found');
|
||||
}
|
||||
|
||||
$this->storedObjectManager->write($storedObject, $message->content);
|
||||
|
||||
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate($this->clock->now());
|
||||
$this->entityManager->flush();
|
||||
$this->entityManager->clear();
|
||||
}
|
||||
}
|
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner;
|
||||
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
|
||||
/**
|
||||
* Decode (and requeue) @see{PdfSignedMessage}, which comes from an external producer.
|
||||
*/
|
||||
final readonly class PdfSignedMessageSerializer implements SerializerInterface
|
||||
{
|
||||
public function decode(array $encodedEnvelope): Envelope
|
||||
{
|
||||
$body = $encodedEnvelope['body'];
|
||||
|
||||
try {
|
||||
$decoded = json_decode((string) $body, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (\JsonException $e) {
|
||||
throw new MessageDecodingFailedException('Could not deserialize message', previous: $e);
|
||||
}
|
||||
|
||||
if (!array_key_exists('signatureId', $decoded) || !array_key_exists('content', $decoded)) {
|
||||
throw new MessageDecodingFailedException('Could not find expected keys: signatureId or content');
|
||||
}
|
||||
|
||||
$content = base64_decode((string) $decoded['content'], true);
|
||||
|
||||
if (false === $content) {
|
||||
throw new MessageDecodingFailedException('Invalid character found in the base64 encoded content');
|
||||
}
|
||||
|
||||
$message = new PdfSignedMessage($decoded['signatureId'], $content);
|
||||
|
||||
return new Envelope($message);
|
||||
}
|
||||
|
||||
public function encode(Envelope $envelope): array
|
||||
{
|
||||
$message = $envelope->getMessage();
|
||||
|
||||
if (!$message instanceof PdfSignedMessage) {
|
||||
throw new MessageDecodingFailedException('Expected a PdfSignedMessage');
|
||||
}
|
||||
|
||||
$data = [
|
||||
'signatureId' => $message->signatureId,
|
||||
'content' => base64_encode($message->content),
|
||||
];
|
||||
|
||||
return [
|
||||
'body' => json_encode($data, JSON_THROW_ON_ERROR),
|
||||
'headers' => [],
|
||||
];
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
<?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\Signature\Driver\BaseSigner;
|
||||
|
||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
|
||||
|
||||
/**
|
||||
* Message which is sent when we request a signature on a pdf.
|
||||
*/
|
||||
final readonly class RequestPdfSignMessage
|
||||
{
|
||||
public function __construct(
|
||||
public int $signatureId,
|
||||
public PDFSignatureZone $PDFSignatureZone,
|
||||
public int $signatureZoneIndex,
|
||||
public string $reason,
|
||||
public string $signerText,
|
||||
public string $content,
|
||||
) {}
|
||||
}
|
@@ -0,0 +1,105 @@
|
||||
<?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\Signature\Driver\BaseSigner;
|
||||
|
||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
|
||||
use Symfony\Component\Messenger\Stamp\NonSendableStampInterface;
|
||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
/**
|
||||
* Serialize a RequestPdfSignMessage, for external consumer.
|
||||
*/
|
||||
final readonly class RequestPdfSignMessageSerializer implements SerializerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private NormalizerInterface $normalizer,
|
||||
private DenormalizerInterface $denormalizer,
|
||||
) {}
|
||||
|
||||
public function decode(array $encodedEnvelope): Envelope
|
||||
{
|
||||
$body = $encodedEnvelope['body'];
|
||||
$headers = $encodedEnvelope['headers'];
|
||||
|
||||
if (RequestPdfSignMessage::class !== ($headers['Message'] ?? null)) {
|
||||
throw new MessageDecodingFailedException('serializer does not support this message');
|
||||
}
|
||||
|
||||
$data = json_decode((string) $body, true);
|
||||
|
||||
$zoneSignature = $this->denormalizer->denormalize($data['signatureZone'], PDFSignatureZone::class, 'json', [
|
||||
AbstractNormalizer::GROUPS => ['write'],
|
||||
]);
|
||||
|
||||
$content = base64_decode((string) $data['content'], true);
|
||||
|
||||
if (false === $content) {
|
||||
throw new MessageDecodingFailedException('the content could not be converted from base64 encoding');
|
||||
}
|
||||
|
||||
$message = new RequestPdfSignMessage(
|
||||
$data['signatureId'],
|
||||
$zoneSignature,
|
||||
$data['signatureZoneIndex'],
|
||||
$data['reason'],
|
||||
$data['signerText'],
|
||||
$content,
|
||||
);
|
||||
|
||||
// in case of redelivery, unserialize any stamps
|
||||
$stamps = [];
|
||||
if (isset($headers['stamps'])) {
|
||||
$stamps = unserialize($headers['stamps']);
|
||||
}
|
||||
|
||||
return new Envelope($message, $stamps);
|
||||
}
|
||||
|
||||
public function encode(Envelope $envelope): array
|
||||
{
|
||||
$message = $envelope->getMessage();
|
||||
|
||||
if (!$message instanceof RequestPdfSignMessage) {
|
||||
throw new MessageDecodingFailedException('Message is not a RequestPdfSignMessage');
|
||||
}
|
||||
|
||||
$data = [
|
||||
'signatureId' => $message->signatureId,
|
||||
'signatureZoneIndex' => $message->signatureZoneIndex,
|
||||
'signatureZone' => $this->normalizer->normalize($message->PDFSignatureZone, 'json', [AbstractNormalizer::GROUPS => ['read']]),
|
||||
'reason' => $message->reason,
|
||||
'signerText' => $message->signerText,
|
||||
'content' => base64_encode($message->content),
|
||||
];
|
||||
|
||||
$allStamps = [];
|
||||
foreach ($envelope->all() as $stamp) {
|
||||
if ($stamp instanceof NonSendableStampInterface) {
|
||||
continue;
|
||||
}
|
||||
$allStamps = [...$allStamps, ...$stamp];
|
||||
}
|
||||
|
||||
return [
|
||||
'body' => json_encode($data, JSON_THROW_ON_ERROR, 512),
|
||||
'headers' => [
|
||||
'stamps' => serialize($allStamps),
|
||||
'Message' => RequestPdfSignMessage::class,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@@ -16,6 +16,8 @@ use Symfony\Component\Serializer\Annotation\Groups;
|
||||
final readonly class PDFSignatureZone
|
||||
{
|
||||
public function __construct(
|
||||
#[Groups(['read'])]
|
||||
public int $index,
|
||||
#[Groups(['read'])]
|
||||
public float $x,
|
||||
#[Groups(['read'])]
|
||||
@@ -31,7 +33,8 @@ final readonly class PDFSignatureZone
|
||||
public function equals(self $other): bool
|
||||
{
|
||||
return
|
||||
$this->x == $other->x
|
||||
$this->index == $other->index
|
||||
&& $this->x == $other->x
|
||||
&& $this->y == $other->y
|
||||
&& $this->height == $other->height
|
||||
&& $this->width == $other->width
|
||||
|
@@ -17,10 +17,10 @@ class PDFSignatureZoneParser
|
||||
{
|
||||
public const ZONE_SIGNATURE_START = 'signature_zone';
|
||||
|
||||
private Parser $parser;
|
||||
private readonly Parser $parser;
|
||||
|
||||
public function __construct(
|
||||
public float $defaultHeight = 180.0,
|
||||
public float $defaultHeight = 90.0,
|
||||
public float $defaultWidth = 180.0,
|
||||
) {
|
||||
$this->parser = new Parser();
|
||||
@@ -37,6 +37,7 @@ class PDFSignatureZoneParser
|
||||
$defaults = $pdf->getObjectsByType('Pages');
|
||||
$defaultPage = reset($defaults);
|
||||
$defaultPageDetails = $defaultPage->getDetails();
|
||||
$zoneIndex = 0;
|
||||
|
||||
foreach ($pdf->getPages() as $index => $page) {
|
||||
$details = $page->getDetails();
|
||||
@@ -47,8 +48,9 @@ class PDFSignatureZoneParser
|
||||
);
|
||||
|
||||
foreach ($page->getDataTm() as $dataTm) {
|
||||
if (str_starts_with($dataTm[1], self::ZONE_SIGNATURE_START)) {
|
||||
$zones[] = new PDFSignatureZone((float) $dataTm[0][4], (float) $dataTm[0][5], $this->defaultHeight, $this->defaultWidth, $pdfPage);
|
||||
if (str_starts_with((string) $dataTm[1], self::ZONE_SIGNATURE_START)) {
|
||||
$zones[] = new PDFSignatureZone($zoneIndex, (float) $dataTm[0][4], (float) $dataTm[0][5], $this->defaultHeight, $this->defaultWidth, $pdfPage);
|
||||
++$zoneIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Service;
|
||||
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
class WorkflowStoredObjectPermissionHelper
|
||||
{
|
||||
public function __construct(private readonly Security $security, private readonly EntityWorkflowManager $entityWorkflowManager) {}
|
||||
|
||||
public function notBlockedByWorkflow(object $entity): bool
|
||||
{
|
||||
$workflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
|
||||
$currentUser = $this->security->getUser();
|
||||
|
||||
foreach ($workflows as $workflow) {
|
||||
if ($workflow->isFinal()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$workflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user