Compare commits

..

207 Commits

Author SHA1 Message Date
246ee58d8b fix type for condition in ParticipationController 2022-04-27 08:44:49 +02:00
c2061110dd fix cs 2022-04-26 21:16:55 +02:00
b2fb86111d fix social action consistency 2022-04-26 21:12:31 +02:00
db6c4f15f8 Merge remote-tracking branch 'origin/master' into issue471_activity_socialissue-action 2022-04-26 20:29:07 +02:00
12231912d6 remove dead code 2022-04-26 20:27:24 +02:00
7b0c8d21ee sort properties alphabetically 2022-04-26 20:27:08 +02:00
f713b1ddc8 allow to edit encrypted documents 2022-04-26 17:46:25 +02:00
faf1559770 Merge branch 'master' into feat/add-document-encrypter-service 2022-04-26 14:52:13 +02:00
5a1ce399f6 fix creation of evaluation in js 2022-04-26 14:26:51 +02:00
505384c2a0 fix generation of document 2022-04-26 14:11:59 +02:00
a7654edefa add missing services 2022-04-26 14:11:28 +02:00
81689e3507 fix catching response 400 in relatorio driver 2022-04-26 14:11:04 +02:00
b891ba842d update changelog 2022-04-26 12:03:06 +02:00
3ceae0473d Merge remote-tracking branch 'origin/master' 2022-04-26 11:57:37 +02:00
795a8e9daf Merge remote-tracking branch 'origin/master' 2022-04-26 11:54:01 +02:00
Pol Dellaiera
81edaef062 Fix: Route name change. 2022-04-26 11:50:59 +02:00
Pol Dellaiera
dc48b4b9c7 Fix #581: Collabora / wopi: le bouton "imprimer" ne fonctionne pas. 2022-04-26 11:47:46 +02:00
e72a2004d7 Merge remote-tracking branch 'origin/master' into feat/add-document-encrypter-service 2022-04-26 09:37:30 +02:00
329fb05db0 Merge branch 'issue571_ordering_socialactions' into 'master'
Ordering social actions

See merge request Chill-Projet/chill-bundles!412
2022-04-25 20:14:59 +00:00
75250402c8 fix ordering of social actions 2022-04-25 22:14:36 +02:00
e5209ca6d9 do not make a loop to normalizer for a native php object 2022-04-25 22:14:23 +02:00
4512b94293 Merge remote-tracking branch 'origin/master' into issue571_ordering_socialactions 2022-04-25 21:35:16 +02:00
217f95472b Merge branch 'issue574_accordeon_parcours' into 'master'
Accordeon for closed parcours in person search results

See merge request Chill-Projet/chill-bundles!413
2022-04-25 19:34:49 +00:00
b7abf6b6bf Merge remote-tracking branch 'origin/master' into issue571_ordering_socialactions 2022-04-25 21:30:48 +02:00
c39fa1c49b remove legacy folded/unfolded style requirements for showing label on accordion 2022-04-25 21:27:56 +02:00
81c8d0ec77 move extension of accordion bootstrap to dedicated file 2022-04-25 21:23:41 +02:00
72012a70d9 button "closed" with same layout than in course header 2022-04-25 21:23:12 +02:00
916560c522 list with period: full sentences in translation component 2022-04-25 21:21:38 +02:00
57d6240cd1 Merge remote-tracking branch 'origin/master' into issue574_accordeon_parcours 2022-04-25 20:46:57 +02:00
2d460adc4d Merge branch 'issue568_agents_traitants' into 'master'
Display agents traitants

See merge request Chill-Projet/chill-bundles!411
2022-04-25 17:30:18 +00:00
d155c76013 Merge remote-tracking branch 'origin/master' into issue568_agents_traitants 2022-04-25 19:22:52 +02:00
529a9e5f58 fix notification counter on entity 2022-04-25 16:34:49 +02:00
2b837e0445 Merge branch 'issue545_datepicker' into 'master'
Fix datepicker

See merge request Chill-Projet/chill-bundles!405
2022-04-25 14:19:36 +00:00
4279acc2f8 convert date for new household 2022-04-25 16:11:42 +02:00
8fb3870ca0 remove dead code 2022-04-25 15:59:35 +02:00
ff72b28861 Revert "private comment added to activity"
This reverts commit 1f2638626c.
2022-04-25 14:52:28 +02:00
1f2638626c private comment added to activity 2022-04-25 14:43:44 +02:00
41ff21ee23 update changelog with last release 2022-04-25 09:38:36 +02:00
30bb2e8393 Merge remote-tracking branch 'origin/master' into issue545_datepicker 2022-04-25 09:37:35 +02:00
ca166655da changelog updated 2022-04-22 18:05:47 +02:00
8f69f852b9 fix merge conflict 2022-04-22 18:04:20 +02:00
6747cad210 add amount of closed periods in accordeon message 2022-04-22 18:02:59 +02:00
0cbcd91c3f closed periods in accordeon + styling 2022-04-22 17:49:57 +02:00
d757e07dfc macro added + closed parcours displayed in accordeon 2022-04-22 17:22:46 +02:00
83dd8f810c Merge branch 'notification/deferring-sending-notification-to-terminate' into 'master'
Notification/deferring sending notification to terminate

See merge request Chill-Projet/chill-bundles!414
2022-04-22 10:48:18 +00:00
bd0b45b4dd fix cs 2022-04-22 12:47:50 +02:00
6dbe8068ae update changelog 2022-04-22 12:31:33 +02:00
19561c63cb do not persist if no notification is scheduled 2022-04-22 12:31:23 +02:00
354f130e9e use NotificationPersister into PersonMoveEventSubscriber 2022-04-22 12:12:50 +02:00
2d9af8f8c0 period: fix method hasPreviousUser and create method isChangedUser, and use it in Notification 2022-04-22 12:12:30 +02:00
f7d9551dc1 fix cs 2022-04-22 11:41:38 +02:00
abc3caee00 AccompanyingPeriod: fix method hasPreviousUser 2022-04-22 11:40:29 +02:00
33f93d484d deferring the sending of notification to kernel.terminate: prepare 2022-04-22 11:34:41 +02:00
8470604b8f macro added + closed parcours displayed in accordeon 2022-04-21 22:06:22 +02:00
b85e6aae3f csfixes 2022-04-21 21:22:18 +02:00
a4baf89a6a social actions ordered in store 2022-04-21 21:05:32 +02:00
0a05dcd753 add ordering to serialization 2022-04-21 20:49:54 +02:00
c6be7955fd remove save button datepicker parcours. set timeout 2022-04-21 13:59:45 +02:00
e246ccbcd9 [Course comment] add validationConstraint NotNull and NotBlank on comment content, to avoid sql error 2022-04-20 18:34:29 +02:00
44b537f05e [Accompanying period work evaluations] list documents associated to a work by creation date, and then by id, from the most recent to older 2022-04-20 18:34:27 +02:00
0fd23c61a1 Wording changed when user is not associated to household 2022-04-20 17:19:20 +02:00
6cad0be181 Ignore thirdparty for socialaction created via activity 2022-04-20 17:08:27 +02:00
9a0eb1c74c Renaming of tabs and removal of social actions tab 2022-04-20 16:33:05 +02:00
a70c561596 Number of parcours of user displayed in blue pill 2022-04-20 16:23:08 +02:00
daa3eabd43 Change wording for save document button 2022-04-20 16:12:58 +02:00
b24de76d77 display agents traitants 2022-04-20 15:16:26 +02:00
92d394b669 changelog updated 2022-04-20 14:22:35 +02:00
c17d20f945 comment out success message parcours start date 2022-04-20 14:18:24 +02:00
be77c3729b take empty date strings into account 2022-04-20 14:09:28 +02:00
ad1e7b576c remove success toast for parcours startdate + take empty string values into account for action dates 2022-04-20 13:58:45 +02:00
bf0578b6d7 [Accompanying period work] list evaluations associated to a work by startDate, and then by id, from the most recent to older 2022-04-20 13:49:47 +02:00
0ce23230da fix evaluation datepickers in edit 2022-04-20 13:32:51 +02:00
8b7404e531 Email on designation: allow raw characters
As the content-type of the email is text/plain, we should not escape
special character.

Add the raw filter on string which will be escaped by twig.
2022-04-20 13:05:11 +02:00
bc550ea42a updating openingdate parcours fixed 2022-04-20 12:14:19 +02:00
65e6471a02 fix datepicker for social action edit form.
still a problem with the display of already set evaluation dates... need to be transformed to correct format to display
2022-04-20 12:14:19 +02:00
de9d2aa885 fix datepicker for householdmember editor 2022-04-20 12:14:19 +02:00
66ab38c60f prefill startdate with now 2022-04-20 12:14:19 +02:00
8882c99f5a fix datepicker for social action 2022-04-20 12:14:19 +02:00
8512a5e1bc person list with period: use sameas twig operator instead of using '==' operator to introduce requestor 2022-04-20 11:24:20 +02:00
7f97215fe9 activity form: keep the same order for attendee field between edit and new form 2022-04-20 10:03:58 +02:00
6427fc64cd invert 'incoming' and 'receiving' 2022-04-20 10:00:48 +02:00
26d5bf03bd add order by on document list 2022-04-20 10:00:48 +02:00
Pol Dellaiera
7048f670ed Fix #552: Return AccessDeniedException when trying to access a document and user is not logged in. 2022-04-19 15:45:47 +02:00
c757fa068a Translations added for error message in parcours 2022-04-19 15:21:23 +02:00
ebc4ec0d7c updating openingdate parcours fixed 2022-04-19 15:09:25 +02:00
617473f6a5 notification - grant access by key - temporarily disable email verification due to character escaping in email 2022-04-14 01:36:01 +02:00
124a6a58fc fix cs 2022-04-14 01:13:16 +02:00
afa38e9dd5 addresses: add constraint in database to avoid error later 2022-04-14 01:11:39 +02:00
a52f29bb78 accompanyingWorkEditor: add label on form document title 2022-04-14 01:11:14 +02:00
b95ff337b6 add more time duration for aside activities 2022-04-14 00:41:15 +02:00
92a169be1e Merge branch 'issue541_change_moving_date' into 'master'
household address: add a form for editing the validFrom date

See merge request Chill-Projet/chill-bundles!402
2022-04-13 22:11:34 +00:00
e6d59bc837 fix cs 2022-04-14 00:11:15 +02:00
31731c6f44 Merge remote-tracking branch 'origin/master' into issue541_change_moving_date 2022-04-14 00:04:30 +02:00
86ec020f80 fixes on address valid from edit for household 2022-04-14 00:02:57 +02:00
07351e2946 Merge branch 'issue543_double_click_household' into 'master'
householdmemberseditor: fix composition type bug

See merge request Chill-Projet/chill-bundles!406
2022-04-13 21:37:41 +00:00
405694a0b4 Merge remote-tracking branch 'origin/master' into issue541_change_moving_date 2022-04-13 23:30:51 +02:00
bad39364c2 Merge remote-tracking branch 'origin/master' into issue543_double_click_household 2022-04-13 23:26:38 +02:00
c0b39e4e7c Merge branch 'notifications/allow-to-send-to-email' into 'master'
Notifications/allow to send to email

See merge request Chill-Projet/chill-bundles!409
2022-04-13 21:25:35 +00:00
f98b35f009 Merge remote-tracking branch 'origin/master' into issue543_double_click_household 2022-04-13 23:20:23 +02:00
8770188d54 update changelog 2022-04-13 23:18:50 +02:00
35c7d55b8c fix cs 2022-04-13 23:17:16 +02:00
2a53fb9341 show email adresses on notification list 2022-04-13 23:16:02 +02:00
e7f0cd50c9 controller to grant access to notification by access key 2022-04-13 23:05:38 +02:00
a41d6cf744 notification: send an email to addressesEmails 2022-04-13 22:50:32 +02:00
24d28b0a52 notification: alter form type to add and remove email addresses 2022-04-13 22:11:01 +02:00
4425f2ad49 fix type for Notification email addresses 2022-04-13 21:43:41 +02:00
a8db07a383 notification / add email: fix entity Notification 2022-04-13 21:33:18 +02:00
ef9fd80ad5 update schema to send to emails 2022-04-13 18:02:11 +02:00
e6169ddffa Merge branch 'docgen/improve-context-courses' into 'master'
docgen: some fixes and improvements

See merge request Chill-Projet/chill-bundles!408
2022-04-13 07:53:43 +00:00
ccf7c885bb fix normalization for phonenumber on person when phonenumber is null 2022-04-13 09:44:21 +02:00
01c571ab06 fix path to table 2022-04-11 21:04:36 +02:00
69b2dce7ee docgen/budget: add comment for each budget line (resource and charge)
The comments are cocatenated by lines of each type, and separated by a
`|`
2022-04-11 18:11:20 +02:00
910245f855 update changelog 2022-04-11 17:45:57 +02:00
7fbb3dfd07 docgen: add more persons choices in person1, person2 and mainPerson
Allow to pick amongst:

* requestor (if person)
* resources of course (if person)
* resources of person (if person)
2022-04-11 17:43:31 +02:00
9765bc5663 fix cs 2022-04-11 17:12:59 +02:00
bb65909bfa add docgen context for a list of activities in a course 2022-04-11 16:52:11 +02:00
03b0a8766e remove dumps 2022-04-08 19:12:11 +02:00
5eea202586 fix adding sibling action 2022-04-08 19:04:13 +02:00
a78c62789c if parent in collection replace with child, problem, sibling isn't added 2022-04-08 18:56:37 +02:00
68671e297c Merge branch 'issue471_activity_socialissue-action' of gitlab.com:Chill-Projet/chill-bundles into issue471_activity_socialissue-action 2022-04-08 13:24:49 +02:00
4257a918f3 fix error when search pattern is empty 2022-04-08 00:11:08 +02:00
7ffb3dc74f task: fix route name 2022-04-07 23:49:32 +02:00
0f926e9dfe allow to display pinned comment in item of accompanying course list 2022-04-07 22:40:20 +02:00
fc55567a64 update changelog 2022-04-07 22:12:13 +02:00
009030b5fb notification list: move action buttons outside of the toggle 2022-04-07 21:59:02 +02:00
6e1cabc8fc fix detecting of non-read notification 2022-04-07 21:55:04 +02:00
572c602387 filter users which are disabled 2022-04-07 21:47:56 +02:00
26a4577420 order query for location and add pagination in list 2022-04-07 21:08:11 +02:00
f09870931c switch to using getDescendantsWithThis() 2022-04-07 16:11:00 +02:00
ea21f2d9c4 improvement, but still not correct 2022-04-07 16:11:00 +02:00
d81a41bb17 Test added for activity-social action and social issue 2022-04-07 16:11:00 +02:00
7851d9956e csfixes 2022-04-07 16:11:00 +02:00
f35479e4d2 fix logic in activity entity 2022-04-07 16:11:00 +02:00
432b105be5 update changelog 2022-04-07 16:11:00 +02:00
988b67bd4b logic added to only keep youngest descendant. works for issue, seems not to for action 2022-04-07 16:11:00 +02:00
f64409e5e6 allow every person which has part for a workflow to see the workflow page 2022-04-07 16:06:26 +02:00
a1baf99294 able to see the workflow if the evaluation document has been deleted 2022-04-07 15:54:53 +02:00
d9bb18e042 hardcode the list of supported mime types for edition with collabora 2022-04-07 15:37:17 +02:00
00a6ef0598 fix creating of accompanying period work 2022-04-07 11:05:34 +02:00
nobohan
c03e38f4ff upd CHANGELOG 2022-04-07 10:47:14 +02:00
nobohan
f9bcc5b1e9 householdmemberseditor: fix composition type bug 2022-04-07 10:45:46 +02:00
934a066910 remove error disappearing from phpstan 2022-04-06 22:40:09 +02:00
095afb90c7 AddPerson search: fix aborting query when the query is altered by user 2022-04-06 21:47:01 +02:00
645549ae34 effectively filter on all words in third party search 2022-04-06 21:45:49 +02:00
nobohan
d34462d849 php code fix 2022-04-06 15:34:11 +02:00
nobohan
c65a73604d upd CHANGELOG 2022-04-06 15:33:07 +02:00
nobohan
6a4edd000f household address: add max date for changing the validFrom date 2022-04-06 15:31:17 +02:00
nobohan
017b7bca87 address: handle cases when null street and streetnumber are given 2022-04-06 15:14:51 +02:00
612d053892 fix phpstan and phpcs 2022-04-06 13:59:05 +02:00
8edc68859e do not allow to freeze a document 2022-04-06 13:41:34 +02:00
a4afe73efe take into account system notification in counter and fix hardcoded user id 2022-04-06 13:35:24 +02:00
2b67f105f7 Merge branch 'issues521_531_540' into 'master'
issues graphiques 521 531 540

See merge request Chill-Projet/chill-bundles!399
2022-04-06 10:43:29 +00:00
95b114a144 do not show button to send notification on draft courses 2022-04-06 12:42:12 +02:00
5477a70c84 add ACL around creation of work in course 2022-04-06 12:41:50 +02:00
7d69ec6d6b Merge remote-tracking branch 'origin/master' into issues521_531_540 2022-04-06 12:29:11 +02:00
17594b58ab Merge branch 'issue548_notifs_buttons' into 'master'
notification toggle read: correct js syntax

See merge request Chill-Projet/chill-bundles!404
2022-04-06 10:24:27 +00:00
cddd3c20c8 Merge remote-tracking branch 'origin/master' into issue548_notifs_buttons 2022-04-06 12:16:23 +02:00
25a37974e5 Merge branch 'issues522_523' into 'master'
activity: add spacing between buttons + better alignment

