mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-10-05 04:49:44 +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