mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-10-06 21:39:42 +00:00
Compare commits
13 Commits
425-rename
...
375-notifi
Author | SHA1 | Date | |
---|---|---|---|
10de431c48 | |||
cb173c6341 | |||
20bbb6b485 | |||
c731f1967b | |||
2434d91e4a | |||
13a4795333 | |||
2bb5776002 | |||
34dde37789 | |||
609b8f9af1 | |||
57d922c05e | |||
ad579f3269 | |||
30e1416018 | |||
b6b03cfcec |
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
|
@@ -1,6 +0,0 @@
|
|||||||
kind: Fixed
|
|
||||||
body: Fix the rendering of list of StoredObjectVersions, where there are kept version (before converting to pdf) and intermediate versions deleted
|
|
||||||
time: 2025-10-03T22:40:44.685474863+02:00
|
|
||||||
custom:
|
|
||||||
Issue: ""
|
|
||||||
SchemaChange: No schema change
|
|
@@ -1,6 +0,0 @@
|
|||||||
kind: Fixed
|
|
||||||
body: 'Notification: fix editing of sent notification by removing form.addressesEmails, a field that no longer exists'
|
|
||||||
time: 2025-10-06T12:13:15.45905994+02:00
|
|
||||||
custom:
|
|
||||||
Issue: "434"
|
|
||||||
SchemaChange: No schema change
|
|
@@ -1,6 +0,0 @@
|
|||||||
kind: UX
|
|
||||||
body: Change the terms 'cercle' and 'centre' to 'service', and 'territoire' respectively.
|
|
||||||
time: 2025-10-06T12:39:32.514056818+02:00
|
|
||||||
custom:
|
|
||||||
Issue: "425"
|
|
||||||
SchemaChange: No schema change
|
|
@@ -16,5 +16,5 @@
|
|||||||
- ajout d'un filtre et regroupement par usager participant sur les échanges
|
- ajout d'un filtre et regroupement par usager participant sur les échanges
|
||||||
- ajout d'un regroupement: par type d'activité associé au parcours;
|
- ajout d'un regroupement: par type d'activité associé au parcours;
|
||||||
- trie les filtre et regroupements par ordre alphabétique dans els exports
|
- trie les filtre et regroupements par ordre alphabétique dans els exports
|
||||||
- ajout d'un paramètre qui permet de désactiver le filtre par territoire dans les exports
|
- ajout d'un paramètre qui permet de désactiver le filtre par centre dans les exports
|
||||||
- correction de l'interface de date dans les filtres et regroupements "par statut du parcours à la date"
|
- correction de l'interface de date dans les filtres et regroupements "par statut du parcours à la date"
|
||||||
|
@@ -29,7 +29,7 @@
|
|||||||
- ajout d'un regroupement par métier des intervenants sur un parcours;
|
- ajout d'un regroupement par métier des intervenants sur un parcours;
|
||||||
- ajout d'un regroupement par service des intervenants sur un parcours;
|
- ajout d'un regroupement par service des intervenants sur un parcours;
|
||||||
- ajout d'un regroupement par utilisateur intervenant sur un parcours
|
- ajout d'un regroupement par utilisateur intervenant sur un parcours
|
||||||
- ajout d'un regroupement "par territoire de l'usager";
|
- ajout d'un regroupement "par centre de l'usager";
|
||||||
- ajout d'un filtre "par métier intervenant sur un parcours";
|
- ajout d'un filtre "par métier intervenant sur un parcours";
|
||||||
- ajout d'un filtre "par service intervenant sur un parcours";
|
- ajout d'un filtre "par service intervenant sur un parcours";
|
||||||
- création d'un rôle spécifique pour voir les parcours confidentiels (et séparer de celui de la liste qui permet de ré-assigner les parcours en lot);
|
- création d'un rôle spécifique pour voir les parcours confidentiels (et séparer de celui de la liste qui permet de ré-assigner les parcours en lot);
|
||||||
|
@@ -1,13 +0,0 @@
|
|||||||
## v4.5.0 - 2025-10-03
|
|
||||||
### Feature
|
|
||||||
* Only allow delete of attachment on workflows that are not final
|
|
||||||
* Move up signature buttons on index workflow page for easier access
|
|
||||||
* Filter out document from attachment list if it is the same as the workflow document
|
|
||||||
* Block edition on attached document on workflow, if the workflow is finalized or sent external
|
|
||||||
* Convert workflow's attached document to pdf while sending them external
|
|
||||||
* After a signature is canceled or rejected, going to a waiting page until the post-process routines apply a workflow transition
|
|
||||||
### Fixed
|
|
||||||
* ([#426](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/426)) Increased the number of required characters when setting a new password in Chill from 9 to 14 - GDPR compliance
|
|
||||||
* Fix permissions on storedObject which are subject by a workflow
|
|
||||||
### DX
|
|
||||||
* Introduce a WaitingScreen component to display a waiting screen
|
|
@@ -1,4 +0,0 @@
|
|||||||
## v4.5.1 - 2025-10-03
|
|
||||||
### Fixed
|
|
||||||
* Add missing javascript dependency
|
|
||||||
* Add exception handling for conversion of attachment on sending external, when documens are already in pdf
|
|
23
CHANGELOG.md
23
CHANGELOG.md
@@ -6,25 +6,6 @@ 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.5.1 - 2025-10-03
|
|
||||||
### Fixed
|
|
||||||
* Add missing javascript dependency
|
|
||||||
* Add exception handling for conversion of attachment on sending external, when documens are already in pdf
|
|
||||||
|
|
||||||
## v4.5.0 - 2025-10-03
|
|
||||||
### Feature
|
|
||||||
* Only allow delete of attachment on workflows that are not final
|
|
||||||
* Move up signature buttons on index workflow page for easier access
|
|
||||||
* Filter out document from attachment list if it is the same as the workflow document
|
|
||||||
* Block edition on attached document on workflow, if the workflow is finalized or sent external
|
|
||||||
* Convert workflow's attached document to pdf while sending them external
|
|
||||||
* After a signature is canceled or rejected, going to a waiting page until the post-process routines apply a workflow transition
|
|
||||||
### Fixed
|
|
||||||
* ([#426](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/426)) Increased the number of required characters when setting a new password in Chill from 9 to 14 - GDPR compliance
|
|
||||||
* Fix permissions on storedObject which are subject by a workflow
|
|
||||||
### DX
|
|
||||||
* Introduce a WaitingScreen component to display a waiting screen
|
|
||||||
|
|
||||||
## v4.4.2 - 2025-09-12
|
## v4.4.2 - 2025-09-12
|
||||||
### Fixed
|
### Fixed
|
||||||
* Fix document generation and workflow generation do not work on accompanying period work documents
|
* Fix document generation and workflow generation do not work on accompanying period work documents
|
||||||
@@ -761,7 +742,7 @@ Fix color of Chill footer
|
|||||||
- ajout d'un filtre et regroupement par usager participant sur les échanges
|
- ajout d'un filtre et regroupement par usager participant sur les échanges
|
||||||
- ajout d'un regroupement: par type d'activité associé au parcours;
|
- ajout d'un regroupement: par type d'activité associé au parcours;
|
||||||
- trie les filtre et regroupements par ordre alphabétique dans els exports
|
- trie les filtre et regroupements par ordre alphabétique dans els exports
|
||||||
- ajout d'un paramètre qui permet de désactiver le filtre par territoire dans les exports
|
- ajout d'un paramètre qui permet de désactiver le filtre par centre dans les exports
|
||||||
- correction de l'interface de date dans les filtres et regroupements "par statut du parcours à la date"
|
- correction de l'interface de date dans les filtres et regroupements "par statut du parcours à la date"
|
||||||
|
|
||||||
## v2.9.2 - 2023-10-17
|
## v2.9.2 - 2023-10-17
|
||||||
@@ -941,7 +922,7 @@ error when trying to reedit a saved export
|
|||||||
- ajout d'un regroupement par métier des intervenants sur un parcours;
|
- ajout d'un regroupement par métier des intervenants sur un parcours;
|
||||||
- ajout d'un regroupement par service des intervenants sur un parcours;
|
- ajout d'un regroupement par service des intervenants sur un parcours;
|
||||||
- ajout d'un regroupement par utilisateur intervenant sur un parcours
|
- ajout d'un regroupement par utilisateur intervenant sur un parcours
|
||||||
- ajout d'un regroupement "par territoire de l'usager";
|
- ajout d'un regroupement "par centre de l'usager";
|
||||||
- ajout d'un filtre "par métier intervenant sur un parcours";
|
- ajout d'un filtre "par métier intervenant sur un parcours";
|
||||||
- ajout d'un filtre "par service intervenant sur un parcours";
|
- ajout d'un filtre "par service intervenant sur un parcours";
|
||||||
- création d'un rôle spécifique pour voir les parcours confidentiels (et séparer de celui de la liste qui permet de ré-assigner les parcours en lot);
|
- création d'un rôle spécifique pour voir les parcours confidentiels (et séparer de celui de la liste qui permet de ré-assigner les parcours en lot);
|
||||||
|
@@ -23,8 +23,8 @@ class "Document" {
|
|||||||
- text description
|
- text description
|
||||||
- ArrayCollection_DocumentCategory categories
|
- ArrayCollection_DocumentCategory categories
|
||||||
- varchar_150 content #link to openstack
|
- varchar_150 content #link to openstack
|
||||||
- Territoire territoire
|
- Center center
|
||||||
- Service service
|
- Cercle cercle
|
||||||
- User user
|
- User user
|
||||||
- DateTime date # Creation date
|
- DateTime date # Creation date
|
||||||
}
|
}
|
||||||
|
@@ -38,7 +38,7 @@ Certaines données sont historisées:
|
|||||||
|
|
||||||
- les référents d'un parcours;
|
- les référents d'un parcours;
|
||||||
- les statuts d'un parcours;
|
- les statuts d'un parcours;
|
||||||
- la liaison entre les territoires et les usagers;
|
- la liaison entre les centres et les usagers;
|
||||||
- etc.
|
- etc.
|
||||||
|
|
||||||
Dans ces cas-là, Chill crée généralement deux colonnes, qui sont habituellement nommées :code:`startDate` et :code:`endDate`. Lorsque la colonne :code:`endDate` est à :code:`NULL`, cela signifie que la période n'est pas "fermée". La colonne :code:`startDate` n'est pas nullable.
|
Dans ces cas-là, Chill crée généralement deux colonnes, qui sont habituellement nommées :code:`startDate` et :code:`endDate`. Lorsque la colonne :code:`endDate` est à :code:`NULL`, cela signifie que la période n'est pas "fermée". La colonne :code:`startDate` n'est pas nullable.
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
order,table_schema,table_name,commentaire
|
order,table_schema,table_name,commentaire
|
||||||
1,chill_3party,party_category,Catégorie de tiers
|
1,chill_3party,party_category,Catégorie de tiers
|
||||||
2,chill_3party,party_center,Association entre les tiers et les territoires (déprécié)
|
2,chill_3party,party_center,Association entre les tiers et les centres (déprécié)
|
||||||
3,chill_3party,party_profession,Profession du tiers (déprécié)
|
3,chill_3party,party_profession,Profession du tiers (déprécié)
|
||||||
4,chill_3party,third_party,Tiers
|
4,chill_3party,third_party,Tiers
|
||||||
5,chill_3party,thirdparty_category,association tiers - catégories
|
5,chill_3party,thirdparty_category,association tiers - catégories
|
||||||
@@ -54,7 +54,7 @@ order,table_schema,table_name,commentaire
|
|||||||
53,public,activitytpresence,Présence aux échanges
|
53,public,activitytpresence,Présence aux échanges
|
||||||
54,public,activitytype,Types d'échanges
|
54,public,activitytype,Types d'échanges
|
||||||
55,public,activitytypecategory,Catégories de types d'échanges
|
55,public,activitytypecategory,Catégories de types d'échanges
|
||||||
56,public,centers,"Territoires (territoires, agences, etc.)"
|
56,public,centers,"Centres (territoires, agences, etc.)"
|
||||||
57,public,chill_activity_activity_chill_person_socialaction,
|
57,public,chill_activity_activity_chill_person_socialaction,
|
||||||
58,public,chill_activity_activity_chill_person_socialissue
|
58,public,chill_activity_activity_chill_person_socialissue
|
||||||
59,public,chill_docgen_template,Gabarits de documents
|
59,public,chill_docgen_template,Gabarits de documents
|
||||||
@@ -111,7 +111,7 @@ order,table_schema,table_name,commentaire
|
|||||||
110,public,chill_person_marital_status,Etats civils
|
110,public,chill_person_marital_status,Etats civils
|
||||||
111,public,chill_person_not_duplicate,
|
111,public,chill_person_not_duplicate,
|
||||||
112,public,chill_person_person,Usagers
|
112,public,chill_person_person,Usagers
|
||||||
113,public,chill_person_person_center_history,Historique des territoires d'un usagers
|
113,public,chill_person_person_center_history,Historique des centres d'un usagers
|
||||||
114,public,chill_person_persons_to_addresses,Déprécié
|
114,public,chill_person_persons_to_addresses,Déprécié
|
||||||
115,public,chill_person_phone,Numéros d etéléphone supplémentaires d'un usager
|
115,public,chill_person_phone,Numéros d etéléphone supplémentaires d'un usager
|
||||||
116,public,chill_person_relations,Types de relations de filiation
|
116,public,chill_person_relations,Types de relations de filiation
|
||||||
@@ -142,7 +142,7 @@ order,table_schema,table_name,commentaire
|
|||||||
141,public,permission_groups
|
141,public,permission_groups
|
||||||
142,public,permissionsgroup_rolescope
|
142,public,permissionsgroup_rolescope
|
||||||
143,public,persons_spoken_languages
|
143,public,persons_spoken_languages
|
||||||
144,public,regroupment,Regroupement de territoires
|
144,public,regroupment,Regroupement de centres
|
||||||
145,public,regroupment_center,
|
145,public,regroupment_center,
|
||||||
146,public,role_scopes,
|
146,public,role_scopes,
|
||||||
147,public,scopes,Services
|
147,public,scopes,Services
|
||||||
|
Can't render this file because it has a wrong number of fields in line 28.
|
@@ -55,7 +55,6 @@
|
|||||||
"@tsconfig/node20": "^20.1.4",
|
"@tsconfig/node20": "^20.1.4",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
"@types/leaflet": "^1.9.3",
|
"@types/leaflet": "^1.9.3",
|
||||||
"@vueuse/core": "^13.9.0",
|
|
||||||
"bootstrap-icons": "^1.11.3",
|
"bootstrap-icons": "^1.11.3",
|
||||||
"dropzone": "^5.7.6",
|
"dropzone": "^5.7.6",
|
||||||
"es6-promise": "^4.2.8",
|
"es6-promise": "^4.2.8",
|
||||||
|
@@ -10,7 +10,7 @@ Attendee: Présence de l'usager
|
|||||||
attendee: présence de l'usager
|
attendee: présence de l'usager
|
||||||
list_reasons: liste des sujets
|
list_reasons: liste des sujets
|
||||||
user_username: nom de l'utilisateur
|
user_username: nom de l'utilisateur
|
||||||
circle_name: nom du service
|
circle_name: nom du cercle
|
||||||
Remark: Commentaire
|
Remark: Commentaire
|
||||||
No comments: Aucun commentaire
|
No comments: Aucun commentaire
|
||||||
Add a new activity: Ajouter une nouvel échange
|
Add a new activity: Ajouter une nouvel échange
|
||||||
@@ -20,7 +20,7 @@ not present: absent
|
|||||||
Delete: Supprimer
|
Delete: Supprimer
|
||||||
Update: Mettre à jour
|
Update: Mettre à jour
|
||||||
Update activity: Modifier l'échange
|
Update activity: Modifier l'échange
|
||||||
Scope: Service
|
Scope: Cercle
|
||||||
Activity data: Données de l'échange
|
Activity data: Données de l'échange
|
||||||
Activity location: Localisation de l'échange
|
Activity location: Localisation de l'échange
|
||||||
No reason associated: Aucun sujet
|
No reason associated: Aucun sujet
|
||||||
@@ -398,7 +398,7 @@ export:
|
|||||||
sent received: Envoyé ou reçu
|
sent received: Envoyé ou reçu
|
||||||
emergency: Urgence
|
emergency: Urgence
|
||||||
accompanying course id: Identifiant du parcours
|
accompanying course id: Identifiant du parcours
|
||||||
course circles: Services du parcours
|
course circles: Cercles du parcours
|
||||||
travelTime: Durée de déplacement
|
travelTime: Durée de déplacement
|
||||||
durationTime: Durée
|
durationTime: Durée
|
||||||
id: Identifiant
|
id: Identifiant
|
||||||
|
@@ -177,7 +177,7 @@ export:
|
|||||||
agent_id: Utilisateur
|
agent_id: Utilisateur
|
||||||
creator_id: Créateur
|
creator_id: Créateur
|
||||||
main_scope: Service principal de l'utilisateur
|
main_scope: Service principal de l'utilisateur
|
||||||
main_center: Territoire principal de l'utilisateur
|
main_center: Centre principal de l'utilisateur
|
||||||
aside_activity_type: Catégorie d'activité annexe
|
aside_activity_type: Catégorie d'activité annexe
|
||||||
date: Date
|
date: Date
|
||||||
duration: Durée
|
duration: Durée
|
||||||
|
@@ -59,7 +59,7 @@ final readonly class StoredObjectVersionApiController
|
|||||||
|
|
||||||
return new JsonResponse(
|
return new JsonResponse(
|
||||||
$this->serializer->serialize(
|
$this->serializer->serialize(
|
||||||
new Collection(array_values($items->toArray()), $paginator),
|
new Collection($items, $paginator),
|
||||||
'json',
|
'json',
|
||||||
[AbstractNormalizer::GROUPS => ['read', StoredObjectVersionNormalizer::WITH_POINT_IN_TIMES_CONTEXT, StoredObjectVersionNormalizer::WITH_RESTORED_CONTEXT]]
|
[AbstractNormalizer::GROUPS => ['read', StoredObjectVersionNormalizer::WITH_POINT_IN_TIMES_CONTEXT, StoredObjectVersionNormalizer::WITH_RESTORED_CONTEXT]]
|
||||||
),
|
),
|
||||||
|
@@ -1,20 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\DocStoreBundle\Exception;
|
|
||||||
|
|
||||||
class ConversionWithSameMimeTypeException extends \RuntimeException
|
|
||||||
{
|
|
||||||
public function __construct(string $mimeType, ?\Throwable $previous = null)
|
|
||||||
{
|
|
||||||
parent::__construct("Conversion to same MIME type '{$mimeType}' is not allowed: already at the same MIME type", 0, $previous);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -25,7 +25,7 @@ export interface GenericDoc {
|
|||||||
type: "doc_store_generic_doc";
|
type: "doc_store_generic_doc";
|
||||||
uniqueKey: string;
|
uniqueKey: string;
|
||||||
key: string;
|
key: string;
|
||||||
identifiers: { id: number };
|
identifiers: object;
|
||||||
context: "person" | "accompanying-period";
|
context: "person" | "accompanying-period";
|
||||||
doc_date: DateTime;
|
doc_date: DateTime;
|
||||||
metadata: GenericDocMetadata;
|
metadata: GenericDocMetadata;
|
||||||
|
@@ -3,9 +3,9 @@ import {
|
|||||||
StoredObject,
|
StoredObject,
|
||||||
StoredObjectPointInTime,
|
StoredObjectPointInTime,
|
||||||
StoredObjectVersionWithPointInTime,
|
StoredObjectVersionWithPointInTime,
|
||||||
} from "ChillDocStoreAssets/types";
|
} from "./../../../types";
|
||||||
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
|
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
|
||||||
import { ISOToDatetime } from "ChillMainAssets/chill/js/date";
|
import { ISOToDatetime } from "./../../../../../../ChillMainBundle/Resources/public/chill/js/date";
|
||||||
import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue";
|
import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue";
|
||||||
import RestoreVersionButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/RestoreVersionButton.vue";
|
import RestoreVersionButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/RestoreVersionButton.vue";
|
||||||
import DownloadButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DownloadButton.vue";
|
import DownloadButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DownloadButton.vue";
|
||||||
|
@@ -46,16 +46,6 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
|
|||||||
|
|
||||||
public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool
|
public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool
|
||||||
{
|
{
|
||||||
// we first try to get the permission from the workflow, as attachement (this is the less intensive query)
|
|
||||||
$workflowPermissionAsAttachment = match ($attribute) {
|
|
||||||
StoredObjectRoleEnum::SEE => $this->workflowDocumentService->isAllowedByWorkflowForReadOperation($subject),
|
|
||||||
StoredObjectRoleEnum::EDIT => $this->workflowDocumentService->isAllowedByWorkflowForWriteOperation($subject),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (WorkflowRelatedEntityPermissionHelper::FORCE_DENIED === $workflowPermissionAsAttachment) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the related entity
|
// Retrieve the related entity
|
||||||
$entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);
|
$entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);
|
||||||
|
|
||||||
@@ -76,7 +66,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
|
|||||||
return match ($workflowPermission) {
|
return match ($workflowPermission) {
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT => true,
|
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT => true,
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED => false,
|
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED => false,
|
||||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN => WorkflowRelatedEntityPermissionHelper::FORCE_GRANT === $workflowPermissionAsAttachment || $regularPermission,
|
WorkflowRelatedEntityPermissionHelper::ABSTAIN => $regularPermission,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,12 +14,6 @@ namespace Chill\DocStoreBundle\Security\Authorization;
|
|||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface for voting on stored object permissions.
|
|
||||||
*
|
|
||||||
* Each time a stored object is attached to a document, the voter is responsible for determining
|
|
||||||
* whether the user has the necessary permissions to access or modify the stored object.
|
|
||||||
*/
|
|
||||||
interface StoredObjectVoterInterface
|
interface StoredObjectVoterInterface
|
||||||
{
|
{
|
||||||
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool;
|
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool;
|
||||||
|
@@ -15,7 +15,6 @@ use Chill\DocStoreBundle\Entity\StoredObject;
|
|||||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTime;
|
use Chill\DocStoreBundle\Entity\StoredObjectPointInTime;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum;
|
use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||||
use Chill\DocStoreBundle\Exception\ConversionWithSameMimeTypeException;
|
|
||||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||||
use Chill\WopiBundle\Service\WopiConverter;
|
use Chill\WopiBundle\Service\WopiConverter;
|
||||||
use Symfony\Component\Mime\MimeTypesInterface;
|
use Symfony\Component\Mime\MimeTypesInterface;
|
||||||
@@ -42,10 +41,9 @@ class StoredObjectToPdfConverter
|
|||||||
*
|
*
|
||||||
* @return array{0: StoredObjectPointInTime, 1: StoredObjectVersion, 2?: string} contains the point in time before conversion and the new version of the stored object. The converted content is included in the response if $includeConvertedContent is true
|
* @return array{0: StoredObjectPointInTime, 1: StoredObjectVersion, 2?: string} contains the point in time before conversion and the new version of the stored object. The converted content is included in the response if $includeConvertedContent is true
|
||||||
*
|
*
|
||||||
* @throws \UnexpectedValueException if the preferred mime type for the conversion is not found
|
* @throws \UnexpectedValueException if the preferred mime type for the conversion is not found
|
||||||
* @throws \RuntimeException if the conversion or storage of the new version fails
|
* @throws \RuntimeException if the conversion or storage of the new version fails
|
||||||
* @throws StoredObjectManagerException
|
* @throws StoredObjectManagerException
|
||||||
* @throws ConversionWithSameMimeTypeException if the document has already the same mime type79*
|
|
||||||
*/
|
*/
|
||||||
public function addConvertedVersion(StoredObject $storedObject, string $lang, $convertTo = 'pdf', bool $includeConvertedContent = false): array
|
public function addConvertedVersion(StoredObject $storedObject, string $lang, $convertTo = 'pdf', bool $includeConvertedContent = false): array
|
||||||
{
|
{
|
||||||
@@ -58,7 +56,7 @@ class StoredObjectToPdfConverter
|
|||||||
$currentVersion = $storedObject->getCurrentVersion();
|
$currentVersion = $storedObject->getCurrentVersion();
|
||||||
|
|
||||||
if ($currentVersion->getType() === $newMimeType) {
|
if ($currentVersion->getType() === $newMimeType) {
|
||||||
throw new ConversionWithSameMimeTypeException($newMimeType);
|
throw new \UnexpectedValueException('Already at the same mime type');
|
||||||
}
|
}
|
||||||
|
|
||||||
$content = $this->storedObjectManager->read($currentVersion);
|
$content = $this->storedObjectManager->read($currentVersion);
|
||||||
|
@@ -40,10 +40,6 @@ class StoredObjectVersionApiControllerTest extends \PHPUnit\Framework\TestCase
|
|||||||
$storedObject->registerVersion();
|
$storedObject->registerVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove one version in the history
|
|
||||||
$v5 = $storedObject->getVersions()->get(5);
|
|
||||||
$storedObject->removeVersion($v5);
|
|
||||||
|
|
||||||
$security = $this->prophesize(Security::class);
|
$security = $this->prophesize(Security::class);
|
||||||
$security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)
|
$security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)
|
||||||
->willReturn(true)
|
->willReturn(true)
|
||||||
@@ -57,7 +53,6 @@ class StoredObjectVersionApiControllerTest extends \PHPUnit\Framework\TestCase
|
|||||||
self::assertEquals($response->getStatusCode(), 200);
|
self::assertEquals($response->getStatusCode(), 200);
|
||||||
self::assertIsArray($body);
|
self::assertIsArray($body);
|
||||||
self::assertArrayHasKey('results', $body);
|
self::assertArrayHasKey('results', $body);
|
||||||
self::assertIsList($body['results']);
|
|
||||||
self::assertCount(10, $body['results']);
|
self::assertCount(10, $body['results']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -86,165 +86,9 @@ class AbstractStoredObjectVoterTest extends TestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dataProvider dataProviderVoteOnAttributeWithStoredObjectPermission
|
* @dataProvider dataProviderVoteOnAttribute
|
||||||
*/
|
*/
|
||||||
public function testVoteOnAttributeWithStoredObjectPermission(
|
public function testVoteOnAttribute(
|
||||||
StoredObjectRoleEnum $attribute,
|
|
||||||
bool $expected,
|
|
||||||
bool $isGrantedRegularPermission,
|
|
||||||
string $isGrantedWorkflowPermission,
|
|
||||||
string $isGrantedStoredObjectAttachment,
|
|
||||||
): void {
|
|
||||||
$storedObject = new StoredObject();
|
|
||||||
$repository = new DummyRepository($related = new \stdClass());
|
|
||||||
$token = new UsernamePasswordToken(new User(), 'dummy');
|
|
||||||
|
|
||||||
$security = $this->prophesize(Security::class);
|
|
||||||
$security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
|
|
||||||
|
|
||||||
$workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class);
|
|
||||||
|
|
||||||
$security = $this->prophesize(Security::class);
|
|
||||||
$security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
|
|
||||||
|
|
||||||
if (StoredObjectRoleEnum::SEE === $attribute) {
|
|
||||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject)
|
|
||||||
->shouldBeCalled()
|
|
||||||
->willReturn($isGrantedStoredObjectAttachment);
|
|
||||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
|
|
||||||
->willReturn($isGrantedWorkflowPermission);
|
|
||||||
} elseif (StoredObjectRoleEnum::EDIT === $attribute) {
|
|
||||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject)
|
|
||||||
->shouldBeCalled()
|
|
||||||
->willReturn($isGrantedStoredObjectAttachment);
|
|
||||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related)
|
|
||||||
->willReturn($isGrantedWorkflowPermission);
|
|
||||||
} else {
|
|
||||||
throw new \LogicException('Invalid attribute for StoredObjectVoter');
|
|
||||||
}
|
|
||||||
|
|
||||||
$storedObjectVoter = new class ($repository, $workflowRelatedEntityPermissionHelper->reveal(), $security->reveal()) extends AbstractStoredObjectVoter {
|
|
||||||
public function __construct(private $repository, $helper, $security)
|
|
||||||
{
|
|
||||||
parent::__construct($security, $helper);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
|
||||||
{
|
|
||||||
return $this->repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getClass(): string
|
|
||||||
{
|
|
||||||
return \stdClass::class;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
|
|
||||||
{
|
|
||||||
return 'SOME_ROLE';
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function canBeAssociatedWithWorkflow(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$actual = $storedObjectVoter->voteOnAttribute($attribute, $storedObject, $token);
|
|
||||||
|
|
||||||
self::assertEquals($expected, $actual);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function dataProviderVoteOnAttributeWithStoredObjectPermission(): iterable
|
|
||||||
{
|
|
||||||
foreach (['read' => StoredObjectRoleEnum::SEE, 'write' => StoredObjectRoleEnum::EDIT] as $action => $attribute) {
|
|
||||||
yield 'Not related to any workflow nor attachment ('.$action.')' => [
|
|
||||||
$attribute,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
|
||||||
];
|
|
||||||
|
|
||||||
yield 'Not related to any workflow nor attachment (refuse) ('.$action.')' => [
|
|
||||||
$attribute,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
|
||||||
];
|
|
||||||
|
|
||||||
yield 'Is granted by a workflow takes precedence (workflow) ('.$action.')' => [
|
|
||||||
$attribute,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
|
||||||
];
|
|
||||||
|
|
||||||
yield 'Is granted by a workflow takes precedence (stored object) ('.$action.')' => [
|
|
||||||
$attribute,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
|
|
||||||
];
|
|
||||||
|
|
||||||
yield 'Is granted by a workflow takes precedence (workflow) although grant ('.$action.')' => [
|
|
||||||
$attribute,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
|
||||||
];
|
|
||||||
|
|
||||||
yield 'Is granted by a workflow takes precedence (stored object) although grant ('.$action.')' => [
|
|
||||||
$attribute,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
|
|
||||||
];
|
|
||||||
|
|
||||||
yield 'Is granted by a workflow takes precedence (initially refused) (workflow) although grant ('.$action.')' => [
|
|
||||||
$attribute,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
|
||||||
];
|
|
||||||
|
|
||||||
yield 'Is granted by a workflow takes precedence (initially refused) (stored object) although grant ('.$action.')' => [
|
|
||||||
$attribute,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED,
|
|
||||||
];
|
|
||||||
|
|
||||||
yield 'Force grant inverse the regular permission (workflow) ('.$action.')' => [
|
|
||||||
$attribute,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
|
||||||
];
|
|
||||||
|
|
||||||
yield 'Force grant inverse the regular permission (so) ('.$action.')' => [
|
|
||||||
$attribute,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN,
|
|
||||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
|
||||||
];
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider dataProviderVoteOnAttributeWithoutStoredObjectPermission
|
|
||||||
*/
|
|
||||||
public function testVoteOnAttributeWithoutStoredObjectPermission(
|
|
||||||
StoredObjectRoleEnum $attribute,
|
StoredObjectRoleEnum $attribute,
|
||||||
bool $expected,
|
bool $expected,
|
||||||
bool $canBeAssociatedWithWorkflow,
|
bool $canBeAssociatedWithWorkflow,
|
||||||
@@ -261,10 +105,6 @@ class AbstractStoredObjectVoterTest extends TestCase
|
|||||||
$security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
|
$security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
|
||||||
|
|
||||||
$workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class);
|
$workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class);
|
||||||
|
|
||||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN);
|
|
||||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN);
|
|
||||||
|
|
||||||
if (null !== $isGrantedWorkflowPermissionRead) {
|
if (null !== $isGrantedWorkflowPermissionRead) {
|
||||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
|
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
|
||||||
->willReturn($isGrantedWorkflowPermissionRead)->shouldBeCalled();
|
->willReturn($isGrantedWorkflowPermissionRead)->shouldBeCalled();
|
||||||
@@ -283,7 +123,7 @@ class AbstractStoredObjectVoterTest extends TestCase
|
|||||||
self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message);
|
self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function dataProviderVoteOnAttributeWithoutStoredObjectPermission(): iterable
|
public static function dataProviderVoteOnAttribute(): iterable
|
||||||
{
|
{
|
||||||
// not associated on a workflow
|
// not associated on a workflow
|
||||||
yield [StoredObjectRoleEnum::SEE, true, false, true, null, null, 'not associated on a workflow, granted by regular access, must not rely on helper'];
|
yield [StoredObjectRoleEnum::SEE, true, false, true, null, null, 'not associated on a workflow, granted by regular access, must not rely on helper'];
|
||||||
|
@@ -246,7 +246,7 @@ final class EventController extends AbstractController
|
|||||||
'class' => Center::class,
|
'class' => Center::class,
|
||||||
'choices' => $centers,
|
'choices' => $centers,
|
||||||
'placeholder' => $this->translator->trans('Pick a center'),
|
'placeholder' => $this->translator->trans('Pick a center'),
|
||||||
'label' => 'To which territory should the event be associated ?',
|
'label' => 'To which centre should the event be associated ?',
|
||||||
])
|
])
|
||||||
->add('submit', SubmitType::class, [
|
->add('submit', SubmitType::class, [
|
||||||
'label' => 'Next step',
|
'label' => 'Next step',
|
||||||
|
@@ -64,7 +64,7 @@ CHILL_EVENT_PARTICIPATION_SEE_DETAILS: Voir le détail d'une participation
|
|||||||
|
|
||||||
# TODO check place to put this
|
# TODO check place to put this
|
||||||
Next step: Étape suivante
|
Next step: Étape suivante
|
||||||
To which territory should the event be associated ?: À quel territoire doit être associé l'événement ?
|
To which centre should the event be associated ?: À quel centre doit être associé l'événement ?
|
||||||
|
|
||||||
# timeline
|
# timeline
|
||||||
past: passé
|
past: passé
|
||||||
@@ -151,7 +151,7 @@ event:
|
|||||||
filter:
|
filter:
|
||||||
event_types: Par types d'événement
|
event_types: Par types d'événement
|
||||||
event_dates: Par date d'événement
|
event_dates: Par date d'événement
|
||||||
center: Par territoire
|
center: Par centre
|
||||||
by_responsable: Par responsable
|
by_responsable: Par responsable
|
||||||
pick_responsable: Filtrer par responsables
|
pick_responsable: Filtrer par responsables
|
||||||
budget:
|
budget:
|
||||||
@@ -188,7 +188,7 @@ event_id: Identifiant
|
|||||||
event_name: Nom
|
event_name: Nom
|
||||||
event_date: Date
|
event_date: Date
|
||||||
event_type: Type d'évenement
|
event_type: Type d'évenement
|
||||||
event_center: Territoire
|
event_center: Centre
|
||||||
event_moderator: Responsable
|
event_moderator: Responsable
|
||||||
event_participants_count: Nombre de participants
|
event_participants_count: Nombre de participants
|
||||||
event_location: Localisation
|
event_location: Localisation
|
||||||
|
@@ -11,7 +11,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\MainBundle\Controller;
|
namespace Chill\MainBundle\Controller;
|
||||||
|
|
||||||
use Chill\MainBundle\CRUD\Controller\ApiController;
|
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||||
@@ -28,7 +27,7 @@ use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
|||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
use Symfony\Component\Serializer\SerializerInterface;
|
use Symfony\Component\Serializer\SerializerInterface;
|
||||||
|
|
||||||
class WorkflowApiController extends ApiController
|
class WorkflowApiController
|
||||||
{
|
{
|
||||||
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly EntityWorkflowRepository $entityWorkflowRepository, private readonly PaginatorFactory $paginatorFactory, private readonly Security $security, private readonly SerializerInterface $serializer) {}
|
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly EntityWorkflowRepository $entityWorkflowRepository, private readonly PaginatorFactory $paginatorFactory, private readonly Security $security, private readonly SerializerInterface $serializer) {}
|
||||||
|
|
||||||
|
@@ -44,7 +44,7 @@ final readonly class WorkflowSignatureStateChangeController
|
|||||||
$signature,
|
$signature,
|
||||||
$request,
|
$request,
|
||||||
EntityWorkflowStepSignatureVoter::CANCEL,
|
EntityWorkflowStepSignatureVoter::CANCEL,
|
||||||
fn (EntityWorkflowStepSignature $signature): string => $this->signatureStepStateChanger->markSignatureAsCanceled($signature),
|
function (EntityWorkflowStepSignature $signature) {$this->signatureStepStateChanger->markSignatureAsCanceled($signature); },
|
||||||
'@ChillMain/WorkflowSignature/cancel.html.twig',
|
'@ChillMain/WorkflowSignature/cancel.html.twig',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -56,18 +56,11 @@ final readonly class WorkflowSignatureStateChangeController
|
|||||||
$signature,
|
$signature,
|
||||||
$request,
|
$request,
|
||||||
EntityWorkflowStepSignatureVoter::REJECT,
|
EntityWorkflowStepSignatureVoter::REJECT,
|
||||||
fn (EntityWorkflowStepSignature $signature): string => $this->signatureStepStateChanger->markSignatureAsRejected($signature),
|
function (EntityWorkflowStepSignature $signature) {$this->signatureStepStateChanger->markSignatureAsRejected($signature); },
|
||||||
'@ChillMain/WorkflowSignature/reject.html.twig',
|
'@ChillMain/WorkflowSignature/reject.html.twig',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param callable(EntityWorkflowStepSignature): string $markSignature
|
|
||||||
*
|
|
||||||
* @throws \Twig\Error\LoaderError
|
|
||||||
* @throws \Twig\Error\RuntimeError
|
|
||||||
* @throws \Twig\Error\SyntaxError
|
|
||||||
*/
|
|
||||||
private function markSignatureAction(
|
private function markSignatureAction(
|
||||||
EntityWorkflowStepSignature $signature,
|
EntityWorkflowStepSignature $signature,
|
||||||
Request $request,
|
Request $request,
|
||||||
@@ -86,13 +79,12 @@ final readonly class WorkflowSignatureStateChangeController
|
|||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
$expectedStep = $this->entityManager->wrapInTransaction(fn () => $markSignature($signature));
|
$this->entityManager->wrapInTransaction(function () use ($signature, $markSignature) {
|
||||||
|
$markSignature($signature);
|
||||||
|
});
|
||||||
|
|
||||||
return new RedirectResponse(
|
return new RedirectResponse(
|
||||||
$this->chillUrlGenerator->forwardReturnPath(
|
$this->chillUrlGenerator->returnPathOr('chill_main_workflow_show', ['id' => $signature->getStep()->getEntityWorkflow()->getId()])
|
||||||
'chill_main_workflow_wait',
|
|
||||||
['id' => $signature->getStep()->getEntityWorkflow()->getId(), 'expectedStep' => $expectedStep]
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,41 +0,0 @@
|
|||||||
<?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\MainBundle\Controller;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
|
||||||
use Chill\MainBundle\Routing\ChillUrlGeneratorInterface;
|
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
|
||||||
use Twig\Environment;
|
|
||||||
|
|
||||||
final readonly class WorkflowWaitStepChangeController
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private ChillUrlGeneratorInterface $chillUrlGenerator,
|
|
||||||
private Environment $twig,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
#[Route('/{_locale}/main/workflow/{id}/wait/{expectedStep}', name: 'chill_main_workflow_wait', methods: ['GET'])]
|
|
||||||
public function waitForSignatureChange(EntityWorkflow $entityWorkflow, string $expectedStep): Response
|
|
||||||
{
|
|
||||||
if ($entityWorkflow->getStep() === $expectedStep) {
|
|
||||||
return new RedirectResponse(
|
|
||||||
$this->chillUrlGenerator->returnPathOr('chill_main_workflow_show', ['id' => $entityWorkflow->getId()])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response(
|
|
||||||
$this->twig->render('@ChillMain/Workflow/waiting.html.twig', ['workflow' => $entityWorkflow, 'expectedStep' => $expectedStep])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -30,7 +30,6 @@ use Chill\MainBundle\Controller\UserGroupAdminController;
|
|||||||
use Chill\MainBundle\Controller\UserGroupApiController;
|
use Chill\MainBundle\Controller\UserGroupApiController;
|
||||||
use Chill\MainBundle\Controller\UserJobApiController;
|
use Chill\MainBundle\Controller\UserJobApiController;
|
||||||
use Chill\MainBundle\Controller\UserJobController;
|
use Chill\MainBundle\Controller\UserJobController;
|
||||||
use Chill\MainBundle\Controller\WorkflowApiController;
|
|
||||||
use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface;
|
use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface;
|
||||||
use Chill\MainBundle\Doctrine\DQL\Age;
|
use Chill\MainBundle\Doctrine\DQL\Age;
|
||||||
use Chill\MainBundle\Doctrine\DQL\Extract;
|
use Chill\MainBundle\Doctrine\DQL\Extract;
|
||||||
@@ -67,7 +66,6 @@ use Chill\MainBundle\Entity\Regroupment;
|
|||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Entity\UserGroup;
|
use Chill\MainBundle\Entity\UserGroup;
|
||||||
use Chill\MainBundle\Entity\UserJob;
|
use Chill\MainBundle\Entity\UserJob;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
|
||||||
use Chill\MainBundle\Form\CenterType;
|
use Chill\MainBundle\Form\CenterType;
|
||||||
use Chill\MainBundle\Form\CivilityType;
|
use Chill\MainBundle\Form\CivilityType;
|
||||||
use Chill\MainBundle\Form\CountryType;
|
use Chill\MainBundle\Form\CountryType;
|
||||||
@@ -81,7 +79,6 @@ use Chill\MainBundle\Form\UserGroupType;
|
|||||||
use Chill\MainBundle\Form\UserJobType;
|
use Chill\MainBundle\Form\UserJobType;
|
||||||
use Chill\MainBundle\Form\UserType;
|
use Chill\MainBundle\Form\UserType;
|
||||||
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
|
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
|
||||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
|
|
||||||
use Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType;
|
use Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType;
|
||||||
use Ramsey\Uuid\Doctrine\UuidType;
|
use Ramsey\Uuid\Doctrine\UuidType;
|
||||||
use Symfony\Component\Config\FileLocator;
|
use Symfony\Component\Config\FileLocator;
|
||||||
@@ -943,21 +940,6 @@ class ChillMainExtension extends Extension implements
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
|
||||||
'class' => EntityWorkflow::class,
|
|
||||||
'name' => 'workflow',
|
|
||||||
'base_path' => '/api/1.0/main/workflow',
|
|
||||||
'base_role' => EntityWorkflowVoter::SEE,
|
|
||||||
'controller' => WorkflowApiController::class,
|
|
||||||
'actions' => [
|
|
||||||
'_entity' => [
|
|
||||||
'methods' => [
|
|
||||||
Request::METHOD_GET => true,
|
|
||||||
Request::METHOD_HEAD => true,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +0,0 @@
|
|||||||
/**
|
|
||||||
* Extracts the "returnPath" parameter from the current URL's query string and returns it.
|
|
||||||
* If the parameter is not present, returns the provided fallback path.
|
|
||||||
*
|
|
||||||
* @param {string} fallbackPath - The fallback path to use if "returnPath" is not found in the query string.
|
|
||||||
* @return {string} The "returnPath" from the query string, or the fallback path if "returnPath" is not present.
|
|
||||||
*/
|
|
||||||
export function returnPathOr(fallbackPath: string): string {
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const returnPath = urlParams.get("returnPath");
|
|
||||||
|
|
||||||
return returnPath ?? fallbackPath;
|
|
||||||
}
|
|
@@ -1,16 +0,0 @@
|
|||||||
import { EntityWorkflow } from "ChillMainAssets/types";
|
|
||||||
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
|
|
||||||
|
|
||||||
export const fetchWorkflow = async (
|
|
||||||
workflowId: number,
|
|
||||||
): Promise<EntityWorkflow> => {
|
|
||||||
try {
|
|
||||||
return await makeFetch<null, EntityWorkflow>(
|
|
||||||
"GET",
|
|
||||||
`/api/1.0/main/workflow/${workflowId}.json`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to fetch workflow ${workflowId}:`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
@@ -1,6 +1,5 @@
|
|||||||
import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
|
import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
|
||||||
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
|
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
|
||||||
import { Person } from "../../../ChillPersonBundle/Resources/public/types";
|
|
||||||
|
|
||||||
export interface DateTime {
|
export interface DateTime {
|
||||||
datetime: string;
|
datetime: string;
|
||||||
@@ -203,58 +202,6 @@ export interface WorkflowAttachment {
|
|||||||
genericDoc: null | GenericDoc;
|
genericDoc: null | GenericDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Workflow {
|
|
||||||
name: string;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EntityWorkflowStep {
|
|
||||||
type: "entity_workflow_step";
|
|
||||||
id: number;
|
|
||||||
comment: string;
|
|
||||||
currentStep: StepDefinition;
|
|
||||||
isFinal: boolean;
|
|
||||||
isFreezed: boolean;
|
|
||||||
isFinalized: boolean;
|
|
||||||
transitionPrevious: Transition | null;
|
|
||||||
transitionAfter: Transition | null;
|
|
||||||
previousId: number | null;
|
|
||||||
nextId: number | null;
|
|
||||||
transitionPreviousBy: User | null;
|
|
||||||
transitionPreviousAt: DateTime | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Transition {
|
|
||||||
name: string;
|
|
||||||
text: string;
|
|
||||||
isForward: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StepDefinition {
|
|
||||||
name: string;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EntityWorkflow {
|
|
||||||
type: "entity_workflow";
|
|
||||||
id: number;
|
|
||||||
relatedEntityClass: string;
|
|
||||||
relatedEntityId: number;
|
|
||||||
workflow: Workflow;
|
|
||||||
currentStep: EntityWorkflowStep;
|
|
||||||
steps: EntityWorkflowStep[];
|
|
||||||
datas: WorkflowData;
|
|
||||||
title: string;
|
|
||||||
isOnHoldAtCurrentStep: boolean;
|
|
||||||
_permissions: {
|
|
||||||
CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkflowData {
|
|
||||||
persons: Person[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ExportGeneration {
|
export interface ExportGeneration {
|
||||||
id: string;
|
id: string;
|
||||||
type: "export_generation";
|
type: "export_generation";
|
||||||
@@ -268,8 +215,3 @@ export interface ExportGeneration {
|
|||||||
export interface PrivateCommentEmbeddable {
|
export interface PrivateCommentEmbeddable {
|
||||||
comments: Record<number, string>;
|
comments: Record<number, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Possible states for the WaitingScreen Component.
|
|
||||||
*/
|
|
||||||
export type WaitingScreenState = "pending" | "failure" | "stopped" | "ready";
|
|
||||||
|
@@ -10,8 +10,7 @@ import { computed, onMounted, ref } from "vue";
|
|||||||
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
|
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
|
||||||
import { fetchExportGenerationStatus } from "ChillMainAssets/lib/api/export";
|
import { fetchExportGenerationStatus } from "ChillMainAssets/lib/api/export";
|
||||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
|
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
|
||||||
import WaitingScreen from "../_components/WaitingScreen.vue";
|
import { ExportGeneration } from "ChillMainAssets/types";
|
||||||
import { ExportGeneration, WaitingScreenState } from "ChillMainAssets/types";
|
|
||||||
|
|
||||||
interface AppProps {
|
interface AppProps {
|
||||||
exportGenerationId: string;
|
exportGenerationId: string;
|
||||||
@@ -35,16 +34,13 @@ const storedObject = computed<null | StoredObject>(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isPending = computed<boolean>(() => status.value === "pending");
|
const isPending = computed<boolean>(() => status.value === "pending");
|
||||||
|
const isFetching = computed<boolean>(
|
||||||
|
() => tryiesForReady.value < maxTryiesForReady,
|
||||||
|
);
|
||||||
|
const isReady = computed<boolean>(() => status.value === "ready");
|
||||||
|
const isFailure = computed<boolean>(() => status.value === "failure");
|
||||||
const filename = computed<string>(() => `${props.title}-${props.createdDate}`);
|
const filename = computed<string>(() => `${props.title}-${props.createdDate}`);
|
||||||
|
|
||||||
const state = computed<WaitingScreenState>((): WaitingScreenState => {
|
|
||||||
if (status.value === "empty") {
|
|
||||||
return "pending";
|
|
||||||
}
|
|
||||||
|
|
||||||
return status.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* counter for the number of times that we check for a new status
|
* counter for the number of times that we check for a new status
|
||||||
*/
|
*/
|
||||||
@@ -89,36 +85,57 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<WaitingScreen :state="state">
|
<div id="waiting-screen">
|
||||||
<template v-slot:pending>
|
<div
|
||||||
<p>
|
v-if="isPending && isFetching"
|
||||||
{{ trans(EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING) }}
|
class="alert alert-danger text-center"
|
||||||
</p>
|
>
|
||||||
</template>
|
<div>
|
||||||
|
<p>
|
||||||
|
{{ trans(EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<template v-slot:stopped>
|
<div>
|
||||||
<p>
|
<i class="fa fa-cog fa-spin fa-3x fa-fw"></i>
|
||||||
{{ trans(EXPORT_GENERATION_TOO_MANY_RETRIES) }}
|
<span class="sr-only">Loading...</span>
|
||||||
</p>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
|
<div v-if="isPending && !isFetching" class="alert alert-info">
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{{ trans(EXPORT_GENERATION_TOO_MANY_RETRIES) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="isFailure" class="alert alert-danger text-center">
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{{ trans(EXPORT_GENERATION_ERROR_WHILE_GENERATING_EXPORT) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="isReady" class="alert alert-success text-center">
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{{ trans(EXPORT_GENERATION_EXPORT_READY) }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<template v-slot:failure>
|
<p v-if="storedObject !== null">
|
||||||
<p>
|
<document-action-buttons-group
|
||||||
{{ trans(EXPORT_GENERATION_ERROR_WHILE_GENERATING_EXPORT) }}
|
:stored-object="storedObject"
|
||||||
</p>
|
:filename="filename"
|
||||||
</template>
|
></document-action-buttons-group>
|
||||||
|
</p>
|
||||||
<template v-slot:ready>
|
</div>
|
||||||
<p>
|
</div>
|
||||||
{{ trans(EXPORT_GENERATION_EXPORT_READY) }}
|
</div>
|
||||||
</p>
|
|
||||||
|
|
||||||
<p v-if="storedObject !== null">
|
|
||||||
<document-action-buttons-group
|
|
||||||
:stored-object="storedObject"
|
|
||||||
:filename="filename"
|
|
||||||
></document-action-buttons-group>
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
</WaitingScreen>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
#waiting-screen {
|
||||||
|
> .alert {
|
||||||
|
min-height: 350px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@@ -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";
|
||||||
}
|
}
|
||||||
|
@@ -1,75 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { useIntervalFn } from "@vueuse/core";
|
|
||||||
import { fetchWorkflow } from "ChillMainAssets/lib/workflow/api";
|
|
||||||
import { returnPathOr } from "ChillMainAssets/lib/return_path/returnPathHelper";
|
|
||||||
import { ref } from "vue";
|
|
||||||
import WaitingScreen from "ChillMainAssets/vuejs/_components/WaitingScreen.vue";
|
|
||||||
import { WaitingScreenState } from "ChillMainAssets/types";
|
|
||||||
import {
|
|
||||||
trans,
|
|
||||||
WORKFLOW_WAIT_TITLE,
|
|
||||||
WORKFLOW_WAIT_ERROR_WHILE_WAITING,
|
|
||||||
WORKFLOW_WAIT_SUCCESS,
|
|
||||||
} from "translator";
|
|
||||||
|
|
||||||
interface WaitPostProcessWorkflowComponentProps {
|
|
||||||
workflowId: number;
|
|
||||||
expectedStep: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<WaitPostProcessWorkflowComponentProps>();
|
|
||||||
const counter = ref<number>(0);
|
|
||||||
const MAX_TRYIES = 50;
|
|
||||||
|
|
||||||
const state = ref<WaitingScreenState>("pending");
|
|
||||||
|
|
||||||
const { pause, resume } = useIntervalFn(
|
|
||||||
async () => {
|
|
||||||
try {
|
|
||||||
const workflow = await fetchWorkflow(props.workflowId);
|
|
||||||
counter.value++;
|
|
||||||
if (workflow.currentStep.currentStep.name === props.expectedStep) {
|
|
||||||
window.location.assign(
|
|
||||||
returnPathOr("/fr/main/workflow" + workflow.id + "/show"),
|
|
||||||
);
|
|
||||||
resume();
|
|
||||||
state.value = "ready";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (counter.value > MAX_TRYIES) {
|
|
||||||
pause();
|
|
||||||
state.value = "failure";
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
pause();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
2000,
|
|
||||||
{ immediate: true },
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="container">
|
|
||||||
<WaitingScreen :state="state">
|
|
||||||
<template v-slot:pending>
|
|
||||||
<p>
|
|
||||||
{{ trans(WORKFLOW_WAIT_TITLE) }}
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
<template v-slot:failure>
|
|
||||||
<p>
|
|
||||||
{{ trans(WORKFLOW_WAIT_ERROR_WHILE_WAITING) }}
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
<template v-slot:ready>
|
|
||||||
<p>
|
|
||||||
{{ trans(WORKFLOW_WAIT_SUCCESS) }}
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
</WaitingScreen>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
|
@@ -1,51 +0,0 @@
|
|||||||
import { createApp } from "vue";
|
|
||||||
import App from "./App.vue";
|
|
||||||
|
|
||||||
function mountApp(): void {
|
|
||||||
const el = document.querySelector<HTMLDivElement>(".screen-wait");
|
|
||||||
if (!el) {
|
|
||||||
console.error(
|
|
||||||
"WaitPostProcessWorkflow: mount element .screen-wait not found",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const workflowIdAttr = el.getAttribute("data-workflow-id");
|
|
||||||
const expectedStep = el.getAttribute("data-expected-step") || "";
|
|
||||||
|
|
||||||
if (!workflowIdAttr) {
|
|
||||||
console.error(
|
|
||||||
"WaitPostProcessWorkflow: data-workflow-id attribute missing on mount element",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!expectedStep) {
|
|
||||||
console.error(
|
|
||||||
"WaitPostProcessWorkflow: data-expected-step attribute missing on mount element",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const workflowId = Number(workflowIdAttr);
|
|
||||||
if (Number.isNaN(workflowId)) {
|
|
||||||
console.error(
|
|
||||||
"WaitPostProcessWorkflow: data-workflow-id is not a valid number:",
|
|
||||||
workflowIdAttr,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = createApp(App, {
|
|
||||||
workflowId,
|
|
||||||
expectedStep,
|
|
||||||
});
|
|
||||||
|
|
||||||
app.mount(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.readyState === "loading") {
|
|
||||||
document.addEventListener("DOMContentLoaded", mountApp);
|
|
||||||
} else {
|
|
||||||
mountApp();
|
|
||||||
}
|
|
@@ -1,11 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref, useTemplateRef } from "vue";
|
import { computed, useTemplateRef } from "vue";
|
||||||
import type { EntityWorkflow, WorkflowAttachment } from "ChillMainAssets/types";
|
import type { WorkflowAttachment } from "ChillMainAssets/types";
|
||||||
import PickGenericDocModal from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDocModal.vue";
|
import PickGenericDocModal from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDocModal.vue";
|
||||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
|
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
|
||||||
import AttachmentList from "ChillMainAssets/vuejs/WorkflowAttachment/Component/AttachmentList.vue";
|
import AttachmentList from "ChillMainAssets/vuejs/WorkflowAttachment/Component/AttachmentList.vue";
|
||||||
import { GenericDoc } from "ChillDocStoreAssets/types";
|
import { GenericDoc } from "ChillDocStoreAssets/types";
|
||||||
import { fetchWorkflow } from "ChillMainAssets/lib/workflow/api";
|
|
||||||
|
|
||||||
interface AppConfig {
|
interface AppConfig {
|
||||||
workflowId: number;
|
workflowId: number;
|
||||||
@@ -35,13 +34,6 @@ const attachedGenericDoc = computed<GenericDocForAccompanyingPeriod[]>(
|
|||||||
) as GenericDocForAccompanyingPeriod[],
|
) as GenericDocForAccompanyingPeriod[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const workflow = ref<EntityWorkflow | null>(null);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
workflow.value = await fetchWorkflow(Number(props.workflowId));
|
|
||||||
console.log("workflow", workflow.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const openModal = function () {
|
const openModal = function () {
|
||||||
pickDocModal.value?.openModal();
|
pickDocModal.value?.openModal();
|
||||||
};
|
};
|
||||||
@@ -57,30 +49,20 @@ const onPickGenericDoc = ({
|
|||||||
const onRemoveAttachment = (payload: { attachment: WorkflowAttachment }) => {
|
const onRemoveAttachment = (payload: { attachment: WorkflowAttachment }) => {
|
||||||
emit("removeAttachment", payload);
|
emit("removeAttachment", payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
const canEditAttachement = computed<boolean>(() => {
|
|
||||||
if (null === workflow.value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return workflow.value._permissions.CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<pick-generic-doc-modal
|
<pick-generic-doc-modal
|
||||||
:workflow="workflow"
|
|
||||||
:accompanying-period-id="props.accompanyingPeriodId"
|
:accompanying-period-id="props.accompanyingPeriodId"
|
||||||
:to-remove="attachedGenericDoc"
|
:to-remove="attachedGenericDoc"
|
||||||
ref="pickDocModal"
|
ref="pickDocModal"
|
||||||
@pickGenericDoc="onPickGenericDoc"
|
@pickGenericDoc="onPickGenericDoc"
|
||||||
></pick-generic-doc-modal>
|
></pick-generic-doc-modal>
|
||||||
<attachment-list
|
<attachment-list
|
||||||
:workflow="workflow"
|
|
||||||
:attachments="props.attachments"
|
:attachments="props.attachments"
|
||||||
@removeAttachment="onRemoveAttachment"
|
@removeAttachment="onRemoveAttachment"
|
||||||
></attachment-list>
|
></attachment-list>
|
||||||
<ul v-if="canEditAttachement" class="record_actions">
|
<ul class="record_actions">
|
||||||
<li>
|
<li>
|
||||||
<button type="button" class="btn btn-create" @click="openModal">
|
<button type="button" class="btn btn-create" @click="openModal">
|
||||||
Ajouter une pièce jointe
|
Ajouter une pièce jointe
|
||||||
|
@@ -1,11 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { EntityWorkflow, WorkflowAttachment } from "ChillMainAssets/types";
|
import { WorkflowAttachment } from "ChillMainAssets/types";
|
||||||
import GenericDocItemBox from "ChillMainAssets/vuejs/WorkflowAttachment/Component/GenericDocItemBox.vue";
|
import GenericDocItemBox from "ChillMainAssets/vuejs/WorkflowAttachment/Component/GenericDocItemBox.vue";
|
||||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
|
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
|
||||||
|
|
||||||
interface AttachmentListProps {
|
interface AttachmentListProps {
|
||||||
attachments: WorkflowAttachment[];
|
attachments: WorkflowAttachment[];
|
||||||
workflow: EntityWorkflow | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -37,12 +36,7 @@ const props = defineProps<AttachmentListProps>();
|
|||||||
:stored-object="a.genericDoc.storedObject"
|
:stored-object="a.genericDoc.storedObject"
|
||||||
></document-action-buttons-group>
|
></document-action-buttons-group>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li>
|
||||||
v-if="
|
|
||||||
!workflow?._permissions
|
|
||||||
.CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-delete"
|
class="btn btn-delete"
|
||||||
|
@@ -6,10 +6,8 @@ import {
|
|||||||
import PickGenericDocItem from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDocItem.vue";
|
import PickGenericDocItem from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDocItem.vue";
|
||||||
import { fetch_generic_docs_by_accompanying_period } from "ChillDocStoreAssets/js/generic-doc-api";
|
import { fetch_generic_docs_by_accompanying_period } from "ChillDocStoreAssets/js/generic-doc-api";
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import { EntityWorkflow } from "ChillMainAssets/types";
|
|
||||||
|
|
||||||
interface PickGenericDocProps {
|
interface PickGenericDocProps {
|
||||||
workflow: EntityWorkflow | null;
|
|
||||||
accompanyingPeriodId: number;
|
accompanyingPeriodId: number;
|
||||||
pickedList: GenericDocForAccompanyingPeriod[];
|
pickedList: GenericDocForAccompanyingPeriod[];
|
||||||
toRemove: GenericDocForAccompanyingPeriod[];
|
toRemove: GenericDocForAccompanyingPeriod[];
|
||||||
@@ -38,21 +36,9 @@ const isPicked = (genericDoc: GenericDocForAccompanyingPeriod): boolean =>
|
|||||||
) !== -1;
|
) !== -1;
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const fetchedGenericDocs = await fetch_generic_docs_by_accompanying_period(
|
genericDocs.value = await fetch_generic_docs_by_accompanying_period(
|
||||||
props.accompanyingPeriodId,
|
props.accompanyingPeriodId,
|
||||||
);
|
);
|
||||||
const documentClasses = [
|
|
||||||
"Chill\\DocStoreBundle\\Entity\\AccompanyingCourseDocument",
|
|
||||||
"Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument",
|
|
||||||
"Chill\\DocStoreBundle\\Entity\\PersonDocument",
|
|
||||||
];
|
|
||||||
|
|
||||||
genericDocs.value = fetchedGenericDocs.filter(
|
|
||||||
(doc) =>
|
|
||||||
!documentClasses.includes(
|
|
||||||
props.workflow?.relatedEntityClass || "",
|
|
||||||
) || props.workflow?.relatedEntityId !== doc.identifiers.id,
|
|
||||||
);
|
|
||||||
loaded.value = true;
|
loaded.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -3,10 +3,8 @@ import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
|||||||
import { computed, ref, useTemplateRef } from "vue";
|
import { computed, ref, useTemplateRef } from "vue";
|
||||||
import PickGenericDoc from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDoc.vue";
|
import PickGenericDoc from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDoc.vue";
|
||||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
|
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
|
||||||
import { EntityWorkflow } from "ChillMainAssets/types";
|
|
||||||
|
|
||||||
interface PickGenericDocModalProps {
|
interface PickGenericDocModalProps {
|
||||||
workflow: EntityWorkflow | null;
|
|
||||||
accompanyingPeriodId: number;
|
accompanyingPeriodId: number;
|
||||||
toRemove: GenericDocForAccompanyingPeriod[];
|
toRemove: GenericDocForAccompanyingPeriod[];
|
||||||
}
|
}
|
||||||
@@ -82,7 +80,6 @@ defineExpose({ openModal, closeModal });
|
|||||||
</template>
|
</template>
|
||||||
<template v-slot:body>
|
<template v-slot:body>
|
||||||
<pick-generic-doc
|
<pick-generic-doc
|
||||||
:workflow="props.workflow"
|
|
||||||
:accompanying-period-id="props.accompanyingPeriodId"
|
:accompanying-period-id="props.accompanyingPeriodId"
|
||||||
:to-remove="props.toRemove"
|
:to-remove="props.toRemove"
|
||||||
:picked-list="pickeds"
|
:picked-list="pickeds"
|
||||||
|
@@ -1,62 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { WaitingScreenState } from "ChillMainAssets/types";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
state: WaitingScreenState;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div id="waiting-screen">
|
|
||||||
<div
|
|
||||||
v-if="props.state === 'pending' && !!$slots.pending"
|
|
||||||
class="alert alert-danger text-center"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<slot name="pending"></slot>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<i class="fa fa-cog fa-spin fa-3x fa-fw"></i>
|
|
||||||
<span class="sr-only">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="props.state === 'stopped' && !!$slots.stopped"
|
|
||||||
class="alert alert-info"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<slot name="stopped"></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="props.state === 'failure' && !!$slots.failure"
|
|
||||||
class="alert alert-danger text-center"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<slot name="failure"></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="props.state === 'ready' && !!$slots.ready"
|
|
||||||
class="alert alert-success text-center"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<slot name="ready"></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
#waiting-screen {
|
|
||||||
> .alert {
|
|
||||||
min-height: 350px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@@ -21,6 +21,8 @@
|
|||||||
{{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
|
{{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
|
||||||
{{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
|
{{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
|
||||||
|
|
||||||
|
{{ form_row(form.addressesEmails) }}
|
||||||
|
|
||||||
{% include handler.template(notification) with handler.templateData(notification) %}
|
{% include handler.template(notification) with handler.templateData(notification) %}
|
||||||
|
|
||||||
<div class="mb-3 row">
|
<div class="mb-3 row">
|
||||||
|
@@ -61,7 +61,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="dt">service/territoire:</span>
|
<span class="dt">cercle/centre:</span>
|
||||||
{% if entity.mainScope %}
|
{% if entity.mainScope %}
|
||||||
{{ entity.mainScope.name|localize_translatable_string }}
|
{{ entity.mainScope.name|localize_translatable_string }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@@ -58,14 +58,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{% if signatures|length > 0 %}
|
|
||||||
<section class="step my-4">{% include '@ChillMain/Workflow/_signature.html.twig' %}</section>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<section class="step my-4">{% include '@ChillMain/Workflow/_attachment.html.twig' %}</section>
|
<section class="step my-4">{% include '@ChillMain/Workflow/_attachment.html.twig' %}</section>
|
||||||
|
|
||||||
<section class="step my-4">{% include '@ChillMain/Workflow/_follow.html.twig' %}</section>
|
<section class="step my-4">{% include '@ChillMain/Workflow/_follow.html.twig' %}</section>
|
||||||
{% if entity_workflow.currentStep.sends|length > 0 %}
|
{% if signatures|length > 0 %}
|
||||||
|
<section class="step my-4">{% include '@ChillMain/Workflow/_signature.html.twig' %}</section>
|
||||||
|
{% elseif entity_workflow.currentStep.sends|length > 0 %}
|
||||||
<section class="step my-4">
|
<section class="step my-4">
|
||||||
<h2>{{ 'workflow.external_views.title'|trans({'numberOfSends': entity_workflow.currentStep.sends|length }) }}</h2>
|
<h2>{{ 'workflow.external_views.title'|trans({'numberOfSends': entity_workflow.currentStep.sends|length }) }}</h2>
|
||||||
{% include '@ChillMain/Workflow/_send_views_list.html.twig' with {'sends': entity_workflow.currentStep.sends} %}
|
{% include '@ChillMain/Workflow/_send_views_list.html.twig' with {'sends': entity_workflow.currentStep.sends} %}
|
||||||
|
@@ -1,18 +0,0 @@
|
|||||||
{% extends '@ChillMain/layout.html.twig' %}
|
|
||||||
|
|
||||||
{% block title %}{{ 'workflow.signature.waiting_for'|trans }}{% endblock %}
|
|
||||||
|
|
||||||
{% block css %}
|
|
||||||
{{ encore_entry_link_tags('page_workflow_waiting_post_process') }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block js %}
|
|
||||||
{{ encore_entry_script_tags('page_workflow_waiting_post_process') }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<h1>{{ block('title') }}</h1>
|
|
||||||
|
|
||||||
<div class="screen-wait" data-workflow-id="{{ workflow.id|e('html_attr') }}" data-expected-step="{{ expectedStep|e('html_attr') }}"></div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@@ -49,7 +49,7 @@ class AdminUserMenuBuilder implements LocalMenuBuilderInterface
|
|||||||
'route' => 'chill_crud_center_index',
|
'route' => 'chill_crud_center_index',
|
||||||
])->setExtras(['order' => 1010]);
|
])->setExtras(['order' => 1010]);
|
||||||
|
|
||||||
$menu->addChild('Regroupements des territoires', [
|
$menu->addChild('Regroupements des centres', [
|
||||||
'route' => 'chill_crud_regroupment_index',
|
'route' => 'chill_crud_regroupment_index',
|
||||||
])->setExtras(['order' => 1015]);
|
])->setExtras(['order' => 1015]);
|
||||||
|
|
||||||
|
@@ -1,53 +0,0 @@
|
|||||||
<?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\MainBundle\Security\Authorization;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
|
||||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
|
||||||
use Symfony\Component\Workflow\Registry;
|
|
||||||
|
|
||||||
final class EntityWorkflowAttachmentVoter extends Voter
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly Registry $registry,
|
|
||||||
) {}
|
|
||||||
public const EDIT = 'CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT';
|
|
||||||
|
|
||||||
protected function supports(string $attribute, $subject): bool
|
|
||||||
{
|
|
||||||
return $subject instanceof EntityWorkflow && self::EDIT === $attribute;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
|
|
||||||
{
|
|
||||||
if (!$subject instanceof EntityWorkflow) {
|
|
||||||
throw new \UnexpectedValueException('Subject must be an instance of EntityWorkflow');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($subject->isFinal()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$workflow = $this->registry->get($subject, $subject->getWorkflowName());
|
|
||||||
|
|
||||||
$marking = $workflow->getMarking($subject);
|
|
||||||
foreach ($marking->getPlaces() as $place => $int) {
|
|
||||||
$placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
|
|
||||||
if ($placeMetadata['isSentExternal'] ?? false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -12,25 +12,18 @@ declare(strict_types=1);
|
|||||||
namespace Chill\MainBundle\Serializer\Normalizer;
|
namespace Chill\MainBundle\Serializer\Normalizer;
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowAttachmentVoter;
|
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
|
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
use Symfony\Component\Workflow\Registry;
|
use Symfony\Component\Workflow\Registry;
|
||||||
|
|
||||||
final class EntityWorkflowNormalizer implements NormalizerInterface, NormalizerAwareInterface
|
class EntityWorkflowNormalizer implements NormalizerInterface, NormalizerAwareInterface
|
||||||
{
|
{
|
||||||
use NormalizerAwareTrait;
|
use NormalizerAwareTrait;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly MetadataExtractor $metadataExtractor, private readonly Registry $registry) {}
|
||||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
|
||||||
private readonly MetadataExtractor $metadataExtractor,
|
|
||||||
private readonly Registry $registry,
|
|
||||||
private readonly Security $security,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param EntityWorkflow $object
|
* @param EntityWorkflow $object
|
||||||
@@ -53,9 +46,6 @@ final class EntityWorkflowNormalizer implements NormalizerInterface, NormalizerA
|
|||||||
'datas' => $this->normalizer->normalize($handler->getEntityData($object), $format, $context),
|
'datas' => $this->normalizer->normalize($handler->getEntityData($object), $format, $context),
|
||||||
'title' => $handler->getEntityTitle($object),
|
'title' => $handler->getEntityTitle($object),
|
||||||
'isOnHoldAtCurrentStep' => $object->isOnHoldAtCurrentStep(),
|
'isOnHoldAtCurrentStep' => $object->isOnHoldAtCurrentStep(),
|
||||||
'_permissions' => [
|
|
||||||
EntityWorkflowAttachmentVoter::EDIT => $this->security->isGranted(EntityWorkflowAttachmentVoter::EDIT, $object),
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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),
|
||||||
|
@@ -35,7 +35,7 @@ final class ScopeControllerTest extends WebTestCase
|
|||||||
$client->getResponse()->getStatusCode(),
|
$client->getResponse()->getStatusCode(),
|
||||||
'Unexpected HTTP status code for GET /fr/admin/scope/'
|
'Unexpected HTTP status code for GET /fr/admin/scope/'
|
||||||
);
|
);
|
||||||
$crawler = $client->click($crawler->selectLink('Créer un nouveau service')->link());
|
$crawler = $client->click($crawler->selectLink('Créer un nouveau cercle')->link());
|
||||||
// Fill in the form and submit it
|
// Fill in the form and submit it
|
||||||
$form = $crawler->selectButton('Créer')->form([
|
$form = $crawler->selectButton('Créer')->form([
|
||||||
'chill_mainbundle_scope[name][fr]' => 'Test en fr',
|
'chill_mainbundle_scope[name][fr]' => 'Test en fr',
|
||||||
|
@@ -1,173 +0,0 @@
|
|||||||
<?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\MainBundle\Tests\Security\Authorization;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\User;
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
|
||||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowAttachmentVoter;
|
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
|
|
||||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Prophecy\PhpUnit\ProphecyTrait;
|
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
|
||||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
|
||||||
use Symfony\Component\Workflow\DefinitionBuilder;
|
|
||||||
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
|
|
||||||
use Symfony\Component\Workflow\Registry;
|
|
||||||
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
|
|
||||||
use Symfony\Component\Workflow\Transition;
|
|
||||||
use Symfony\Component\Workflow\Workflow;
|
|
||||||
use Symfony\Component\Workflow\WorkflowInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @coversNothing
|
|
||||||
*/
|
|
||||||
class EntityWorkflowAttachmentVoterTest extends TestCase
|
|
||||||
{
|
|
||||||
use ProphecyTrait;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider dataVoteOnAttribute
|
|
||||||
*/
|
|
||||||
public function testVoteOnAttribute(EntityWorkflow $entityWorkflow, int $expected): void
|
|
||||||
{
|
|
||||||
$voter = new EntityWorkflowAttachmentVoter($this->buildRegistry());
|
|
||||||
$actual = $voter->vote(
|
|
||||||
new UsernamePasswordToken(new User(), 'default'),
|
|
||||||
$entityWorkflow,
|
|
||||||
['CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT'],
|
|
||||||
);
|
|
||||||
$this->assertEquals($expected, $actual);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function dataVoteOnAttribute(): iterable
|
|
||||||
{
|
|
||||||
$entity = new EntityWorkflow();
|
|
||||||
$entity->setWorkflowName('dummy');
|
|
||||||
|
|
||||||
$workflow = static::buildRegistry()->get($entity, 'dummy');
|
|
||||||
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entity);
|
|
||||||
$dto->futureDestUsers[] = new User();
|
|
||||||
$workflow->apply(
|
|
||||||
$entity,
|
|
||||||
'to_final_positive',
|
|
||||||
['context' => $dto,
|
|
||||||
'byUser' => new User(),
|
|
||||||
'transition' => 'to_final_positive',
|
|
||||||
'transitionAt' => new \DateTimeImmutable()],
|
|
||||||
);
|
|
||||||
// we need to mark manually as final, as the listener is not registered
|
|
||||||
$entity->getCurrentStep()->setIsFinal(true);
|
|
||||||
|
|
||||||
yield 'on final positive' => [
|
|
||||||
$entity,
|
|
||||||
VoterInterface::ACCESS_DENIED,
|
|
||||||
];
|
|
||||||
|
|
||||||
$entity = new EntityWorkflow();
|
|
||||||
$entity->setWorkflowName('dummy');
|
|
||||||
|
|
||||||
$workflow = static::buildRegistry()->get($entity, 'dummy');
|
|
||||||
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entity);
|
|
||||||
$dto->futureDestUsers[] = new User();
|
|
||||||
$workflow->apply(
|
|
||||||
$entity,
|
|
||||||
'to_final_negative',
|
|
||||||
['context' => $dto,
|
|
||||||
'byUser' => new User(),
|
|
||||||
'transition' => 'to_final_negative',
|
|
||||||
'transitionAt' => new \DateTimeImmutable()],
|
|
||||||
);
|
|
||||||
// we need to mark manually as final, as the listener is not registered
|
|
||||||
$entity->getCurrentStep()->setIsFinal(true);
|
|
||||||
|
|
||||||
yield 'on final negative' => [
|
|
||||||
$entity,
|
|
||||||
VoterInterface::ACCESS_DENIED,
|
|
||||||
];
|
|
||||||
|
|
||||||
$entity = new EntityWorkflow();
|
|
||||||
$entity->setWorkflowName('dummy');
|
|
||||||
|
|
||||||
$workflow = static::buildRegistry()->get($entity, 'dummy');
|
|
||||||
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entity);
|
|
||||||
$dto->futureDestUsers[] = new User();
|
|
||||||
$workflow->apply(
|
|
||||||
$entity,
|
|
||||||
'to_sent_external',
|
|
||||||
['context' => $dto,
|
|
||||||
'byUser' => new User(),
|
|
||||||
'transition' => 'to_sent_external',
|
|
||||||
'transitionAt' => new \DateTimeImmutable()],
|
|
||||||
);
|
|
||||||
|
|
||||||
yield 'on sent_external' => [
|
|
||||||
$entity,
|
|
||||||
VoterInterface::ACCESS_DENIED,
|
|
||||||
];
|
|
||||||
|
|
||||||
$entity = new EntityWorkflow();
|
|
||||||
$entity->setWorkflowName('dummy');
|
|
||||||
|
|
||||||
yield 'on initial' => [
|
|
||||||
$entity,
|
|
||||||
VoterInterface::ACCESS_GRANTED,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function buildRegistry(): Registry
|
|
||||||
{
|
|
||||||
$builder = new DefinitionBuilder();
|
|
||||||
$builder
|
|
||||||
->setInitialPlaces(['initial'])
|
|
||||||
->addPlaces(['initial', 'sent_external', 'final_positive', 'final_negative'])
|
|
||||||
->addTransitions([
|
|
||||||
new Transition('to_final_positive', ['initial'], 'final_positive'),
|
|
||||||
new Transition('to_sent_external', ['initial'], 'sent_external'),
|
|
||||||
new Transition('to_final_negative', ['initial'], 'final_negative'),
|
|
||||||
|
|
||||||
])
|
|
||||||
->setMetadataStore(
|
|
||||||
new InMemoryMetadataStore(
|
|
||||||
placesMetadata: [
|
|
||||||
'sent_external' => [
|
|
||||||
'isSentExternal' => true,
|
|
||||||
],
|
|
||||||
'final_positive' => [
|
|
||||||
'isFinal' => true,
|
|
||||||
'isFinalPositive' => true,
|
|
||||||
],
|
|
||||||
'final_negative' => [
|
|
||||||
'isFinal' => true,
|
|
||||||
'isFinalPositive' => false,
|
|
||||||
],
|
|
||||||
]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
$workflow = new Workflow($builder->build(), new EntityWorkflowMarkingStore(), name: 'dummy');
|
|
||||||
$registry = new Registry();
|
|
||||||
$registry->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface {
|
|
||||||
public function supports(WorkflowInterface $workflow, object $subject): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return $registry;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -11,9 +11,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\MainBundle\Tests\Workflow\Helper;
|
namespace Chill\MainBundle\Tests\Workflow\Helper;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
|
|
||||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
|
|
||||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
@@ -151,11 +148,8 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
|
|||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
||||||
|
|
||||||
yield [[], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
|
yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
|
||||||
'abstain because there is no workflow'];
|
'abstain because the user is not present as a dest user'];
|
||||||
|
|
||||||
yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(),
|
|
||||||
'force deny because the user is not present as a dest user'];
|
|
||||||
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
$entityWorkflow = new EntityWorkflow();
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
@@ -177,9 +171,6 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
|
|||||||
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
|
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
|
||||||
'force grant because the user was a previous user'];
|
'force grant because the user was a previous user'];
|
||||||
|
|
||||||
yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(),
|
|
||||||
'force denied because the user was not a previous user'];
|
|
||||||
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
$entityWorkflow = new EntityWorkflow();
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
$dto->futureDestUsers[] = $user = new User();
|
$dto->futureDestUsers[] = $user = new User();
|
||||||
@@ -241,13 +232,6 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
|
|||||||
|
|
||||||
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
|
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
|
||||||
'abstain: there is a signature on a canceled workflow'];
|
'abstain: there is a signature on a canceled workflow'];
|
||||||
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futureDestUsers[] = $user = new User();
|
|
||||||
$entityWorkflow->setStep('sent_external', $dto, 'to_sent_external', new \DateTimeImmutable(), $user);
|
|
||||||
|
|
||||||
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(), 'force denied: the workflow is sent to an external user'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testNoWorkflow(): void
|
public function testNoWorkflow(): void
|
||||||
@@ -269,217 +253,7 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
|
|||||||
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
||||||
$entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn($entityWorkflows);
|
$entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn($entityWorkflows);
|
||||||
|
|
||||||
$repository = $this->prophesize(EntityWorkflowAttachmentRepository::class);
|
return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable()));
|
||||||
$repository->findByStoredObject(Argument::type(StoredObject::class))->willReturn([]);
|
|
||||||
|
|
||||||
return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $repository->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider provideDataAllowedByWorkflowReadOperationByAttachment
|
|
||||||
*
|
|
||||||
* @param list<EntityWorkflow> $entityWorkflows
|
|
||||||
*/
|
|
||||||
public function testAllowedByWorkflowReadByAttachment(
|
|
||||||
array $entityWorkflows,
|
|
||||||
User $user,
|
|
||||||
string $expected,
|
|
||||||
?\DateTimeImmutable $atDate,
|
|
||||||
string $message,
|
|
||||||
): void {
|
|
||||||
// all entities must have this workflow name, so we are ok to set it here
|
|
||||||
foreach ($entityWorkflows as $entityWorkflow) {
|
|
||||||
$entityWorkflow->setWorkflowName('dummy');
|
|
||||||
}
|
|
||||||
$helper = $this->buildHelperForAttachment($entityWorkflows, $user, $atDate);
|
|
||||||
|
|
||||||
self::assertEquals($expected, $helper->isAllowedByWorkflowForReadOperation(new StoredObject()), $message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function provideDataAllowedByWorkflowReadOperationByAttachment(): iterable
|
|
||||||
{
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
|
||||||
|
|
||||||
yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
|
|
||||||
'abstain because the user is not present as a dest user'];
|
|
||||||
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futureDestUsers[] = $user = new User();
|
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
|
||||||
|
|
||||||
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
|
|
||||||
'force grant because the user is a current user'];
|
|
||||||
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futureDestUsers[] = $user = new User();
|
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
|
||||||
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futureDestUsers[] = new User();
|
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
|
||||||
|
|
||||||
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
|
|
||||||
'force grant because the user was a previous user'];
|
|
||||||
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futurePersonSignatures[] = new Person();
|
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
|
||||||
|
|
||||||
yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
|
|
||||||
'Abstain: there is a signature for person, but the attachment is not concerned'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider provideDataAllowedByWorkflowWriteOperationByAttachment
|
|
||||||
*
|
|
||||||
* @param list<EntityWorkflow> $entityWorkflows
|
|
||||||
*/
|
|
||||||
public function testAllowedByWorkflowWriteByAttachment(
|
|
||||||
array $entityWorkflows,
|
|
||||||
User $user,
|
|
||||||
string $expected,
|
|
||||||
?\DateTimeImmutable $atDate,
|
|
||||||
string $message,
|
|
||||||
): void {
|
|
||||||
// all entities must have this workflow name, so we are ok to set it here
|
|
||||||
foreach ($entityWorkflows as $entityWorkflow) {
|
|
||||||
$entityWorkflow->setWorkflowName('dummy');
|
|
||||||
}
|
|
||||||
$helper = $this->buildHelperForAttachment($entityWorkflows, $user, $atDate);
|
|
||||||
|
|
||||||
self::assertEquals($expected, $helper->isAllowedByWorkflowForWriteOperation(new StoredObject()), $message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function provideDataAllowedByWorkflowWriteOperationByAttachment(): iterable
|
|
||||||
{
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
|
||||||
|
|
||||||
yield [[], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
|
|
||||||
'abstain because there is no workflow'];
|
|
||||||
|
|
||||||
yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
|
|
||||||
'abstain because the user is not present as a dest user (and attachment)'];
|
|
||||||
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futureDestUsers[] = $user = new User();
|
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
|
||||||
|
|
||||||
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
|
|
||||||
'force grant because the user is a current user'];
|
|
||||||
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futureDestUsers[] = $user = new User();
|
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
|
||||||
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futureDestUsers[] = new User();
|
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
|
||||||
|
|
||||||
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
|
|
||||||
'force grant because the user was a previous user'];
|
|
||||||
|
|
||||||
yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
|
|
||||||
'abstain because the user was not a previous user'];
|
|
||||||
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futureDestUsers[] = $user = new User();
|
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$entityWorkflow->setStep('final_positive', $dto, 'to_final_positive', new \DateTimeImmutable(), new User());
|
|
||||||
$entityWorkflow->getCurrentStep()->setIsFinal(true);
|
|
||||||
|
|
||||||
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(),
|
|
||||||
'force denied: user was a previous user, but it is finalized positive'];
|
|
||||||
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futureDestUsers[] = $user = new User();
|
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable());
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable());
|
|
||||||
$entityWorkflow->getCurrentStep()->setIsFinal(true);
|
|
||||||
|
|
||||||
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
|
|
||||||
'abstain: user was a previous user, it is finalized, but finalized negative'];
|
|
||||||
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futureDestUsers[] = $user = new User();
|
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futurePersonSignatures[] = new Person();
|
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
|
||||||
$signature = $entityWorkflow->getCurrentStep()->getSignatures()->first();
|
|
||||||
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate(new \DateTimeImmutable());
|
|
||||||
|
|
||||||
yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
|
|
||||||
'abstain: there is a signature, but not on the attachment'];
|
|
||||||
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futureDestUsers[] = $user = new User();
|
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futurePersonSignatures[] = new Person();
|
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
|
||||||
|
|
||||||
yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
|
|
||||||
'abstain: there is a signature, but the signature is not on the attachment'];
|
|
||||||
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futureDestUsers[] = $user = new User();
|
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futurePersonSignatures[] = new Person();
|
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
|
||||||
$signature = $entityWorkflow->getCurrentStep()->getSignatures()->first();
|
|
||||||
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate(new \DateTimeImmutable());
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable(), new User());
|
|
||||||
$entityWorkflow->getCurrentStep()->setIsFinal(true);
|
|
||||||
|
|
||||||
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
|
|
||||||
'abstain: there is a signature on a canceled workflow'];
|
|
||||||
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futureDestUsers[] = $user = new User();
|
|
||||||
$entityWorkflow->setStep('sent_external', $dto, 'to_sent_external', new \DateTimeImmutable(), $user);
|
|
||||||
|
|
||||||
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(),
|
|
||||||
'force denied: the workflow is sent to an external user'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param list<EntityWorkflow> $entityWorkflows
|
|
||||||
*/
|
|
||||||
private function buildHelperForAttachment(array $entityWorkflows, User $user, ?\DateTimeImmutable $atDateTime): WorkflowRelatedEntityPermissionHelper
|
|
||||||
{
|
|
||||||
$security = $this->prophesize(Security::class);
|
|
||||||
$security->getUser()->willReturn($user);
|
|
||||||
|
|
||||||
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
|
||||||
$entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->shouldNotBeCalled();
|
|
||||||
|
|
||||||
$repository = $this->prophesize(EntityWorkflowAttachmentRepository::class);
|
|
||||||
$attachments = [];
|
|
||||||
foreach ($entityWorkflows as $entityWorkflow) {
|
|
||||||
$attachments[] = new EntityWorkflowAttachment('dummy', ['id' => 1], $entityWorkflow, new StoredObject());
|
|
||||||
}
|
|
||||||
$repository->findByStoredObject(Argument::type(StoredObject::class))->willReturn($attachments);
|
|
||||||
|
|
||||||
return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $repository->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function buildRegistry(): Registry
|
private static function buildRegistry(): Registry
|
||||||
@@ -487,13 +261,10 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
|
|||||||
$builder = new DefinitionBuilder();
|
$builder = new DefinitionBuilder();
|
||||||
$builder
|
$builder
|
||||||
->setInitialPlaces(['initial'])
|
->setInitialPlaces(['initial'])
|
||||||
->addPlaces(['initial', 'test', 'sent_external', 'final_positive', 'final_negative'])
|
->addPlaces(['initial', 'test', 'final_positive', 'final_negative'])
|
||||||
->setMetadataStore(
|
->setMetadataStore(
|
||||||
new InMemoryMetadataStore(
|
new InMemoryMetadataStore(
|
||||||
placesMetadata: [
|
placesMetadata: [
|
||||||
'sent_external' => [
|
|
||||||
'isSentExternal' => true,
|
|
||||||
],
|
|
||||||
'final_positive' => [
|
'final_positive' => [
|
||||||
'isFinal' => true,
|
'isFinal' => true,
|
||||||
'isFinalPositive' => true,
|
'isFinalPositive' => true,
|
||||||
|
@@ -11,11 +11,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\MainBundle\Tests\Workflow\Messenger;
|
namespace Chill\MainBundle\Tests\Workflow\Messenger;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
|
||||||
use Chill\DocStoreBundle\Service\StoredObjectToPdfConverter;
|
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
|
|
||||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
|
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
@@ -26,7 +23,6 @@ use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
|||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
use Prophecy\PhpUnit\ProphecyTrait;
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||||
use Symfony\Component\Mailer\MailerInterface;
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
use Symfony\Component\Mime\Address;
|
use Symfony\Component\Mime\Address;
|
||||||
@@ -43,54 +39,24 @@ class PostSendExternalMessageHandlerTest extends TestCase
|
|||||||
public function testSendMessageHappyScenario(): void
|
public function testSendMessageHappyScenario(): void
|
||||||
{
|
{
|
||||||
$entityWorkflow = $this->buildEntityWorkflow();
|
$entityWorkflow = $this->buildEntityWorkflow();
|
||||||
|
|
||||||
// Prepare attachments (2 attachments)
|
|
||||||
$attachmentStoredObject1 = new StoredObject();
|
|
||||||
$attachmentStoredObject2 = new StoredObject();
|
|
||||||
new EntityWorkflowAttachment('generic_doc', ['id' => 1], $entityWorkflow, $attachmentStoredObject1);
|
|
||||||
new EntityWorkflowAttachment('generic_doc', ['id' => 2], $entityWorkflow, $attachmentStoredObject2);
|
|
||||||
|
|
||||||
// Prepare transition DTO and sends
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
$dto->futureDestineeEmails = ['external@example.com'];
|
$dto->futureDestineeEmails = ['external@example.com'];
|
||||||
$dto->futureDestineeThirdParties = [(new ThirdParty())->setEmail('3party@example.com')];
|
$dto->futureDestineeThirdParties = [(new ThirdParty())->setEmail('3party@example.com')];
|
||||||
$entityWorkflow->setStep('send_external', $dto, 'to_send_external', new \DateTimeImmutable(), new User());
|
$entityWorkflow->setStep('send_external', $dto, 'to_send_external', new \DateTimeImmutable(), new User());
|
||||||
|
|
||||||
// Repository returns our workflow
|
|
||||||
$repository = $this->prophesize(EntityWorkflowRepository::class);
|
$repository = $this->prophesize(EntityWorkflowRepository::class);
|
||||||
$repository->find(1)->willReturn($entityWorkflow);
|
$repository->find(1)->willReturn($entityWorkflow);
|
||||||
|
|
||||||
// Mailer must send to both recipients
|
|
||||||
$mailer = $this->prophesize(MailerInterface::class);
|
$mailer = $this->prophesize(MailerInterface::class);
|
||||||
$mailer->send(Argument::that($this->buildCheckAddressCallback('3party@example.com')))->shouldBeCalledOnce();
|
$mailer->send(Argument::that($this->buildCheckAddressCallback('3party@example.com')))->shouldBeCalledOnce();
|
||||||
$mailer->send(Argument::that($this->buildCheckAddressCallback('external@example.com')))->shouldBeCalledOnce();
|
$mailer->send(Argument::that($this->buildCheckAddressCallback('external@example.com')))->shouldBeCalledOnce();
|
||||||
|
|
||||||
// Workflow manager and handler
|
|
||||||
$workflowHandler = $this->prophesize(EntityWorkflowHandlerInterface::class);
|
$workflowHandler = $this->prophesize(EntityWorkflowHandlerInterface::class);
|
||||||
$workflowHandler->getEntityTitle($entityWorkflow, Argument::any())->willReturn('title');
|
$workflowHandler->getEntityTitle($entityWorkflow, Argument::any())->willReturn('title');
|
||||||
$workflowManager = $this->prophesize(EntityWorkflowManager::class);
|
$workflowManager = $this->prophesize(EntityWorkflowManager::class);
|
||||||
$workflowManager->getHandler($entityWorkflow)->willReturn($workflowHandler->reveal());
|
$workflowManager->getHandler($entityWorkflow)->willReturn($workflowHandler->reveal());
|
||||||
|
|
||||||
// Associated stored object for the workflow
|
$handler = new PostSendExternalMessageHandler($repository->reveal(), $mailer->reveal(), $workflowManager->reveal());
|
||||||
$associatedStoredObject = new StoredObject();
|
|
||||||
$workflowManager->getAssociatedStoredObject($entityWorkflow)->willReturn($associatedStoredObject);
|
|
||||||
|
|
||||||
// Converter should be called for each attachment and the associated stored object
|
|
||||||
$converter = $this->prophesize(StoredObjectToPdfConverter::class);
|
|
||||||
$converter->addConvertedVersion($attachmentStoredObject1, 'fr')->shouldBeCalledOnce();
|
|
||||||
$converter->addConvertedVersion($attachmentStoredObject2, 'fr')->shouldBeCalledOnce();
|
|
||||||
$converter->addConvertedVersion($associatedStoredObject, 'fr')->shouldBeCalledOnce();
|
|
||||||
|
|
||||||
// Logger (not used in happy path, but required by handler)
|
|
||||||
$logger = $this->prophesize(LoggerInterface::class);
|
|
||||||
|
|
||||||
$handler = new PostSendExternalMessageHandler(
|
|
||||||
$repository->reveal(),
|
|
||||||
$mailer->reveal(),
|
|
||||||
$workflowManager->reveal(),
|
|
||||||
$converter->reveal(),
|
|
||||||
$logger->reveal(),
|
|
||||||
);
|
|
||||||
|
|
||||||
$handler(new PostSendExternalMessage(1, 'fr'));
|
$handler(new PostSendExternalMessage(1, 'fr'));
|
||||||
|
|
||||||
|
@@ -11,12 +11,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\MainBundle\Workflow\Helper;
|
namespace Chill\MainBundle\Workflow\Helper;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
|
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
use Symfony\Component\Clock\ClockInterface;
|
use Symfony\Component\Clock\ClockInterface;
|
||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
@@ -61,39 +58,21 @@ class WorkflowRelatedEntityPermissionHelper
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Security $security,
|
private readonly Security $security,
|
||||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||||
private readonly EntityWorkflowAttachmentRepository $entityWorkflowAttachmentRepository,
|
|
||||||
private readonly Registry $registry,
|
private readonly Registry $registry,
|
||||||
private readonly ClockInterface $clock,
|
private readonly ClockInterface $clock,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param object $entity The entity may be an
|
|
||||||
*
|
|
||||||
* @return 'FORCE_GRANT'|'FORCE_DENIED'|'ABSTAIN'
|
* @return 'FORCE_GRANT'|'FORCE_DENIED'|'ABSTAIN'
|
||||||
*/
|
*/
|
||||||
public function isAllowedByWorkflowForReadOperation(object $entity): string
|
public function isAllowedByWorkflowForReadOperation(object $entity): string
|
||||||
{
|
{
|
||||||
if ($entity instanceof StoredObject) {
|
$entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
|
||||||
$attachments = $this->entityWorkflowAttachmentRepository->findByStoredObject($entity);
|
|
||||||
$entityWorkflows = array_map(static fn (EntityWorkflowAttachment $attachment) => $attachment->getEntityWorkflow(), $attachments);
|
|
||||||
$isAttached = true;
|
|
||||||
} else {
|
|
||||||
$entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
|
|
||||||
$isAttached = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([] === $entityWorkflows) {
|
|
||||||
return self::ABSTAIN;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->isUserInvolvedInAWorkflow($entityWorkflows)) {
|
if ($this->isUserInvolvedInAWorkflow($entityWorkflows)) {
|
||||||
return self::FORCE_GRANT;
|
return self::FORCE_GRANT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($isAttached) {
|
|
||||||
return self::ABSTAIN;
|
|
||||||
}
|
|
||||||
|
|
||||||
// give a view permission if there is a Person signature pending, or in the 12 hours following
|
// give a view permission if there is a Person signature pending, or in the 12 hours following
|
||||||
// the signature last state
|
// the signature last state
|
||||||
foreach ($entityWorkflows as $workflow) {
|
foreach ($entityWorkflows as $workflow) {
|
||||||
@@ -121,51 +100,33 @@ class WorkflowRelatedEntityPermissionHelper
|
|||||||
*/
|
*/
|
||||||
public function isAllowedByWorkflowForWriteOperation(object $entity): string
|
public function isAllowedByWorkflowForWriteOperation(object $entity): string
|
||||||
{
|
{
|
||||||
if ($entity instanceof StoredObject) {
|
$entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
|
||||||
$attachments = $this->entityWorkflowAttachmentRepository->findByStoredObject($entity);
|
$runningWorkflows = [];
|
||||||
$entityWorkflows = array_map(static fn (EntityWorkflowAttachment $attachment) => $attachment->getEntityWorkflow(), $attachments);
|
|
||||||
$isAttached = true;
|
|
||||||
} else {
|
|
||||||
$entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
|
|
||||||
$isAttached = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([] === $entityWorkflows) {
|
// if a workflow is finalized positive, we are not allowed to edit to document any more
|
||||||
return self::ABSTAIN;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if a workflow is finalized positive, anyone is allowed to edit the document anymore
|
|
||||||
foreach ($entityWorkflows as $entityWorkflow) {
|
foreach ($entityWorkflows as $entityWorkflow) {
|
||||||
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
if ($entityWorkflow->isFinal()) {
|
||||||
$marking = $workflow->getMarkingStore()->getMarking($entityWorkflow);
|
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||||
foreach ($marking->getPlaces() as $place => $int) {
|
$marking = $workflow->getMarkingStore()->getMarking($entityWorkflow);
|
||||||
$placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
|
foreach ($marking->getPlaces() as $place => $int) {
|
||||||
if (
|
$placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
|
||||||
($entityWorkflow->isFinal() && ($placeMetadata['isFinalPositive'] ?? false))
|
if (true === ($placeMetadata['isFinalPositive'] ?? false)) {
|
||||||
|| ($placeMetadata['isSentExternal'] ?? false)
|
// the workflow is final, and final positive, so we stop here.
|
||||||
) {
|
return self::FORCE_DENIED;
|
||||||
// the workflow is final, and final positive, or is sentExternal, so we stop here.
|
}
|
||||||
return self::FORCE_DENIED;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
// if not finalized positive
|
|
||||||
$entityWorkflow->isFinal() && !($placeMetadata['isFinalPositive'] ?? false)
|
|
||||||
) {
|
|
||||||
return self::ABSTAIN;
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
$runningWorkflows[] = $entityWorkflow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$runningWorkflows = array_filter($entityWorkflows, fn (EntityWorkflow $ew) => !$ew->isFinal());
|
// if there is a signature on a **running workflow**, no one can edit the workflow any more
|
||||||
|
foreach ($runningWorkflows as $entityWorkflow) {
|
||||||
// if there is a signature on a **running workflow**, no one is allowed edit the workflow anymore
|
foreach ($entityWorkflow->getSteps() as $step) {
|
||||||
if (!$isAttached) {
|
foreach ($step->getSignatures() as $signature) {
|
||||||
foreach ($runningWorkflows as $entityWorkflow) {
|
if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) {
|
||||||
foreach ($entityWorkflow->getSteps() as $step) {
|
return self::FORCE_DENIED;
|
||||||
foreach ($step->getSignatures() as $signature) {
|
|
||||||
if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) {
|
|
||||||
return self::FORCE_DENIED;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,11 +137,7 @@ class WorkflowRelatedEntityPermissionHelper
|
|||||||
return self::FORCE_GRANT;
|
return self::FORCE_GRANT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($isAttached) {
|
return self::ABSTAIN;
|
||||||
return self::ABSTAIN;
|
|
||||||
}
|
|
||||||
|
|
||||||
return self::FORCE_DENIED;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -11,14 +11,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\MainBundle\Workflow\Messenger;
|
namespace Chill\MainBundle\Workflow\Messenger;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Exception\ConversionWithSameMimeTypeException;
|
|
||||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
|
||||||
use Chill\DocStoreBundle\Service\StoredObjectToPdfConverter;
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
|
||||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||||
use Symfony\Component\Mailer\MailerInterface;
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
|
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
|
||||||
@@ -30,8 +25,6 @@ final readonly class PostSendExternalMessageHandler implements MessageHandlerInt
|
|||||||
private EntityWorkflowRepository $entityWorkflowRepository,
|
private EntityWorkflowRepository $entityWorkflowRepository,
|
||||||
private MailerInterface $mailer,
|
private MailerInterface $mailer,
|
||||||
private EntityWorkflowManager $workflowManager,
|
private EntityWorkflowManager $workflowManager,
|
||||||
private StoredObjectToPdfConverter $storedObjectToPdfConverter,
|
|
||||||
private LoggerInterface $logger,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function __invoke(PostSendExternalMessage $message): void
|
public function __invoke(PostSendExternalMessage $message): void
|
||||||
@@ -42,38 +35,11 @@ final readonly class PostSendExternalMessageHandler implements MessageHandlerInt
|
|||||||
throw new UnrecoverableMessageHandlingException(sprintf('Entity workflow with id %d not found', $message->entityWorkflowId));
|
throw new UnrecoverableMessageHandlingException(sprintf('Entity workflow with id %d not found', $message->entityWorkflowId));
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->convertToPdf($entityWorkflow, $message->lang);
|
|
||||||
|
|
||||||
foreach ($entityWorkflow->getCurrentStep()->getSends() as $send) {
|
foreach ($entityWorkflow->getCurrentStep()->getSends() as $send) {
|
||||||
$this->sendEmailToDestinee($send, $message);
|
$this->sendEmailToDestinee($send, $message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function convertToPdf(EntityWorkflow $entityWorkflow, string $locale): void
|
|
||||||
{
|
|
||||||
foreach ($entityWorkflow->getAttachments() as $attachment) {
|
|
||||||
try {
|
|
||||||
$this->storedObjectToPdfConverter->addConvertedVersion($attachment->getProxyStoredObject(), $locale);
|
|
||||||
} catch (StoredObjectManagerException $e) {
|
|
||||||
$this->logger->error('Error converting attachment to PDF', ['backtrace' => $e->getTraceAsString(), 'attachment_id' => $attachment->getId()]);
|
|
||||||
} catch (ConversionWithSameMimeTypeException $e) {
|
|
||||||
$this->logger->error('Error converting attachment to PDF: already at the same MIME type', ['backtrace' => $e->getTraceAsString(), 'attachment_id' => $attachment->getId()]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$storedObject = $this->workflowManager->getAssociatedStoredObject($entityWorkflow);
|
|
||||||
|
|
||||||
if (null !== $storedObject) {
|
|
||||||
try {
|
|
||||||
$this->storedObjectToPdfConverter->addConvertedVersion($storedObject, $locale);
|
|
||||||
} catch (StoredObjectManagerException $e) {
|
|
||||||
$this->logger->error('Error converting stored object to PDF', ['backtrace' => $e->getTraceAsString(), 'stored_object_id' => $storedObject->getId(), 'workflow_id' => $entityWorkflow->getId()]);
|
|
||||||
} catch (ConversionWithSameMimeTypeException $e) {
|
|
||||||
$this->logger->error('Error converting stored object to PDF: already at the same MIME type', ['backtrace' => $e->getTraceAsString(), 'stored_object_id' => $storedObject->getId(), 'workflow_id' => $entityWorkflow->getId()]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function sendEmailToDestinee(EntityWorkflowSend $send, PostSendExternalMessage $message): void
|
private function sendEmailToDestinee(EntityWorkflowSend $send, PostSendExternalMessage $message): void
|
||||||
{
|
{
|
||||||
$entityWorkflow = $send->getEntityWorkflowStep()->getEntityWorkflow();
|
$entityWorkflow = $send->getEntityWorkflowStep()->getEntityWorkflow();
|
||||||
|
@@ -22,7 +22,6 @@ use Psr\Log\LoggerInterface;
|
|||||||
use Symfony\Component\Clock\ClockInterface;
|
use Symfony\Component\Clock\ClockInterface;
|
||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
use Symfony\Component\Workflow\Registry;
|
use Symfony\Component\Workflow\Registry;
|
||||||
use Symfony\Component\Workflow\Transition;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles state changes for signature steps within a workflow.
|
* Handles state changes for signature steps within a workflow.
|
||||||
@@ -51,10 +50,8 @@ class SignatureStepStateChanger
|
|||||||
*
|
*
|
||||||
* @param EntityWorkflowStepSignature $signature the signature entity to be marked as signed
|
* @param EntityWorkflowStepSignature $signature the signature entity to be marked as signed
|
||||||
* @param int|null $atIndex optional index position for the signature within the zone
|
* @param int|null $atIndex optional index position for the signature within the zone
|
||||||
*
|
|
||||||
* @return string The expected new workflow's step, after transition is applyied
|
|
||||||
*/
|
*/
|
||||||
public function markSignatureAsSigned(EntityWorkflowStepSignature $signature, ?int $atIndex): string
|
public function markSignatureAsSigned(EntityWorkflowStepSignature $signature, ?int $atIndex): void
|
||||||
{
|
{
|
||||||
$this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
|
$this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
|
||||||
|
|
||||||
@@ -63,14 +60,7 @@ class SignatureStepStateChanger
|
|||||||
->setZoneSignatureIndex($atIndex)
|
->setZoneSignatureIndex($atIndex)
|
||||||
->setStateDate($this->clock->now());
|
->setStateDate($this->clock->now());
|
||||||
$this->logger->info(self::LOG_PREFIX.'Mark signature entity as signed', ['signatureId' => $signature->getId(), 'index' => (string) $atIndex]);
|
$this->logger->info(self::LOG_PREFIX.'Mark signature entity as signed', ['signatureId' => $signature->getId(), 'index' => (string) $atIndex]);
|
||||||
['transition' => $transition, 'futureUser' => $futureUser] = $this->decideTransition($signature);
|
|
||||||
|
|
||||||
$this->messageBus->dispatch(new PostSignatureStateChangeMessage((int) $signature->getId()));
|
$this->messageBus->dispatch(new PostSignatureStateChangeMessage((int) $signature->getId()));
|
||||||
if (null === $transition) {
|
|
||||||
return $signature->getStep()->getEntityWorkflow()->getStep();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $transition->getTos()[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,10 +71,8 @@ class SignatureStepStateChanger
|
|||||||
*
|
*
|
||||||
* This method updates the signature state to 'canceled' and logs the action.
|
* This method updates the signature state to 'canceled' and logs the action.
|
||||||
* It also dispatches a message to notify about the state change.
|
* It also dispatches a message to notify about the state change.
|
||||||
*
|
|
||||||
* @return string The expected new workflow's step, after transition is applyied
|
|
||||||
*/
|
*/
|
||||||
public function markSignatureAsCanceled(EntityWorkflowStepSignature $signature): string
|
public function markSignatureAsCanceled(EntityWorkflowStepSignature $signature): void
|
||||||
{
|
{
|
||||||
$this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
|
$this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
|
||||||
|
|
||||||
@@ -92,15 +80,7 @@ class SignatureStepStateChanger
|
|||||||
->setState(EntityWorkflowSignatureStateEnum::CANCELED)
|
->setState(EntityWorkflowSignatureStateEnum::CANCELED)
|
||||||
->setStateDate($this->clock->now());
|
->setStateDate($this->clock->now());
|
||||||
$this->logger->info(self::LOG_PREFIX.'Mark signature entity as canceled', ['signatureId' => $signature->getId()]);
|
$this->logger->info(self::LOG_PREFIX.'Mark signature entity as canceled', ['signatureId' => $signature->getId()]);
|
||||||
|
|
||||||
['transition' => $transition, 'futureUser' => $futureUser] = $this->decideTransition($signature);
|
|
||||||
|
|
||||||
$this->messageBus->dispatch(new PostSignatureStateChangeMessage((int) $signature->getId()));
|
$this->messageBus->dispatch(new PostSignatureStateChangeMessage((int) $signature->getId()));
|
||||||
if (null === $transition) {
|
|
||||||
return $signature->getStep()->getEntityWorkflow()->getStep();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $transition->getTos()[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -113,10 +93,8 @@ class SignatureStepStateChanger
|
|||||||
* a state change has occurred.
|
* a state change has occurred.
|
||||||
*
|
*
|
||||||
* @param EntityWorkflowStepSignature $signature the signature entity to be marked as rejected
|
* @param EntityWorkflowStepSignature $signature the signature entity to be marked as rejected
|
||||||
*
|
|
||||||
* @return string The expected new workflow's step, after transition is applyied
|
|
||||||
*/
|
*/
|
||||||
public function markSignatureAsRejected(EntityWorkflowStepSignature $signature): string
|
public function markSignatureAsRejected(EntityWorkflowStepSignature $signature): void
|
||||||
{
|
{
|
||||||
$this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
|
$this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
|
||||||
|
|
||||||
@@ -124,16 +102,7 @@ class SignatureStepStateChanger
|
|||||||
->setState(EntityWorkflowSignatureStateEnum::REJECTED)
|
->setState(EntityWorkflowSignatureStateEnum::REJECTED)
|
||||||
->setStateDate($this->clock->now());
|
->setStateDate($this->clock->now());
|
||||||
$this->logger->info(self::LOG_PREFIX.'Mark signature entity as rejected', ['signatureId' => $signature->getId()]);
|
$this->logger->info(self::LOG_PREFIX.'Mark signature entity as rejected', ['signatureId' => $signature->getId()]);
|
||||||
|
|
||||||
['transition' => $transition, 'futureUser' => $futureUser] = $this->decideTransition($signature);
|
|
||||||
|
|
||||||
$this->messageBus->dispatch(new PostSignatureStateChangeMessage((int) $signature->getId()));
|
$this->messageBus->dispatch(new PostSignatureStateChangeMessage((int) $signature->getId()));
|
||||||
|
|
||||||
if (null === $transition) {
|
|
||||||
return $signature->getStep()->getEntityWorkflow()->getStep();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $transition->getTos()[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -148,35 +117,10 @@ class SignatureStepStateChanger
|
|||||||
{
|
{
|
||||||
$this->entityManager->refresh($signature, LockMode::PESSIMISTIC_READ);
|
$this->entityManager->refresh($signature, LockMode::PESSIMISTIC_READ);
|
||||||
|
|
||||||
['transition' => $transition, 'futureUser' => $futureUser] = $this->decideTransition($signature);
|
|
||||||
|
|
||||||
if (null === $transition) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
|
|
||||||
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
|
||||||
$transitionDto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$transitionDto->futureDestUsers[] = $futureUser;
|
|
||||||
|
|
||||||
$workflow->apply($entityWorkflow, $transition->getName(), [
|
|
||||||
'context' => $transitionDto,
|
|
||||||
'transitionAt' => $this->clock->now(),
|
|
||||||
'transition' => $transition->getName(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->logger->info(self::LOG_PREFIX.'Transition automatically applied', ['signatureId' => $signature->getId()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{transition: Transition|null, futureUser: User|null}
|
|
||||||
*/
|
|
||||||
private function decideTransition(EntityWorkflowStepSignature $signature): array
|
|
||||||
{
|
|
||||||
if (!EntityWorkflowStepSignature::isAllSignatureNotPendingForStep($signature->getStep())) {
|
if (!EntityWorkflowStepSignature::isAllSignatureNotPendingForStep($signature->getStep())) {
|
||||||
$this->logger->info(self::LOG_PREFIX.'This is not the last signature, skipping transition to another place', ['signatureId' => $signature->getId()]);
|
$this->logger->info(self::LOG_PREFIX.'This is not the last signature, skipping transition to another place', ['signatureId' => $signature->getId()]);
|
||||||
|
|
||||||
return ['transition' => null, 'futureUser' => null];
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->logger->debug(self::LOG_PREFIX.'Continuing the process to find a transition', ['signatureId' => $signature->getId()]);
|
$this->logger->debug(self::LOG_PREFIX.'Continuing the process to find a transition', ['signatureId' => $signature->getId()]);
|
||||||
@@ -200,7 +144,7 @@ class SignatureStepStateChanger
|
|||||||
if (null === $transition) {
|
if (null === $transition) {
|
||||||
$this->logger->info(self::LOG_PREFIX.'The transition is not configured, will not apply a transition', ['signatureId' => $signature->getId()]);
|
$this->logger->info(self::LOG_PREFIX.'The transition is not configured, will not apply a transition', ['signatureId' => $signature->getId()]);
|
||||||
|
|
||||||
return ['transition' => null, 'futureUser' => null];
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('person' === $signature->getSignerKind()) {
|
if ('person' === $signature->getSignerKind()) {
|
||||||
@@ -212,16 +156,19 @@ class SignatureStepStateChanger
|
|||||||
if (null === $futureUser) {
|
if (null === $futureUser) {
|
||||||
$this->logger->info(self::LOG_PREFIX.'No previous user, will not apply a transition', ['signatureId' => $signature->getId()]);
|
$this->logger->info(self::LOG_PREFIX.'No previous user, will not apply a transition', ['signatureId' => $signature->getId()]);
|
||||||
|
|
||||||
return ['transition' => null, 'futureUser' => null];
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($workflow->getDefinition()->getTransitions() as $transitionObj) {
|
$transitionDto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
if ($transitionObj->getName() === $transition) {
|
$transitionDto->futureDestUsers[] = $futureUser;
|
||||||
return ['transition' => $transitionObj, 'futureUser' => $futureUser];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new \RuntimeException('Transition not found');
|
$workflow->apply($entityWorkflow, $transition, [
|
||||||
|
'context' => $transitionDto,
|
||||||
|
'transitionAt' => $this->clock->now(),
|
||||||
|
'transition' => $transition,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->logger->info(self::LOG_PREFIX.'Transition automatically applied', ['signatureId' => $signature->getId()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getPreviousSender(EntityWorkflowStep $entityWorkflowStep): ?User
|
private function getPreviousSender(EntityWorkflowStep $entityWorkflowStep): ?User
|
||||||
|
@@ -965,31 +965,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/UserJob"
|
$ref: "#/components/schemas/UserJob"
|
||||||
/1.0/main/workflow/{id}.json:
|
|
||||||
get:
|
|
||||||
tags:
|
|
||||||
- workflow
|
|
||||||
summary: Return a workflow
|
|
||||||
parameters:
|
|
||||||
- name: id
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
description: The workflow id
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: integer
|
|
||||||
minimum: 1
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: "ok"
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: "#/components/schemas/Workflow"
|
|
||||||
404:
|
|
||||||
description: "not found"
|
|
||||||
401:
|
|
||||||
description: "Unauthorized"
|
|
||||||
/1.0/main/workflow/my:
|
/1.0/main/workflow/my:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
@@ -120,8 +120,5 @@ module.exports = function (encore, entries) {
|
|||||||
"vue_onthefly",
|
"vue_onthefly",
|
||||||
__dirname + "/Resources/public/vuejs/OnTheFly/index.js",
|
__dirname + "/Resources/public/vuejs/OnTheFly/index.js",
|
||||||
);
|
);
|
||||||
encore.addEntry(
|
|
||||||
"page_workflow_waiting_post_process",
|
|
||||||
__dirname + "/Resources/public/vuejs/WaitPostProcessWorkflow/index.ts"
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
@@ -54,7 +54,7 @@ user:
|
|||||||
title: Mon profil
|
title: Mon profil
|
||||||
Profile successfully updated!: Votre profil a été mis à jour!
|
Profile successfully updated!: Votre profil a été mis à jour!
|
||||||
no job: Pas de métier assigné
|
no job: Pas de métier assigné
|
||||||
no scope: Pas de service assigné
|
no scope: Pas de cercle assigné
|
||||||
notification_preferences: Préférences pour mes notifications
|
notification_preferences: Préférences pour mes notifications
|
||||||
|
|
||||||
user_group:
|
user_group:
|
||||||
@@ -102,9 +102,9 @@ createdAt: Créé le
|
|||||||
createdBy: Créé par
|
createdBy: Créé par
|
||||||
|
|
||||||
#elements used in software
|
#elements used in software
|
||||||
centers: territoires
|
centers: centres
|
||||||
Centers: Territoires
|
Centers: Centres
|
||||||
center: territoire
|
center: centre
|
||||||
comment: commentaire
|
comment: commentaire
|
||||||
Comment: Commentaire
|
Comment: Commentaire
|
||||||
Comments: Commentaires
|
Comments: Commentaires
|
||||||
@@ -227,12 +227,12 @@ Location Menu: Localisations et types de localisation
|
|||||||
Management of location: Gestion des localisations et types de localisation
|
Management of location: Gestion des localisations et types de localisation
|
||||||
|
|
||||||
#admin section for center's administration
|
#admin section for center's administration
|
||||||
Create a new center: Créer une nouveau territoire
|
Create a new center: Créer un nouveau centre
|
||||||
Center list: Liste des territoires
|
Center list: Liste des centres
|
||||||
Center edit: Édition d'un territoire
|
Center edit: Édition d'un centre
|
||||||
Center creation: Création d'un territoire
|
Center creation: Création d'un centre
|
||||||
New center: Nouveau territoire
|
New center: Nouveau centre
|
||||||
Center: Territoire
|
Center: Centre
|
||||||
|
|
||||||
#admin section for permissions group
|
#admin section for permissions group
|
||||||
Permissions group list: Groupes de permissions
|
Permissions group list: Groupes de permissions
|
||||||
@@ -246,15 +246,15 @@ New permission group: Nouveau groupe de permissions
|
|||||||
PermissionsGroup "%name%" edit: Modification du groupe de permission '%name%'
|
PermissionsGroup "%name%" edit: Modification du groupe de permission '%name%'
|
||||||
Role: Rôle
|
Role: Rôle
|
||||||
Choose amongst roles: Choisir un rôle
|
Choose amongst roles: Choisir un rôle
|
||||||
Choose amongst scopes: Choisir un service
|
Choose amongst scopes: Choisir un cercle
|
||||||
Add permission: Ajouter les permissions
|
Add permission: Ajouter les permissions
|
||||||
This group does not provide any permission: Ce groupe n'attribue aucune permission
|
This group does not provide any permission: Ce groupe n'attribue aucune permission
|
||||||
The role '%role%' has been removed: Le rôle "%role%" a été enlevé de ce groupe de permission
|
The role '%role%' has been removed: Le rôle "%role%" a été enlevé de ce groupe de permission
|
||||||
The role '%role%' on circle '%scope%' has been removed: Le rôle "%role%" sur le service "%scope%" a été enlevé de ce groupe de permission
|
The role '%role%' on circle '%scope%' has been removed: Le rôle "%role%" sur le cercle "%scope%" a été enlevé de ce groupe de permission
|
||||||
Unclassified: Non classifié
|
Unclassified: Non classifié
|
||||||
Help to pick role and scope: Certains rôles ne nécessitent pas de service.
|
Help to pick role and scope: Certains rôles ne nécessitent pas de cercle.
|
||||||
The role need scope: Ce rôle nécessite un service.
|
The role need scope: Ce rôle nécessite un cercle.
|
||||||
The role does not need scope: Ce rôle ne nécessite pas de service !
|
The role does not need scope: Ce rôle ne nécessite pas de cercle !
|
||||||
|
|
||||||
#admin section for users
|
#admin section for users
|
||||||
User configuration: Gestion des utilisateurs
|
User configuration: Gestion des utilisateurs
|
||||||
@@ -270,7 +270,7 @@ Grant new permissions: Ajout de permissions
|
|||||||
Add a new groupCenter: Ajout de permissions
|
Add a new groupCenter: Ajout de permissions
|
||||||
The permissions have been successfully added to the user: Les permissions ont été accordées à l'utilisateur
|
The permissions have been successfully added to the user: Les permissions ont été accordées à l'utilisateur
|
||||||
The permissions where removed.: Les permissions ont été enlevées.
|
The permissions where removed.: Les permissions ont été enlevées.
|
||||||
Center & groups: Territoire et groupes
|
Center & groups: Centre et groupes
|
||||||
User %username%: Utilisateur %username%
|
User %username%: Utilisateur %username%
|
||||||
Add a new user: Ajouter un nouvel utilisateur
|
Add a new user: Ajouter un nouvel utilisateur
|
||||||
The permissions have been added: Les permissions ont été ajoutées
|
The permissions have been added: Les permissions ont été ajoutées
|
||||||
@@ -280,13 +280,13 @@ Back to the user edition: Retour au formulaire d'édition
|
|||||||
Password successfully updated!: Mot de passe mis à jour
|
Password successfully updated!: Mot de passe mis à jour
|
||||||
Flags: Drapeaux
|
Flags: Drapeaux
|
||||||
Main location: Localisation principale
|
Main location: Localisation principale
|
||||||
Main scope: Service
|
Main scope: Cercle
|
||||||
Main center: Territoire
|
Main center: Centre
|
||||||
user job: Métier de l'utilisateur
|
user job: Métier de l'utilisateur
|
||||||
Job: Métier
|
Job: Métier
|
||||||
Jobs: Métiers
|
Jobs: Métiers
|
||||||
Choose a main center: Choisir un territoire
|
Choose a main center: Choisir un centre
|
||||||
Choose a main scope: Choisir un service
|
Choose a main scope: Choisir un cercle
|
||||||
choose a job: Choisir un métier
|
choose a job: Choisir un métier
|
||||||
choose a location: Choisir une localisation
|
choose a location: Choisir une localisation
|
||||||
|
|
||||||
@@ -302,12 +302,12 @@ Current location successfully updated: Localisation actuelle mise à jour
|
|||||||
Pick a location: Choisir un lieu
|
Pick a location: Choisir un lieu
|
||||||
|
|
||||||
#admin section for circles (old: scopes)
|
#admin section for circles (old: scopes)
|
||||||
List circles: Services
|
List circles: Cercles
|
||||||
New circle: Nouveau service
|
New circle: Nouveau cercle
|
||||||
Circle: Service
|
Circle: Cercle
|
||||||
Circle edit: Modification du service
|
Circle edit: Modification du cercle
|
||||||
Circle creation: Création d'un service
|
Circle creation: Création d'un cercle
|
||||||
Create a new circle: Créer un nouveau service
|
Create a new circle: Créer un nouveau cercle
|
||||||
|
|
||||||
#admin section for location
|
#admin section for location
|
||||||
Location: Localisation
|
Location: Localisation
|
||||||
@@ -347,9 +347,9 @@ Country list: Liste des pays
|
|||||||
Country code: Code du pays
|
Country code: Code du pays
|
||||||
|
|
||||||
# circles / scopes
|
# circles / scopes
|
||||||
Choose the circle: Choisir le service
|
Choose the circle: Choisir le cercle
|
||||||
Scope: Service
|
Scope: Cercle
|
||||||
Scopes: Services
|
Scopes: Cercles
|
||||||
|
|
||||||
#export
|
#export
|
||||||
|
|
||||||
@@ -357,14 +357,14 @@ Scopes: Services
|
|||||||
Exports list: Liste des exports
|
Exports list: Liste des exports
|
||||||
Create an export: Créer un export
|
Create an export: Créer un export
|
||||||
#export creation step 'center' : pick a center
|
#export creation step 'center' : pick a center
|
||||||
Pick centers: Choisir les territoires
|
Pick centers: Choisir les centres
|
||||||
Pick a center: Choisir un territoire
|
Pick a center: Choisir un centre
|
||||||
The export will contains only data from the picked centers.: L'export ne contiendra que les données des territoires choisis.
|
The export will contains only data from the picked centers.: L'export ne contiendra que les données des centres choisis.
|
||||||
This will eventually restrict your possibilities in filtering the data.: Les possibilités de filtrages seront adaptées aux droits de consultation pour les territoires choisis.
|
This will eventually restrict your possibilities in filtering the data.: Les possibilités de filtrages seront adaptées aux droits de consultation pour les centres choisis.
|
||||||
Go to export options: Vers la préparation de l'export
|
Go to export options: Vers la préparation de l'export
|
||||||
Pick aggregated centers: Regroupement de territoires
|
Pick aggregated centers: Regroupement de centres
|
||||||
uncheck all centers: Désélectionner tous les territoires
|
uncheck all centers: Désélectionner tous les centres
|
||||||
check all centers: Sélectionner tous les territoires
|
check all centers: Sélectionner tous les centres
|
||||||
# export creation step 'export' : choose aggregators, filtering and formatter
|
# export creation step 'export' : choose aggregators, filtering and formatter
|
||||||
Formatter: Mise en forme
|
Formatter: Mise en forme
|
||||||
Choose the formatter: Choisissez le format d'export voulu.
|
Choose the formatter: Choisissez le format d'export voulu.
|
||||||
@@ -510,10 +510,10 @@ crud:
|
|||||||
title_edit: Modifier un regroupement
|
title_edit: Modifier un regroupement
|
||||||
center:
|
center:
|
||||||
index:
|
index:
|
||||||
title: Liste des territoires
|
title: Liste des centres
|
||||||
add_new: Ajouter un territoire
|
add_new: Ajouter un centre
|
||||||
title_new: Nouveau territoire
|
title_new: Nouveau centre
|
||||||
title_edit: Modifier un territoire
|
title_edit: Modifier un centre
|
||||||
news_item:
|
news_item:
|
||||||
index:
|
index:
|
||||||
title: Liste des actualités
|
title: Liste des actualités
|
||||||
@@ -666,17 +666,10 @@ workflow:
|
|||||||
cancel_are_you_sure: Êtes-vous sûr de vouloir annuler la signature de %signer%
|
cancel_are_you_sure: Êtes-vous sûr de vouloir annuler la signature de %signer%
|
||||||
reject_signature_of: Rejet de la signature de %signer%
|
reject_signature_of: Rejet de la signature de %signer%
|
||||||
reject_are_you_sure: Êtes-vous sûr de vouloir rejeter la signature de %signer%
|
reject_are_you_sure: Êtes-vous sûr de vouloir rejeter la signature de %signer%
|
||||||
waiting_for: En attente de modification de l'état de la signature
|
|
||||||
|
|
||||||
attachments:
|
attachments:
|
||||||
title: Pièces jointes
|
title: Pièces jointes
|
||||||
|
|
||||||
wait:
|
|
||||||
title: En attente de traitement
|
|
||||||
error_while_waiting: Le traitement a échoué
|
|
||||||
success: Traitement terminé. Redirection en cours...
|
|
||||||
|
|
||||||
|
|
||||||
Subscribe final: Recevoir une notification à l'étape finale
|
Subscribe final: Recevoir une notification à l'étape finale
|
||||||
Subscribe all steps: Recevoir une notification à chaque étape
|
Subscribe all steps: Recevoir une notification à chaque étape
|
||||||
CHILL_MAIN_WORKFLOW_APPLY_ALL_TRANSITION: Appliquer les transitions sur tous les workflows
|
CHILL_MAIN_WORKFLOW_APPLY_ALL_TRANSITION: Appliquer les transitions sur tous les workflows
|
||||||
@@ -860,7 +853,7 @@ absence:
|
|||||||
admin:
|
admin:
|
||||||
users:
|
users:
|
||||||
export_list_csv: Liste des utilisateurs (format CSV)
|
export_list_csv: Liste des utilisateurs (format CSV)
|
||||||
export_permissions_csv: Association utilisateurs - groupes de permissions - territoire (format CSV)
|
export_permissions_csv: Association utilisateurs - groupes de permissions - centre (format CSV)
|
||||||
export:
|
export:
|
||||||
id: Identifiant
|
id: Identifiant
|
||||||
username: Nom d'utilisateur
|
username: Nom d'utilisateur
|
||||||
@@ -870,8 +863,8 @@ admin:
|
|||||||
civility_abbreviation: Abbréviation civilité
|
civility_abbreviation: Abbréviation civilité
|
||||||
civility_name: Civilité
|
civility_name: Civilité
|
||||||
label: Label
|
label: Label
|
||||||
mainCenter_id: Identifiant territoire principal
|
mainCenter_id: Identifiant centre principal
|
||||||
mainCenter_name: Territoire principal
|
mainCenter_name: Centre principal
|
||||||
mainScope_id: Identifiant service principal
|
mainScope_id: Identifiant service principal
|
||||||
mainScope_name: Service principal
|
mainScope_name: Service principal
|
||||||
userJob_id: Identifiant métier
|
userJob_id: Identifiant métier
|
||||||
@@ -881,8 +874,8 @@ admin:
|
|||||||
mainLocation_id: Identifiant localisation principale
|
mainLocation_id: Identifiant localisation principale
|
||||||
mainLocation_name: Localisation principale
|
mainLocation_name: Localisation principale
|
||||||
absenceStart: Absent à partir du
|
absenceStart: Absent à partir du
|
||||||
center_id: Identifiant du territoire
|
center_id: Identifiant du centre
|
||||||
center_name: Territoire
|
center_name: Centre
|
||||||
permissionsGroup_id: Identifiant du groupe de permissions
|
permissionsGroup_id: Identifiant du groupe de permissions
|
||||||
permissionsGroup_name: Groupe de permissions
|
permissionsGroup_name: Groupe de permissions
|
||||||
job_scope_histories:
|
job_scope_histories:
|
||||||
|
@@ -1,15 +1,15 @@
|
|||||||
# role_scope constraint
|
# role_scope constraint
|
||||||
# scope presence
|
# scope presence
|
||||||
The role "%role%" require to be associated with a scope.: Le rôle "%role%" doit être associé à un service.
|
The role "%role%" require to be associated with a scope.: Le rôle "%role%" doit être associé à un cercle.
|
||||||
The role "%role%" should not be associated with a scope.: Le rôle "%role%" ne doit pas être associé à un service.
|
The role "%role%" should not be associated with a scope.: Le rôle "%role%" ne doit pas être associé à un cercle.
|
||||||
"The password must contains one letter, one capitalized letter, one number and one special character as *[@#$%!,;:+\"'-/{}~=µ()£]). Other characters are allowed.": "Le mot de passe doit contenir une majuscule, une minuscule, et au moins un caractère spécial parmi *[@#$%!,;:+\"'-/{}~=µ()£]). Les autres caractères sont autorisés."
|
"The password must contains one letter, one capitalized letter, one number and one special character as *[@#$%!,;:+\"'-/{}~=µ()£]). Other characters are allowed.": "Le mot de passe doit contenir une majuscule, une minuscule, et au moins un caractère spécial parmi *[@#$%!,;:+\"'-/{}~=µ()£]). Les autres caractères sont autorisés."
|
||||||
The password fields must match: Les mots de passe doivent correspondre
|
The password fields must match: Les mots de passe doivent correspondre
|
||||||
The password must be greater than {{ limit }} characters: "[1,Inf] Le mot de passe doit contenir au moins {{ limit }} caractères"
|
The password must be greater than {{ limit }} characters: "[1,Inf] Le mot de passe doit contenir au moins {{ limit }} caractères"
|
||||||
|
|
||||||
A permission is already present for the same role and scope: Une permission est déjà présente pour le même rôle et service.
|
A permission is already present for the same role and scope: Une permission est déjà présente pour le même rôle et cercle.
|
||||||
|
|
||||||
#UserCircleConsistency
|
#UserCircleConsistency
|
||||||
"{{ username }} is not allowed to see entities published in this circle": "{{ username }} n'est pas autorisé à voir l'élément publié dans ce service."
|
"{{ username }} is not allowed to see entities published in this circle": "{{ username }} n'est pas autorisé à voir l'élément publié dans ce cercle."
|
||||||
|
|
||||||
The user in cc cannot be a dest user in the same workflow step: Un utilisateur en Cc ne peut pas être un utilisateur qui valide.
|
The user in cc cannot be a dest user in the same workflow step: Un utilisateur en Cc ne peut pas être un utilisateur qui valide.
|
||||||
|
|
||||||
|
@@ -80,7 +80,7 @@ const appMessages = {
|
|||||||
firstName: "Prénom",
|
firstName: "Prénom",
|
||||||
lastName: "Nom",
|
lastName: "Nom",
|
||||||
birthdate: "Date de naissance",
|
birthdate: "Date de naissance",
|
||||||
center: "Territoire",
|
center: "Centre",
|
||||||
phonenumber: "Téléphone",
|
phonenumber: "Téléphone",
|
||||||
mobilenumber: "Mobile",
|
mobilenumber: "Mobile",
|
||||||
altNames: "Autres noms",
|
altNames: "Autres noms",
|
||||||
|
@@ -50,8 +50,8 @@ const visMessages = {
|
|||||||
return "Né·e le";
|
return "Né·e le";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
center_id: "Identifiant du territoire",
|
center_id: "Identifiant du centre",
|
||||||
center_type: "Type de territoire",
|
center_type: "Type de centre",
|
||||||
center_name: "Territoire", // vendée
|
center_name: "Territoire", // vendée
|
||||||
phonenumber: "Téléphone",
|
phonenumber: "Téléphone",
|
||||||
mobilenumber: "Mobile",
|
mobilenumber: "Mobile",
|
||||||
|
@@ -464,7 +464,7 @@ export default {
|
|||||||
this.errors.push("Le genre doit être renseigné");
|
this.errors.push("Le genre doit être renseigné");
|
||||||
}
|
}
|
||||||
if (this.showCenters && this.person.center === null) {
|
if (this.showCenters && this.person.center === null) {
|
||||||
this.errors.push("Le territoire doit être renseigné");
|
this.errors.push("Le centre doit être renseigné");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
loadData() {
|
loadData() {
|
||||||
|
@@ -25,8 +25,8 @@ const personMessages = {
|
|||||||
return "Né·e le";
|
return "Né·e le";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
center_id: "Identifiant du territoire",
|
center_id: "Identifiant du centre",
|
||||||
center_type: "Type de territoire",
|
center_type: "Type de centre",
|
||||||
center_name: "Territoire", // vendée
|
center_name: "Territoire", // vendée
|
||||||
phonenumber: "Téléphone",
|
phonenumber: "Téléphone",
|
||||||
mobilenumber: "Mobile",
|
mobilenumber: "Mobile",
|
||||||
@@ -53,8 +53,8 @@ const personMessages = {
|
|||||||
"Un nouveau ménage va être créé. L'usager sera membre de ce ménage.",
|
"Un nouveau ménage va être créé. L'usager sera membre de ce ménage.",
|
||||||
},
|
},
|
||||||
center: {
|
center: {
|
||||||
placeholder: "Choisissez un territoire",
|
placeholder: "Choisissez un centre",
|
||||||
title: "territoire",
|
title: "Centre",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
error_only_one_person: "Une seule personne peut être sélectionnée !",
|
error_only_one_person: "Une seule personne peut être sélectionnée !",
|
||||||
|
@@ -376,7 +376,7 @@ Create a list of people according to various filters.: Crée une liste d'usagers
|
|||||||
Fields to include in export: Champs à inclure dans l'export
|
Fields to include in export: Champs à inclure dans l'export
|
||||||
Address valid at this date: Addresse valide à cette date
|
Address valid at this date: Addresse valide à cette date
|
||||||
Data valid at this date: Données valides à cette date
|
Data valid at this date: Données valides à cette date
|
||||||
Data regarding center, addresses, and so on will be computed at this date: Les données concernant le territoire, l'adresse, le ménage, sera calculé à cette date.
|
Data regarding center, addresses, and so on will be computed at this date: Les données concernant le centre, l'adresse, le ménage, sera calculé à cette date.
|
||||||
List duplicates: Liste des doublons
|
List duplicates: Liste des doublons
|
||||||
Create a list of duplicate people: Créer la liste des usagers détectés comme doublons.
|
Create a list of duplicate people: Créer la liste des usagers détectés comme doublons.
|
||||||
Count people participating in an accompanying course: Nombre d'usagers concernés par un parcours
|
Count people participating in an accompanying course: Nombre d'usagers concernés par un parcours
|
||||||
@@ -1110,9 +1110,9 @@ export:
|
|||||||
Group course by household composition: Grouper les usagers par composition familiale
|
Group course by household composition: Grouper les usagers par composition familiale
|
||||||
Calc date: Date de calcul de la composition du ménage
|
Calc date: Date de calcul de la composition du ménage
|
||||||
by_center:
|
by_center:
|
||||||
title: Grouper les usagers par territoire
|
title: Grouper les usagers par centre
|
||||||
at_date: Date de calcul du territoire
|
at_date: Date de calcul du centre
|
||||||
center: Territoire de l'usager
|
center: Centre de l'usager
|
||||||
by_postal_code:
|
by_postal_code:
|
||||||
title: Grouper les usagers par code postal de l'adresse
|
title: Grouper les usagers par code postal de l'adresse
|
||||||
at_date: Date de calcul de l'adresse
|
at_date: Date de calcul de l'adresse
|
||||||
@@ -1437,7 +1437,7 @@ export:
|
|||||||
acpParticipantPersons: Usagers concernés
|
acpParticipantPersons: Usagers concernés
|
||||||
acpParticipantPersonsIds: Usagers concernés (identifiants)
|
acpParticipantPersonsIds: Usagers concernés (identifiants)
|
||||||
duration: Durée du parcours (en jours)
|
duration: Durée du parcours (en jours)
|
||||||
centers: Territoires des usagers
|
centers: Centres des usagers
|
||||||
|
|
||||||
eval:
|
eval:
|
||||||
List of evaluations: Liste des évaluations
|
List of evaluations: Liste des évaluations
|
||||||
|
@@ -23,7 +23,7 @@ The gender must be set: Le genre doit être renseigné
|
|||||||
You are not allowed to perform this action: Vous n'avez pas le droit de changer cette valeur.
|
You are not allowed to perform this action: Vous n'avez pas le droit de changer cette valeur.
|
||||||
Sorry, but someone else has already changed this entity. Please refresh the page and apply the changes again: Désolé, mais quelqu'un d'autre a déjà modifié cette entité. Veuillez actualiser la page et appliquer à nouveau les modifications
|
Sorry, but someone else has already changed this entity. Please refresh the page and apply the changes again: Désolé, mais quelqu'un d'autre a déjà modifié cette entité. Veuillez actualiser la page et appliquer à nouveau les modifications
|
||||||
|
|
||||||
A center is required: Un territoire est requis
|
A center is required: Un centre est requis
|
||||||
|
|
||||||
#export list
|
#export list
|
||||||
You must select at least one element: Vous devez sélectionner au moins un élément
|
You must select at least one element: Vous devez sélectionner au moins un élément
|
||||||
|
@@ -9,7 +9,7 @@
|
|||||||
'Report list': 'Liste des rapports'
|
'Report list': 'Liste des rapports'
|
||||||
Details: Détails
|
Details: Détails
|
||||||
Person: Usager
|
Person: Usager
|
||||||
Scope: Service
|
Scope: Cercle
|
||||||
Date: Date
|
Date: Date
|
||||||
User: Utilisateur
|
User: Utilisateur
|
||||||
'Report type': 'Type de rapport'
|
'Report type': 'Type de rapport'
|
||||||
|
@@ -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: ~
|
@@ -4,7 +4,7 @@ Tasks: "Tâches"
|
|||||||
Title: Titre
|
Title: Titre
|
||||||
Description: Description
|
Description: Description
|
||||||
Assignee: "Personne assignée"
|
Assignee: "Personne assignée"
|
||||||
Scope: Service
|
Scope: Cercle
|
||||||
"Start date": "Date de début"
|
"Start date": "Date de début"
|
||||||
"End date": "Date d'échéance"
|
"End date": "Date d'échéance"
|
||||||
"Warning date": "Date d'avertissement"
|
"Warning date": "Date d'avertissement"
|
||||||
@@ -106,7 +106,7 @@ My tasks over deadline: Mes tâches à échéance dépassée
|
|||||||
#transition page
|
#transition page
|
||||||
Apply transition on task <em>%title%</em>: Appliquer la transition sur la tâche <em>%title%</em>
|
Apply transition on task <em>%title%</em>: Appliquer la transition sur la tâche <em>%title%</em>
|
||||||
|
|
||||||
All centers: Tous les territoires
|
All centers: Tous les centres
|
||||||
|
|
||||||
# ROLES
|
# ROLES
|
||||||
CHILL_TASK_TASK_CREATE: Ajouter une tâche
|
CHILL_TASK_TASK_CREATE: Ajouter une tâche
|
||||||
@@ -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"
|
||||||
|
|
||||||
|
@@ -73,8 +73,8 @@ No acronym given: Aucun sigle renseigné
|
|||||||
No phone given: Aucun téléphone renseigné
|
No phone given: Aucun téléphone renseigné
|
||||||
No email given: Aucune adresse courriel renseignée
|
No email given: Aucune adresse courriel renseignée
|
||||||
|
|
||||||
The party is visible in those centers: Le tiers est visible dans ces territoires
|
The party is visible in those centers: Le tiers est visible dans ces centres
|
||||||
The party is not visible in any center: Le tiers n'est associé à aucun territoire
|
The party is not visible in any center: Le tiers n'est associé à aucun centre
|
||||||
No third parties: Aucun tiers
|
No third parties: Aucun tiers
|
||||||
Any third party selected: Aucun tiers sélectionné
|
Any third party selected: Aucun tiers sélectionné
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user