See merge request Chill-Projet/chill-bundles!397
2022-04-06 10:08:59 +00:00
066afc07a8 update changelog 2022-04-06 12:08:44 +02:00
56c3f05ec9 Merge remote-tracking branch 'origin/master' into issues522_523 2022-04-06 12:04:54 +02:00
1907beaf6e Merge branch 'change_interlocuteurs_display' into 'master'
Change display of interlocuteurs (parcours)

See merge request Chill-Projet/chill-bundles!396
2022-04-06 10:01:26 +00:00
37c04d3f12 update changelog 2022-04-06 12:00:49 +02:00
a7fbce0add Merge remote-tracking branch 'origin/master' into change_interlocuteurs_display 2022-04-06 11:59:18 +02:00
e663bae5c4 fix datepicker for social action edit form.
still a problem with the display of already set evaluation dates... need to be transformed to correct format to display
2022-04-06 11:26:04 +02:00
ad6a68487c fix datepicker for householdmember editor 2022-04-06 11:24:01 +02:00
nobohan
88f377778c upd CHANGELOG 2022-04-06 09:28:57 +02:00
nobohan
d6deaeb324 notification unread: correct class 2022-04-06 09:26:14 +02:00
nobohan
f2744fba43 notification toggle read: correct js syntax 2022-04-05 22:28:35 +02:00
72a62a3a1b prefill startdate with now 2022-04-05 16:03:07 +02:00
bcde4497cc fix datepicker for social action 2022-04-05 15:56:02 +02:00
nobohan
ddce7603ad household address: add condition for minimum value for vaildFrom date 2022-04-02 10:53:54 +02:00
nobohan
3a3eb68288 household address: add a form for editing the validFrom date 2022-04-01 16:17:04 +02:00
a4ece21f2b in household, force validTo of address to be NULL 2022-04-01 15:06:00 +02:00
c7762dd6d2 fix trimming for email in person / thirdparty 2022-03-31 12:51:53 +02:00
f47fb17b8d fix denormalization of invalid dates 2022-03-31 12:48:27 +02:00
51dc255be5 fix phonenumber denormalization when null value is given 2022-03-31 12:48:27 +02:00
6ddbb79157 format phonenumber in third party normalization 2022-03-31 12:48:27 +02:00
nobohan
1d6d8dc002 accompanyingcourse work: add ACL rights for create and edit buttons 2022-03-30 12:01:35 +02:00
nobohan
f29ead4961 accompanying course: fix display bug in accompanying course resume 2022-03-30 11:26:19 +02:00
nobohan
b361ab2d74 workflow: add div for delete workflow form 2022-03-30 11:10:23 +02:00
nobohan
10fcd4f732 accompanying course: evaluation documents: align buttons 2022-03-28 15:12:16 +02:00
nobohan
bc8709f9a1 activity: add spacing between buttons + better alignment 2022-03-28 14:30:49 +02:00
3d6745e535 display of interlocuteurs changed to flex-table to prevent cut-off of information 2022-03-28 12:00:13 +02:00
02571bf727 switch to using getDescendantsWithThis() 2022-03-22 13:45:20 +01:00
247f1879e3 Merge remote-tracking branch 'origin/master' into issue471_activity_socialissue-action 2022-03-21 11:16:19 +01:00
9551e10d2b improvement, but still not correct 2022-03-16 11:55:19 +01:00
39ba21f308 merge master into branch 2022-03-16 11:36:07 +01:00
Pol Dellaiera
22755de1dd fix: Remove PrependExtensionInterface.
It is now handled by `chill_main` bundle.
2022-03-15 13:45:07 +01:00
Pol Dellaiera
3d7fcd99a8 fix: Remove console.log. 2022-03-15 13:45:07 +01:00
Pol Dellaiera
35d723e5fb refactor: Use StoredObjectManager. 2022-03-15 13:45:07 +01:00
Pol Dellaiera
62af980ea5 feat: Add new StoredObjectManager service.
To read and write onto `StoredObject` document using a common interface.
2022-03-15 13:41:38 +01:00
Pol Dellaiera
8abed67e1c refactor: Return a string instead of a resource. 2022-03-15 09:40:32 +01:00
Pol Dellaiera
b8992b8eeb misc: Update interface doc. 2022-03-15 09:40:32 +01:00
Pol Dellaiera
6ddbf35a7b misc: Add return types. 2022-03-15 09:40:32 +01:00
Pol Dellaiera
325ab0daf3 chore: Update composer.json files. 2022-03-15 09:40:32 +01:00
16be28681a Test added for activity-social action and social issue 2022-03-07 15:13:47 +01:00
68aa80269f rebase onto master 2022-03-04 17:29:36 +01:00
2012df512c merge conflict fixed 2022-03-04 17:27:26 +01:00
1c21b80703 csfixes 2022-03-04 17:25:37 +01:00
25fe105590 fix logic in activity entity 2022-03-04 17:25:37 +01:00
b6e530fec6 update changelog 2022-03-04 17:25:37 +01:00
4ef2274803 logic added to only keep youngest descendant. works for issue, seems not to for action 2022-03-04 17:25:22 +01:00
474fffcbb5 changelog updated 2022-03-04 12:08:00 +01:00
0333e79b0a csfixes 2022-03-04 12:07:29 +01:00
7c043e9d85 fix logic in activity entity 2022-03-04 12:06:53 +01:00
214ef09fe7 update changelog 2022-03-03 16:29:35 +01:00
18694a34cf logic added to only keep youngest descendant. works for issue, seems not to for action 2022-03-03 16:24:37 +01:00
139 changed files with 3049 additions and 928 deletions

View File

@@ -11,9 +11,69 @@ and this project adheres to
## Unreleased
<!-- write down unreleased development here -->
## Test releases
### 2021-04-26
* [Datepickers] datepickers fixed when using keyboard to enter date (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/545)
* [social_action] Display 'agents traitants' in parcours resumé and social action list (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/568)
* [Person_search] Closed parcours shown within an accordeon that can be opened/closed (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/574)
### 2021-04-24
* [notification email on course designation] allow raw string in email content generation
* [Accompanying period work] list evaluations associated to a work by startDate, and then by id, from the most recent to older
* [Documents] Change wording 'créer' to 'enregistrer' (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/634)
* [Parcours]: The number of 'mes parcours' displayed (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/572)
* [Hompage_widget]: Renaming of tabs and removal of social actions tab (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/570)
* [activity]: Ignore thirdparties when creating a social action via an activity (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/573)
* [parcours]: change wording of warning message and button when user is not associated to a household yet (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/590#note_918370943)
* [Accompanying period work evaluations] list documents associated to a work by creation date, and then by id, from the most recent to older
* [Course comment] add validationConstraint NotNull and NotBlank on comment content, to avoid sql error
* [Notifications] delay the sending of notificaiton to kernel.terminate
* [Notifications / Period user change] fix the sending of notification when user changes
* [Activity form] invert 'incoming' and 'receiving' in Activity form
* [Activity form] keep the same order for 'attendee' field in new and edit form
* [list with period] use "sameas" test operator to introduce requestor in list
* [notification email on course designation] allow raw string in email content generation
* [Accompanying period work] list evaluations associated to a work by startDate, and then by id, from the most recent to older
### 2021-04-13
* [person] household address: add a form for editing the validFrom date (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/541)
* [person] householdmemberseditor: fix composition type bug in select form (vuejs) (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/543)
* [docgen] add more persons choices in docgen for course: amongst requestor (if person), resources of course (if person), and PersonResource (if person);
* [docgen] add a new context with a list of activities in course
* [docgen] add a comment in budget lines
* [notifications] allow to send a notification to an email address. The address receive an access link
* [adresses] add constraints in database to avoid errors later: postcode not null, and validfrom <= validto
* [accompanying work editor] add a label on document title input
### 2021-04-07
* notification list: move action buttons outside of the toggle
* fix detecting of non-read notification
* filter users which are disabled in search user api
* order query for location and add pagination in list
* allow every person which has part for a workflow to see the workflow page
* able to see the workflow if the evaluation document has been deleted
* hardcode the list of supported mime types for edition with collabora
* list of accompanying course: allow to see the pinned comment in list_item
### 2021-04-06
* [main] notification toggle read: correct js syntax for compilation in production (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/548)
* [parcours] Display of interlocuteurs changed to flex-table in parcours edit page to prevent cut-off of information (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/535)
* [activity] espace entre les boutons pour supprimer les documents
### continuous release in February and March
* Creation of PickCivilityType, and implementation in PersonType and ThirdpartyType
* [person] Accompanying course evaluation documents: disable the WOPI edit link if mimetype not supported and if no keyInfos
(https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/585)
* [activity] display error messages above the form in creating a new location (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/481)
* [activity] show required field in activity edit/new by an asterix in the vuejs fields (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/494)
* [ACL] fix allow to see the course, event if the scope'course does not contains the scope's user
@@ -52,6 +112,7 @@ and this project adheres to
* [household] bugfix if position of member is null, renderbox no longer throws an error (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/480)
* [parcours] location cannot be removed if linked to a user (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/478)
* [person] email added to twig personRenderbox (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/490)
* [activity] Only youngest descendant is kept for social issues and actions (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/471)
* [person] Add link to current household in person banner (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/484)
* [address] person badge in address history changed to open OnTheFly with all person info (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/489)
* [person] Change 'personne' with 'usager' and '&' with 'ET' (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/499)
@@ -78,8 +139,8 @@ and this project adheres to
* [person] Trailing guillemet removed (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/530)
* [notification] Display of social action within workflow notification set to display block (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/537)
* [onthefly] trim trailing whitespace in email of person and thirdparty (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/542)
* [person] fix bug when phonenumber or mobile number is left empty (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/546)
* [action] Only youngest descendant is kept for social issues and actions (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/471)
## Test releases
### test release 2022-02-21
@@ -108,8 +169,6 @@ and this project adheres to
* [bug]: fix confidential toggle of address in thirdpartyrenderbox (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/460)
## Test releases
* Creation of PickCivilityType, and implementation in PersonType and ThirdpartyType
### test release 2022-02-14
@@ -136,6 +195,7 @@ and this project adheres to
* [address]: Correction residential address 'depuis le' (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/459)
* [Documents]: List view adapted to display more information (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/414)
* [Thirdparty_contact]: address blurred if confidential in view page (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/450)
* [thirdparty] Add a contact to a thirdparty from within onTheFly (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/345)
### test release 2021-02-01
@@ -158,18 +218,9 @@ and this project adheres to
* [fast_actions] improve fast-actions buttons override mechanism, fix https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/413
* [homepage widget] add vue homepage_widget with asynchone loading, give a global view resume of the user concerned actions, notifications, etc.
* [thirdparty] Add a contact to a thirdparty from within onTheFly (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/345)
## Test releases
=======
* [homepage widget] add vue homepage_widget with asynchone loading, give a global view resume of the user concerned actions, notifications, etc.
>>>>>>> issue422_and_others_on_AddPersons
=======
* [homepage widget] add vue homepage_widget with asynchone loading, give a global view resume of the user concerned actions, notifications, etc.
* [thirdparty] Add a contact to a thirdparty from within onTheFly (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/345)
>>>>>>> b0d50d315c8e00959a967badac9cf5057ab2b4bc
### test release 2021-01-31
* [person] accompanying course: optimisation: do not fetch some resources for the banner (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/409)

View File

@@ -21,7 +21,7 @@
"knplabs/knp-time-bundle": "^1.12",
"league/csv": "^9.7.1",
"nyholm/psr7": "^1.4",
"ocramius/package-versions": "^1.10",
"ocramius/package-versions": "^1.10 || ^2",
"odolbeau/phone-number-bundle": "^3.6",
"phpoffice/phpspreadsheet": "^1.16",
"ramsey/uuid-doctrine": "^1.7",
@@ -33,6 +33,7 @@
"symfony/expression-language": "^4.4",
"symfony/form": "^4.4",
"symfony/framework-bundle": "^4.4",
"symfony/http-foundation": "^4.4",
"symfony/intl": "^4.4",
"symfony/mailer": "^5.4",
"symfony/mime": "^5.4",
@@ -103,7 +104,8 @@
"ergebnis/composer-normalize": true,
"ocramius/package-versions": true,
"phpro/grumphp": true,
"phpstan/extension-installer": true
"phpstan/extension-installer": true,
"roave/you-are-using-it-wrong": true
},
"bin-dir": "bin",
"optimize-autoloader": true,

View File

@@ -450,11 +450,6 @@ parameters:
count: 1
path: src/Bundle/ChillThirdPartyBundle/Repository/ThirdPartyRepository.php
-
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
count: 1
path: src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php
-
message: "#^Method Chill\\\\ThirdPartyBundle\\\\Search\\\\ThirdPartySearch\\:\\:renderResult\\(\\) should return string but return statement is missing\\.$#"
count: 1

View File

@@ -231,11 +231,22 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
{
if (!$this->socialActions->contains($socialAction)) {
$this->socialActions[] = $socialAction;
$this->ensureSocialActionConsistency();
}
return $this;
}
/**
* Add a social issue.
*
* Note: the social issue consistency (the fact that only yougest social issues
* are kept) is processed by an entity listener:
*
* @see{\Chill\PersonBundle\AccompanyingPeriod\SocialIssueConsistency\AccompanyingPeriodSocialIssueConsistencyEntityListener}
*
* @return $this
*/
public function addSocialIssue(SocialIssue $socialIssue): self
{
if (!$this->socialIssues->contains($socialIssue)) {
@@ -631,4 +642,13 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
return $this;
}
private function ensureSocialActionConsistency(): void
{
$ancestors = SocialAction::findAncestorSocialActions($this->getSocialActions());
foreach ($ancestors as $ancestor) {
$this->removeSocialAction($ancestor);
}
}
}

View File

@@ -66,9 +66,6 @@ class ActivityEntityListener
$newAction->addPerson($person);
}
foreach ($associatedThirdparties as $thirdparty) {
$newAction->setHandlingThierparty($thirdparty);
}
$this->em->persist($newAction);
$this->em->flush();
}

View File

@@ -373,8 +373,8 @@ class ActivityType extends AbstractType
'label' => $activityType->getLabel('sentReceived'),
'required' => $activityType->isRequired('sentReceived'),
'choices' => [
'Sent' => Activity::SENTRECEIVED_SENT,
'Received' => Activity::SENTRECEIVED_RECEIVED,
'Sent' => Activity::SENTRECEIVED_SENT,
],
]);
}

View File

@@ -12,13 +12,20 @@ declare(strict_types=1);
namespace Chill\ActivityBundle\Repository;
use Chill\ActivityBundle\Entity\Activity;
use Chill\ActivityBundle\Entity\ActivityPresence;
use Chill\ActivityBundle\Entity\ActivityType;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\LocationType;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Security\Core\Security;
@@ -72,6 +79,86 @@ final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInte
->findByAccompanyingPeriod($period, $scopes, true, $limit, $start, $orderBy);
}
public function findByAccompanyingPeriodSimplified(AccompanyingPeriod $period, ?int $limit = 1000): array
{
$rsm = new ResultSetMappingBuilder($this->em);
$sql = '
SELECT
a.id AS activity_id,
date,
CASE WHEN durationtime IS NOT NULL THEN (EXTRACT(EPOCH from durationtime) / 60)::int ELSE 0 END AS durationtimeminute,
attendee_id,
comment_comment,
emergency,
sentreceived,
CASE WHEN traveltime IS NOT NULL THEN (EXTRACT(EPOCH from traveltime) / 60)::int ELSE 0 END AS traveltimeminute,
t.id AS type_id, t.name as type_name,
p.id AS presence_id, p.name AS presence_name,
location.id AS location_id, location.address_id, location.name AS location_name, location.phonenumber1, location.phonenumber2, location.email,
location.locationtype_id, locationtype.title AS locationtype_title,
users.userids AS userids,
thirdparties.thirdpartyids,
persons.personids,
actions.socialactionids,
issues.socialissueids
FROM activity a
LEFT JOIN chill_main_location location ON a.location_id = location.id
LEFT JOIN chill_main_location_type locationtype ON location.locationtype_id = locationtype.id
LEFT JOIN activitytpresence p ON a.attendee_id = p.id
LEFT JOIN activitytype t ON a.type_id = t.id
LEFT JOIN LATERAL (SELECT jsonb_agg(user_id) userids, activity_id FROM activity_user AS au WHERE a.id = au.activity_id GROUP BY activity_id) AS users ON TRUE
LEFT JOIN LATERAL (SELECT jsonb_agg(thirdparty_id) thirdpartyids, activity_id FROM activity_thirdparty AS au WHERE a.id = au.activity_id GROUP BY activity_id) AS thirdparties ON TRUE
LEFT JOIN LATERAL (SELECT jsonb_agg(person_id) personids, activity_id FROM activity_person AS au WHERE a.id = au.activity_id GROUP BY activity_id) AS persons ON TRUE
LEFT JOIN LATERAL (SELECT jsonb_agg(socialaction_id) socialactionids, activity_id FROM chill_activity_activity_chill_person_socialaction AS au WHERE a.id = au.activity_id GROUP BY activity_id) AS actions ON TRUE
LEFT JOIN LATERAL (SELECT jsonb_agg(socialissue_id) socialissueids, activity_id FROM chill_activity_activity_chill_person_socialissue AS au WHERE a.id = au.activity_id GROUP BY activity_id) AS issues ON TRUE
WHERE accompanyingperiod_id = ?
ORDER BY a.date DESC, a.id DESC
LIMIT ?
';
$rsm
->addEntityResult(Activity::class, 'a')
->addFieldResult('a', 'activity_id', 'id')
->addFieldResult('a', 'date', 'date')
->addFieldResult('a', 'comment', 'comment')
->addFieldResult('a', 'sentreceived', 'sentReceived')
->addFieldResult('a', 'emergency', 'emergency')
->addJoinedEntityResult(Location::class, 'location', 'a', 'location')
->addFieldResult('location', 'location_id', 'id')
->addFieldResult('location', 'location_name', 'name')
->addFieldResult('location', 'phonenumber1', 'phonenumber1')
->addFieldResult('location', 'phonenumber2', 'phonenumber2')
->addFieldResult('location', 'email', 'email')
->addJoinedEntityResult(LocationType::class, 'locationType', 'location', 'locationType')
->addFieldResult('locationType', 'locationtype_id', 'id')
->addFieldResult('locationType', 'locationtype_title', 'title')
->addJoinedEntityResult(ActivityType::class, 'activityType', 'a', 'activityType')
->addFieldResult('activityType', 'type_id', 'id')
->addFieldResult('activityType', 'type_name', 'name')
->addJoinedEntityResult(ActivityPresence::class, 'activityPresence', 'a', 'attendee')
->addFieldResult('activityPresence', 'presence_id', 'id')
->addFieldResult('activityPresence', 'presence_name', 'name')
// results which cannot be mapped into entity
->addScalarResult('comment_comment', 'comment', Types::TEXT)
->addScalarResult('userids', 'userIds', Types::JSON)
->addScalarResult('thirdpartyids', 'thirdPartyIds', Types::JSON)
->addScalarResult('personids', 'personIds', Types::JSON)
->addScalarResult('socialactionids', 'socialActionIds', Types::JSON)
->addScalarResult('socialissueids', 'socialIssueIds', Types::JSON)
->addScalarResult('durationtimeminute', 'durationTimeMinute', Types::INTEGER)
->addScalarResult('traveltimeminute', 'travelTimeMinute', Types::INTEGER);
$nq = $this->em->createNativeQuery($sql, $rsm);
$nq->setParameter(0, $period->getId())->setParameter(1, $limit);
return $nq->getResult(AbstractQuery::HYDRATE_ARRAY);
}
/**
* @param array $orderBy
*

View File

@@ -21,6 +21,15 @@ interface ActivityACLAwareRepositoryInterface
*/
public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array;
/**
* Return a list of activities, simplified as array (not object).
*
* The aim of this method is to get a long list of activities and keep performance.
*
* @return array an array of array, each item representing an activity
*/
public function findByAccompanyingPeriodSimplified(AccompanyingPeriod $period, ?int $limit = 1000): array;
/**
* @return Activity[]|array
*/

