mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-31 01:08:26 +00:00 
			
		
		
		
	Compare commits
	
		
			18 Commits
		
	
	
		
			v4.4.1
			...
			375-notifi
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 10de431c48 | |||
| cb173c6341 | |||
| 20bbb6b485 | |||
| c731f1967b | |||
| 2434d91e4a | |||
| 13a4795333 | |||
| 2bb5776002 | |||
| 34dde37789 | |||
| 609b8f9af1 | |||
| 57d922c05e | |||
| ad579f3269 | |||
| 30e1416018 | |||
| b6b03cfcec | |||
| c8bb7575e7 | |||
|  | 80a3734171 | ||
| ab98f3a102 | |||
| 7516e68d77 | |||
| 7b60b7a8af | 
							
								
								
									
										6
									
								
								.changes/unreleased/Fixed-20250918-114044.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.changes/unreleased/Fixed-20250918-114044.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| kind: Fixed | ||||
| body: Increased the number of required characters when setting a new password in Chill from 9 to 14 - GDPR compliance | ||||
| time: 2025-09-18T11:40:44.858533536+02:00 | ||||
| custom: | ||||
|     Issue: "426" | ||||
|     SchemaChange: No schema change | ||||
							
								
								
									
										3
									
								
								.changes/v4.4.2.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/v4.4.2.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ## v4.4.2 - 2025-09-12 | ||||
