mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-11-07 12:48:24 +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).
|
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
|
## v4.4.1 - 2025-09-11
|
||||||
### Fixed
|
### Fixed
|
||||||
* fix translations in duplicate evaluation document modal and realign close modal button
|
* 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 DropFileWidget from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileWidget.vue";
|
||||||
import { computed, reactive } from "vue";
|
import { computed, reactive } from "vue";
|
||||||
import { useToast } from "vue-toast-notification";
|
import { useToast } from "vue-toast-notification";
|
||||||
import { DOCUMENT_REPLACE, DOCUMENT_ADD, trans } from "translator";
|
import { DOCUMENT_ADD, trans } from "translator";
|
||||||
|
|
||||||
interface DropFileConfig {
|
interface DropFileConfig {
|
||||||
allowRemove: boolean;
|
allowRemove: boolean;
|
||||||
@@ -78,9 +78,7 @@ function closeModal(): void {
|
|||||||
>
|
>
|
||||||
{{ trans(DOCUMENT_ADD) }}
|
{{ trans(DOCUMENT_ADD) }}
|
||||||
</button>
|
</button>
|
||||||
<button v-else @click="openModal" class="dropdown-item">
|
<button v-else @click="openModal" class="btn btn-edit"></button>
|
||||||
{{ trans(DOCUMENT_REPLACE) }}
|
|
||||||
</button>
|
|
||||||
<modal
|
<modal
|
||||||
v-if="state.showModal"
|
v-if="state.showModal"
|
||||||
:modal-dialog-class="modalClasses"
|
:modal-dialog-class="modalClasses"
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ class UserPasswordType extends AbstractType
|
|||||||
'invalid_message' => 'The password fields must match',
|
'invalid_message' => 'The password fields must match',
|
||||||
'constraints' => [
|
'constraints' => [
|
||||||
new Length([
|
new Length([
|
||||||
'min' => 9,
|
'min' => 14,
|
||||||
'minMessage' => 'The password must be greater than {{ limit }} characters',
|
'minMessage' => 'The password must be greater than {{ limit }} characters',
|
||||||
]),
|
]),
|
||||||
new NotBlank(),
|
new NotBlank(),
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ export default {
|
|||||||
return appMessages.fr.the_evaluation_document;
|
return appMessages.fr.the_evaluation_document;
|
||||||
case "Chill\\MainBundle\\Entity\\Workflow\\EntityWorkflow":
|
case "Chill\\MainBundle\\Entity\\Workflow\\EntityWorkflow":
|
||||||
return appMessages.fr.the_workflow;
|
return appMessages.fr.the_workflow;
|
||||||
|
case "Chill\\TaskBundle\\Entity\\SingleTask":
|
||||||
|
return appMessages.fr.the_task;
|
||||||
default:
|
default:
|
||||||
throw "notification type unknown";
|
throw "notification type unknown";
|
||||||
}
|
}
|
||||||
@@ -96,6 +98,8 @@ export default {
|
|||||||
return `/fr/person/accompanying-period/work/evaluation/document/${n.relatedEntityId}/show`;
|
return `/fr/person/accompanying-period/work/evaluation/document/${n.relatedEntityId}/show`;
|
||||||
case "Chill\\MainBundle\\Entity\\Workflow\\EntityWorkflow":
|
case "Chill\\MainBundle\\Entity\\Workflow\\EntityWorkflow":
|
||||||
return `/fr/main/workflow/${n.relatedEntityId}/show`;
|
return `/fr/main/workflow/${n.relatedEntityId}/show`;
|
||||||
|
case "Chill\\TaskBundle\\Entity\\SingleTask":
|
||||||
|
return `/fr/task/single-task/${n.relatedEntityId}/show`;
|
||||||
default:
|
default:
|
||||||
throw "notification type unknown";
|
throw "notification type unknown";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
role="button"
|
role="button"
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
aria-expanded="false">
|
aria-expanded="false">
|
||||||
<i class="fa fa-flash"></i>
|
<i class="bi bi-lightning-fill"></i>
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
{% for menu in menus %}
|
{% for menu in menus %}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class NotificationNormalizer implements NormalizerAwareInterface, NormalizerInte
|
|||||||
->find($object->getRelatedEntityId());
|
->find($object->getRelatedEntityId());
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'type' => 'notification',
|
'type' => $object->getType(),
|
||||||
'id' => $object->getId(),
|
'id' => $object->getId(),
|
||||||
'addressees' => $this->normalizer->normalize($object->getAddressees(), $format, $context),
|
'addressees' => $this->normalizer->normalize($object->getAddressees(), $format, $context),
|
||||||
'date' => $this->normalizer->normalize($object->getDate(), $format, $context),
|
'date' => $this->normalizer->normalize($object->getDate(), $format, $context),
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ final class UserControllerTest extends WebTestCase
|
|||||||
self::assertResponseIsSuccessful();
|
self::assertResponseIsSuccessful();
|
||||||
|
|
||||||
$username = 'Test_user'.uniqid();
|
$username = 'Test_user'.uniqid();
|
||||||
$password = 'Password1234!';
|
$password = 'Password_1234!';
|
||||||
|
|
||||||
// Fill in the form and submit it
|
// Fill in the form and submit it
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ final class UserControllerTest extends WebTestCase
|
|||||||
{
|
{
|
||||||
$client = $this->getClientAuthenticatedAsAdmin();
|
$client = $this->getClientAuthenticatedAsAdmin();
|
||||||
$crawler = $client->request('GET', "/fr/admin/user/{$userId}/edit_password");
|
$crawler = $client->request('GET', "/fr/admin/user/{$userId}/edit_password");
|
||||||
$newPassword = '1234Password!';
|
$newPassword = '1234_Password!';
|
||||||
|
|
||||||
$form = $crawler->selectButton('Changer le mot de passe')->form([
|
$form = $crawler->selectButton('Changer le mot de passe')->form([
|
||||||
'chill_mainbundle_user_password[new_password][first]' => $newPassword,
|
'chill_mainbundle_user_password[new_password][first]' => $newPassword,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
:id="evaluation.id"
|
:id="evaluation.id"
|
||||||
:templates="templates"
|
:templates="templates"
|
||||||
:preventDefaultMoveToGenerate="true"
|
:preventDefaultMoveToGenerate="true"
|
||||||
@go-to-generate-document="$emit('submitBeforeGenerate', $event)"
|
@go-to-generate-document="submitBeforeGenerate"
|
||||||
>
|
>
|
||||||
<template v-slot:title>
|
<template v-slot:title>
|
||||||
<label class="col-form-label">{{
|
<label class="col-form-label">{{
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<li>
|
<li>
|
||||||
<drop-file-modal
|
<drop-file-modal
|
||||||
:allow-remove="false"
|
:allow-remove="false"
|
||||||
@add-document="$emit('addDocument', $event)"
|
@add-document="emit('addDocument', $event)"
|
||||||
></drop-file-modal>
|
></drop-file-modal>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -39,9 +39,34 @@ import {
|
|||||||
EVALUATION_GENERATE_A_DOCUMENT,
|
EVALUATION_GENERATE_A_DOCUMENT,
|
||||||
trans,
|
trans,
|
||||||
} from "translator";
|
} from "translator";
|
||||||
|
import { buildLink } from "ChillDocGeneratorAssets/lib/document-generator";
|
||||||
|
import { useStore } from "vuex";
|
||||||
|
|
||||||
defineProps(["evaluation", "templates"]);
|
const store = useStore();
|
||||||
defineEmits(["addDocument", "submitBeforeGenerate"]);
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
:preventDefaultMoveToGenerate="true"
|
:preventDefaultMoveToGenerate="true"
|
||||||
:goToGenerateWorkflowPayload="{ doc: d }"
|
:goToGenerateWorkflowPayload="{ doc: d }"
|
||||||
@go-to-generate-workflow="
|
@go-to-generate-workflow="
|
||||||
$emit('goToGenerateWorkflow', $event)
|
goToGenerateWorkflowEvaluationDocument
|
||||||
"
|
"
|
||||||
></list-workflow-modal>
|
></list-workflow-modal>
|
||||||
</li>
|
</li>
|
||||||
@@ -95,10 +95,9 @@
|
|||||||
<a
|
<a
|
||||||
class="dropdown-item"
|
class="dropdown-item"
|
||||||
@click="
|
@click="
|
||||||
$emit(
|
goToGenerateDocumentNotification(
|
||||||
'goToGenerateNotification',
|
|
||||||
d,
|
d,
|
||||||
true,
|
false,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@@ -113,8 +112,7 @@
|
|||||||
<a
|
<a
|
||||||
class="dropdown-item"
|
class="dropdown-item"
|
||||||
@click="
|
@click="
|
||||||
$emit(
|
goToGenerateDocumentNotification(
|
||||||
'goToGenerateNotification',
|
|
||||||
d,
|
d,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
@@ -150,15 +148,35 @@
|
|||||||
"
|
"
|
||||||
></document-action-buttons-group>
|
></document-action-buttons-group>
|
||||||
</li>
|
</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)">
|
<li v-if="Number.isInteger(d.id)">
|
||||||
<div class="duplicate-dropdown">
|
<div class="duplicate-dropdown">
|
||||||
<button
|
<button
|
||||||
class="btn btn-edit dropdown-toggle"
|
class="btn btn-outline-primary dropdown-toggle"
|
||||||
type="button"
|
type="button"
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
>
|
>
|
||||||
{{ trans(EVALUATION_DOCUMENT_EDIT) }}
|
<i class="bi bi-lightning-fill"></i>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<!--delete-->
|
<!--delete-->
|
||||||
@@ -180,27 +198,6 @@
|
|||||||
}}
|
}}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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-->
|
<!--duplicate document-->
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
@@ -300,35 +297,45 @@ import {
|
|||||||
EVALUATION_DOCUMENTS,
|
EVALUATION_DOCUMENTS,
|
||||||
EVALUATION_DOCUMENT_MOVE,
|
EVALUATION_DOCUMENT_MOVE,
|
||||||
EVALUATION_DOCUMENT_DELETE,
|
EVALUATION_DOCUMENT_DELETE,
|
||||||
EVALUATION_DOCUMENT_EDIT,
|
|
||||||
EVALUATION_DOCUMENT_DUPLICATE_HERE,
|
EVALUATION_DOCUMENT_DUPLICATE_HERE,
|
||||||
EVALUATION_DOCUMENT_DUPLICATE_TO_OTHER_EVALUATION,
|
EVALUATION_DOCUMENT_DUPLICATE_TO_OTHER_EVALUATION,
|
||||||
trans,
|
trans,
|
||||||
} from "translator";
|
} from "translator";
|
||||||
import { ref, watch } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import AccompanyingPeriodWorkSelectorModal from "ChillPersonAssets/vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkSelectorModal.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",
|
"documents",
|
||||||
"docAnchorId",
|
"docAnchorId",
|
||||||
"accompanyingPeriodId",
|
"accompanyingPeriodId",
|
||||||
"accompanyingPeriodWorkId",
|
"evaluation",
|
||||||
]);
|
]);
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
"inputDocumentTitle",
|
"inputDocumentTitle",
|
||||||
"removeDocument",
|
"removeDocument",
|
||||||
"duplicateDocument",
|
"duplicateDocument",
|
||||||
"statusDocumentChanged",
|
"statusDocumentChanged",
|
||||||
"goToGenerateWorkflow",
|
|
||||||
"goToGenerateNotification",
|
|
||||||
"duplicateDocumentToWork",
|
"duplicateDocumentToWork",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
const showAccompanyingPeriodSelector = ref(false);
|
const showAccompanyingPeriodSelector = ref(false);
|
||||||
const selectedEvaluation = ref(null);
|
const selectedEvaluation = ref(null);
|
||||||
const selectedDocumentToDuplicate = ref(null);
|
const selectedDocumentToDuplicate = ref(null);
|
||||||
const selectedDocumentToMove = 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) => {
|
const prepareDocumentDuplicationToWork = (d) => {
|
||||||
selectedDocumentToDuplicate.value = d;
|
selectedDocumentToDuplicate.value = d;
|
||||||
/** ensure selectedDocumentToMove is null */
|
/** 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>
|
</script>
|
||||||
|
|||||||
@@ -24,8 +24,8 @@
|
|||||||
v-if="evaluation.documents.length > 0"
|
v-if="evaluation.documents.length > 0"
|
||||||
:documents="evaluation.documents"
|
:documents="evaluation.documents"
|
||||||
:docAnchorId="docAnchorId"
|
:docAnchorId="docAnchorId"
|
||||||
|
:evaluation="evaluation"
|
||||||
:accompanyingPeriodId="store.state.work.accompanyingPeriod.id"
|
:accompanyingPeriodId="store.state.work.accompanyingPeriod.id"
|
||||||
:accompanying-period-work-id="store.state.work.id"
|
|
||||||
@inputDocumentTitle="onInputDocumentTitle"
|
@inputDocumentTitle="onInputDocumentTitle"
|
||||||
@removeDocument="removeDocument"
|
@removeDocument="removeDocument"
|
||||||
@duplicateDocument="duplicateDocument"
|
@duplicateDocument="duplicateDocument"
|
||||||
@@ -34,7 +34,6 @@
|
|||||||
"
|
"
|
||||||
@move-document-to-evaluation="moveDocumentToEvaluation"
|
@move-document-to-evaluation="moveDocumentToEvaluation"
|
||||||
@statusDocumentChanged="onStatusDocumentChanged"
|
@statusDocumentChanged="onStatusDocumentChanged"
|
||||||
@goToGenerateWorkflow="goToGenerateWorkflowEvaluationDocument"
|
|
||||||
@goToGenerateNotification="goToGenerateDocumentNotification"
|
@goToGenerateNotification="goToGenerateDocumentNotification"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -42,7 +41,6 @@
|
|||||||
:evaluation="evaluation"
|
:evaluation="evaluation"
|
||||||
:templates="getTemplatesAvailables"
|
:templates="getTemplatesAvailables"
|
||||||
@addDocument="addDocument"
|
@addDocument="addDocument"
|
||||||
@submitBeforeGenerate="submitBeforeGenerate"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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) {
|
function goToGenerateDocumentNotification(document, tos) {
|
||||||
const callback = (data) => {
|
const callback = (data) => {
|
||||||
let evaluation = data.accompanyingPeriodWorkEvaluations.find(
|
let evaluation = data.accompanyingPeriodWorkEvaluations.find(
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ namespace Chill\TaskBundle\Controller;
|
|||||||
|
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||||
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface;
|
|
||||||
use Chill\MainBundle\Serializer\Model\Collection;
|
use Chill\MainBundle\Serializer\Model\Collection;
|
||||||
use Chill\MainBundle\Serializer\Model\Counter;
|
use Chill\MainBundle\Serializer\Model\Counter;
|
||||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||||
@@ -23,6 +22,7 @@ use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
|||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
use Chill\PersonBundle\Privacy\PrivacyEvent;
|
use Chill\PersonBundle\Privacy\PrivacyEvent;
|
||||||
use Chill\TaskBundle\Entity\SingleTask;
|
use Chill\TaskBundle\Entity\SingleTask;
|
||||||
|
use Chill\TaskBundle\Event\AssignTaskEvent;
|
||||||
use Chill\TaskBundle\Event\TaskEvent;
|
use Chill\TaskBundle\Event\TaskEvent;
|
||||||
use Chill\TaskBundle\Event\UI\UIEvent;
|
use Chill\TaskBundle\Event\UI\UIEvent;
|
||||||
use Chill\TaskBundle\Form\SingleTaskType;
|
use Chill\TaskBundle\Form\SingleTaskType;
|
||||||
@@ -48,7 +48,6 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
|||||||
final class SingleTaskController extends AbstractController
|
final class SingleTaskController extends AbstractController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly CenterResolverDispatcherInterface $centerResolverDispatcher,
|
|
||||||
private readonly PaginatorFactory $paginatorFactory,
|
private readonly PaginatorFactory $paginatorFactory,
|
||||||
private readonly SingleTaskAclAwareRepositoryInterface $singleTaskAclAwareRepository,
|
private readonly SingleTaskAclAwareRepositoryInterface $singleTaskAclAwareRepository,
|
||||||
private readonly TranslatorInterface $translator,
|
private readonly TranslatorInterface $translator,
|
||||||
@@ -169,6 +168,9 @@ final class SingleTaskController extends AbstractController
|
|||||||
->setForm($this->setCreateForm($task, TaskVoter::UPDATE));
|
->setForm($this->setCreateForm($task, TaskVoter::UPDATE));
|
||||||
$this->eventDispatcher->dispatch($event, UIEvent::EDIT_FORM);
|
$this->eventDispatcher->dispatch($event, UIEvent::EDIT_FORM);
|
||||||
|
|
||||||
|
// To keep track of specific assignee change
|
||||||
|
$initialAssignee = $task->getAssignee();
|
||||||
|
|
||||||
$form = $event->getForm();
|
$form = $event->getForm();
|
||||||
|
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
@@ -178,6 +180,13 @@ final class SingleTaskController extends AbstractController
|
|||||||
$em = $this->managerRegistry->getManager();
|
$em = $this->managerRegistry->getManager();
|
||||||
$em->persist($task);
|
$em->persist($task);
|
||||||
|
|
||||||
|
if ($initialAssignee !== $task->getAssignee()) {
|
||||||
|
$this->eventDispatcher->dispatch(
|
||||||
|
new AssignTaskEvent($task, $initialAssignee),
|
||||||
|
AssignTaskEvent::PERSIST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$em->flush();
|
$em->flush();
|
||||||
|
|
||||||
$this->addFlash('success', $this->translator
|
$this->addFlash('success', $this->translator
|
||||||
@@ -525,6 +534,13 @@ final class SingleTaskController extends AbstractController
|
|||||||
|
|
||||||
$this->eventDispatcher->dispatch(new TaskEvent($task), TaskEvent::PERSIST);
|
$this->eventDispatcher->dispatch(new TaskEvent($task), TaskEvent::PERSIST);
|
||||||
|
|
||||||
|
if (null !== $task->getAssignee()) {
|
||||||
|
$this->eventDispatcher->dispatch(
|
||||||
|
new AssignTaskEvent($task, null),
|
||||||
|
AssignTaskEvent::PERSIST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$em->flush();
|
$em->flush();
|
||||||
|
|
||||||
$this->addFlash('success', $this->translator->trans('The task is created'));
|
$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/timeline.yaml');
|
||||||
$loader->load('services/fixtures.yaml');
|
$loader->load('services/fixtures.yaml');
|
||||||
$loader->load('services/form.yaml');
|
$loader->load('services/form.yaml');
|
||||||
|
$loader->load('services/notification.yaml');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function prepend(ContainerBuilder $container)
|
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>
|
<div>
|
||||||
{% if task.person is not null %}
|
{% if task.person is not null %}
|
||||||
<span class="chill-task-list__row__person">
|
<span class="chill-task-list__row__person">
|
||||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||||
targetEntity: { name: 'person', id: task.person.id },
|
targetEntity: { name: 'person', id: task.person.id },
|
||||||
action: 'show',
|
action: 'show',
|
||||||
displayBadge: true,
|
displayBadge: true,
|
||||||
buttonText: task.person|chill_entity_render_string,
|
buttonText: task.person|chill_entity_render_string,
|
||||||
isDead: task.person.deathdate is not null
|
isDead: task.person.deathdate is not null
|
||||||
} %}
|
} %}
|
||||||
</span>
|
</span>
|
||||||
{% elseif task.course is not null %}
|
{% elseif task.course is not null %}
|
||||||
<div style="margin-bottom: 1rem;">
|
<div style="margin-bottom: 1rem;">
|
||||||
{% for part in task.course.currentParticipations %}
|
{% for part in task.course.currentParticipations %}
|
||||||
|
|||||||
@@ -110,4 +110,5 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% 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:
|
services:
|
||||||
|
_defaults:
|
||||||
|
autowire: true
|
||||||
|
autoconfigure: true
|
||||||
|
|
||||||
Chill\TaskBundle\Event\Lifecycle\TaskLifecycleEvent:
|
Chill\TaskBundle\Event\Lifecycle\TaskLifecycleEvent:
|
||||||
arguments:
|
arguments:
|
||||||
$em: '@Doctrine\ORM\EntityManagerInterface'
|
$em: '@Doctrine\ORM\EntityManagerInterface'
|
||||||
$tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'
|
$tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'
|
||||||
tags:
|
tags:
|
||||||
- { name: kernel.event_subscriber }
|
- { 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_COURSE: Créer une tâche pour un parcours
|
||||||
CHILL_TASK_TASK_CREATE_FOR_PERSON: Créer une tâche pour un usager
|
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