View File

@@ -4,6 +4,13 @@ div.chill-dropzone__below-zone {
}
}
ul[data-collection-name="documents"] {
button.remove-entry {
margin: 0.5rem 0!important;
}
}
// do it in js does not work
// document.addEventListener('DOMContentLoaded', e => {
// const dropzoneBelow = document.querySelectorAll('div.chill-dropzone__below-zone');

View File

@@ -54,19 +54,19 @@
{{ $t('activity.select_first_a_social_issue') }}
</span>
<template v-else-if="socialActionsList.length > 0">
<check-social-action
v-if="socialIssuesSelected.length || socialActionsSelected.length"
v-for="action in socialActionsList"
:key="action.id"
:action="action"
:selection="socialActionsSelected"
@updateSelected="updateActionsSelected">
</check-social-action>
</template>
<template v-else-if="socialActionsList.length > 0">
<check-social-action
v-if="socialIssuesSelected.length || socialActionsSelected.length"
v-for="action in socialActionsList"
:key="action.id"
:action="action"
:selection="socialActionsSelected"
@updateSelected="updateActionsSelected">
</check-social-action>
</template>
<span v-else-if="actionAreLoaded && socialActionsList.length === 0" class="inline-choice chill-no-data-statement mt-3">
{{ $t('activity.social_action_list_empty') }}
{{ $t('activity.social_action_list_empty') }}
</span>
@@ -111,7 +111,7 @@ export default {
return this.$store.state.socialIssuesOther;
},
socialActionsList() {
return this.$store.state.socialActionsList;
return this.$store.getters.socialActionsListSorted;
},
socialActionsSelected() {
return this.$store.state.activity.socialActions;

View File

@@ -101,6 +101,9 @@ const store = createStore({
state.activity.activityType.thirdPartiesVisible !== 0)
);
},
socialActionsListSorted(state) {
return [ ...state.socialActionsList].sort((a, b) => a.ordering - b.ordering);
},
},
mutations: {
// SocialIssueAcc
@@ -131,7 +134,6 @@ const store = createStore({
state.socialActionsList = [];
},
addActionInList(state, action) {
//console.log('add action list', action.id);
state.socialActionsList.push(action);
},
updateActionsSelected(state, actions) {

View File

@@ -85,15 +85,15 @@
{{ form_row(form.comment) }}
{% endif %}
{%- if form.attendee is defined -%}
{{ form_row(form.attendee) }}
{% endif %}
{%- if form.documents is defined -%}
{{ form_row(form.documents) }}
<div data-docgen-template-picker="data-docgen-template-picker" data-entity-class="Chill\ActivityBundle\Entity\Activity" data-entity-id="{{ entity.id }}"></div>
{% endif %}
{%- if form.attendee is defined -%}
{{ form_row(form.attendee) }}
{% endif %}
{# TODO .. status #}
<ul class="record_actions sticky-form-buttons">

View File

@@ -0,0 +1,286 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\ActivityBundle\Service\DocGenerator;
use Chill\ActivityBundle\Entity\ActivityPresence;
use Chill\ActivityBundle\Entity\ActivityType;
use Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface;
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithAdminFormInterface;
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\UserRepository;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\PersonBundle\Repository\PersonRepository;
use Chill\PersonBundle\Repository\SocialWork\SocialActionRepository;
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
use Chill\PersonBundle\Service\DocGenerator\AccompanyingPeriodContext;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository;
use DateTime;
use libphonenumber\PhoneNumber;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class ListActivitiesByAccompanyingPeriodContext implements
DocGeneratorContextWithAdminFormInterface,
DocGeneratorContextWithPublicFormInterface
{
private AccompanyingPeriodContext $accompanyingPeriodContext;
private ActivityACLAwareRepositoryInterface $activityACLAwareRepository;
private NormalizerInterface $normalizer;
private PersonRepository $personRepository;
private SocialActionRepository $socialActionRepository;
private SocialIssueRepository $socialIssueRepository;
private ThirdPartyRepository $thirdPartyRepository;
private TranslatableStringHelperInterface $translatableStringHelper;
private UserRepository $userRepository;
public function __construct(
AccompanyingPeriodContext $accompanyingPeriodContext,
ActivityACLAwareRepositoryInterface $activityACLAwareRepository,
NormalizerInterface $normalizer,
PersonRepository $personRepository,
SocialActionRepository $socialActionRepository,
SocialIssueRepository $socialIssueRepository,
ThirdPartyRepository $thirdPartyRepository,
TranslatableStringHelperInterface $translatableStringHelper,
UserRepository $userRepository
) {
$this->accompanyingPeriodContext = $accompanyingPeriodContext;
$this->activityACLAwareRepository = $activityACLAwareRepository;
$this->normalizer = $normalizer;
$this->personRepository = $personRepository;
$this->socialActionRepository = $socialActionRepository;
$this->socialIssueRepository = $socialIssueRepository;
$this->thirdPartyRepository = $thirdPartyRepository;
$this->translatableStringHelper = $translatableStringHelper;
$this->userRepository = $userRepository;
}
public function adminFormReverseTransform(array $data): array
{
return $this->accompanyingPeriodContext->adminFormReverseTransform($data);
}
public function adminFormTransform(array $data): array
{
return $this->accompanyingPeriodContext->adminFormTransform($data);
}
public function buildAdminForm(FormBuilderInterface $builder): void
{
$this->accompanyingPeriodContext->buildAdminForm($builder);
}
public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void
{
$this->accompanyingPeriodContext->buildPublicForm($builder, $template, $entity);
}
public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array
{
$data = $this->accompanyingPeriodContext->getData($template, $entity, $contextGenerationData);
$data['activities'] = $this->getActivitiesSimplified($entity);
return $data;
}
public function getDescription(): string
{
return 'docgen.Accompanying period with a list of activities description';
}
public function getEntityClass(): string
{
return AccompanyingPeriod::class;
}
public function getFormData(DocGeneratorTemplate $template, $entity): array
{
return $this->accompanyingPeriodContext->getFormData($template, $entity);
}
public static function getKey(): string
{
return self::class;
}
public function getName(): string
{
return 'docgen.Accompanying period with a list of activities';
}
public function hasAdminForm(): bool
{
return $this->accompanyingPeriodContext->hasAdminForm();
}
public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool
{
return $this->accompanyingPeriodContext->hasPublicForm($template, $entity);
}
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void
{
$this->accompanyingPeriodContext->storeGenerated($template, $storedObject, $entity, $contextGenerationData);
}
private function getActivitiesSimplified(AccompanyingPeriod $period)
{
$activities =
$this->activityACLAwareRepository->findByAccompanyingPeriodSimplified($period);
$results = [];
foreach ($activities as $row) {
$activity = $row[0];
$activity['date'] = $this->normalizer->normalize($activity['date'], 'docgen', [
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => DateTime::class,
]);
if (null === $activity['location']) {
$activity['location'] = $this->normalizer->normalize(null, 'docgen', [
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => Location::class,
]);
$activity['location']['type'] = 'location';
} else {
$activity['location']['isNull'] = false;
$activity['location']['type'] = 'location';
foreach (['1', '2'] as $key) {
$activity['location']['phonenumber' . $key] = $this->normalizer->normalize(
$activity['location']['phonenumber' . $key],
'docgen',
[AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => PhoneNumber::class]
);
}
}
if (is_numeric($activity['location']['locationType']['id'])) {
$activity['location']['locationType']['title'] = $this->translatableStringHelper->localize(
$activity['location']['locationType']['title']
);
$activity['location']['locationType']['isNull'] = false;
$activity['location']['locationType']['type'] = 'locationType';
}
if (null !== $activity['activityType']) {
$activity['activityType']['name'] = $this->translatableStringHelper->localize(
$activity['activityType']['name']
);
$activity['activityType']['isNull'] = false;
$activity['activityType']['type'] = 'activityType';
} else {
$activity['activityType'] = $this->normalizer->normalize(null, 'docgen', [
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => ActivityType::class,
]);
}
if (null !== $activity['attendee']) {
$activity['attendee']['name'] = $this->translatableStringHelper->localize(
$activity['attendee']['name']
);
$activity['attendee']['isNull'] = false;
$activity['attendee']['type'] = 'activityPresence';
} else {
$activity['attendee'] = $this->normalizer->normalize(null, 'docgen', [
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => ActivityPresence::class,
]);
}
$activity['comment'] = (string) $row['comment'];
$activity['travelTimeMinute'] = $row['travelTimeMinute'];
$activity['durationTimeMinute'] = $row['durationTimeMinute'];
if (null !== $row['userIds']) {
foreach ($row['userIds'] as $id) {
$activity['users'][] = $this->normalizer->normalize(
$this->userRepository->find($id),
'docgen',
[AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => User::class]
);
}
} else {
$activity['users'] = [];
}
if (null !== $row['personIds']) {
foreach ($row['personIds'] as $id) {
$activity['persons'][] = $this->normalizer->normalize(
$this->personRepository->find($id),
'docgen',
[AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => Person::class]
);
}
} else {
$activity['persons'] = [];
}
if (null !== $row['thirdPartyIds']) {
foreach ($row['thirdPartyIds'] as $id) {
$activity['thirdParties'][] = $this->normalizer->normalize(
$this->thirdPartyRepository->find($id),
'docgen',
[AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => ThirdParty::class]
);
}
} else {
$activity['thirdParties'] = [];
}
if (null !== $row['socialActionIds']) {
foreach ($row['socialActionIds'] as $id) {
$activity['socialActions'][] = $this->normalizer->normalize(
$this->socialActionRepository->find($id),
'docgen',
[AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => SocialAction::class]
);
}
} else {
$activity['socialActions'] = [];
}
if (null !== $row['socialIssueIds']) {
foreach ($row['socialIssueIds'] as $id) {
$activity['socialIssues'][] = $this->normalizer->normalize(
$this->socialIssueRepository->find($id),
'docgen',
[AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => SocialIssue::class]
);
}
} else {
$activity['socialIssues'] = [];
}
$results[] = $activity;
}
return $results;
}
}

View File

@@ -0,0 +1,114 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\ActivityBundle\Tests\Entity;
use Chill\ActivityBundle\Entity\Activity;
use Chill\PersonBundle\AccompanyingPeriod\SocialIssueConsistency\AccompanyingPeriodSocialIssueConsistencyEntityListener;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Doctrine\ORM\Event\LifecycleEventArgs;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
/**
* @internal
* @coversNothing
*/
final class ActivityTest extends TestCase
{
use ProphecyTrait;
public function testHierarchySocialActions(): void
{
$parent = new SocialAction();
$child = new SocialAction();
$parent->addChild($child);
$grandChild = new SocialAction();
$child->addChild($grandChild);
$activity = new Activity();
$activity->addSocialAction($parent);
$this->assertCount(1, $activity->getSocialActions());
$this->assertContains($parent, $activity->getSocialActions());
$activity->addSocialAction($grandChild);
$this->assertCount(1, $activity->getSocialActions());
$this->assertContains($grandChild, $activity->getSocialActions());
$this->assertNotContains($parent, $activity->getSocialActions());
$activity->addSocialAction($child);
$this->assertCount(1, $activity->getSocialActions());
$this->assertContains($grandChild, $activity->getSocialActions());
$this->assertNotContains($parent, $activity->getSocialActions());
$this->assertNotContains($child, $activity->getSocialActions());
$activity->addSocialAction($another = new SocialAction());
$this->assertCount(2, $activity->getSocialActions());
$this->assertContains($grandChild, $activity->getSocialActions());
$this->assertContains($another, $activity->getSocialActions());
$this->assertNotContains($parent, $activity->getSocialActions());
$this->assertNotContains($child, $activity->getSocialActions());
}
public function testHierarchySocialIssues(): void
{
$listener = new AccompanyingPeriodSocialIssueConsistencyEntityListener();
$event = $this->prophesize(LifecycleEventArgs::class)->reveal();
$parent = new SocialIssue();
$child = new SocialIssue();
$parent->addChild($child);
$grandChild = new SocialIssue();
$child->addChild($grandChild);
$activity = new Activity();
$activity->setAccompanyingPeriod(new AccompanyingPeriod());
$activity->addSocialIssue($parent);
$listener->preUpdate($activity, $event);
$this->assertCount(1, $activity->getSocialIssues());
$this->assertContains($parent, $activity->getSocialIssues());
$activity->addSocialIssue($grandChild);
$listener->preUpdate($activity, $event);
$this->assertCount(1, $activity->getSocialIssues());
$this->assertContains($grandChild, $activity->getSocialIssues());
$this->assertNotContains($parent, $activity->getSocialIssues());
$activity->addSocialIssue($child);
$listener->preUpdate($activity, $event);
$this->assertCount(1, $activity->getSocialIssues());
$this->assertContains($grandChild, $activity->getSocialIssues());
$this->assertNotContains($parent, $activity->getSocialIssues());
$this->assertNotContains($child, $activity->getSocialIssues());
$activity->addSocialIssue($another = new SocialIssue());
$listener->preUpdate($activity, $event);
$this->assertCount(2, $activity->getSocialIssues());
$this->assertContains($grandChild, $activity->getSocialIssues());
$this->assertContains($another, $activity->getSocialIssues());
$this->assertNotContains($parent, $activity->getSocialIssues());
$this->assertNotContains($child, $activity->getSocialIssues());
}
}

View File

@@ -66,6 +66,14 @@ Choose a type: Choisir un type
1 hour 30: 1 heure 30
1 hour 45: 1 heure 45
2 hours: 2 heures
2 hours 15: 2 heures 15
2 hours 30: 2 heures 30
2 hours 45: 2 heures 45
3 hours: 3 heures
3 hours 30: 3 heures 30
4 hours: 4 heures
4 hours 30: 4 heures 30
5 hours: 5 heures
Concerned groups: Parties concernées
Persons in accompanying course: Usagers du parcours
Third persons: Tiers non-pro.
@@ -232,3 +240,5 @@ This is the minimal activity data: Activité n°
docgen:
Activity basic: Echange
A basic context for activity: Contexte pour les échanges
Accompanying period with a list of activities: Parcours d'accompagnement avec liste des échanges
Accompanying period with a list of activities description: Ce contexte reprend les informations du parcours, et tous les échanges pour un parcours. Les échanges ne sont pas filtrés.

View File

@@ -31,6 +31,10 @@ class Configuration implements ConfigurationInterface
->isRequired()
->defaultValue(
[
['label' => '1 minutes', 'seconds' => 60],
['label' => '2 minutes', 'seconds' => 120],
['label' => '3 minutes', 'seconds' => 180],
['label' => '4 minutes', 'seconds' => 240],
['label' => '5 minutes', 'seconds' => 300],
['label' => '10 minutes', 'seconds' => 600],
['label' => '15 minutes', 'seconds' => 900],

View File

@@ -25,13 +25,13 @@ use function count;
*/
class SummaryBudget implements SummaryBudgetInterface
{
private const QUERY_CHARGE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, type FROM chill_budget.charge WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
private const QUERY_CHARGE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, type FROM chill_budget.charge WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
private const QUERY_CHARGE_BY_PERSON = 'select SUM(amount) AS sum, type FROM chill_budget.charge WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
private const QUERY_CHARGE_BY_PERSON = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, type FROM chill_budget.charge WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
private const QUERY_RESOURCE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, type FROM chill_budget.resource WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
private const QUERY_RESOURCE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, type FROM chill_budget.resource WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
private const QUERY_RESOURCE_BY_PERSON = 'select SUM(amount) AS sum, type FROM chill_budget.resource WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
private const QUERY_RESOURCE_BY_PERSON = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, type FROM chill_budget.resource WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
private array $chargeLabels;
@@ -110,7 +110,8 @@ class SummaryBudget implements SummaryBudgetInterface
$rsm = new ResultSetMapping();
$rsm
->addScalarResult('sum', 'sum')
->addScalarResult('type', 'type');
->addScalarResult('type', 'type')
->addScalarResult('comment', 'comment');
return $rsm;
}
@@ -121,7 +122,7 @@ class SummaryBudget implements SummaryBudgetInterface
$labels = $this->chargeLabels;
return array_combine($keys, array_map(function ($i) use ($labels) {
return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($labels[$i])];
return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($labels[$i]), 'comment' => ''];
}, $keys));
}
@@ -131,7 +132,7 @@ class SummaryBudget implements SummaryBudgetInterface
$labels = $this->resourcesLabels;
return array_combine($keys, array_map(function ($i) use ($labels) {
return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($labels[$i])];
return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($labels[$i]), 'comment' => ''];
}, $keys));
}
@@ -158,6 +159,7 @@ class SummaryBudget implements SummaryBudgetInterface
$result[$row['type']] = [
'sum' => (float) $row['sum'],
'label' => $this->translatableStringHelper->localize($label[$row['type']]),
'comment' => (string) $row['comment'],
];
}