| ### Fixed | ||||
| * Fix document generation and workflow generation do not work on accompanying period work documents    | ||||
| @@ -6,6 +6,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), | ||||
| and is generated by [Changie](https://github.com/miniscruff/changie). | ||||
|  | ||||
|  | ||||
| ## v4.4.2 - 2025-09-12 | ||||
| ### Fixed | ||||
| * Fix document generation and workflow generation do not work on accompanying period work documents    | ||||
|  | ||||
| ## v4.4.1 - 2025-09-11 | ||||
| ### Fixed | ||||
| * fix translations in duplicate evaluation document modal and realign close modal button    | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import { StoredObject, StoredObjectVersion } from "../../types"; | ||||
| import DropFileWidget from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileWidget.vue"; | ||||
| import { computed, reactive } from "vue"; | ||||
| import { useToast } from "vue-toast-notification"; | ||||
| import { DOCUMENT_REPLACE, DOCUMENT_ADD, trans } from "translator"; | ||||
| import { DOCUMENT_ADD, trans } from "translator"; | ||||
|  | ||||
| interface DropFileConfig { | ||||
|     allowRemove: boolean; | ||||
| @@ -78,9 +78,7 @@ function closeModal(): void { | ||||
|     > | ||||
|         {{ trans(DOCUMENT_ADD) }} | ||||
|     </button> | ||||
|     <button v-else @click="openModal" class="dropdown-item"> | ||||
|         {{ trans(DOCUMENT_REPLACE) }} | ||||
|     </button> | ||||
|     <button v-else @click="openModal" class="btn btn-edit"></button> | ||||
|     <modal | ||||
|         v-if="state.showModal" | ||||
|         :modal-dialog-class="modalClasses" | ||||
|   | ||||
| @@ -59,7 +59,7 @@ class UserPasswordType extends AbstractType | ||||
|                 'invalid_message' => 'The password fields must match', | ||||
|                 'constraints' => [ | ||||
|                     new Length([ | ||||
|                         'min' => 9, | ||||
|                         'min' => 14, | ||||
|                         'minMessage' => 'The password must be greater than {{ limit }} characters', | ||||
|                     ]), | ||||
|                     new NotBlank(), | ||||
|   | ||||
| @@ -80,6 +80,8 @@ export default { | ||||
|                     return appMessages.fr.the_evaluation_document; | ||||
|                 case "Chill\\MainBundle\\Entity\\Workflow\\EntityWorkflow": | ||||
|                     return appMessages.fr.the_workflow; | ||||
|                 case "Chill\\TaskBundle\\Entity\\SingleTask": | ||||
|                     return appMessages.fr.the_task; | ||||
|                 default: | ||||
|                     throw "notification type unknown"; | ||||
|             } | ||||
| @@ -96,6 +98,8 @@ export default { | ||||
|                     return `/fr/person/accompanying-period/work/evaluation/document/${n.relatedEntityId}/show`; | ||||
|                 case "Chill\\MainBundle\\Entity\\Workflow\\EntityWorkflow": | ||||
|                     return `/fr/main/workflow/${n.relatedEntityId}/show`; | ||||
|                 case "Chill\\TaskBundle\\Entity\\SingleTask": | ||||
|                     return `/fr/task/single-task/${n.relatedEntityId}/show`; | ||||
|                 default: | ||||
|                     throw "notification type unknown"; | ||||
|             } | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|         role="button" | ||||
|         data-bs-toggle="dropdown" | ||||
|         aria-expanded="false"> | ||||
|         <i class="fa fa-flash"></i> | ||||
|         <i class="bi bi-lightning-fill"></i> | ||||
|     </a> | ||||
|     <div class="dropdown-menu"> | ||||
|         {% for menu in menus %} | ||||
|   | ||||
| @@ -37,7 +37,7 @@ class NotificationNormalizer implements NormalizerAwareInterface, NormalizerInte | ||||
|             ->find($object->getRelatedEntityId()); | ||||
|  | ||||
|         return [ | ||||
|             'type' => 'notification', | ||||
|             'type' => $object->getType(), | ||||
|             'id' => $object->getId(), | ||||
|             'addressees' => $this->normalizer->normalize($object->getAddressees(), $format, $context), | ||||
|             'date' => $this->normalizer->normalize($object->getDate(), $format, $context), | ||||
|   | ||||
| @@ -45,7 +45,7 @@ final class UserControllerTest extends WebTestCase | ||||
|         self::assertResponseIsSuccessful(); | ||||
|  | ||||
|         $username = 'Test_user'.uniqid(); | ||||
|         $password = 'Password1234!'; | ||||
|         $password = 'Password_1234!'; | ||||
|  | ||||
|         // Fill in the form and submit it | ||||
|  | ||||
| @@ -99,7 +99,7 @@ final class UserControllerTest extends WebTestCase | ||||
|     { | ||||
|         $client = $this->getClientAuthenticatedAsAdmin(); | ||||
|         $crawler = $client->request('GET', "/fr/admin/user/{$userId}/edit_password"); | ||||
|         $newPassword = '1234Password!'; | ||||
|         $newPassword = '1234_Password!'; | ||||
|  | ||||
|         $form = $crawler->selectButton('Changer le mot de passe')->form([ | ||||
|             'chill_mainbundle_user_password[new_password][first]' => $newPassword, | ||||
|   | ||||
| @@ -6,7 +6,7 @@ | ||||
|             :id="evaluation.id" | ||||
|             :templates="templates" | ||||
|             :preventDefaultMoveToGenerate="true" | ||||
|             @go-to-generate-document="$emit('submitBeforeGenerate', $event)" | ||||
|             @go-to-generate-document="submitBeforeGenerate" | ||||
|         > | ||||
|             <template v-slot:title> | ||||
|                 <label class="col-form-label">{{ | ||||
| @@ -22,7 +22,7 @@ | ||||
|                 <li> | ||||
|                     <drop-file-modal | ||||
|                         :allow-remove="false" | ||||
|                         @add-document="$emit('addDocument', $event)" | ||||
|                         @add-document="emit('addDocument', $event)" | ||||
|                     ></drop-file-modal> | ||||
|                 </li> | ||||
|             </ul> | ||||
| @@ -39,9 +39,34 @@ import { | ||||
|     EVALUATION_GENERATE_A_DOCUMENT, | ||||
|     trans, | ||||
| } from "translator"; | ||||
| import { buildLink } from "ChillDocGeneratorAssets/lib/document-generator"; | ||||
| import { useStore } from "vuex"; | ||||
|  | ||||
| defineProps(["evaluation", "templates"]); | ||||
| defineEmits(["addDocument", "submitBeforeGenerate"]); | ||||
| const store = useStore(); | ||||
|  | ||||
| const props = defineProps(["evaluation", "templates"]); | ||||
| const emit = defineEmits(["addDocument"]); | ||||
|  | ||||
| async function submitBeforeGenerate({ template }) { | ||||
|     const callback = (data) => { | ||||
|         let evaluationId = data.accompanyingPeriodWorkEvaluations.find( | ||||
|             (e) => e.key === props.evaluation.key, | ||||
|         ).id; | ||||
|  | ||||
|         window.location.assign( | ||||
|             buildLink( | ||||
|                 template, | ||||
|                 evaluationId, | ||||
|                 "Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluation", | ||||
|             ), | ||||
|         ); | ||||
|     }; | ||||
|  | ||||
|     return store.dispatch("submit", callback).catch((e) => { | ||||
|         console.log(e); | ||||
|         throw e; | ||||
|     }); | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
|   | ||||
| @@ -58,7 +58,7 @@ | ||||
|                                     :preventDefaultMoveToGenerate="true" | ||||
|                                     :goToGenerateWorkflowPayload="{ doc: d }" | ||||
|                                     @go-to-generate-workflow=" | ||||
|                                         $emit('goToGenerateWorkflow', $event) | ||||
|                                         goToGenerateWorkflowEvaluationDocument | ||||
|                                     " | ||||
|                                 ></list-workflow-modal> | ||||
|                             </li> | ||||
| @@ -95,10 +95,9 @@ | ||||
|                                             <a | ||||
|                                                 class="dropdown-item" | ||||
|                                                 @click=" | ||||
|                                                     $emit( | ||||
|                                                         'goToGenerateNotification', | ||||
|                                                     goToGenerateDocumentNotification( | ||||
|                                                         d, | ||||
|                                                         true, | ||||
|                                                         false, | ||||
|                                                     ) | ||||
|                                                 " | ||||
|                                             > | ||||
| @@ -113,8 +112,7 @@ | ||||
|                                             <a | ||||
|                                                 class="dropdown-item" | ||||
|                                                 @click=" | ||||
|                                                     $emit( | ||||
|                                                         'goToGenerateNotification', | ||||
|                                                     goToGenerateDocumentNotification( | ||||
|                                                         d, | ||||
|                                                         false, | ||||
|                                                     ) | ||||
| @@ -150,15 +148,35 @@ | ||||
|                                     " | ||||
|                                 ></document-action-buttons-group> | ||||
|                             </li> | ||||
|                             <!--replace document--> | ||||
|                             <li | ||||
|                                 v-if=" | ||||
|                                     Number.isInteger(d.id) && | ||||
|                                     d.storedObject._permissions.canEdit | ||||
|                                 " | ||||
|                             > | ||||
|                                 <drop-file-modal | ||||
|                                     :existing-doc="d.storedObject" | ||||
|                                     :allow-remove="false" | ||||
|                                     @add-document=" | ||||
|                                         (arg) => | ||||
|                                             replaceDocument( | ||||
|                                                 d, | ||||
|                                                 arg.stored_object, | ||||
|                                                 arg.stored_object_version, | ||||
|                                             ) | ||||
|                                     " | ||||
|                                 ></drop-file-modal> | ||||
|                             </li> | ||||
|                             <li v-if="Number.isInteger(d.id)"> | ||||
|                                 <div class="duplicate-dropdown"> | ||||
|                                     <button | ||||
|                                         class="btn btn-edit dropdown-toggle" | ||||
|                                         class="btn btn-outline-primary dropdown-toggle" | ||||
|                                         type="button" | ||||
|                                         data-bs-toggle="dropdown" | ||||
|                                         aria-expanded="false" | ||||
|                                     > | ||||
|                                         {{ trans(EVALUATION_DOCUMENT_EDIT) }} | ||||
|                                         <i class="bi bi-lightning-fill"></i> | ||||
|                                     </button> | ||||
|                                     <ul class="dropdown-menu"> | ||||
|                                         <!--delete--> | ||||
| @@ -180,27 +198,6 @@ | ||||
|                                                 }} | ||||
|                                             </a> | ||||
|                                         </li> | ||||
|                                         <!--replace document--> | ||||
|                                         <li | ||||
|                                             v-if=" | ||||
|                                                 d.storedObject._permissions | ||||
|                                                     .canEdit | ||||
|                                             " | ||||
|                                         > | ||||
|                                             <drop-file-modal | ||||
|                                                 :existing-doc="d.storedObject" | ||||
|                                                 :allow-remove="false" | ||||
|                                                 @add-document=" | ||||
|                                                     (arg) => | ||||
|                                                         $emit( | ||||
|                                                             'replaceDocument', | ||||
|                                                             d, | ||||
|                                                             arg.stored_object, | ||||
|                                                             arg.stored_object_version, | ||||
|                                                         ) | ||||
|                                                 " | ||||
|                                             ></drop-file-modal> | ||||
|                                         </li> | ||||
|                                         <!--duplicate document--> | ||||
|                                         <li> | ||||
|                                             <a | ||||
| @@ -300,35 +297,45 @@ import { | ||||
|     EVALUATION_DOCUMENTS, | ||||
|     EVALUATION_DOCUMENT_MOVE, | ||||
|     EVALUATION_DOCUMENT_DELETE, | ||||
|     EVALUATION_DOCUMENT_EDIT, | ||||
|     EVALUATION_DOCUMENT_DUPLICATE_HERE, | ||||
|     EVALUATION_DOCUMENT_DUPLICATE_TO_OTHER_EVALUATION, | ||||
|     trans, | ||||
| } from "translator"; | ||||
| import { ref, watch } from "vue"; | ||||
| import { computed, ref, watch } from "vue"; | ||||
| import AccompanyingPeriodWorkSelectorModal from "ChillPersonAssets/vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkSelectorModal.vue"; | ||||
| import { buildLinkCreate } from "ChillMainAssets/lib/entity-workflow/api"; | ||||
| import { buildLinkCreate as buildLinkCreateNotification } from "ChillMainAssets/lib/entity-notification/api"; | ||||
| import { useStore } from "vuex"; | ||||
|  | ||||
| defineProps([ | ||||
| const props = defineProps([ | ||||
|     "documents", | ||||
|     "docAnchorId", | ||||
|     "accompanyingPeriodId", | ||||
|     "accompanyingPeriodWorkId", | ||||
|     "evaluation", | ||||
| ]); | ||||
| const emit = defineEmits([ | ||||
|     "inputDocumentTitle", | ||||
|     "removeDocument", | ||||
|     "duplicateDocument", | ||||
|     "statusDocumentChanged", | ||||
|     "goToGenerateWorkflow", | ||||
|     "goToGenerateNotification", | ||||
|     "duplicateDocumentToWork", | ||||
| ]); | ||||
|  | ||||
| const store = useStore(); | ||||
|  | ||||
| const showAccompanyingPeriodSelector = ref(false); | ||||
| const selectedEvaluation = ref(null); | ||||
| const selectedDocumentToDuplicate = ref(null); | ||||
| const selectedDocumentToMove = ref(null); | ||||
|  | ||||
| const AmIRefferer = computed(() => { | ||||
|     return !( | ||||
|         store.state.work.accompanyingPeriod.user && | ||||
|         store.state.me && | ||||
|         store.state.work.accompanyingPeriod.user.id !== store.state.me.id | ||||
|     ); | ||||
| }); | ||||
|  | ||||
| const prepareDocumentDuplicationToWork = (d) => { | ||||
|     selectedDocumentToDuplicate.value = d; | ||||
|     /** ensure selectedDocumentToMove is null */ | ||||
| @@ -358,4 +365,91 @@ watch(selectedEvaluation, (val) => { | ||||
|         }); | ||||
|     } | ||||
| }); | ||||
|  | ||||
| async function goToGenerateWorkflowEvaluationDocument({ | ||||
|     workflowName, | ||||
|     payload, | ||||
| }) { | ||||
|     const callback = (data) => { | ||||
|         let evaluation = data.accompanyingPeriodWorkEvaluations.find( | ||||
|             (e) => e.key === props.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 store.dispatch("submit", callback).catch((e) => { | ||||
|         console.log(e); | ||||
|         throw e; | ||||
|     }); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Replaces a document in the store with a new document. | ||||
|  * | ||||
|  * @param {Object} oldDocument - The document to be replaced. | ||||
|  * @param {StoredObject} storedObject - The stored object of the new document. | ||||
|  * @param {StoredObjectVersion} storedObjectVersion - The new version of the document | ||||
|  * @return {void} | ||||
|  */ | ||||
| async function replaceDocument(oldDocument, storedObject, storedObjectVersion) { | ||||
|     let document = { | ||||
|         type: "accompanying_period_work_evaluation_document", | ||||
|         storedObject: storedObject, | ||||
|         title: oldDocument.title, | ||||
|     }; | ||||
|  | ||||
|     return store.commit("replaceDocument", { | ||||
|         key: props.evaluation.key, | ||||
|         document, | ||||
|         oldDocument: oldDocument, | ||||
|         stored_object_version: storedObjectVersion, | ||||
|     }); | ||||
| } | ||||
|  | ||||
| async function goToGenerateDocumentNotification(document, tos) { | ||||
|     const callback = (data) => { | ||||
|         let evaluation = data.accompanyingPeriodWorkEvaluations.find( | ||||
|             (e) => e.key === props.evaluation.key, | ||||
|         ); | ||||
|         let updatedDocument = evaluation.documents.find( | ||||
|             (d) => d.key === document.key, | ||||
|         ); | ||||
|         window.location.assign( | ||||
|             buildLinkCreateNotification( | ||||
|                 "Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument", | ||||
|                 updatedDocument.id, | ||||
|                 tos === true | ||||
|                     ? store.state.work.accompanyingPeriod.user?.id | ||||
|                     : null, | ||||
|                 window.location.pathname + | ||||
|                     window.location.search + | ||||
|                     window.location.hash, | ||||
|             ), | ||||
|         ); | ||||
|     }; | ||||
|  | ||||
|     return store.dispatch("submit", callback).catch((e) => { | ||||
|         console.log(e); | ||||
|         throw e; | ||||
|     }); | ||||
| } | ||||
|  | ||||
| async function submitBeforeLeaveToEditor() { | ||||
|     console.log("submit beore edit 2"); | ||||
|     // empty callback | ||||
|     const callback = () => null; | ||||
|     return store.dispatch("submit", callback).catch((e) => { | ||||
|         console.log(e); | ||||
|         throw e; | ||||
|     }); | ||||
| } | ||||
| </script> | ||||
|   | ||||
| @@ -24,8 +24,8 @@ | ||||
|                 v-if="evaluation.documents.length > 0" | ||||
|                 :documents="evaluation.documents" | ||||
|                 :docAnchorId="docAnchorId" | ||||
|                 :evaluation="evaluation" | ||||
|                 :accompanyingPeriodId="store.state.work.accompanyingPeriod.id" | ||||
|                 :accompanying-period-work-id="store.state.work.id" | ||||
|                 @inputDocumentTitle="onInputDocumentTitle" | ||||
|                 @removeDocument="removeDocument" | ||||
|                 @duplicateDocument="duplicateDocument" | ||||
| @@ -34,7 +34,6 @@ | ||||
|                 " | ||||
|                 @move-document-to-evaluation="moveDocumentToEvaluation" | ||||
|                 @statusDocumentChanged="onStatusDocumentChanged" | ||||
|                 @goToGenerateWorkflow="goToGenerateWorkflowEvaluationDocument" | ||||
|                 @goToGenerateNotification="goToGenerateDocumentNotification" | ||||
|             /> | ||||
|  | ||||
| @@ -42,7 +41,6 @@ | ||||
|                 :evaluation="evaluation" | ||||
|                 :templates="getTemplatesAvailables" | ||||
|                 @addDocument="addDocument" | ||||
|                 @submitBeforeGenerate="submitBeforeGenerate" | ||||
|             /> | ||||
|         </div> | ||||
|     </div> | ||||
| @@ -290,29 +288,6 @@ function onStatusDocumentChanged(newStatus) { | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function goToGenerateWorkflowEvaluationDocument({ workflowName, payload }) { | ||||
|     const callback = (data) => { | ||||
|         let evaluation = data.accompanyingPeriodWorkEvaluations.find( | ||||
|             (e) => e.key === props.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, | ||||
|             ), | ||||
|         ); | ||||
|     }; | ||||
|  | ||||
|     store.dispatch("submit", callback).catch((e) => { | ||||
|         console.log(e); | ||||
|         throw e; | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function goToGenerateDocumentNotification(document, tos) { | ||||
|     const callback = (data) => { | ||||
|         let evaluation = data.accompanyingPeriodWorkEvaluations.find( | ||||
|   | ||||
| @@ -13,7 +13,6 @@ namespace Chill\TaskBundle\Controller; | ||||
|  | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Pagination\PaginatorFactory; | ||||
| use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface; | ||||
| use Chill\MainBundle\Serializer\Model\Collection; | ||||
| use Chill\MainBundle\Serializer\Model\Counter; | ||||
| use Chill\MainBundle\Templating\Listing\FilterOrderHelper; | ||||
| @@ -23,6 +22,7 @@ use Chill\PersonBundle\Entity\AccompanyingPeriod; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Chill\PersonBundle\Privacy\PrivacyEvent; | ||||
| use Chill\TaskBundle\Entity\SingleTask; | ||||
| use Chill\TaskBundle\Event\AssignTaskEvent; | ||||
| use Chill\TaskBundle\Event\TaskEvent; | ||||
| use Chill\TaskBundle\Event\UI\UIEvent; | ||||
| use Chill\TaskBundle\Form\SingleTaskType; | ||||
| @@ -48,7 +48,6 @@ use Symfony\Contracts\Translation\TranslatorInterface; | ||||
| final class SingleTaskController extends AbstractController | ||||
| { | ||||
|     public function __construct( | ||||
|         private readonly CenterResolverDispatcherInterface $centerResolverDispatcher, | ||||
|         private readonly PaginatorFactory $paginatorFactory, | ||||
|         private readonly SingleTaskAclAwareRepositoryInterface $singleTaskAclAwareRepository, | ||||
|         private readonly TranslatorInterface $translator, | ||||
| @@ -169,6 +168,9 @@ final class SingleTaskController extends AbstractController | ||||
|             ->setForm($this->setCreateForm($task, TaskVoter::UPDATE)); | ||||
|         $this->eventDispatcher->dispatch($event, UIEvent::EDIT_FORM); | ||||
|  | ||||
|         //        To keep track of specific assignee change | ||||
|         $initialAssignee = $task->getAssignee(); | ||||
|  | ||||
|         $form = $event->getForm(); | ||||
|  | ||||
|         $form->handleRequest($request); | ||||
| @@ -178,6 +180,13 @@ final class SingleTaskController extends AbstractController | ||||
|                 $em = $this->managerRegistry->getManager(); | ||||
|                 $em->persist($task); | ||||
|  | ||||
|                 if ($initialAssignee !== $task->getAssignee()) { | ||||
|                     $this->eventDispatcher->dispatch( | ||||
|                         new AssignTaskEvent($task, $initialAssignee), | ||||
|                         AssignTaskEvent::PERSIST | ||||
|                     ); | ||||
|                 } | ||||
|  | ||||
|                 $em->flush(); | ||||
|  | ||||
|                 $this->addFlash('success', $this->translator | ||||
| @@ -525,6 +534,13 @@ final class SingleTaskController extends AbstractController | ||||
|  | ||||
|                 $this->eventDispatcher->dispatch(new TaskEvent($task), TaskEvent::PERSIST); | ||||
|  | ||||
|                 if (null !== $task->getAssignee()) { | ||||
|                     $this->eventDispatcher->dispatch( | ||||
|                         new AssignTaskEvent($task, null), | ||||
|                         AssignTaskEvent::PERSIST | ||||
|                     ); | ||||
|                 } | ||||
|  | ||||
|                 $em->flush(); | ||||
|  | ||||
|                 $this->addFlash('success', $this->translator->trans('The task is created')); | ||||
|   | ||||
| @@ -42,6 +42,7 @@ class ChillTaskExtension extends Extension implements PrependExtensionInterface | ||||
|         $loader->load('services/timeline.yaml'); | ||||
|         $loader->load('services/fixtures.yaml'); | ||||
|         $loader->load('services/form.yaml'); | ||||
|         $loader->load('services/notification.yaml'); | ||||
|     } | ||||
|  | ||||
|     public function prepend(ContainerBuilder $container) | ||||
|   | ||||
							
								
								
									
										41
									
								
								src/Bundle/ChillTaskBundle/Event/AssignTaskEvent.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/Bundle/ChillTaskBundle/Event/AssignTaskEvent.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\TaskBundle\Event; | ||||
|  | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\TaskBundle\Entity\SingleTask; | ||||
| use Symfony\Contracts\EventDispatcher\Event; | ||||
|  | ||||
| class AssignTaskEvent extends Event | ||||
| { | ||||
|     final public const PERSIST = 'chill_task.assign_task'; | ||||
|  | ||||
|     public function __construct( | ||||
|         private readonly SingleTask $task, | ||||
|         private readonly ?User $initialAssignee, | ||||
|     ) {} | ||||
|  | ||||
|     public function getTask(): SingleTask | ||||
|     { | ||||
|         return $this->task; | ||||
|     } | ||||
|  | ||||
|     public function getInitialAssignee(): ?User | ||||
|     { | ||||
|         return $this->initialAssignee; | ||||
|     } | ||||
|  | ||||
|     public function hasAssigneeChanged(): bool | ||||
|     { | ||||
|         return $this->initialAssignee !== $this->task->getAssignee(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,66 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\TaskBundle\Event; | ||||
|  | ||||
| use Chill\MainBundle\Entity\Notification; | ||||
| use Chill\TaskBundle\Entity\SingleTask; | ||||
| use Chill\TaskBundle\Notification\AssignTaskNotificationFlagProvider; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Symfony\Component\EventDispatcher\EventSubscriberInterface; | ||||
|  | ||||
| readonly class TaskAssignEventSubscriber implements EventSubscriberInterface | ||||
| { | ||||
|     public function __construct( | ||||
|         private EntityManagerInterface $entityManager, | ||||
|         private \Twig\Environment $engine, | ||||
|     ) {} | ||||
|  | ||||
|     public static function getSubscribedEvents(): array | ||||
|     { | ||||
|         return [ | ||||
|             AssignTaskEvent::PERSIST => ['onTaskAssigned', 0], | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Send a notification when a user is assigned to a task. | ||||
|      * Only triggers when the assignee actually changes. | ||||
|      */ | ||||
|     public function onTaskAssigned(AssignTaskEvent $event): void | ||||
|     { | ||||
|         if (!$event->hasAssigneeChanged()) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         $task = $event->getTask(); | ||||
|         $assignedUser = $task->getAssignee(); | ||||
|  | ||||
|         $title = $task->getTitle(); | ||||
|  | ||||
|         $context = [ | ||||
|             'task' => $task, | ||||
|             'assignedUser' => $assignedUser, | ||||
|             'title' => $title, | ||||
|         ]; | ||||
|  | ||||
|         $notification = new Notification(); | ||||
|         $notification | ||||
|             ->setRelatedEntityId($task->getId()) | ||||
|             ->setRelatedEntityClass(SingleTask::class) | ||||
|             ->setTitle($this->engine->render('@ChillTask/Notification/task_assignment_notification_title.txt.twig', $context)) | ||||
|             ->setMessage($this->engine->render('@ChillTask/Notification/task_assignment_notification_content.txt.twig', $context)) | ||||
|             ->addAddressee($assignedUser) | ||||
|             ->setType(AssignTaskNotificationFlagProvider::FLAG); | ||||
|  | ||||
|         $this->entityManager->persist($notification); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,31 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\TaskBundle\Notification; | ||||
|  | ||||
| use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterface; | ||||
| use Symfony\Component\Translation\TranslatableMessage; | ||||
| use Symfony\Contracts\Translation\TranslatableInterface; | ||||
|  | ||||
| class AssignTaskNotificationFlagProvider implements NotificationFlagProviderInterface | ||||
| { | ||||
|     public const FLAG = 'task-assign-notif'; | ||||
|  | ||||
|     public function getFlag(): string | ||||
|     { | ||||
|         return self::FLAG; | ||||
|     } | ||||
|  | ||||
|     public function getLabel(): TranslatableInterface | ||||
|     { | ||||
|         return new TranslatableMessage('notification.flags.task_assign'); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,69 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\TaskBundle\Notification; | ||||
|  | ||||
| use Chill\MainBundle\Entity\Notification; | ||||
| use Chill\MainBundle\Notification\NotificationHandlerInterface; | ||||
| use Chill\TaskBundle\Entity\SingleTask; | ||||
| use Chill\TaskBundle\Repository\SingleTaskRepository; | ||||
| use Symfony\Component\Translation\TranslatableMessage; | ||||
| use Symfony\Contracts\Translation\TranslatableInterface; | ||||
|  | ||||
| final readonly class TaskNotificationHandler implements NotificationHandlerInterface | ||||
| { | ||||
|     public function __construct(private SingleTaskRepository $taskRepository) {} | ||||
|  | ||||
|     public function getTemplate(Notification $notification, array $options = []): string | ||||
|     { | ||||
|         return '@ChillTask/SingleTask/showInNotification.html.twig'; | ||||
|     } | ||||
|  | ||||
|     public function getTemplateData(Notification $notification, array $options = []): array | ||||
|     { | ||||
|         return [ | ||||
|             'notification' => $notification, | ||||
|             'task' => $this->taskRepository->find($notification->getRelatedEntityId()), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     public function supports(Notification $notification, array $options = []): bool | ||||
|     { | ||||
|         return SingleTask::class === $notification->getRelatedEntityClass(); | ||||
|     } | ||||
|  | ||||
|     public function getTitle(Notification $notification, array $options = []): TranslatableInterface | ||||
|     { | ||||
|         if (null === $task = $this->getRelatedEntity($notification)) { | ||||
|             return new TranslatableMessage('task.deleted'); | ||||
|         } | ||||
|  | ||||
|         return new TranslatableMessage('notification.task.title %title%', ['title' => $task->getTitle()]); | ||||
|     } | ||||
|  | ||||
|     public function getAssociatedPersons(Notification $notification, array $options = []): array | ||||
|     { | ||||
|         if (null === $task = $this->getRelatedEntity($notification)) { | ||||
|             return []; | ||||
|         } | ||||
|  | ||||
|         if (null !== $task->getCourse()) { | ||||
|             return $task->getCourse()->getParticipations()->getValues(); | ||||
|         } | ||||
|  | ||||
|         return [$task->getPerson()]; | ||||
|     } | ||||
|  | ||||
|     public function getRelatedEntity(Notification $notification): ?object | ||||
|     { | ||||
|         return $this->taskRepository->find($notification->getRelatedEntityId()); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,15 @@ | ||||
| {{ assignedUser.label }}, | ||||
|  | ||||
| {{ 'notification.email.task_assigned'|trans({}, null, assignedUser.getLocale) }} | ||||
|  | ||||
| {{ 'notification.email.title_label'|trans({}, null, assignedUser.getLocale) }} "{{ task.title }}". | ||||
| {% if task.endDate %} | ||||
|  | ||||
| {{ 'notification.email.deadline'|trans({'%date%': task.endDate|format_date('long')}, null, assignedUser.getLocale) }} | ||||
| {% endif %} | ||||
|  | ||||
| {{ 'notification.email.view_task'|trans({}, null, assignedUser.getLocale) }} | ||||
|  | ||||
| {{ absolute_url(path('chill_task_single_task_show', {'id': task.id, '_locale': assignedUser.getLocale})) }} | ||||
|  | ||||
| {{ 'notification.email.regards'|trans({}, null, assignedUser.getLocale) }}, | ||||
| @@ -0,0 +1,3 @@ | ||||
| {{ 'notification.email.title'|trans({}, null, assignedUser.getLocale) }} | ||||
|  | ||||
|  | ||||
| @@ -18,14 +18,14 @@ | ||||
|                 <div> | ||||
|                     {% if task.person is not null %} | ||||
|                         <span class="chill-task-list__row__person"> | ||||
|                                                 {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { | ||||
|                                                     targetEntity: { name: 'person', id: task.person.id }, | ||||
|                                                     action: 'show', | ||||
|                                                     displayBadge: true, | ||||
|                                                     buttonText: task.person|chill_entity_render_string, | ||||
|                                                     isDead: task.person.deathdate is not null | ||||
|                                                 } %} | ||||
|                                             </span> | ||||
|                             {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { | ||||
|                                 targetEntity: { name: 'person', id: task.person.id }, | ||||
|                                 action: 'show', | ||||
|                                 displayBadge: true, | ||||
|                                 buttonText: task.person|chill_entity_render_string, | ||||
|                                 isDead: task.person.deathdate is not null | ||||
|                             } %} | ||||
|                         </span> | ||||
|                     {% elseif task.course is not null %} | ||||
|                         <div style="margin-bottom: 1rem;"> | ||||
|                         {% for part in task.course.currentParticipations %} | ||||
|   | ||||
| @@ -110,4 +110,5 @@ | ||||
| 		</li> | ||||
| 	{% endif %} | ||||
|  | ||||
| </ul></div> | ||||
| </ul> | ||||
| </div> | ||||
|   | ||||
| @@ -0,0 +1,14 @@ | ||||
| {% macro recordAction(task) %} | ||||
|     <li> | ||||
|         <a href="{{ path('chill_person_accompanying_course_index', { 'task_id': task }) }}" | ||||
|            class="btn btn-show" title="{{ 'See task'|trans }}"></a> | ||||
|     </li> | ||||
| {% endmacro %} | ||||
|  | ||||
| {% if task is not null %} | ||||
| {#    <div>Todo : display task? </div>#} | ||||
| {% else %} | ||||
|     <div class="alert alert-warning border-warning border-1"> | ||||
|         {{ 'You are getting a notification for a task which does not exist any more'|trans }} | ||||
|     </div> | ||||
| {% endif %} | ||||
| @@ -0,0 +1,138 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\TaskBundle\Tests\EventSubscriber; | ||||
|  | ||||
| use Chill\MainBundle\Entity\Notification; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\TaskBundle\Entity\SingleTask; | ||||
| use Chill\TaskBundle\Event\AssignTaskEvent; | ||||
| use Chill\TaskBundle\Event\TaskAssignEventSubscriber; | ||||
| use Chill\TaskBundle\Notification\AssignTaskNotificationFlagProvider; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Prophecy\Argument; | ||||
| use Prophecy\PhpUnit\ProphecyTrait; | ||||
| use Prophecy\Prophecy\ObjectProphecy; | ||||
| use Twig\Environment; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class TaskAssignEventSubscriberTest extends TestCase | ||||
| { | ||||
|     use ProphecyTrait; | ||||
|  | ||||
|     private ObjectProphecy $entityManager; | ||||
|     private ObjectProphecy $twig; | ||||
|     private TaskAssignEventSubscriber $subscriber; | ||||
|  | ||||
|     protected function setUp(): void | ||||
|     { | ||||
|         $this->entityManager = $this->prophesize(EntityManagerInterface::class); | ||||
|         $this->twig = $this->prophesize(Environment::class); | ||||
|         $this->subscriber = new TaskAssignEventSubscriber( | ||||
|             $this->entityManager->reveal(), | ||||
|             $this->twig->reveal() | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     private function setEntityId(object $entity, int $id): void | ||||
|     { | ||||
|         $reflection = new \ReflectionClass($entity); | ||||
|         $property = $reflection->getProperty('id'); | ||||
|         $property->setAccessible(true); | ||||
|         $property->setValue($entity, $id); | ||||
|     } | ||||
|  | ||||
|     public function testOnTaskAssignedCreatesNotificationWhenAssigneeChanges(): void | ||||
|     { | ||||
|         // Arrange | ||||
|         $initialAssignee = new User(); | ||||
|         $newAssignee = new User(); | ||||
|  | ||||
|         $task = new SingleTask(); | ||||
|         $task->setTitle('Test Task'); | ||||
|         $task->setAssignee($newAssignee); | ||||
|         $this->setEntityId($task, 123); | ||||
|  | ||||
|         $event = new AssignTaskEvent($task, $initialAssignee); | ||||
|  | ||||
|         $this->twig->render('@ChillTask/Notification/task_assignment_notification_title.txt.twig', Argument::type('array')) | ||||
|             ->shouldBeCalledOnce() | ||||
|             ->willReturn('Notification Title'); | ||||
|  | ||||
|         $this->twig->render('@ChillTask/Notification/task_assignment_notification_content.txt.twig', Argument::type('array')) | ||||
|             ->shouldBeCalledOnce() | ||||
|             ->willReturn('Notification Content'); | ||||
|  | ||||
|         $this->entityManager->persist(Argument::type(Notification::class)) | ||||
|             ->shouldBeCalledOnce(); | ||||
|  | ||||
|         // Act | ||||
|         $this->subscriber->onTaskAssigned($event); | ||||
|     } | ||||
|  | ||||
|     public function testOnTaskAssignedDoesNothingWhenAssigneeDoesNotChange(): void | ||||
|     { | ||||
|         // Arrange | ||||
|         $assignee = new User(); | ||||
|  | ||||
|         $task = new SingleTask(); | ||||
|         $task->setTitle('Test Task'); | ||||
|         $task->setAssignee($assignee); | ||||
|  | ||||
|         $event = new AssignTaskEvent($task, $assignee); | ||||
|  | ||||
|         $this->twig->render(Argument::any(), Argument::any())->shouldNotBeCalled(); | ||||
|         $this->entityManager->persist(Argument::any())->shouldNotBeCalled(); | ||||
|  | ||||
|         // Act | ||||
|         $this->subscriber->onTaskAssigned($event); | ||||
|     } | ||||
|  | ||||
|     public function testNotificationHasCorrectProperties(): void | ||||
|     { | ||||
|         // Arrange | ||||
|         $initialAssignee = new User(); | ||||
|         $newAssignee = new User(); | ||||
|  | ||||
|         $task = new SingleTask(); | ||||
|         $task->setTitle('Important Task'); | ||||
|         $task->setAssignee($newAssignee); | ||||
|         $this->setEntityId($task, 456); | ||||
|  | ||||
|         $event = new AssignTaskEvent($task, $initialAssignee); | ||||
|  | ||||
|         $this->twig->render(Argument::any(), Argument::any())->willReturn('Test Content'); | ||||
|  | ||||
|         // Capture the persisted notification | ||||
|         $persistedNotification = null; | ||||
|         $this->entityManager->persist(Argument::type(Notification::class)) | ||||
|             ->shouldBeCalledOnce() | ||||
|             ->will(function ($args) use (&$persistedNotification) { | ||||
|                 $persistedNotification = $args[0]; | ||||
|             }); | ||||
|  | ||||
|         // Act | ||||
|         $this->subscriber->onTaskAssigned($event); | ||||
|  | ||||
|         // Assert | ||||
|         $this->assertInstanceOf(Notification::class, $persistedNotification); | ||||
|         $this->assertEquals($task->getId(), $persistedNotification->getRelatedEntityId()); | ||||
|         $this->assertEquals(SingleTask::class, $persistedNotification->getRelatedEntityClass()); | ||||
|         $this->assertEquals(AssignTaskNotificationFlagProvider::FLAG, $persistedNotification->getType()); | ||||
|         $this->assertEquals('Test Content', $persistedNotification->getTitle()); | ||||
|         $this->assertEquals('Test Content', $persistedNotification->getMessage()); | ||||
|     } | ||||
| } | ||||
| @@ -1,7 +1,13 @@ | ||||
| services: | ||||
|     _defaults: | ||||
|         autowire: true | ||||
|         autoconfigure: true | ||||
|  | ||||
|     Chill\TaskBundle\Event\Lifecycle\TaskLifecycleEvent: | ||||
|         arguments: | ||||
|             $em: '@Doctrine\ORM\EntityManagerInterface' | ||||
|             $tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface' | ||||
|         tags: | ||||
|             - { name: kernel.event_subscriber } | ||||
|  | ||||
|     Chill\TaskBundle\Event\TaskAssignEventSubscriber: ~ | ||||
|   | ||||
| @@ -0,0 +1,7 @@ | ||||
| services: | ||||
|     _defaults: | ||||
|         autowire: true | ||||
|         autoconfigure: true | ||||
|  | ||||
|     Chill\TaskBundle\Notification\TaskNotificationHandler: ~ | ||||
|     Chill\TaskBundle\Notification\AssignTaskNotificationFlagProvider: ~ | ||||
| @@ -116,3 +116,16 @@ CHILL_TASK_TASK_UPDATE: Modifier une tâche | ||||
| CHILL_TASK_TASK_CREATE_FOR_COURSE: Créer une tâche pour un parcours | ||||
| CHILL_TASK_TASK_CREATE_FOR_PERSON: Créer une tâche pour un usager | ||||
|  | ||||
| notification: | ||||
|     task: | ||||
|         title %title%: "Tâche: title" | ||||
|     flags: | ||||
|         task_assign: Lorsqu'un autre utilisateur m'assigne à une tâche. | ||||
|     email: | ||||
|         title: "Une tâche demande votre attention" | ||||
|         task_assigned: "Une tâche vous a été assignée." | ||||
|         title_label: "Titre de la tâche:" | ||||
|         deadline: "Vous êtes invités à accomplir cette tâche avant le %date%" | ||||
|         view_task: "Vous pouvez visualiser la tâche sur cette page:" | ||||
|         regards: "Cordialement" | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user