View File

@@ -14,7 +14,7 @@ namespace Chill\DocGeneratorBundle\Context;
use Chill\DocGeneratorBundle\Context\Exception\ContextNotFoundException;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
class ContextManager
final class ContextManager implements ContextManagerInterface
{
/**
* @var DocGeneratorContextInterface[]|iterable
@@ -26,9 +26,6 @@ class ContextManager
$this->contexts = $contexts;
}
/**
* @throw ContextNotFoundException when the context is not found
*/
public function getContextByDocGeneratorTemplate(DocGeneratorTemplate $docGeneratorTemplate): DocGeneratorContextInterface
{
foreach ($this->contexts as $key => $context) {

View File

@@ -0,0 +1,30 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\DocGeneratorBundle\Context;
use Chill\DocGeneratorBundle\Context\Exception\ContextNotFoundException;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
interface ContextManagerInterface
{
/**
* @throws ContextNotFoundException When the context is not found.
*/
public function getContextByDocGeneratorTemplate(DocGeneratorTemplate $docGeneratorTemplate): DocGeneratorContextInterface;
/**
* @throws ContextNotFoundException When the context is not found.
*/
public function getContextByKey(string $searchedKey): DocGeneratorContextInterface;
public function getContexts(): array;
}

View File

@@ -11,8 +11,6 @@ declare(strict_types=1);
namespace Chill\DocGeneratorBundle\Controller;
use Base64Url\Base64Url;
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
use Chill\DocGeneratorBundle\Context\ContextManager;
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface;
use Chill\DocGeneratorBundle\Context\Exception\ContextNotFoundException;
@@ -21,11 +19,11 @@ use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface;
use Chill\DocGeneratorBundle\GeneratorDriver\Exception\TemplateException;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Serializer\Model\Collection;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\TransferException;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
@@ -35,14 +33,14 @@ use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
// TODO à mettre dans services
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Throwable;
use function strlen;
final class DocGeneratorTemplateController extends AbstractController
{
@@ -54,13 +52,13 @@ final class DocGeneratorTemplateController extends AbstractController
private DriverInterface $driver;
private KernelInterface $kernel;
private EntityManagerInterface $entityManager;
private LoggerInterface $logger;
private PaginatorFactory $paginatorFactory;
private TempUrlGeneratorInterface $tempUrlGenerator;
private StoredObjectManagerInterface $storedObjectManager;
public function __construct(
ContextManager $contextManager,
@@ -68,18 +66,18 @@ final class DocGeneratorTemplateController extends AbstractController
DriverInterface $driver,
LoggerInterface $logger,
PaginatorFactory $paginatorFactory,
TempUrlGeneratorInterface $tempUrlGenerator,
KernelInterface $kernel,
HttpClientInterface $client
HttpClientInterface $client,
StoredObjectManagerInterface $storedObjectManager,
EntityManagerInterface $entityManager
) {
$this->contextManager = $contextManager;
$this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository;
$this->driver = $driver;
$this->logger = $logger;
$this->paginatorFactory = $paginatorFactory;
$this->tempUrlGenerator = $tempUrlGenerator;
$this->kernel = $kernel;
$this->client = $client;
$this->storedObjectManager = $storedObjectManager;
$this->entityManager = $entityManager;
}
/**
@@ -178,8 +176,10 @@ final class DocGeneratorTemplateController extends AbstractController
return $this->redirectToRoute(
'chill_docgenerator_test_generate_from_template',
['template' => $template, 'entityClassName' => $entityClassName, 'entityId' => $entityId,
'returnPath' => $request->query->get('returnPath', '/'), ]
[
'template' => $template, 'entityClassName' => $entityClassName, 'entityId' => $entityId,
'returnPath' => $request->query->get('returnPath', '/'),
]
);
}
@@ -193,16 +193,26 @@ final class DocGeneratorTemplateController extends AbstractController
try {
$context = $this->contextManager->getContextByDocGeneratorTemplate($template);
} catch (ContextNotFoundException $e) {
throw new NotFoundHttpException($e->getMessage(), $e);
throw new NotFoundHttpException(
'Context not found.',
$e
);
}
$entity = $this->getDoctrine()->getRepository($context->getEntityClass())->find($entityId);
$entity = $this
->entityManager
->getRepository($context->getEntityClass())
->find($entityId);
if (null === $entity) {
throw new NotFoundHttpException("Entity with classname {$entityClassName} and id {$entityId} is not found");
throw new NotFoundHttpException(
sprintf('Entity with classname %s and id %s is not found', $entityClassName, $entityId)
);
}
$contextGenerationData = [];
$contextGenerationData = [
'test_file' => null,
];
if (
$context instanceof DocGeneratorContextWithPublicFormInterface
@@ -240,128 +250,114 @@ final class DocGeneratorTemplateController extends AbstractController
$contextGenerationData = $form->getData();
} elseif (!$form->isSubmitted() || ($form->isSubmitted() && !$form->isValid())) {
$templatePath = '@ChillDocGenerator/Generator/basic_form.html.twig';
$templateOptions = ['entity' => $entity, 'form' => $form->createView(),
'template' => $template, 'context' => $context, ];
$templateOptions = [
'entity' => $entity, 'form' => $form->createView(),
'template' => $template, 'context' => $context,
];
return $this->render($templatePath, $templateOptions);
}
}
if ($isTest && null !== $contextGenerationData['test_file']) {
/** @var File $file */
$file = $contextGenerationData['test_file'];
$templateResource = fopen($file->getPathname(), 'rb');
$document = $template->getFile();
if ($isTest && ($contextGenerationData['test_file'] instanceof File)) {
$dataDecrypted = file_get_contents($contextGenerationData['test_file']->getPathname());
} else {
$getUrlGen = $this->tempUrlGenerator->generate(
'GET',
$template->getFile()->getFilename()
);
$data = $this->client->request('GET', $getUrlGen->url);
$iv = $template->getFile()->getIv(); // iv as an Array
$ivGoodFormat = pack('C*', ...$iv); // iv as a String (ok for openssl_decrypt)
$method = 'AES-256-CBC';
$key = $template->getFile()->getKeyInfos()['k'];
$keyGoodFormat = Base64Url::decode($key);
$dataDecrypted = openssl_decrypt($data->getContent(), $method, $keyGoodFormat, 1, $ivGoodFormat);
if (false === $dataDecrypted) {
throw new Exception('Error during Decrypt ', 1);
try {
$dataDecrypted = $this->storedObjectManager->read($document);
} catch (Throwable $exception) {
throw $exception;
}
if (false === $templateResource = fopen('php://memory', 'r+b')) {
$this->logger->error('Could not write data to memory');
throw new HttpException(500);
}
fwrite($templateResource, $dataDecrypted);
rewind($templateResource);
}
$datas = $context->getData($template, $entity, $contextGenerationData);
if ($isTest && isset($form) && $form['show_data']->getData()) {
// very ugly hack...
dd($datas);
dd($context->getData($template, $entity, $contextGenerationData));
}
try {
$generatedResource = $this->driver->generateFromResource($templateResource, $template->getFile()->getType(), $datas, $template->getFile()->getFilename());
$generatedResource = $this
->driver
->generateFromString(
$dataDecrypted,
$template->getFile()->getType(),
$context->getData($template, $entity, $contextGenerationData),
$template->getFile()->getFilename()
);
} catch (TemplateException $e) {
$msg = implode("\n", $e->getErrors());
return new Response($msg, 400, [
'Content-Type' => 'text/plain',
]);
return new Response(
implode("\n", $e->getErrors()),
400,
[
'Content-Type' => 'text/plain',
]
);
}
fclose($templateResource);
if ($isTest) {
return new StreamedResponse(
static function () use ($generatedResource) {
fpassthru($generatedResource);
fclose($generatedResource);
},
return new Response(
$generatedResource,
Response::HTTP_OK,
[
'Content-Transfer-Encoding', 'binary',
'Content-Type' => 'application/vnd.oasis.opendocument.text',
'Content-Disposition' => sprintf('attachment; filename="%s.odt"', 'generated'),
'Content-Length' => fstat($generatedResource)['size'],
'Content-Disposition' => 'attachment; filename="generated.odt"',
'Content-Length' => strlen($generatedResource),
],
);
}
$genDocName = 'doc_' . sprintf('%010d', mt_rand()) . 'odt';
$getUrlGen = $this->tempUrlGenerator->generate(
'PUT',
$genDocName
);
$client = new Client();
/** @var StoredObject $storedObject */
$storedObject = (new ObjectNormalizer())
->denormalize(
[
'type' => $template->getFile()->getType(),
'filename' => sprintf('%s_odt', uniqid('doc_', true)),
],
StoredObject::class
);
try {
$putResponse = $client->request('PUT', $getUrlGen->url, [
'body' => $generatedResource,
]);
$this->storedObjectManager->write($storedObject, $generatedResource);
} catch (Throwable $exception) {
throw $exception;
}
if ($putResponse->getStatusCode() === 201) {
$em = $this->getDoctrine()->getManager();
$storedObject = new StoredObject();
$storedObject
->setType($template->getFile()->getType())
->setFilename($genDocName);
$this->entityManager->persist($storedObject);
$em->persist($storedObject);
try {
$context->storeGenerated($template, $storedObject, $entity, $contextGenerationData);
} catch (Exception $e) {
$this->logger->error('Could not store the associated document to entity', [
try {
$context
->storeGenerated(
$template,
$storedObject,
$entity,
$contextGenerationData
);
} catch (Exception $e) {
$this
->logger
->error(
'Unable to store the associated document to entity',
[
'entityClassName' => $entityClassName,
'entityId' => $entityId,
'contextKey' => $context->getName(),
]);
]
);
throw $e;
}
$em->flush();
return $this->redirectToRoute('chill_wopi_file_edit', [
'fileId' => $storedObject->getUuid(),
'returnPath' => $request->query->get('returnPath', '/'),
]);
}
} catch (TransferException $e) {
throw $e;
}
throw new Exception('Unable to generate document.');
$this->entityManager->flush();
return $this
->redirectToRoute(
'chill_wopi_file_edit',
[
'fileId' => $storedObject->getUuid(),
'returnPath' => $request->query->get('returnPath', '/'),
]
);
}
}

View File

@@ -13,10 +13,5 @@ namespace Chill\DocGeneratorBundle\GeneratorDriver;
interface DriverInterface
{
/**
* @param resource $template
*
* @return resource
*/
public function generateFromResource($template, string $resourceType, array $data, ?string $templateName = null);
public function generateFromString(string $template, string $resourceType, array $data, ?string $templateName = null): string;
}

View File

@@ -16,45 +16,47 @@ use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Mime\Part\Multipart\FormDataPart;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Throwable;
class RelatorioDriver implements DriverInterface
final class RelatorioDriver implements DriverInterface
{
private LoggerInterface $logger;
private HttpClientInterface $client;
private HttpClientInterface $relatorioClient;
private LoggerInterface $logger;
private string $url;
public function __construct(
HttpClientInterface $relatorioClient,
HttpClientInterface $client,
ParameterBagInterface $parameterBag,
LoggerInterface $logger
) {
$this->relatorioClient = $relatorioClient;
$this->client = $client;
$this->logger = $logger;
$this->url = $parameterBag->get('chill_doc_generator')['driver']['relatorio']['url'];
}
public function generateFromResource($template, string $resourceType, array $data, ?string $templateName = null)
public function generateFromString(string $template, string $resourceType, array $data, ?string $templateName = null): string
{
$formFields = [
'variables' => json_encode($data),
'template' => new DataPart($template, $templateName ?? uniqid('template_'), $resourceType),
];
$form = new FormDataPart($formFields);
$form = new FormDataPart(
[
'variables' => json_encode($data),
'template' => new DataPart($template, $templateName ?? uniqid('template_'), $resourceType),
]
);
try {
$response = $this->relatorioClient->request('POST', $this->url, [
$response = $this->client->request('POST', $this->url, [
'headers' => $form->getPreparedHeaders()->toArray(),
'body' => $form->bodyToIterable(),
]);
return $response->toStream();
} catch (HttpExceptionInterface $e) {
return $response->getContent();
} catch (ClientExceptionInterface $e) {
$content = $e->getResponse()->getContent(false);
if (400 === $e->getResponse()->getStatusCode()) {
@@ -87,6 +89,18 @@ class RelatorioDriver implements DriverInterface
]);
throw $e;
} catch (Throwable $exception) {
$this
->logger
->error(
'relatorio: Unable to get content from response.',
[
'msg' => $exception->getMessage(),
'e' => $exception->getTraceAsString(),
]
);
throw $exception;
}
}
}

View File

@@ -18,6 +18,7 @@
}
],
"require": {
"spomky-labs/base64url": "^2"
},
"require-dev": {
},

View File

@@ -155,7 +155,7 @@ class DocumentAccompanyingCourseController extends AbstractController
$documents = $this->courseRepository
->findBy(
['course' => $course],
['date' => 'DESC'],
['date' => 'DESC', 'id' => 'DESC'],
$pagination->getItemsPerPage(),
$pagination->getCurrentPageFirstItemNumber()
);

View File

@@ -178,7 +178,7 @@ class DocumentPersonController extends AbstractController
$documents = $this->personDocumentACLAwareRepository->findByPerson(
$person,
[],
['date' => 'DESC', 'id' => 'DESC'],
$pagination->getItemsPerPage(),
$pagination->getCurrentPageFirstItemNumber()
);

View File

@@ -109,12 +109,12 @@ class StoredObject implements AsyncFileInterface, Document
return $this->id;
}
public function getIv()
public function getIv(): array
{
return $this->iv;
}
public function getKeyInfos()
public function getKeyInfos(): array
{
return $this->keyInfos;
}
@@ -163,21 +163,21 @@ class StoredObject implements AsyncFileInterface, Document
return $this;
}
public function setIv($iv)
public function setIv(array $iv)
{
$this->iv = $iv;
return $this;
}
public function setKeyInfos($keyInfos)
public function setKeyInfos(array $keyInfos)
{
$this->keyInfos = $keyInfos;
return $this;
}
public function setType($type)
public function setType(string $type)
{
$this->type = $type;

View File

@@ -0,0 +1,40 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\DocStoreBundle\Exception;
use Exception;
use Throwable;
final class StoredObjectManagerException extends Exception
{
public static function errorDuringHttpRequest(Throwable $exception): self
{
return new self('Error during HTTP request.', 500, $exception);
}
public static function invalidStatusCode(int $code): self
{
return new self(
sprintf('Invalid status code received (%s).', $code)
);
}
public static function unableToDecrypt(string $message): self
{
return new self(sprintf('Unable to decrypt content (reason: %s).', $message));
}
public static function unableToGetResponseContent(Throwable $exception): self
{
return new self('Unable to get content from response.', 500, $exception);
}
}

View File

@@ -64,7 +64,7 @@ class PersonDocumentACLAwareRepository implements PersonDocumentACLAwareReposito
$this->addACL($qb, $person);
foreach ($orderBy as [$field, $order]) {
foreach ($orderBy as $field => $order) {
$qb->addOrderBy($field, $order);
}

View File

@@ -30,7 +30,7 @@
</a>
</li>
<li class="create">
<button class="btn btn-create">{{ 'Create'|trans }}</button>
<button class="btn btn-save">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(form) }}

View File

@@ -1,7 +1,3 @@
{# Twig way
TODO: une route, un template avec un header CHILL et un iframe
#}
<a href="{{ chill_path_add_return_path('chill_wopi_file_edit', {'fileId': document.uuid}) }}"
class="btn btn-wopilink">
{{ 'online_edit_document'|trans }}

View File

@@ -53,6 +53,11 @@
<li>
<a href="{{ path('accompanying_course_document_edit', {'course': accompanyingCourse.id, 'id': document.id }) }}" class="btn btn-update"></a>
</li>
{% if chill_document_is_editable(document.object) %}
<li>
{{ document.object|chill_document_edit_button }}
</li>
{% endif %}
{% endif %}
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
<li>
@@ -75,6 +80,11 @@
<li>
<a href="{{ path('person_document_edit', {'person': person.id, 'id': document.id}) }}" class="btn btn-update"></a>
</li>
{% if chill_document_is_editable(document.object) %}
<li>
{{ document.object|chill_document_edit_button }}
</li>
{% endif %}
{% endif %}
{% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %}
<li>

View File

@@ -47,7 +47,7 @@
</a>
</li>
<li class="create">
<button class="btn btn-create">{{ 'Create'|trans }}</button>
<button class="btn btn-save">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(form) }}

View File

@@ -0,0 +1,133 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\DocStoreBundle\Service;
use Base64Url\Base64Url;
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Throwable;
use const OPENSSL_RAW_DATA;
final class StoredObjectManager implements StoredObjectManagerInterface
{
private const ALGORITHM = 'AES-256-CBC';
private HttpClientInterface $client;
private TempUrlGeneratorInterface $tempUrlGenerator;
public function __construct(
HttpClientInterface $client,
TempUrlGeneratorInterface $tempUrlGenerator
) {
$this->client = $client;
$this->tempUrlGenerator = $tempUrlGenerator;
}
public function read(StoredObject $document): string
{
try {
$response = $this
->client
->request(
Request::METHOD_GET,
$this
->tempUrlGenerator
->generate(
Request::METHOD_GET,
$document->getFilename()
)
->url
);
} catch (Throwable $e) {
throw StoredObjectManagerException::errorDuringHttpRequest($e);
}
if ($response->getStatusCode() !== Response::HTTP_OK) {
throw StoredObjectManagerException::invalidStatusCode($response->getStatusCode());
}
try {
$data = $response->getContent();
} catch (Throwable $e) {
throw StoredObjectManagerException::unableToGetResponseContent($e);
}
if (false === $this->hasKeysAndIv($document)) {
return $data;
}
$clearData = openssl_decrypt(
$data,
self::ALGORITHM,
// TODO: Why using this library and not use base64_decode() ?
Base64Url::decode($document->getKeyInfos()['k']),
OPENSSL_RAW_DATA,
pack('C*', ...$document->getIv())
);
if (false === $clearData) {
throw StoredObjectManagerException::unableToDecrypt(openssl_error_string());
}
return $clearData;
}
public function write(StoredObject $document, string $clearContent): void
{
$encryptedContent = $this->hasKeysAndIv($document)
? openssl_encrypt(
$clearContent,
self::ALGORITHM,
// TODO: Why using this library and not use base64_decode() ?
Base64Url::decode($document->getKeyInfos()['k']),
OPENSSL_RAW_DATA,
pack('C*', ...$document->getIv())
)
: $clearContent;
try {
$response = $this
->client
->request(
Request::METHOD_PUT,
$this
->tempUrlGenerator
->generate(
Request::METHOD_PUT,
$document->getFilename()
)
->url,
[
'body' => $encryptedContent,
]
);
} catch (TransportExceptionInterface $exception) {
throw StoredObjectManagerException::errorDuringHttpRequest($exception);
}
if ($response->getStatusCode() !== Response::HTTP_CREATED) {
throw StoredObjectManagerException::invalidStatusCode($response->getStatusCode());
}
}
private function hasKeysAndIv(StoredObject $storedObject): bool
{
return ([] !== $storedObject->getKeyInfos()) && ([] !== $storedObject->getIv());
}
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\DocStoreBundle\Service;
use Chill\DocStoreBundle\Entity\StoredObject;
interface StoredObjectManagerInterface
{
/**
* Get the content of a StoredObject.
*
* @param StoredObject $document The document.
*
* @return string The retrieved content in clear.
*/
public function read(StoredObject $document): string;
/**
* Set the content of a StoredObject.
*
* @param StoredObject $document The document.
* @param $clearContent The content to store in clear.
*/
public function write(StoredObject $document, string $clearContent): void;
}

View File

@@ -16,10 +16,102 @@ use Chill\DocStoreBundle\Entity\StoredObject;
use Twig\Environment;
use Twig\Extension\RuntimeExtensionInterface;
use function array_key_exists;
use function in_array;
final class WopiEditTwigExtensionRuntime implements RuntimeExtensionInterface
{
public const SUPPORTED_MIMES = [
'image/svg+xml',
'application/vnd.ms-powerpoint',
'application/vnd.ms-excel',
'application/vnd.sun.xml.writer',
'application/vnd.oasis.opendocument.text',
'application/vnd.oasis.opendocument.text-flat-xml',
'application/vnd.sun.xml.calc',
'application/vnd.oasis.opendocument.spreadsheet',
'application/vnd.oasis.opendocument.spreadsheet-flat-xml',
'application/vnd.sun.xml.impress',
'application/vnd.oasis.opendocument.presentation',
'application/vnd.oasis.opendocument.presentation-flat-xml',
'application/vnd.sun.xml.draw',
'application/vnd.oasis.opendocument.graphics',
'application/vnd.oasis.opendocument.graphics-flat-xml',
'application/vnd.oasis.opendocument.chart',
'application/vnd.sun.xml.writer.global',
'application/vnd.oasis.opendocument.text-master',
'application/vnd.sun.xml.writer.template',
'application/vnd.oasis.opendocument.text-template',
'application/vnd.oasis.opendocument.text-master-template',
'application/vnd.sun.xml.calc.template',
'application/vnd.oasis.opendocument.spreadsheet-template',
'application/vnd.sun.xml.impress.template',
'application/vnd.oasis.opendocument.presentation-template',
'application/vnd.sun.xml.draw.template',
'application/vnd.oasis.opendocument.graphics-template',
'application/msword',
'application/msword',
'application/vnd.ms-excel',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-word.document.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
'application/vnd.ms-word.template.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
'application/vnd.ms-excel.template.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
'application/vnd.ms-excel.sheet.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.presentationml.template',
'application/vnd.ms-powerpoint.template.macroEnabled.12',
'application/vnd.wordperfect',
'application/x-aportisdoc',
'application/x-hwp',
'application/vnd.ms-works',
'application/x-mswrite',
'application/x-dif-document',
'text/spreadsheet',
'text/csv',
'application/x-dbase',
'application/vnd.lotus-1-2-3',
'image/cgm',
'image/vnd.dxf',
'image/x-emf',
'image/x-wmf',
'application/coreldraw',
'application/vnd.visio2013',
'application/vnd.visio',
'application/vnd.ms-visio.drawing',
'application/x-mspublisher',
'application/x-sony-bbeb',
'application/x-gnumeric',
'application/macwriteii',
'application/x-iwork-numbers-sffnumbers',
'application/vnd.oasis.opendocument.text-web',
'application/x-pagemaker',
'text/rtf',
'text/plain',
'application/x-fictionbook+xml',
'application/clarisworks',
'image/x-wpg',
'application/x-iwork-pages-sffpages',
'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
'application/x-iwork-keynote-sffkey',
'application/x-abiword',
'image/x-freehand',
'application/vnd.sun.xml.chart',
'application/x-t602',
'image/bmp',
'image/png',
'image/gif',
'image/tiff',
'image/jpg',
'image/jpeg',
'application/pdf',
];
private const TEMPLATE = '@ChillDocStore/Button/wopi_edit_document.html.twig';
private DiscoveryInterface $discovery;
@@ -31,15 +123,7 @@ final class WopiEditTwigExtensionRuntime implements RuntimeExtensionInterface
public function isEditable(StoredObject $document): bool
{
$mime_type = $this->discovery->discoverMimeType($document->getType());
foreach ($mime_type as $item) {
if (array_key_exists('default', $item) && 'true' === $item['default']) {
return true;
}
}
return false;
return in_array($document->getType(), self::SUPPORTED_MIMES, true);
}
public function renderEditButton(Environment $environment, StoredObject $document, ?array $options = null): string

View File

@@ -0,0 +1,183 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\DocStoreBundle\Tests;
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
use Chill\DocStoreBundle\Service\StoredObjectManager;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Generator;
use PHPUnit\Framework\TestCase;
use stdClass;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* @internal
*
* @covers \Chill\DocStoreBundle\Service\StoredObjectManager
*/
final class StoredObjectManagerTest extends TestCase
{
public function getDataProvider(): Generator
{
/* HAPPY SCENARIO */
// Encrypted object
yield [
(new StoredObject())
->setFilename('encrypted.txt')
->setKeyInfos(['k' => base64_encode('S9NIHMaFHOWzLPez3jZOIHBaNfBrMQUR5zvqBz6kme8')])
->setIv(unpack('C*', 'abcdefghijklmnop')),
hex2bin('741237d255fd4f7eddaaa9058912a84caae28a41b10b34d4e3e3abe41d3b9b47cb0dd8f22c3c883d4f0e9defa75ff662'), // Binary encoded string
'The quick brown fox jumps over the lazy dog', // clear
];
// Non-encrypted object
yield [
(new StoredObject())->setFilename('non-encrypted.txt'), // The StoredObject
'The quick brown fox jumps over the lazy dog', // Encrypted
'The quick brown fox jumps over the lazy dog', // Clear
];
/* UNHAPPY SCENARIO */
// Encrypted object with issue during HTTP communication
yield [
(new StoredObject())
->setFilename('error_during_http_request.txt')
->setKeyInfos(['k' => base64_encode('S9NIHMaFHOWzLPez3jZOIHBaNfBrMQUR5zvqBz6kme8')])
->setIv(unpack('C*', 'abcdefghijklmnop')),
hex2bin('741237d255fd4f7eddaaa9058912a84caae28a41b10b34d4e3e3abe41d3b9b47cb0dd8f22c3c883d4f0e9defa75ff662'), // Binary encoded string
'The quick brown fox jumps over the lazy dog', // clear
StoredObjectManagerException::class,
];
// Encrypted object with issue during HTTP communication: Invalid status code
yield [
(new StoredObject())
->setFilename('invalid_statuscode.txt')
->setKeyInfos(['k' => base64_encode('S9NIHMaFHOWzLPez3jZOIHBaNfBrMQUR5zvqBz6kme8')])
->setIv(unpack('C*', 'abcdefghijklmnop')),
hex2bin('741237d255fd4f7eddaaa9058912a84caae28a41b10b34d4e3e3abe41d3b9b47cb0dd8f22c3c883d4f0e9defa75ff662'), // Binary encoded string
'The quick brown fox jumps over the lazy dog', // clear
StoredObjectManagerException::class,
];
// Erroneous encrypted: Unable to decrypt exception.
yield [
(new StoredObject())
->setFilename('unable_to_decrypt.txt')
->setKeyInfos(['k' => base64_encode('WRONG_PASS_PHRASE')])
->setIv(unpack('C*', 'abcdefghijklmnop')),
'WRONG_ENCODED_VALUE', // Binary encoded string
'The quick brown fox jumps over the lazy dog', // clear
StoredObjectManagerException::class,
];
}
/**
* @dataProvider getDataProvider
*/
public function testRead(StoredObject $storedObject, string $encodedContent, string $clearContent, ?string $exceptionClass = null)
{
if (null !== $exceptionClass) {
$this->expectException($exceptionClass);
}
$storedObjectManager = $this->getSubject($storedObject, $encodedContent);
self::assertEquals($clearContent, $storedObjectManager->read($storedObject));
}
/**
* @dataProvider getDataProvider
*/
public function testWrite(StoredObject $storedObject, string $encodedContent, string $clearContent, ?string $exceptionClass = null)
{
if (null !== $exceptionClass) {
$this->expectException($exceptionClass);
}
$storedObjectManager = $this->getSubject($storedObject, $encodedContent);
$storedObjectManager->write($storedObject, $clearContent);
self::assertEquals($clearContent, $storedObjectManager->read($storedObject));
}
private function getHttpClient(string $encodedContent): HttpClientInterface
{
$callback = static function ($method, $url, $options) use ($encodedContent) {
if (Request::METHOD_GET === $method) {
switch ($url) {
case 'https://example.com/non-encrypted.txt':
case 'https://example.com/encrypted.txt':
return new MockResponse($encodedContent, ['http_code' => 200]);
case 'https://example.com/error_during_http_request.txt':
return new TransportException('error_during_http_request.txt');
case 'https://example.com/invalid_statuscode.txt':
return new MockResponse($encodedContent, ['http_code' => 404]);
}
}
if (Request::METHOD_PUT === $method) {
switch ($url) {
case 'https://example.com/non-encrypted.txt':
case 'https://example.com/encrypted.txt':
return new MockResponse($encodedContent, ['http_code' => 201]);
case 'https://example.com/error_during_http_request.txt':
throw new TransportException('error_during_http_request.txt');
case 'https://example.com/invalid_statuscode.txt':
return new MockResponse($encodedContent, ['http_code' => 404]);
}
}
return new MockResponse('Not found');
};
return new MockHttpClient($callback);
}
private function getSubject(StoredObject $storedObject, string $encodedContent): StoredObjectManagerInterface
{
return new StoredObjectManager(
$this->getHttpClient($encodedContent),
$this->getTempUrlGenerator($storedObject)
);
}
private function getTempUrlGenerator(StoredObject $storedObject): TempUrlGeneratorInterface
{
$response = new stdClass();
$response->url = $storedObject->getFilename();
$tempUrlGenerator = $this
->getMockBuilder(TempUrlGeneratorInterface::class)
->getMock();
$tempUrlGenerator
->method('generate')
->withAnyParameters()
->willReturn($response);
return $tempUrlGenerator;
}
}

View File

@@ -116,6 +116,6 @@ class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandler
public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool
{
return true;
return false;
}
}

View File

@@ -8,7 +8,8 @@
}
},
"require": {
"symfony/mime": "^4 || ^5"
"symfony/mime": "^4 || ^5",
"symfony/http-foundation": "^4"
},
"license": "AGPL-3.0"
}

View File

@@ -40,3 +40,8 @@ services:
tags:
- { name: 'serializer.normalizer', priority: 16 }
Chill\DocStoreBundle\Service\:
autowire: true
autoconfigure: true
resource: '../Service/'

View File

@@ -235,7 +235,7 @@ class ParticipationController extends AbstractController
'The participation was created'
));
if ($request->query->get('return_path')) {
if ($request->query->has('return_path')) {
return $this->redirect($request->query->get('return_path'));
}
@@ -716,7 +716,7 @@ class ParticipationController extends AbstractController
*/
protected function newSingle(Request $request)
{
$returnPath = $request->query->get('return_path') ?
$returnPath = $request->query->has('return_path') ?
$request->query->get('return_path') : null;
$participation = $this->handleRequest($request, new Participation(), false);

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\CRUDController;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Symfony\Component\HttpFoundation\Request;
class LocationController extends CRUDController
@@ -29,4 +30,9 @@ class LocationController extends CRUDController
{
$query->where('e.availableForUsers = true'); //TODO not working
}
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
{
return $query->addOrderBy('e.name', 'DESC');
}
}

View File

@@ -22,6 +22,7 @@ use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\NotificationRepository;
use Chill\MainBundle\Security\Authorization\NotificationVoter;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
@@ -31,14 +32,19 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface;
use function in_array;
/**
* @Route("/{_locale}/notification")
*/
class NotificationController extends AbstractController
{
private LoggerInterface $chillLogger;
private EntityManagerInterface $em;
private LoggerInterface $logger;
private NotificationHandlerManager $notificationHandlerManager;
private NotificationRepository $notificationRepository;
@@ -51,6 +57,8 @@ class NotificationController extends AbstractController
public function __construct(
EntityManagerInterface $em,
LoggerInterface $chillLogger,
LoggerInterface $logger,
Security $security,
NotificationRepository $notificationRepository,
NotificationHandlerManager $notificationHandlerManager,
@@ -58,6 +66,8 @@ class NotificationController extends AbstractController
TranslatorInterface $translator
) {
$this->em = $em;
$this->logger = $logger;
$this->chillLogger = $chillLogger;
$this->security = $security;
$this->notificationRepository = $notificationRepository;
$this->notificationHandlerManager = $notificationHandlerManager;
@@ -150,6 +160,52 @@ class NotificationController extends AbstractController
]);
}
/**
* @Route("/{id}/access_key", name="chill_main_notification_grant_access_by_access_key")
*/
public function getAccessByAccessKey(Notification $notification, Request $request): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
if (!$this->security->getUser() instanceof User) {
throw new AccessDeniedHttpException('You must be authenticated and a user to create a notification');
}
foreach (['accessKey'/*, 'email'*/] as $param) {
if (!$request->query->has($param)) {
throw new BadRequestHttpException("Missing {$param} parameter");
}
}
if ($notification->getAccessKey() !== $request->query->getAlnum('accessKey')) {
throw new AccessDeniedHttpException('access key is invalid');
}
/*
desactivated due to escaped '&' in email links
if (!in_array($request->query->get('email'), $notification->getAddressesEmails(), true)) {
return (new Response('The email address is no more associated with this notification'))
->setStatusCode(Response::HTTP_FORBIDDEN);
}
*/
$notification->addAddressee($this->security->getUser());
$this->getDoctrine()->getManager()->flush();
$logMsg = '[Notification] a user is granted access to notification trough an access key';
$context = [
'notificationId' => $notification->getId(),
'email' => $request->query->get('email'),
'user' => $this->security->getUser()->getId(),
];
$this->logger->info($logMsg, $context);
$this->chillLogger->info($logMsg, $context);
return $this->redirectToRoute('chill_main_notification_show', ['id' => $notification->getId()]);
}
/**
* @Route("/inbox", name="chill_main_notification_my")
*/

View File

@@ -122,7 +122,7 @@ class SearchController extends AbstractController
public function searchAction(Request $request, $_format)
{
$pattern = $request->query->get('q', '');
$pattern = trim($request->query->get('q', ''));
if ('' === $pattern) {
switch ($_format) {

View File

@@ -140,6 +140,7 @@ class Address
/**
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode")
* @ORM\JoinColumn(nullable=false)
* @Groups({"write"})
*/
private ?PostalCode $postcode = null;
@@ -480,8 +481,11 @@ class Address
return $this;
}
public function setStreet(string $street): self
public function setStreet(?string $street): self
{
if (null === $street) {
$street = '';
}
$this->street = $street;
return $this;
@@ -515,8 +519,11 @@ class Address
return $this;
}
public function setStreetNumber(string $streetNumber): self
public function setStreetNumber(?string $streetNumber): self
{
if (null === $streetNumber) {
$streetNumber = '';
}
$this->streetNumber = $streetNumber;
return $this;

View File

@@ -18,6 +18,9 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use function count;
use function in_array;
/**
* @ORM\Entity
@@ -31,15 +34,34 @@ use Symfony\Component\Validator\Constraints as Assert;
*/
class Notification implements TrackUpdateInterface
{
/**
* @ORM\Column(type="text", nullable=false)
*/
private string $accessKey;
private array $addedAddresses = [];
/**
* @ORM\ManyToMany(targetEntity=User::class)
* @ORM\JoinTable(name="chill_main_notification_addresses_user")
* @Assert\Count(min="1", minMessage="notification.At least one addressee")
*/
private Collection $addressees;
/**
* a list of destinee which will receive notifications.
*
* @var array|string[]
* @ORM\Column(type="json")
*/
private array $addressesEmails = [];
/**
* a list of emails adresses which were added to the notification.
*
* @var array|string[]
*/
private array $addressesEmailsAdded = [];
private ?ArrayCollection $addressesOnLoad = null;
/**
@@ -111,6 +133,7 @@ class Notification implements TrackUpdateInterface
$this->unreadBy = new ArrayCollection();
$this->comments = new ArrayCollection();
$this->setDate(new DateTimeImmutable());
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(24));
}
public function addAddressee(User $addressee): self
@@ -123,6 +146,14 @@ class Notification implements TrackUpdateInterface
return $this;
}
public function addAddressesEmail(string $email)
{
if (!in_array($email, $this->addressesEmails, true)) {
$this->addressesEmails[] = $email;
$this->addressesEmailsAdded[] = $email;
}
}
public function addComment(NotificationComment $comment): self
{
if (!$this->comments->contains($comment)) {
@@ -142,6 +173,30 @@ class Notification implements TrackUpdateInterface
return $this;
}
/**
* @Assert\Callback
*
* @param array $payload
*/
public function assertCountAddresses(ExecutionContextInterface $context, $payload): void
{
if (0 === (count($this->getAddressesEmails()) + count($this->getAddressees()))) {
$context->buildViolation('notification.At least one addressee')
->atPath('addressees')
->addViolation();
}
}
public function getAccessKey(): string
{
return $this->accessKey;
}
public function getAddedAddresses(): array
{
return $this->addedAddresses;
}
/**
* @return Collection|User[]
*/
@@ -155,6 +210,22 @@ class Notification implements TrackUpdateInterface
return $this->addressees;
}
/**
* @return array|string[]
*/
public function getAddressesEmails(): array
{
return $this->addressesEmails;
}
/**
* @return array|string[]
*/
public function getAddressesEmailsAdded(): array
{
return $this->addressesEmailsAdded;
}
public function getComments(): Collection
{
return $this->comments;
@@ -271,6 +342,14 @@ class Notification implements TrackUpdateInterface
return $this;
}
public function removeAddressesEmail(string $email)
{
if (in_array($email, $this->addressesEmails, true)) {
$this->addressesEmails = array_filter($this->addressesEmails, static fn ($e) => $e !== $email);
$this->addressesEmailsAdded = array_filter($this->addressesEmailsAdded, static fn ($e) => $e !== $email);
}
}
public function removeComment(NotificationComment $comment): self
{
$this->comments->removeElement($comment);

View File

@@ -12,12 +12,17 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Form\Type\ChillCollectionType;
use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotNull;
class NotificationType extends AbstractType
{
@@ -30,9 +35,27 @@ class NotificationType extends AbstractType
])
->add('addressees', PickUserDynamicType::class, [
'multiple' => true,
'required' => false,
])
->add('message', ChillTextareaType::class, [
'required' => false,
])
->add('addressesEmails', ChillCollectionType::class, [
'label' => 'notification.dest by email',
'help' => 'notification.dest by email help',
'by_reference' => false,
'allow_add' => true,
'allow_delete' => true,
'entry_type' => EmailType::class,
'button_add_label' => 'notification.Add an email',
'button_remove_label' => 'notification.Remove an email',
'empty_collection_explain' => 'notification.Any email',
'entry_options' => [
'constraints' => [
new NotNull(), new NotBlank(), new Email(['checkMX' => true]),
],
'label' => 'Email',
],
]);
}

View File

@@ -0,0 +1,37 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Form\Type;
use Chill\MainBundle\Entity\Address;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AddressDateType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'validFrom',
ChillDateType::class,
[
'required' => true,
]
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('data_class', Address::class);
}
}

View File

@@ -73,6 +73,17 @@ class NotificationMailer
* Send a email after a notification is persisted.
*/
public function postPersistNotification(Notification $notification, LifecycleEventArgs $eventArgs): void
{
$this->sendNotificationEmailsToAddresses($notification);
$this->sendNotificationEmailsToAddressesEmails($notification);
}
public function postUpdateNotification(Notification $notification, LifecycleEventArgs $eventArgs): void
{
$this->sendNotificationEmailsToAddressesEmails($notification);
}
private function sendNotificationEmailsToAddresses(Notification $notification): void
{
foreach ($notification->getAddressees() as $addressee) {
if (null === $addressee->getEmail()) {
@@ -108,4 +119,31 @@ class NotificationMailer
}
}
}
private function sendNotificationEmailsToAddressesEmails(Notification $notification): void
{
foreach ($notification->getAddressesEmailsAdded() as $emailAddress) {
$email = new TemplatedEmail();
$email
->textTemplate('@ChillMain/Notification/email_non_system_notification_content_to_email.fr.md.twig')
->context([
'notification' => $notification,
'dest' => $emailAddress,
]);
$email
->subject($notification->getTitle())
->to($emailAddress);
try {
$this->mailer->send($email);
} catch (TransportExceptionInterface $e) {
$this->logger->warning('[NotificationMailer] could not send an email notification', [
'to' => $emailAddress,
'error_message' => $e->getMessage(),
'error_trace' => $e->getTraceAsString(),
]);
}
}
}
}

View File

@@ -0,0 +1,58 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Notification\EventListener;
use Chill\MainBundle\Notification\NotificationPersisterInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\TerminateEvent;
use function count;
class PersistNotificationOnTerminateEventSubscriber implements EventSubscriberInterface
{
private EntityManagerInterface $em;
private NotificationPersisterInterface $persister;
public function __construct(EntityManagerInterface $em, NotificationPersisterInterface $persister)
{
$this->em = $em;
$this->persister = $persister;
}
public static function getSubscribedEvents()
{
return [
'kernel.terminate' => [
['onKernelTerminate', 1024], // we must ensure that the priority is before sending email
],
];
}
public function onKernelTerminate(TerminateEvent $event): void
{
if ($event->isMasterRequest()) {
$this->persistNotifications();
}
}
private function persistNotifications(): void
{
if (0 < count($this->persister->getWaitingNotifications())) {
foreach ($this->persister->getWaitingNotifications() as $notification) {
$this->em->persist($notification);
}
$this->em->flush();
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Notification;
use Chill\MainBundle\Entity\Notification;
class NotificationPersister implements NotificationPersisterInterface
{
private array $waitingNotifications = [];
/**
* @return array|Notification[]
*/
public function getWaitingNotifications(): array
{
return $this->waitingNotifications;
}
public function persist(Notification $notification): void
{
$this->waitingNotifications[] = $notification;
}
}

View File

@@ -0,0 +1,31 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Notification;
use Chill\MainBundle\Entity\Notification;
/**
* Store the notification.
*
* Those notification will be stored into database by the kernel. This
* will also ensure that this happens outside of regular operations
* operated by the entity manager.
*/
interface NotificationPersisterInterface
{
/**
* @return array|Notification[]
*/
public function getWaitingNotifications(): array;
public function persist(Notification $notification): void;
}

View File

@@ -56,17 +56,18 @@ final class NotificationRepository implements ObjectRepository
if (null === $this->notificationByRelatedEntityAndUserAssociatedStatement) {
$sql =
'SELECT
SUM((EXISTS (SELECT 1 AS c FROM chill_main_notification_addresses_unread cmnau WHERE user_id = 1812 and cmnau.notification_id = cmn.id))::int) AS unread,
SUM((cmn.sender_id = 1812)::int) AS sent,
SUM((EXISTS (SELECT 1 AS c FROM chill_main_notification_addresses_unread cmnau WHERE user_id = :userid and cmnau.notification_id = cmn.id))::int) AS unread,
SUM((cmn.sender_id = :userid)::int) AS sent,
COUNT(cmn.*) AS total
FROM chill_main_notification cmn
WHERE relatedentityclass = :relatedEntityClass AND relatedentityid = :relatedEntityId AND sender_id IS NOT NULL';
$this->notificationByRelatedEntityAndUserAssociatedStatement =
$this->em->getConnection()->prepare($sql);
}
$results = $this->notificationByRelatedEntityAndUserAssociatedStatement
->executeQuery(['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId]);
->executeQuery(['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId, 'userid' => $user->getId()]);
$result = $results->fetchAssociative();

View File

@@ -16,6 +16,9 @@
// Chill forms
@import './scss/forms';
// Extend bootstrap accordion
@import './scss/accordion';
// Chill record_actions
@import './scss/record_actions';

View File

@@ -0,0 +1,11 @@
.accordion {
// show a folded / label on accordion
button[aria-expanded="true"] > span.folded,
button[aria-expanded="false"] > span.unfolded { display: none; }
button[aria-expanded="false"] > span.folded,
button[aria-expanded="true"] > span.unfolded { display: inline; }
.accordion-item {
margin-bottom: 1rem;
}
}

View File

@@ -8,30 +8,31 @@ window.addEventListener('DOMContentLoaded', function (e) {
document.querySelectorAll('.notification_toggle_read_status')
.forEach(function (el, i) {
createApp({
template: '<notification-read-toggle ' +
':notificationId="notificationId" ' +
':buttonClass="buttonClass" ' +
':buttonNoText="buttonNoText" ' +
':showUrl="showUrl" ' +
':isRead="isRead"' +
'@markRead="onMarkRead" @markUnread="onMarkUnread"' +
'></notification-read-toggle>',
template: `<notification-read-toggle
:notificationId="notificationId"
:buttonClass="buttonClass"
:buttonNoText="buttonNoText"
:showUrl="showUrl"
:isRead="isRead"
@markRead="onMarkRead"
@markUnread="onMarkUnread">
</notification-read-toggle>`,
components: {
NotificationReadToggle,
},
data() {
return {
notificationId: +el.dataset.notificationId,
notificationId: el.dataset.notificationId,
buttonClass: el.dataset.buttonClass,
buttonNoText: 'false' === el.dataset.buttonText,
showUrl: el.dataset.showButtonUrl,
isRead: 1 === +el.dataset.notificationCurrentIsRead,
isRead: 1 === Number.parseInt(el.dataset.notificationCurrentIsRead),
container: el.dataset.container
}
},
computed: {
getContainer() {
return document.querySelectorAll('div.' + this.container);
return document.querySelectorAll(`div.${this.container}`);
}
},
methods: {

View File

@@ -25,14 +25,14 @@
{{ $t('my_accompanying_courses.tab') }}
</a>
</li>
<li class="nav-item">
<!-- <li class="nav-item">
<a class="nav-link"
:class="{'active': activeTab === 'MyWorks'}"
@click="selectTab('MyWorks')">
{{ $t('my_works.tab') }}
<tab-counter :count="state.works.count"></tab-counter>
</a>
</li>
</li> -->
<li class="nav-item">
<a class="nav-link"
:class="{'active': activeTab === 'MyEvaluations'}"
@@ -135,7 +135,7 @@ export default {
for (const m of [
'MyNotifications',
'MyAccompanyingCourses',
'MyWorks',
// 'MyWorks',
'MyEvaluations',
'MyTasks',
'MyWorkflows',

View File

@@ -1,3 +1,4 @@
// CURRENTLY NOT IN USE
<template>
<div class="accompanying_course_work">
<div class="alert alert-light">{{ $t('my_works.description') }}</div>

View File

@@ -15,11 +15,11 @@ const appMessages = {
description_warning: "Liste des tâches auxquelles je suis assigné et dont la date d'échéance est dépassée.",
},
my_accompanying_courses: {
tab: "Mes parcours",
description: "Liste des parcours d'accompagnement que l'on vient de m'attribuer.",
tab: "Mes nouveaux parcours",
description: "Liste des parcours d'accompagnement que l'on vient de m'attribuer depuis moins de 15 jours.",
},
my_notifications: {
tab: "Mes notifications",
tab: "Mes nouvelles notifications",
description: "Liste des notifications reçues et non lues.",
},
my_workflows: {

View File

@@ -13,7 +13,7 @@ const isEmpty = (obj) => {
const store = createStore({
strict: debug,
state: {
works: {},
// works: {},
evaluations: {},
tasks: {
warning: {},
@@ -26,9 +26,9 @@ const store = createStore({
loading: false
},
getters: {
isWorksLoaded(state) {
return !isEmpty(state.works);
},
// isWorksLoaded(state) {
// return !isEmpty(state.works);
// },
isEvaluationsLoaded(state) {
return !isEmpty(state.evaluations);
},
@@ -49,7 +49,7 @@ const store = createStore({
},
counter(state) {
return {
works: state.works.count,
// works: state.works.count,
evaluations: state.evaluations.count,
tasksWarning: state.tasks.warning.count,
tasksAlert: state.tasks.alert.count,
@@ -60,10 +60,10 @@ const store = createStore({
}
},
mutations: {
addWorks(state, works) {
//console.log('addWorks', works);
state.works = works;
},
// addWorks(state, works) {
// //console.log('addWorks', works);
// state.works = works;
// },
addEvaluations(state, evaluations) {
//console.log('addEvaluations', evaluations);
state.evaluations = evaluations;
@@ -99,22 +99,22 @@ const store = createStore({
switch (tab) {
case 'MyCustoms':
break;
case 'MyWorks':
if (!getters.isWorksLoaded) {
commit('setLoading', true);
const url = `/api/1.0/person/accompanying-period/work/my-near-end${'?'+ param}`;
makeFetch('GET', url)
.then((response) => {
commit('addWorks', response);
commit('setLoading', false);
})
.catch((error) => {
commit('catchError', error);
throw error;
})
;
}
break;
// case 'MyWorks':
// if (!getters.isWorksLoaded) {
// commit('setLoading', true);
// const url = `/api/1.0/person/accompanying-period/work/my-near-end${'?'+ param}`;
// makeFetch('GET', url)
// .then((response) => {
// commit('addWorks', response);
// commit('setLoading', false);
// })
// .catch((error) => {
// commit('catchError', error);
// throw error;
// })
// ;
// }
// break;
case 'MyEvaluations':
if (!getters.isEvaluationsLoaded) {
commit('setLoading', true);

View File

@@ -81,7 +81,7 @@ export default {
/// [Option] showUrl is href for show page second button.
// When passed, the component return a button-group with 2 buttons.
isButtonGroup() {
return !!this.showUrl
return this.showUrl;
}
},
methods: {

View File

@@ -47,6 +47,8 @@
</tbody>
</table>
{{ chill_pagination(paginator) }}
<ul class="record_actions">
<li>
<a href="{{ path('chill_crud_main_location_new') }}" class="btn btn-create">

View File

@@ -40,6 +40,11 @@
{{ a|chill_entity_render_string }}
</span>
{% endfor %}
{% for a in c.notification.addressesEmails %}
<span class="badge-user" title="{{ 'notification.Email with access link'|trans|e('html_attr') }}">
{{ a }}
</span>
{% endfor %}
</li>
{% endif %}
</ul>
@@ -61,17 +66,28 @@
<div class="item-row">
<div class="notification-content">
{% if c.full_content is defined and c.full_content == true %}
{{ c.notification.message|chill_markdown_to_html }}
{% if c.notification.message is not empty %}
{{ c.notification.message|chill_markdown_to_html }}
{% else %}
<p class="chill-no-data-statement">{{ 'Any comment'|trans }}</p>
{% endif %}
{% else %}
{{ c.notification.message|u.truncate(250, '…', false)|chill_markdown_to_html }}
<p class="read-more"><a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': c.notification.id}) }}">{{ 'Read more'|trans }}</a></p>
{% if c.notification.message is not empty %}
{{ c.notification.message|u.truncate(250, '…', false)|chill_markdown_to_html }}
<p class="read-more"><a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': c.notification.id}) }}">{{ 'Read more'|trans }}</a></p>
{% else %}
<p class="chill-no-data-statement">{{ 'Any comment'|trans }}</p>
{% endif %}
{% endif %}
</div>
</div>
{% endmacro %}
{% macro actions(c) %}
{% if c.action_button is not defined or c.action_button != false %}
<div class="item-row separator">
<div class="item-col item-meta">
{% if c.notification.comments|length > 0 %}
<div class="comment-counter">
<span class="counter">
@@ -79,16 +95,16 @@
</span>
</div>
{% endif %}
</div>
<div class="item-col">
<ul class="record_actions">
<li>
{# Vue component #}
<span class="notification_toggle_read_status"
data-notification-id="{{ c.notification.id }}"
data-notification-current-is-read="{{ c.notification.isReadBy(app.user) }}"
data-container="notification-status"
data-notification-id="{{ c.notification.id }}"
data-notification-current-is-read="{{ c.notification.isReadBy(app.user) }}"
data-container="notification-status"
></span>
</li>
{% if is_granted('CHILL_MAIN_NOTIFICATION_UPDATE', c.notification) %}
@@ -122,24 +138,25 @@
<button type="button" class="accordion-button collapsed"
data-bs-toggle="collapse" data-bs-target="#flush-collapse-{{ notification.id }}"
aria-expanded="false" aria-controls="flush-collapse-{{ notification.id }}">
{{ _self.title(_context) }}
</button>
{{ _self.header(_context) }}
</div>
<div id="flush-collapse-{{ notification.id }}"
class="accordion-collapse collapse"
aria-labelledby="flush-heading-{{ notification.id }}"
data-bs-parent="#notification-fold">
{{ _self.content(_context) }}
</div>
{{ _self.actions(_context) }}
{% else %}
{{ _self.title(_context) }}
{{ _self.header(_context) }}
{{ _self.content(_context) }}
{{ _self.actions(_context) }}
{% endif %}
</div>

View File

@@ -20,7 +20,9 @@
{{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
{{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
{{ form_row(form.addressesEmails) }}
{% include handler.template(notification) with handler.templateData(notification) %}
<div class="mb-3 row">

View File

@@ -21,6 +21,8 @@
{{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
{{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
{{ form_row(form.addressesEmails) }}
{% include handler.template(notification) with handler.templateData(notification) %}
<div class="mb-3 row">

View File

@@ -0,0 +1,20 @@
{{ dest }},
{{ notification.sender.label }} a créé une notification pour vous:
> {{ notification.title }}
>
>
{%- for line in notification.message|split("\n") %}
> {{ line }}
{%- if not loop.last %}
>
{%- endif %}
{%- endfor %}
Vous pouvez cliquer sur ce lien pour obtenir un accès permanent à la notification:
{{ absolute_url(path('chill_main_notification_grant_access_by_access_key', {'_locale': 'fr', 'id': notification.id, 'accessKey': notification.accessKey, 'email': dest})) }}
--
Le logiciel Chill

View File

@@ -3,6 +3,7 @@
{% block title 'workflow.Delete workflow ?'|trans %}
{% block display_content %}
<div class="col-10 workflow">
<h2>
{{ handler.entityTitle(entityWorkflow) }}
</h2>
@@ -10,6 +11,7 @@
{% include handler.template(entityWorkflow) with handler.templateData(entityWorkflow)|merge({
'display_action': false
}) %}
{% endblock %}
{% block content %}
@@ -25,5 +27,5 @@
'form' : delete_form
} ) }}
</div>
</div>
{% endblock %}

View File

@@ -49,11 +49,12 @@ class SearchUserApiProvider implements SearchApiInterface
SIMILARITY(LOWER(UNACCENT(?)), u.usernamecanonical))', [$pattern, $pattern])
->setFromClause('users AS u')
->setWhereClauses('
u.enabled IS TRUE and (
SIMILARITY(LOWER(UNACCENT(?)), u.usernamecanonical) > 0.15
OR u.usernamecanonical LIKE \'%\' || LOWER(UNACCENT(?)) || \'%\'
OR SIMILARITY(LOWER(UNACCENT(?)), LOWER(UNACCENT(u.label))) > 0.15
OR u.label LIKE \'%\' || LOWER(UNACCENT(?)) || \'%\'
', [$pattern, $pattern, $pattern, $pattern]);
)', [$pattern, $pattern, $pattern, $pattern]);
return $query;
}

View File

@@ -59,7 +59,23 @@ class EntityWorkflowVoter extends Voter
return true;
}
return $this->security->isGranted($entityAttribute, $handler->getRelatedEntity($subject));
$relatedEntity = $handler->getRelatedEntity($subject);
if (null === $relatedEntity) {
return true;
}
if ($this->security->isGranted($entityAttribute, $relatedEntity)) {
return true;
}
foreach ($subject->getSteps() as $step) {
if ($step->getAllDestUser()->contains($token->getUser())) {
return true;
}
}
return false;
case self::DELETE:
return $subject->getStep() === 'initial';

View File

@@ -44,14 +44,25 @@ class DateNormalizer implements ContextAwareNormalizerInterface, DenormalizerInt
switch ($type) {
case DateTime::class:
return DateTime::createFromFormat(DateTimeInterface::ISO8601, $data['datetime']);
$result = DateTime::createFromFormat(DateTimeInterface::ISO8601, $data['datetime']);
break;
case DateTimeInterface::class:
case DateTimeImmutable::class:
return DateTimeImmutable::createFromFormat(DateTimeInterface::ISO8601, $data['datetime']);
$result = DateTimeImmutable::createFromFormat(DateTimeInterface::ISO8601, $data['datetime']);
break;
default:
throw new UnexpectedValueException();
}
throw new UnexpectedValueException();
if (false === $result) {
return null;
}
return $result;
}
public function normalize($date, $format = null, array $context = [])

View File

@@ -32,7 +32,7 @@ class PhonenumberNormalizer implements ContextAwareNormalizerInterface, Denormal
}
/**
* @param mixed $data
* @param string|null $data
* @param mixed $type
* @param null|mixed $format
*
@@ -40,11 +40,7 @@ class PhonenumberNormalizer implements ContextAwareNormalizerInterface, Denormal
*/
public function denormalize($data, $type, $format = null, array $context = [])
{
if (null === $data) {
return null;
}
if (null !== $data && '' === trim($data)) {
if ('' === trim((string) $data)) {
return null;
}

View File

@@ -88,6 +88,24 @@ final class NotificationTest extends KernelTestCase
$this->assertNotContains($user1, $notification->getUnreadBy()->toArray());
}
public function testAddressesEmail(): void
{
$notification = new Notification();
$notification->addAddressesEmail('test');
$notification->addAddressesEmail('other');
$this->assertContains('test', $notification->getAddressesEmails());
$this->assertContains('other', $notification->getAddressesEmails());
$this->assertContains('test', $notification->getAddressesEmailsAdded());
$this->assertContains('other', $notification->getAddressesEmailsAdded());
$notification->removeAddressesEmail('other');
$this->assertNotContains('other', $notification->getAddressesEmails());
$this->assertNotContains('other', $notification->getAddressesEmailsAdded());
}
/**
* @dataProvider generateNotificationData
*/

View File

@@ -0,0 +1,47 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Notification\EventListener;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Notification\EventListener\PersistNotificationOnTerminateEventSubscriber;
use Chill\MainBundle\Notification\NotificationPersister;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\HttpKernel\Event\TerminateEvent;
/**
* @internal
* @coversNothing
*/
final class PersistNotificationOnTerminateEventSubscriberTest extends TestCase
{
use ProphecyTrait;
public function testNotificationIsPersisted()
{
$persister = new NotificationPersister();
$em = $this->prophesize(EntityManagerInterface::class);
$em->persist(Argument::type(Notification::class))->shouldBeCalledTimes(1);
$em->flush()->shouldBeCalledTimes(1);
$event = $this->prophesize(TerminateEvent::class);
$event->isMasterRequest()->willReturn(true);
$eventSubscriber = new PersistNotificationOnTerminateEventSubscriber($em->reveal(), $persister);
$notification = new Notification();
$persister->persist($notification);
$eventSubscriber->onKernelTerminate($event->reveal());
}
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Serializer\Normalizer;
use Chill\MainBundle\Serializer\Normalizer\PhonenumberNormalizer;
use libphonenumber\PhoneNumber;
use libphonenumber\PhoneNumberUtil;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
/**
* @internal
* @coversNothing
*/
final class PhonenumberNormalizerTest extends TestCase
{
use ProphecyTrait;
public function dataProviderNormalizePhonenumber()
{
$phonenumberUtil = PhoneNumberUtil::getInstance();
yield [$phonenumberUtil->parse('+32486123465'), 'docgen', ['docgen:expects' => PhoneNumber::class], '0486 12 34 65'];
yield [null, 'docgen', ['docgen:expects' => PhoneNumber::class], ''];
}
/**
* @dataProvider dataProviderNormalizePhonenumber
*
* @param mixed $format
* @param mixed $context
* @param mixed $expected
*/
public function testNormalize(?Phonenumber $phonenumber, $format, $context, $expected)
{
$parameterBag = $this->prophesize(ParameterBagInterface::class);
$parameterBag->get(Argument::exact('chill_main'))->willReturn(['phone_helper' => ['default_carrier_code' => 'BE']]);
$normalizer = new PhonenumberNormalizer($parameterBag->reveal());
$this->assertEquals($expected, $normalizer->normalize($phonenumber, $format, $context));
}
}

View File

@@ -3,6 +3,11 @@ services:
autowire: true
autoconfigure: true
Chill\MainBundle\Notification\:
resource: ../../Notification/
autoconfigure: true
autowire: true
Chill\MainBundle\Notification\Mailer:
arguments:
$logger: '@Psr\Log\LoggerInterface'
@@ -17,12 +22,6 @@ services:
arguments:
$handlers: !tagged_iterator chill_main.notification_handler
Chill\MainBundle\Notification\NotificationPresence: ~
Chill\MainBundle\Notification\Templating\NotificationTwigExtension: ~
Chill\MainBundle\Notification\Templating\NotificationTwigExtensionRuntime: ~
Chill\MainBundle\Notification\Counter\NotificationByUserCounter:
autoconfigure: true
autowire: true
@@ -61,6 +60,15 @@ services:
# set the 'lazy' option to TRUE to only instantiate listeners when they are used
lazy: true
method: 'postPersistNotification'
-
name: 'doctrine.orm.entity_listener'
event: 'postUpdate'
entity: 'Chill\MainBundle\Entity\Notification'
# set the 'lazy' option to TRUE to only instantiate listeners when they are used
lazy: true
method: 'postUpdateNotification'
-
name: 'doctrine.orm.entity_listener'
event: 'postPersist'

View File

@@ -0,0 +1,46 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20220413154743 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_notification DROP adressesEmails');
$this->addSql('ALTER TABLE chill_main_notification DROP accessKey');
}
public function getDescription(): string
{
return 'add access keys and emails dest to notifications';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_notification ADD addressesEmails JSON NOT NULL DEFAULT \'[]\';');
$this->addSql('ALTER TABLE chill_main_notification ADD accessKey TEXT DEFAULT NULL');
$this->addSql('WITH randoms AS (select
n.id,
string_agg(substr(characters, (random() * length(characters) + 0.5)::integer, 1), \'\') as random_word
from (values(\'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\')) as symbols(characters)
-- length of word
join generate_series(1, 16) on 1 = 1
JOIN chill_main_notification n ON true
GROUP BY n.id)
UPDATE chill_main_notification SET accessKey = randoms.random_word FROM randoms WHERE chill_main_notification.id = randoms.id');
$this->addSql('ALTER TABLE chill_main_notification ALTER accessKey DROP DEFAULT');
$this->addSql('ALTER TABLE chill_main_notification ALTER accessKey SET NOT NULL');
}
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20220413225830 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_address ALTER COLUMN postcode_id DROP NOT NULL');
}
public function getDescription(): string
{
return 'Set postal code in chill_main_address NOT NULL';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_address ALTER COLUMN postcode_id SET NOT NULL');
}
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20220413230159 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_address DROP CONSTRAINT chill_custom_validfrom_before_validto');
}
public function getDescription(): string
{
return 'in addresses, validfrom must be lower than validto';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_address ADD CONSTRAINT chill_custom_validfrom_before_validto CHECK (validfrom <= COALESCE(validto, \'infinity\'::date))');
}
}

View File

@@ -450,4 +450,10 @@ notification:
subject: Objet
see_comments_thread: Voir le fil de commentaires associé
object_prefix: "[CHILL] notification - "
dest by email: Lien d'accès par email
Any email: Aucun email
Add an email: Ajouter un email
dest by email help: Les adresses email mentionnées ici recevront un lien d'accès. Un compte utilisateur sera toujours nécessaire.
Remove an email: Supprimer l'adresse email
Email with access link: Adresse email ayant reçu un lien d'accès

View File

@@ -13,10 +13,10 @@ namespace Chill\PersonBundle\AccompanyingPeriod\Events;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Notification\NotificationPersisterInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Event\Person\PersonAddressMoveEvent;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Templating\EngineInterface;
@@ -26,7 +26,7 @@ class PersonAddressMoveEventSubscriber implements EventSubscriberInterface
{
private EngineInterface $engine;
private EntityManagerInterface $entityManager;
private NotificationPersisterInterface $notificationPersister;
private Security $security;
@@ -34,12 +34,12 @@ class PersonAddressMoveEventSubscriber implements EventSubscriberInterface
public function __construct(
EngineInterface $engine,
EntityManagerInterface $entityManager,
NotificationPersisterInterface $notificationPersister,
Security $security,
TranslatorInterface $translator
) {
$this->engine = $engine;
$this->entityManager = $entityManager;
$this->notificationPersister = $notificationPersister;
$this->security = $security;
$this->translator = $translator;
}
@@ -87,7 +87,7 @@ class PersonAddressMoveEventSubscriber implements EventSubscriberInterface
'period' => $period,
]));
$this->entityManager->persist($notification);
$this->notificationPersister->persist($notification);
}
}
}

View File

@@ -13,8 +13,8 @@ namespace Chill\PersonBundle\AccompanyingPeriod\Events;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Notification\NotificationPersisterInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\Event\LifecycleEventArgs;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Security;
@@ -24,20 +24,20 @@ use Symfony\Contracts\Translation\TranslatorInterface;
class UserRefEventSubscriber implements EventSubscriberInterface
{
private EntityManagerInterface $em;
private EngineInterface $engine;
private NotificationPersisterInterface $notificationPersister;
private Security $security;
private TranslatorInterface $translator;
public function __construct(Security $security, TranslatorInterface $translator, EngineInterface $engine, EntityManagerInterface $em)
public function __construct(Security $security, TranslatorInterface $translator, EngineInterface $engine, NotificationPersisterInterface $notificationPersister)
{
$this->security = $security;
$this->translator = $translator;
$this->engine = $engine;
$this->em = $em;
$this->notificationPersister = $notificationPersister;
}
public static function getSubscribedEvents()
@@ -58,16 +58,13 @@ class UserRefEventSubscriber implements EventSubscriberInterface
public function postUpdate(AccompanyingPeriod $period, LifecycleEventArgs $args): void
{
if ($period->hasPreviousUser()
if ($period->isChangedUser()
&& $period->getUser() !== $this->security->getUser()
&& null !== $period->getUser()
&& $period->getStep() !== AccompanyingPeriod::STEP_DRAFT
) {
$this->generateNotificationToUser($period);
}
// we are just out of a flush operation. Launch a new one
$this->em->flush();
}
private function generateNotificationToUser(AccompanyingPeriod $period)
@@ -89,7 +86,7 @@ class UserRefEventSubscriber implements EventSubscriberInterface
))
->addAddressee($period->getUser());
$this->em->persist($notification);
$this->notificationPersister->persist($notification);
}
private function onPeriodConfirmed(AccompanyingPeriod $period)

View File

@@ -102,7 +102,6 @@ class HouseholdApiController extends ApiController
$event
->setPreviousAddress($household->getPreviousAddressOf($address))
->setNextAddress($address);
dump($event);
$this->eventDispatcher->dispatch($event);
}

View File

@@ -12,13 +12,16 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Form\Type\AddressDateType;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Form\HouseholdType;
use Chill\PersonBundle\Repository\Household\PositionRepository;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Chill\PersonBundle\Security\Authorization\HouseholdVoter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
@@ -186,6 +189,62 @@ class HouseholdController extends AbstractController
);
}
/**
* @Route(
* "/{household_id}/address/edit_valid_from",
* name="chill_person_household_address_valid_from_edit",
* methods={"GET", "HEAD", "POST"}
* )
* @ParamConverter("household", options={"id": "household_id"})
*/
public function addressValidFromEdit(Request $request, Household $household)
{
$this->denyAccessUnlessGranted(HouseholdVoter::EDIT, $household);
if (!$request->query->has('address_id')) {
throw new BadRequestException('parameter address_id is missing');
}
$address_id = $request->query->getInt('address_id');
// loop over adresses of the household, to be sure that the household is associated
// to the edited address
$address = null;
foreach ($household->getAddresses() as $householdAddress) {
if ($householdAddress->getId() === $address_id) {
$address = $householdAddress;
}
}
if (null === $address) {
throw new BadRequestException('The edited address does not belongs to the household');
}
$form = $this->createForm(AddressDateType::class, $address, []);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$household->makeAddressConsistent();
$this->getDoctrine()->getManager()->flush();
return $this->redirectToRoute('chill_person_household_addresses', [
'household_id' => $household->getId(),
]);
}
return $this->render(
'@ChillPerson/Household/address_valid_from_edit.html.twig',
[
'household' => $household,
'address' => $address,
'form' => $form->createView(),
]
);
}
/**
* @Route(
* "/{household_id}/members/metadata/edit",

View File

@@ -361,6 +361,8 @@ class AccompanyingPeriod implements
*/
private Collection $userHistories;
private bool $userIsChanged = false;
/**
* Temporary field, which is filled when the user is changed.
*
@@ -978,6 +980,11 @@ class AccompanyingPeriod implements
return null !== $this->userPrevious;
}
public function isChangedUser(): bool
{
return $this->userIsChanged && $this->user !== $this->userPrevious;
}
/**
* Returns true if the closing date is after the opening date.
*/
@@ -1103,6 +1110,14 @@ class AccompanyingPeriod implements
$this->setStep(AccompanyingPeriod::STEP_CONFIRMED);
}
public function resetPreviousUser(): self
{
$this->userPrevious = null;
$this->userIsChanged = false;
return $this;
}
/**
* @Groups({"write"})
*/
@@ -1329,6 +1344,7 @@ class AccompanyingPeriod implements
{
if ($this->user !== $user) {
$this->userPrevious = $this->user;
$this->userIsChanged = true;
foreach ($this->userHistories as $history) {
if (null === $history->getEndDate()) {

View File

@@ -57,6 +57,7 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
* orphanRemoval=true
* )
* @Serializer\Groups({"read", "docgen:read"})
* @ORM\OrderBy({"startDate": "DESC", "id": "DESC"})
*
* @internal /!\ the serialization for write evaluations is handled in `AccompanyingPeriodWorkDenormalizer`
*/

View File

@@ -77,6 +77,7 @@ class AccompanyingPeriodWorkEvaluation implements TrackCreationInterface, TrackU
* cascade={"remove", "persist"},
* orphanRemoval=true
* )
* @ORM\OrderBy({"createdAt": "DESC", "id": "DESC"})
* @Serializer\Groups({"read"})
*/
private Collection $documents;

View File

@@ -19,6 +19,7 @@ use DateTimeInterface;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity
@@ -40,6 +41,8 @@ class Comment implements TrackCreationInterface, TrackUpdateInterface
/**
* @ORM\Column(type="text")
* @Groups({"read", "write"})
* @Assert\NotBlank
* @Assert\NotNull
*/
private $content;

View File

@@ -151,6 +151,9 @@ class Household
return $this->addresses;
}
/**
* @return array|Address[]
*/
public function getAddressesOrdered(): array
{
$addresses = $this->getAddresses()->toArray();
@@ -514,6 +517,8 @@ class Household
if ($iterator->valid()) {
$current->setValidTo($iterator->current()->getValidFrom());
} else {
$current->setValidTo(null);
}
}
}

View File

@@ -1568,13 +1568,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
public function setEmail(?string $email): self
{
if (null === $email) {
$email = '';
}
$email = trim($email);
$this->email = $email;
$this->email = trim((string) $email);
return $this;
}

View File

@@ -128,6 +128,44 @@ class SocialAction
return $this;
}
/**
* In a SocialIssues's collection, find the elements which are an ancestor of
* other elements.
*
* The difference of the given list (thus, the elements which are **not** kept
* in the returned collection) are the most-grand-child elements of the list.
*
* Removing those elements of the Collection (which is not done by this method)
* will ensure that only the most descendent elements are present in the collection,
* (any ancestor of another element are present).
*
* @param Collection|SocialAction[] $socialActions
*
* @return Collection|SocialAction[] a list with the elements of the given list which are parent of other elements in the given list
*/
public static function findAncestorSocialActions(Collection $socialActions): Collection
{
$ancestors = new ArrayCollection();
foreach ($socialActions as $candidateChild) {
if ($ancestors->contains($candidateChild)) {
continue;
}
foreach ($socialActions as $candidateParent) {
if ($ancestors->contains($candidateParent)) {
continue;
}
if ($candidateChild->isDescendantOf($candidateParent)) {
$ancestors->add($candidateParent);
}
}
}
return $ancestors;
}
/**
* @return Collection|self[]
*/
@@ -169,7 +207,7 @@ class SocialAction
}
/**
* @return Collection|self[] All the descendants with the current entity (this)
* @return Collection|self[] All the descendants including the current entity (this)
*/
public function getDescendantsWithThis(): Collection
{
@@ -233,6 +271,23 @@ class SocialAction
return $this->getParent() instanceof self;
}
/**
* Recursive method which return true if the current $action
* is a descendant of the $action given in parameter.
*/
public function isDescendantOf(SocialAction $action): bool
{
if (!$this->hasParent()) {
return false;
}
if ($this->getParent() === $action) {
return true;
}
return $this->getParent()->isDescendantOf($action);
}
public function removeChild(self $child): self
{
if ($this->children->removeElement($child)) {

View File

@@ -202,7 +202,7 @@ class SocialIssue
}
/**
* @return Collection|self[] All the descendants with the current entity (this)
* @return Collection|self[] All the descendants including the current entity (this)
*/
public function getDescendantsWithThis(): Collection
{

View File

@@ -136,8 +136,11 @@ final class PersonACLAwareRepository implements PersonACLAwareRepositoryInterfac
$andWhereSearchClause = [];
$andWhereSearchClauseArgs = [];
if ('' !== $default) {
if ('' !== trim($default)) {
foreach (explode(' ', $default) as $str) {
if ('' === trim($str)) {
continue;
}
$pertinence[] =
'STRICT_WORD_SIMILARITY(LOWER(UNACCENT(?)), person.fullnamecanonical) + ' .
"(person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%')::int + " .

View File

@@ -30,6 +30,7 @@ div.list-with-period {
// override wrap-list
div.wrap-list.periods-list {
padding-right: 1rem;
div.wl-row {
flex-wrap: nowrap;
div.wl-col {
@@ -63,6 +64,7 @@ div.list-with-period {
}
div.periods-list {
padding-right: 1rem;
div.title {
text-align: right;
div.date {}

View File

@@ -10,7 +10,7 @@
<label class="chill-no-data-statement">{{ $tc('resources.counter', counter) }}</label>
</div>
<div class="flex-bloc mb-3">
<div class="flex-table mb-3">
<resource-item
v-for="resource in resources"
v-bind:resource="resource"

View File

@@ -5,9 +5,8 @@
</h2>
<div>
<div class="mb-3 row">
<label class="col-form-label col-sm-4">{{ $t('startdate.date') }}</label>
<div class="col-sm-8">
<input class="form-control" type="date" v-model="startDate" @change="updateStartDate" />
<div class="col-sm-12 date-update">
<input class="form-control" type="date" id="startDate" v-model="startDateInput">
</div>
</div>
</div>
@@ -17,30 +16,54 @@
<script>
import { datetimeToISO, dateToISO, ISOToDate, ISOToDatetime} from 'ChillMainAssets/chill/js/date.js';
import { mapState, mapGetters } from 'vuex';
import { dateToISO, ISOToDatetime} from 'ChillMainAssets/chill/js/date.js';
import { mapState } from 'vuex';
export default {
name: 'startDate',
methods: {
updateStartDate(event) {
const date = event.target.value;
// console.log(date)
this.$store.dispatch('updateStartDate', date)
.catch(({name, violations}) => {
if (name === 'ValidationException' || name === 'AccessException') {
violations.forEach((violation) => this.$toast.open({message: violation}));
} else {
this.$toast.open({message: 'An error occurred'})
}
});
},
data() {
return {
lastRecordedDate: null
}
},
computed: {
...mapState({
startDate: state => dateToISO(ISOToDatetime(state.accompanyingCourse.openingDate.datetime))
})
}),
startDateInput: {
get() {
return this.startDate;
},
set(value) {
this.lastRecordedDate = value;
setTimeout(() => {
console.log('timeout finished')
if (this.lastRecordedDate === value) {
console.log('last recorded', this.lastRecordedDate, 'value', value)
this.$store.dispatch('updateStartDate', value)
.catch(({name, violations}) => {
if (name === 'ValidationException' || name === 'AccessException') {
violations.forEach((violation) => this.$toast.open({message: violation}));
} else {
this.$toast.open({message: 'An error occurred'})
}
})
}
}, 3000)
}
}
}
}
</script>
</script>
<style lang="scss" scoped>
.date-update {
display: flex;
justify-content: space-between;
&-btn {
margin-left: 1rem;
}
}
</style>

View File

@@ -59,8 +59,8 @@ const appMessages = {
ok: "Oui, l'usager quitte le parcours",
show_household_number: "Voir le ménage (n° {id})",
show_household: "Voir le ménage",
person_without_household_warning: "Certaines usagers n'appartiennent actuellement à aucun ménage. Renseignez leur appartenance dès que possible.",
update_household: "Renseigner l'appartenance",
person_without_household_warning: "Certaines usagers n'appartiennent actuellement à aucun ménage. Veuillez les associer à un ménage dès que possible.",
update_household: "Associer à un ménage",
participation_not_valid: "Sélectionnez ou créez au minimum 1 usager",
},
requestor: {
@@ -153,7 +153,7 @@ const appMessages = {
},
startdate: {
change: "Date d'ouverture",
date: "Date d'ouverture",
// update: "La nouvelle date d'ouverture a été enregistrée"
},
// catch errors
'Error while updating AccompanyingPeriod Course.': "Erreur du serveur lors de la mise à jour du parcours d'accompagnement.",

View File

@@ -322,7 +322,6 @@ let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCou
}
},
updateStartDate(state, date) {
console.log('new state date', date)
state.accompanyingCourse.openingDate = date;
}
},
@@ -814,11 +813,9 @@ let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCou
})
},
updateStartDate({commit}, payload) {
console.log('payload', payload)
const date = ISOToDate(payload);
const url = `/api/1.0/person/accompanying-course/${id}.json`;
const body = { type: "accompanying_period", openingDate: { datetime: datetimeToISO(date) }};
console.log('body', body)
const date = payload === null || payload === '' ? null : { datetime: datetimeToISO(ISOToDate(payload)) }
const body = { type: "accompanying_period", openingDate: date};
return makeFetch('PATCH', url, body)
.then((response) => {
commit('updateStartDate', response.openingDate);

View File

@@ -227,19 +227,18 @@ export default {
},
startDate: {
get() {
let d = this.$store.state.startDate;
return dateToISO(d);
return this.$store.state.startDate;
},
set(value) {
this.$store.commit('setStartDate', ISOToDate(value));
this.$store.commit('setStartDate', value);
}
},
endDate: {
get() {
return dateToISO(this.$store.state.endDate);
return this.$store.state.endDate;
},
set(value) {
this.$store.commit('setEndDate', ISOToDate(value));
this.$store.commit('setEndDate', value);
}
},
setSocialIssue: {

View File

@@ -1,6 +1,6 @@
import { createStore } from 'vuex';
import { datetimeToISO } from 'ChillMainAssets/chill/js/date.js';
import { datetimeToISO, dateToISO, ISOToDate, ISOToDatetime } from 'ChillMainAssets/chill/js/date.js';
import { findSocialActionsBySocialIssue } from 'ChillPersonAssets/vuejs/_api/SocialWorkSocialAction.js';
// import { create } from 'ChillPersonAssets/vuejs/_api/AccompanyingCourseWork.js';
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
@@ -20,7 +20,7 @@ const store = createStore({
.map(p => p.person),
personsReachables: window.accompanyingCourse.participations.filter(p => p.endDate == null)
.map(p => p.person),
startDate: new Date(),
startDate: dateToISO(new Date()),
endDate: null,
isLoadingSocialActions: false,
isPostingWork: false,
@@ -47,7 +47,7 @@ const store = createStore({
id: state.socialActionPicked.id
},
startDate: {
datetime: datetimeToISO(state.startDate)
datetime: datetimeToISO(ISOToDate(state.startDate))
},
persons: []
};
@@ -61,7 +61,7 @@ const store = createStore({
if (null !== state.endDate) {
payload.endDate = {
datetime: datetimeToISO(state.endDate)
datetime: datetimeToISO(ISOToDate(state.endDate))
};
}
@@ -111,6 +111,7 @@ const store = createStore({
state.startDate = date;
},
setEndDate(state, date) {
console.log(date)
state.endDate = date;
},
setPersonsPickedIds(state, ids) {

View File

@@ -439,18 +439,18 @@ export default {
]),
startDate: {
get() {
return dateToISO(this.$store.state.startDate);
return this.$store.state.startDate;
},
set(v) {
this.$store.commit('setStartDate', ISOToDate(v));
this.$store.commit('setStartDate', v);
}
},
endDate: {
get() {
return dateToISO(this.$store.state.endDate);
return this.$store.state.endDate;
},
set(v) {
this.$store.commit('setEndDate', ISOToDate(v));
this.$store.commit('setEndDate', v);
}
},
note: {
@@ -521,12 +521,12 @@ export default {
this.$store.commit('removeReferrer', u);
},
goToGenerateWorkflow({link}) {
console.log('save before leave to generate workflow')
const callback = (data) => {
window.location.assign(link);
};
// console.log('save before leave to generate workflow')
const callback = (data) => {
window.location.assign(link);
};
return this.$store.dispatch('submit', callback)
return this.$store.dispatch('submit', callback)
.catch(e => { console.log(e); throw e; });
},
submit() {

View File

@@ -67,14 +67,17 @@
<div class="flex-table">
<div class="item-bloc" v-for="(d, i) in evaluation.documents" :key="d.id">
<div class="item-row">
<div class="input-group input-group-lg mb-3">
<input
class="form-control document-title"
type="text"
:value="d.title"
:id="d.id"
:data-key="i"
@input="onInputDocumentTitle"/>
<div class="input-group input-group-lg mb-3 row">
<label class="col-sm-3 col-form-label">Titre du document:</label>
<div class="col-sm-9">
<input
class="form-control document-title"
type="text"
:value="d.title"
:id="d.id"
:data-key="i"
@input="onInputDocumentTitle"/>
</div>
</div>
</div>
<div class="item-row">
@@ -138,12 +141,12 @@
@go-to-generate-document="submitBeforeGenerate"
>
<template v-slot:title>
<label class="col-sm-4 col-form-label">{{ $t('evaluation_generate_a_document') }}</label>
<label class="col-form-label">{{ $t('evaluation_generate_a_document') }}</label>
</template>
</pick-template>
<div>
<label class="col-sm-4 col-form-label">{{ $t('document_upload') }}</label>
<ul class="record_actions">
<label class="col-form-label">{{ $t('document_upload') }}</label>
<ul class="record_actions document-upload">
<li>
<add-async-upload
:buttonTitle="$t('browse')"
@@ -295,38 +298,39 @@ export default {
}
},
computed: {
...mapState([
'isPosting'
]),
...mapState([
'isPosting'
]),
getTemplatesAvailables() {
return this.$store.getters.getTemplatesAvailablesForEvaluation(this.evaluation.evaluation);
return this.$store.getters.getTemplatesAvailablesForEvaluation(this.evaluation.evaluation);
},
canGenerate() {
return !this.$store.state.isPosting && this.template !== null;
return !this.$store.state.isPosting && this.template !== null;
},
startDate: {
get() {
return dateToISO(this.evaluation.startDate);
},
set(v) {
this.$store.commit('setEvaluationStartDate', { key: this.evaluation.key, date: ISOToDate(v) });
}
get() {
console.log('evaluation', this.evaluation);
return this.evaluation.startDate;
},
set(v) {
this.$store.commit('setEvaluationStartDate', { key: this.evaluation.key, date: v });
}
},
endDate: {
get() {
return dateToISO(this.evaluation.endDate);
},
set(v) {
this.$store.commit('setEvaluationEndDate', { key: this.evaluation.key, date: ISOToDate(v) });
}
get() {
return this.evaluation.endDate;
},
set(v) {
this.$store.commit('setEvaluationEndDate', { key: this.evaluation.key, date: v });
}
},
maxDate: {
get() {
return dateToISO(this.evaluation.maxDate);
},
set(v) {
this.$store.commit('setEvaluationMaxDate', { key: this.evaluation.key, date: ISOToDate(v) });
}
get() {
return this.evaluation.maxDate;
},
set(v) {
this.$store.commit('setEvaluationMaxDate', { key: this.evaluation.key, date: v });
}
},
warningInterval: {
get() { return this.evaluation.warningInterval; },
@@ -341,7 +345,7 @@ export default {
ISOToDatetime,
canEditDocument(document) {
return 'storedObject' in document ?
this.mime.includes(document.storedObject.type) && document.storedObject.keyInfos.length === 0 : false;
this.mime.includes(document.storedObject.type) : false;
},
listAllStatus() {
console.log('load all status');
@@ -357,16 +361,16 @@ export default {
},
buildEditLink(storedObject) {
return `/wopi/edit/${storedObject.uuid}?returnPath=` + encodeURIComponent(
window.location.pathname + window.location.search + window.location.hash);
window.location.pathname + window.location.search + window.location.hash);
},
submitBeforeGenerate({template}) {
const callback = (data) => {
let evaluationId = data.accompanyingPeriodWorkEvaluations.find(e => e.key === this.evaluation.key).id;
const callback = (data) => {
let evaluationId = data.accompanyingPeriodWorkEvaluations.find(e => e.key === this.evaluation.key).id;
window.location.assign(buildLink(template, evaluationId, 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluation'));
};
window.location.assign(buildLink(template, evaluationId, 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluation'));
};
return this.$store.dispatch('submit', callback).catch(e => { console.log(e); throw e; });
return this.$store.dispatch('submit', callback).catch(e => { console.log(e); throw e; });
},
onInputDocumentTitle(event) {
const id = Number(event.target.id);
@@ -392,27 +396,30 @@ export default {
},
removeDocument(document) {
if (window.confirm("Êtes-vous sûr·e de vouloir supprimer le document qui a pour titre \"" + document.title +"\" ?")) {
this.$store.commit('removeDocument', {key: this.evaluation.key, document: document});
this.$store.commit('removeDocument', {key: this.evaluation.key, document: document});
}
},
goToGenerateWorkflowEvaluationDocument({event, link, workflowName, payload}) {
const callback = (data) => {
let evaluation = data.accompanyingPeriodWorkEvaluations.find(e => e.key === this.evaluation.key);
let updatedDocument = evaluation.documents.find(d => d.key === payload.doc.key);
window.location.assign(buildLinkCreate(workflowName,
'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument', updatedDocument.id));
};
const callback = (data) => {
let evaluation = data.accompanyingPeriodWorkEvaluations.find(e => e.key === this.evaluation.key);
let updatedDocument = evaluation.documents.find(d => d.key === payload.doc.key);
window.location.assign(buildLinkCreate(workflowName,
'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument', updatedDocument.id));
};
return this.$store.dispatch('submit', callback)
.catch(e => { console.log(e); throw e; });
},
return this.$store.dispatch('submit', callback)
.catch(e => { console.log(e); throw e; });
},
},
}
</script>
<style lang="scss" scoped>
input.document-title{
input.document-title {
font-weight: bold;
font-size: 1rem;
}
ul.document-upload {
justify-content: flex-start;
}
</style>

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