mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-10-06 13:29:43 +00:00
Compare commits
22 Commits
v4.4.0
...
425-rename
Author | SHA1 | Date | |
---|---|---|---|
8bc46d4af3 | |||
09aa8bc829 | |||
6cc6cf3a71 | |||
bc2fbee5c6 | |||
ebd10ca522 | |||
d3a31be412
|
|||
d159a82f88
|
|||
c2d9c73fd4
|
|||
0d6d15fcf7 | |||
f9ad96c78b
|
|||
fcc9529a20
|
|||
955cb817c4
|
|||
823f9546b9 | |||
be39fa16e7 | |||
c8bb7575e7 | |||
|
80a3734171 | ||
ab98f3a102
|
|||
7516e68d77 | |||
7b60b7a8af | |||
d984dec7db
|
|||
46a4dedab8 | |||
db98519e65 |
6
.changes/unreleased/Fixed-20251003-224044.yaml
Normal file
6
.changes/unreleased/Fixed-20251003-224044.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
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
|
6
.changes/unreleased/Fixed-20251006-121315.yaml
Normal file
6
.changes/unreleased/Fixed-20251006-121315.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
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
|
6
.changes/unreleased/UX-20251006-123932.yaml
Normal file
6
.changes/unreleased/UX-20251006-123932.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
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 regroupement: par type d'activité associé au parcours;
|
||||
- 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 centre dans les exports
|
||||
- ajout d'un paramètre qui permet de désactiver le filtre par territoire dans les exports
|
||||
- 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 service des intervenants sur un parcours;
|
||||
- ajout d'un regroupement par utilisateur intervenant sur un parcours
|
||||
- ajout d'un regroupement "par centre de l'usager";
|
||||
- ajout d'un regroupement "par territoire de l'usager";
|
||||
- ajout d'un filtre "par métier 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);
|
||||
|
3
.changes/v4.4.1.md
Normal file
3
.changes/v4.4.1.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## v4.4.1 - 2025-09-11
|
||||
### Fixed
|
||||
* fix translations in duplicate evaluation document modal and realign close modal button
|
3
.changes/v4.4.2.md
Normal file
3
.changes/v4.4.2.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## v4.4.2 - 2025-09-12
|
||||
### Fixed
|
||||
* Fix document generation and workflow generation do not work on accompanying period work documents
|
13
.changes/v4.5.0.md
Normal file
13
.changes/v4.5.0.md
Normal file
@@ -0,0 +1,13 @@
|
||||
## 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
|
4
.changes/v4.5.1.md
Normal file
4
.changes/v4.5.1.md
Normal file
@@ -0,0 +1,4 @@
|
||||
## 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
|
31
CHANGELOG.md
31
CHANGELOG.md
@@ -6,6 +6,33 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
||||
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
|
||||
### Fixed
|
||||
* Fix document generation and workflow generation do not work on accompanying period work documents
|
||||
|
||||
## v4.4.1 - 2025-09-11
|
||||
### Fixed
|
||||
* fix translations in duplicate evaluation document modal and realign close modal button
|
||||
|
||||
## v4.4.0 - 2025-09-11
|
||||
### Feature
|
||||
* ([#359](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/359)) Allow the merge of two accompanying period works
|
||||
@@ -734,7 +761,7 @@ Fix color of Chill footer
|
||||
- ajout d'un filtre et regroupement par usager participant sur les échanges
|
||||
- ajout d'un regroupement: par type d'activité associé au parcours;
|
||||
- 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 centre dans les exports
|
||||
- ajout d'un paramètre qui permet de désactiver le filtre par territoire dans les exports
|
||||
- correction de l'interface de date dans les filtres et regroupements "par statut du parcours à la date"
|
||||
|
||||
## v2.9.2 - 2023-10-17
|
||||
@@ -914,7 +941,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 service des intervenants sur un parcours;
|
||||
- ajout d'un regroupement par utilisateur intervenant sur un parcours
|
||||
- ajout d'un regroupement "par centre de l'usager";
|
||||
- ajout d'un regroupement "par territoire de l'usager";
|
||||
- ajout d'un filtre "par métier 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);
|
||||
|
@@ -23,8 +23,8 @@ class "Document" {
|
||||
- text description
|
||||
- ArrayCollection_DocumentCategory categories
|
||||
- varchar_150 content #link to openstack
|
||||
- Center center
|
||||
- Cercle cercle
|
||||
- Territoire territoire
|
||||
- Service service
|
||||
- User user
|
||||
- DateTime date # Creation date
|
||||
}
|
||||
|
@@ -38,7 +38,7 @@ Certaines données sont historisées:
|
||||
|
||||
- les référents d'un parcours;
|
||||
- les statuts d'un parcours;
|
||||
- la liaison entre les centres et les usagers;
|
||||
- la liaison entre les territoires et les usagers;
|
||||
- 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.
|
||||
|
@@ -1,6 +1,6 @@
|
||||
order,table_schema,table_name,commentaire
|
||||
1,chill_3party,party_category,Catégorie de tiers
|
||||
2,chill_3party,party_center,Association entre les tiers et les centres (déprécié)
|
||||
2,chill_3party,party_center,Association entre les tiers et les territoires (déprécié)
|
||||
3,chill_3party,party_profession,Profession du tiers (déprécié)
|
||||
4,chill_3party,third_party,Tiers
|
||||
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
|
||||
54,public,activitytype,Types d'échanges
|
||||
55,public,activitytypecategory,Catégories de types d'échanges
|
||||
56,public,centers,"Centres (territoires, agences, etc.)"
|
||||
56,public,centers,"Territoires (territoires, agences, etc.)"
|
||||
57,public,chill_activity_activity_chill_person_socialaction,
|
||||
58,public,chill_activity_activity_chill_person_socialissue
|
||||
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
|
||||
111,public,chill_person_not_duplicate,
|
||||
112,public,chill_person_person,Usagers
|
||||
113,public,chill_person_person_center_history,Historique des centres d'un usagers
|
||||
113,public,chill_person_person_center_history,Historique des territoires d'un usagers
|
||||
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
|
||||
116,public,chill_person_relations,Types de relations de filiation
|
||||
@@ -142,7 +142,7 @@ order,table_schema,table_name,commentaire
|
||||
141,public,permission_groups
|
||||
142,public,permissionsgroup_rolescope
|
||||
143,public,persons_spoken_languages
|
||||
144,public,regroupment,Regroupement de centres
|
||||
144,public,regroupment,Regroupement de territoires
|
||||
145,public,regroupment_center,
|
||||
146,public,role_scopes,
|
||||
147,public,scopes,Services
|
||||
|
Can't render this file because it has a wrong number of fields in line 28.
|
@@ -55,6 +55,7 @@
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/leaflet": "^1.9.3",
|
||||
"@vueuse/core": "^13.9.0",
|
||||
"bootstrap-icons": "^1.11.3",
|
||||
"dropzone": "^5.7.6",
|
||||
"es6-promise": "^4.2.8",
|
||||
|
@@ -10,7 +10,7 @@ Attendee: Présence de l'usager
|
||||
attendee: présence de l'usager
|
||||
list_reasons: liste des sujets
|
||||
user_username: nom de l'utilisateur
|
||||
circle_name: nom du cercle
|
||||
circle_name: nom du service
|
||||
Remark: Commentaire
|
||||
No comments: Aucun commentaire
|
||||
Add a new activity: Ajouter une nouvel échange
|
||||
@@ -20,7 +20,7 @@ not present: absent
|
||||
Delete: Supprimer
|
||||
Update: Mettre à jour
|
||||
Update activity: Modifier l'échange
|
||||
Scope: Cercle
|
||||
Scope: Service
|
||||
Activity data: Données de l'échange
|
||||
Activity location: Localisation de l'échange
|
||||
No reason associated: Aucun sujet
|
||||
@@ -398,7 +398,7 @@ export:
|
||||
sent received: Envoyé ou reçu
|
||||
emergency: Urgence
|
||||
accompanying course id: Identifiant du parcours
|
||||
course circles: Cercles du parcours
|
||||
course circles: Services du parcours
|
||||
travelTime: Durée de déplacement
|
||||
durationTime: Durée
|
||||
id: Identifiant
|
||||
|
@@ -177,7 +177,7 @@ export:
|
||||
agent_id: Utilisateur
|
||||
creator_id: Créateur
|
||||
main_scope: Service principal de l'utilisateur
|
||||
main_center: Centre principal de l'utilisateur
|
||||
main_center: Territoire principal de l'utilisateur
|
||||
aside_activity_type: Catégorie d'activité annexe
|
||||
date: Date
|
||||
duration: Durée
|
||||
|
@@ -59,7 +59,7 @@ final readonly class StoredObjectVersionApiController
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize(
|
||||
new Collection($items, $paginator),
|
||||
new Collection(array_values($items->toArray()), $paginator),
|
||||
'json',
|
||||
[AbstractNormalizer::GROUPS => ['read', StoredObjectVersionNormalizer::WITH_POINT_IN_TIMES_CONTEXT, StoredObjectVersionNormalizer::WITH_RESTORED_CONTEXT]]
|
||||
),
|
||||
|
@@ -0,0 +1,20 @@
|
||||
<?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";
|
||||
uniqueKey: string;
|
||||
key: string;
|
||||
identifiers: object;
|
||||
identifiers: { id: number };
|
||||
context: "person" | "accompanying-period";
|
||||
doc_date: DateTime;
|
||||
metadata: GenericDocMetadata;
|
||||
|
@@ -4,7 +4,7 @@ import { StoredObject, StoredObjectVersion } from "../../types";
|
||||
import DropFileWidget from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileWidget.vue";
|
||||
import { computed, reactive } from "vue";
|
||||
import { useToast } from "vue-toast-notification";
|
||||
import { DOCUMENT_REPLACE, DOCUMENT_ADD, trans } from "translator";
|
||||
import { DOCUMENT_ADD, trans } from "translator";
|
||||
|
||||
interface DropFileConfig {
|
||||
allowRemove: boolean;
|
||||
@@ -78,9 +78,7 @@ function closeModal(): void {
|
||||
>
|
||||
{{ trans(DOCUMENT_ADD) }}
|
||||
</button>
|
||||
<button v-else @click="openModal" class="dropdown-item">
|
||||
{{ trans(DOCUMENT_REPLACE) }}
|
||||
</button>
|
||||
<button v-else @click="openModal" class="btn btn-edit"></button>
|
||||
<modal
|
||||
v-if="state.showModal"
|
||||
:modal-dialog-class="modalClasses"
|
||||
|
@@ -3,9 +3,9 @@ import {
|
||||
StoredObject,
|
||||
StoredObjectPointInTime,
|
||||
StoredObjectVersionWithPointInTime,
|
||||
} from "./../../../types";
|
||||
} from "ChillDocStoreAssets/types";
|
||||
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
|
||||
import { ISOToDatetime } from "./../../../../../../ChillMainBundle/Resources/public/chill/js/date";
|
||||
import { ISOToDatetime } from "ChillMainAssets/chill/js/date";
|
||||
import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue";
|
||||
import RestoreVersionButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/RestoreVersionButton.vue";
|
||||
import DownloadButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DownloadButton.vue";
|
||||
|
@@ -46,6 +46,16 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
|
||||
|
||||
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
|
||||
$entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);
|
||||
|
||||
@@ -66,7 +76,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
|
||||
return match ($workflowPermission) {
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT => true,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED => false,
|
||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN => $regularPermission,
|
||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN => WorkflowRelatedEntityPermissionHelper::FORCE_GRANT === $workflowPermissionAsAttachment || $regularPermission,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -14,6 +14,12 @@ namespace Chill\DocStoreBundle\Security\Authorization;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
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
|
||||
{
|
||||
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool;
|
||||
|
@@ -15,6 +15,7 @@ use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTime;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Chill\DocStoreBundle\Exception\ConversionWithSameMimeTypeException;
|
||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||
use Chill\WopiBundle\Service\WopiConverter;
|
||||
use Symfony\Component\Mime\MimeTypesInterface;
|
||||
@@ -44,6 +45,7 @@ class StoredObjectToPdfConverter
|
||||
* @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 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
|
||||
{
|
||||
@@ -56,7 +58,7 @@ class StoredObjectToPdfConverter
|
||||
$currentVersion = $storedObject->getCurrentVersion();
|
||||
|
||||
if ($currentVersion->getType() === $newMimeType) {
|
||||
throw new \UnexpectedValueException('Already at the same mime type');
|
||||
throw new ConversionWithSameMimeTypeException($newMimeType);
|
||||
}
|
||||
|
||||
$content = $this->storedObjectManager->read($currentVersion);
|
||||
|
@@ -40,6 +40,10 @@ class StoredObjectVersionApiControllerTest extends \PHPUnit\Framework\TestCase
|
||||
$storedObject->registerVersion();
|
||||
}
|
||||
|
||||
// remove one version in the history
|
||||
$v5 = $storedObject->getVersions()->get(5);
|
||||
$storedObject->removeVersion($v5);
|
||||
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)
|
||||
->willReturn(true)
|
||||
@@ -53,6 +57,7 @@ class StoredObjectVersionApiControllerTest extends \PHPUnit\Framework\TestCase
|
||||
self::assertEquals($response->getStatusCode(), 200);
|
||||
self::assertIsArray($body);
|
||||
self::assertArrayHasKey('results', $body);
|
||||
self::assertIsList($body['results']);
|
||||
self::assertCount(10, $body['results']);
|
||||
}
|
||||
|
||||
|
@@ -86,9 +86,165 @@ class AbstractStoredObjectVoterTest extends TestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataProviderVoteOnAttribute
|
||||
* @dataProvider dataProviderVoteOnAttributeWithStoredObjectPermission
|
||||
*/
|
||||
public function testVoteOnAttribute(
|
||||
public function testVoteOnAttributeWithStoredObjectPermission(
|
||||
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,
|
||||
bool $expected,
|
||||
bool $canBeAssociatedWithWorkflow,
|
||||
@@ -105,6 +261,10 @@ class AbstractStoredObjectVoterTest extends TestCase
|
||||
$security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
|
||||
|
||||
$workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class);
|
||||
|
||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN);
|
||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN);
|
||||
|
||||
if (null !== $isGrantedWorkflowPermissionRead) {
|
||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
|
||||
->willReturn($isGrantedWorkflowPermissionRead)->shouldBeCalled();
|
||||
@@ -123,7 +283,7 @@ class AbstractStoredObjectVoterTest extends TestCase
|
||||
self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message);
|
||||
}
|
||||
|
||||
public static function dataProviderVoteOnAttribute(): iterable
|
||||
public static function dataProviderVoteOnAttributeWithoutStoredObjectPermission(): iterable
|
||||
{
|
||||
// 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'];
|
||||
|
@@ -246,7 +246,7 @@ final class EventController extends AbstractController
|
||||
'class' => Center::class,
|
||||
'choices' => $centers,
|
||||
'placeholder' => $this->translator->trans('Pick a center'),
|
||||
'label' => 'To which centre should the event be associated ?',
|
||||
'label' => 'To which territory should the event be associated ?',
|
||||
])
|
||||
->add('submit', SubmitType::class, [
|
||||
'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
|
||||
Next step: Étape suivante
|
||||
To which centre should the event be associated ?: À quel centre doit être associé l'événement ?
|
||||
To which territory should the event be associated ?: À quel territoire doit être associé l'événement ?
|
||||
|
||||
# timeline
|
||||
past: passé
|
||||
@@ -151,7 +151,7 @@ event:
|
||||
filter:
|
||||
event_types: Par types d'événement
|
||||
event_dates: Par date d'événement
|
||||
center: Par centre
|
||||
center: Par territoire
|
||||
by_responsable: Par responsable
|
||||
pick_responsable: Filtrer par responsables
|
||||
budget:
|
||||
@@ -188,7 +188,7 @@ event_id: Identifiant
|
||||
event_name: Nom
|
||||
event_date: Date
|
||||
event_type: Type d'évenement
|
||||
event_center: Centre
|
||||
event_center: Territoire
|
||||
event_moderator: Responsable
|
||||
event_participants_count: Nombre de participants
|
||||
event_location: Localisation
|
||||
|
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\CRUD\Controller\ApiController;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
@@ -27,7 +28,7 @@ use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
class WorkflowApiController
|
||||
class WorkflowApiController extends ApiController
|
||||
{
|
||||
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,
|
||||
$request,
|
||||
EntityWorkflowStepSignatureVoter::CANCEL,
|
||||
function (EntityWorkflowStepSignature $signature) {$this->signatureStepStateChanger->markSignatureAsCanceled($signature); },
|
||||
fn (EntityWorkflowStepSignature $signature): string => $this->signatureStepStateChanger->markSignatureAsCanceled($signature),
|
||||
'@ChillMain/WorkflowSignature/cancel.html.twig',
|
||||
);
|
||||
}
|
||||
@@ -56,11 +56,18 @@ final readonly class WorkflowSignatureStateChangeController
|
||||
$signature,
|
||||
$request,
|
||||
EntityWorkflowStepSignatureVoter::REJECT,
|
||||
function (EntityWorkflowStepSignature $signature) {$this->signatureStepStateChanger->markSignatureAsRejected($signature); },
|
||||
fn (EntityWorkflowStepSignature $signature): string => $this->signatureStepStateChanger->markSignatureAsRejected($signature),
|
||||
'@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(
|
||||
EntityWorkflowStepSignature $signature,
|
||||
Request $request,
|
||||
@@ -79,12 +86,13 @@ final readonly class WorkflowSignatureStateChangeController
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->entityManager->wrapInTransaction(function () use ($signature, $markSignature) {
|
||||
$markSignature($signature);
|
||||
});
|
||||
$expectedStep = $this->entityManager->wrapInTransaction(fn () => $markSignature($signature));
|
||||
|
||||
return new RedirectResponse(
|
||||
$this->chillUrlGenerator->returnPathOr('chill_main_workflow_show', ['id' => $signature->getStep()->getEntityWorkflow()->getId()])
|
||||
$this->chillUrlGenerator->forwardReturnPath(
|
||||
'chill_main_workflow_wait',
|
||||
['id' => $signature->getStep()->getEntityWorkflow()->getId(), 'expectedStep' => $expectedStep]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -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\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,6 +30,7 @@ use Chill\MainBundle\Controller\UserGroupAdminController;
|
||||
use Chill\MainBundle\Controller\UserGroupApiController;
|
||||
use Chill\MainBundle\Controller\UserJobApiController;
|
||||
use Chill\MainBundle\Controller\UserJobController;
|
||||
use Chill\MainBundle\Controller\WorkflowApiController;
|
||||
use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface;
|
||||
use Chill\MainBundle\Doctrine\DQL\Age;
|
||||
use Chill\MainBundle\Doctrine\DQL\Extract;
|
||||
@@ -66,6 +67,7 @@ use Chill\MainBundle\Entity\Regroupment;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Chill\MainBundle\Entity\UserJob;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Form\CenterType;
|
||||
use Chill\MainBundle\Form\CivilityType;
|
||||
use Chill\MainBundle\Form\CountryType;
|
||||
@@ -79,6 +81,7 @@ use Chill\MainBundle\Form\UserGroupType;
|
||||
use Chill\MainBundle\Form\UserJobType;
|
||||
use Chill\MainBundle\Form\UserType;
|
||||
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
|
||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
|
||||
use Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType;
|
||||
use Ramsey\Uuid\Doctrine\UuidType;
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
@@ -940,6 +943,21 @@ 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,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
@@ -59,7 +59,7 @@ class UserPasswordType extends AbstractType
|
||||
'invalid_message' => 'The password fields must match',
|
||||
'constraints' => [
|
||||
new Length([
|
||||
'min' => 9,
|
||||
'min' => 14,
|
||||
'minMessage' => 'The password must be greater than {{ limit }} characters',
|
||||
]),
|
||||
new NotBlank(),
|
||||
|
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
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,5 +1,6 @@
|
||||
import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
|
||||
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
|
||||
import { Person } from "../../../ChillPersonBundle/Resources/public/types";
|
||||
|
||||
export interface DateTime {
|
||||
datetime: string;
|
||||
@@ -202,6 +203,58 @@ export interface WorkflowAttachment {
|
||||
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 {
|
||||
id: string;
|
||||
type: "export_generation";
|
||||
@@ -215,3 +268,8 @@ export interface ExportGeneration {
|
||||
export interface PrivateCommentEmbeddable {
|
||||
comments: Record<number, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Possible states for the WaitingScreen Component.
|
||||
*/
|
||||
export type WaitingScreenState = "pending" | "failure" | "stopped" | "ready";
|
||||
|
@@ -10,7 +10,8 @@ import { computed, onMounted, ref } from "vue";
|
||||
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
|
||||
import { fetchExportGenerationStatus } from "ChillMainAssets/lib/api/export";
|
||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
|
||||
import { ExportGeneration } from "ChillMainAssets/types";
|
||||
import WaitingScreen from "../_components/WaitingScreen.vue";
|
||||
import { ExportGeneration, WaitingScreenState } from "ChillMainAssets/types";
|
||||
|
||||
interface AppProps {
|
||||
exportGenerationId: string;
|
||||
@@ -34,13 +35,16 @@ const storedObject = computed<null | StoredObject>(() => {
|
||||
});
|
||||
|
||||
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 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
|
||||
*/
|
||||
@@ -85,38 +89,26 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="waiting-screen">
|
||||
<div
|
||||
v-if="isPending && isFetching"
|
||||
class="alert alert-danger text-center"
|
||||
>
|
||||
<div>
|
||||
<WaitingScreen :state="state">
|
||||
<template v-slot:pending>
|
||||
<p>
|
||||
{{ trans(EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING) }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<i class="fa fa-cog fa-spin fa-3x fa-fw"></i>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isPending && !isFetching" class="alert alert-info">
|
||||
<div>
|
||||
<template v-slot:stopped>
|
||||
<p>
|
||||
{{ trans(EXPORT_GENERATION_TOO_MANY_RETRIES) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isFailure" class="alert alert-danger text-center">
|
||||
<div>
|
||||
</template>
|
||||
|
||||
<template v-slot:failure>
|
||||
<p>
|
||||
{{ trans(EXPORT_GENERATION_ERROR_WHILE_GENERATING_EXPORT) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isReady" class="alert alert-success text-center">
|
||||
<div>
|
||||
</template>
|
||||
|
||||
<template v-slot:ready>
|
||||
<p>
|
||||
{{ trans(EXPORT_GENERATION_EXPORT_READY) }}
|
||||
</p>
|
||||
@@ -127,15 +119,6 @@ onMounted(() => {
|
||||
:filename="filename"
|
||||
></document-action-buttons-group>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
#waiting-screen {
|
||||
> .alert {
|
||||
min-height: 350px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</WaitingScreen>
|
||||
</template>
|
||||
|
@@ -0,0 +1,75 @@
|
||||
<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>
|
@@ -0,0 +1,51 @@
|
||||
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,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, useTemplateRef } from "vue";
|
||||
import type { WorkflowAttachment } from "ChillMainAssets/types";
|
||||
import { computed, onMounted, ref, useTemplateRef } from "vue";
|
||||
import type { EntityWorkflow, WorkflowAttachment } from "ChillMainAssets/types";
|
||||
import PickGenericDocModal from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDocModal.vue";
|
||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
|
||||
import AttachmentList from "ChillMainAssets/vuejs/WorkflowAttachment/Component/AttachmentList.vue";
|
||||
import { GenericDoc } from "ChillDocStoreAssets/types";
|
||||
import { fetchWorkflow } from "ChillMainAssets/lib/workflow/api";
|
||||
|
||||
interface AppConfig {
|
||||
workflowId: number;
|
||||
@@ -34,6 +35,13 @@ const attachedGenericDoc = computed<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 () {
|
||||
pickDocModal.value?.openModal();
|
||||
};
|
||||
@@ -49,20 +57,30 @@ const onPickGenericDoc = ({
|
||||
const onRemoveAttachment = (payload: { attachment: WorkflowAttachment }) => {
|
||||
emit("removeAttachment", payload);
|
||||
};
|
||||
|
||||
const canEditAttachement = computed<boolean>(() => {
|
||||
if (null === workflow.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return workflow.value._permissions.CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<pick-generic-doc-modal
|
||||
:workflow="workflow"
|
||||
:accompanying-period-id="props.accompanyingPeriodId"
|
||||
:to-remove="attachedGenericDoc"
|
||||
ref="pickDocModal"
|
||||
@pickGenericDoc="onPickGenericDoc"
|
||||
></pick-generic-doc-modal>
|
||||
<attachment-list
|
||||
:workflow="workflow"
|
||||
:attachments="props.attachments"
|
||||
@removeAttachment="onRemoveAttachment"
|
||||
></attachment-list>
|
||||
<ul class="record_actions">
|
||||
<ul v-if="canEditAttachement" class="record_actions">
|
||||
<li>
|
||||
<button type="button" class="btn btn-create" @click="openModal">
|
||||
Ajouter une pièce jointe
|
||||
|
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { WorkflowAttachment } from "ChillMainAssets/types";
|
||||
import { EntityWorkflow, WorkflowAttachment } from "ChillMainAssets/types";
|
||||
import GenericDocItemBox from "ChillMainAssets/vuejs/WorkflowAttachment/Component/GenericDocItemBox.vue";
|
||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
|
||||
|
||||
interface AttachmentListProps {
|
||||
attachments: WorkflowAttachment[];
|
||||
workflow: EntityWorkflow | null;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -36,7 +37,12 @@ const props = defineProps<AttachmentListProps>();
|
||||
:stored-object="a.genericDoc.storedObject"
|
||||
></document-action-buttons-group>
|
||||
</li>
|
||||
<li>
|
||||
<li
|
||||
v-if="
|
||||
!workflow?._permissions
|
||||
.CHILL_MAIN_WORKFLOW_ATTACHMENT_EDIT
|
||||
"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-delete"
|
||||
|
@@ -6,8 +6,10 @@ import {
|
||||
import PickGenericDocItem from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDocItem.vue";
|
||||
import { fetch_generic_docs_by_accompanying_period } from "ChillDocStoreAssets/js/generic-doc-api";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { EntityWorkflow } from "ChillMainAssets/types";
|
||||
|
||||
interface PickGenericDocProps {
|
||||
workflow: EntityWorkflow | null;
|
||||
accompanyingPeriodId: number;
|
||||
pickedList: GenericDocForAccompanyingPeriod[];
|
||||
toRemove: GenericDocForAccompanyingPeriod[];
|
||||
@@ -36,9 +38,21 @@ const isPicked = (genericDoc: GenericDocForAccompanyingPeriod): boolean =>
|
||||
) !== -1;
|
||||
|
||||
onMounted(async () => {
|
||||
genericDocs.value = await fetch_generic_docs_by_accompanying_period(
|
||||
const fetchedGenericDocs = await fetch_generic_docs_by_accompanying_period(
|
||||
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;
|
||||
});
|
||||
|
||||
|
@@ -3,8 +3,10 @@ import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
||||
import { computed, ref, useTemplateRef } from "vue";
|
||||
import PickGenericDoc from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDoc.vue";
|
||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
|
||||
import { EntityWorkflow } from "ChillMainAssets/types";
|
||||
|
||||
interface PickGenericDocModalProps {
|
||||
workflow: EntityWorkflow | null;
|
||||
accompanyingPeriodId: number;
|
||||
toRemove: GenericDocForAccompanyingPeriod[];
|
||||
}
|
||||
@@ -80,6 +82,7 @@ defineExpose({ openModal, closeModal });
|
||||
</template>
|
||||
<template v-slot:body>
|
||||
<pick-generic-doc
|
||||
:workflow="props.workflow"
|
||||
:accompanying-period-id="props.accompanyingPeriodId"
|
||||
:to-remove="props.toRemove"
|
||||
:picked-list="pickeds"
|
||||
|
@@ -84,6 +84,8 @@ const emits = defineEmits<{
|
||||
}
|
||||
.modal-header .close {
|
||||
border-top-right-radius: 0.3rem;
|
||||
margin-right: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
/*
|
||||
* The following styles are auto-applied to elements with
|
||||
|
@@ -0,0 +1,62 @@
|
||||
<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>
|
@@ -5,7 +5,7 @@
|
||||
role="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<i class="fa fa-flash"></i>
|
||||
<i class="bi bi-lightning-fill"></i>
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
{% for menu in menus %}
|
||||
|
@@ -21,8 +21,6 @@
|
||||
{{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
|
||||
{{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
|
||||
|
||||
{{ form_row(form.addressesEmails) }}
|
||||
|
||||
{% include handler.template(notification) with handler.templateData(notification) %}
|
||||
|
||||
<div class="mb-3 row">
|
||||
|
@@ -61,7 +61,7 @@
|
||||
{% endif %}
|
||||
</li>
|
||||
<li>
|
||||
<span class="dt">cercle/centre:</span>
|
||||
<span class="dt">service/territoire:</span>
|
||||
{% if entity.mainScope %}
|
||||
{{ entity.mainScope.name|localize_translatable_string }}
|
||||
{% endif %}
|
||||
|
@@ -58,12 +58,14 @@
|
||||
{% endif %}
|
||||
</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/_follow.html.twig' %}</section>
|
||||
{% if signatures|length > 0 %}
|
||||
<section class="step my-4">{% include '@ChillMain/Workflow/_signature.html.twig' %}</section>
|
||||
{% elseif entity_workflow.currentStep.sends|length > 0 %}
|
||||
{% if entity_workflow.currentStep.sends|length > 0 %}
|
||||
<section class="step my-4">
|
||||
<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} %}
|
||||
|
@@ -0,0 +1,18 @@
|
||||
{% 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',
|
||||
])->setExtras(['order' => 1010]);
|
||||
|
||||
$menu->addChild('Regroupements des centres', [
|
||||
$menu->addChild('Regroupements des territoires', [
|
||||
'route' => 'chill_crud_regroupment_index',
|
||||
])->setExtras(['order' => 1015]);
|
||||
|
||||
|
@@ -0,0 +1,53 @@
|
||||
<?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,18 +12,25 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Serializer\Normalizer;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowAttachmentVoter;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
|
||||
class EntityWorkflowNormalizer implements NormalizerInterface, NormalizerAwareInterface
|
||||
final class EntityWorkflowNormalizer implements NormalizerInterface, NormalizerAwareInterface
|
||||
{
|
||||
use NormalizerAwareTrait;
|
||||
|
||||
public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly MetadataExtractor $metadataExtractor, private readonly Registry $registry) {}
|
||||
public function __construct(
|
||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||
private readonly MetadataExtractor $metadataExtractor,
|
||||
private readonly Registry $registry,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param EntityWorkflow $object
|
||||
@@ -46,6 +53,9 @@ class EntityWorkflowNormalizer implements NormalizerInterface, NormalizerAwareIn
|
||||
'datas' => $this->normalizer->normalize($handler->getEntityData($object), $format, $context),
|
||||
'title' => $handler->getEntityTitle($object),
|
||||
'isOnHoldAtCurrentStep' => $object->isOnHoldAtCurrentStep(),
|
||||
'_permissions' => [
|
||||
EntityWorkflowAttachmentVoter::EDIT => $this->security->isGranted(EntityWorkflowAttachmentVoter::EDIT, $object),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -35,7 +35,7 @@ final class ScopeControllerTest extends WebTestCase
|
||||
$client->getResponse()->getStatusCode(),
|
||||
'Unexpected HTTP status code for GET /fr/admin/scope/'
|
||||
);
|
||||
$crawler = $client->click($crawler->selectLink('Créer un nouveau cercle')->link());
|
||||
$crawler = $client->click($crawler->selectLink('Créer un nouveau service')->link());
|
||||
// Fill in the form and submit it
|
||||
$form = $crawler->selectButton('Créer')->form([
|
||||
'chill_mainbundle_scope[name][fr]' => 'Test en fr',
|
||||
|
@@ -45,7 +45,7 @@ final class UserControllerTest extends WebTestCase
|
||||
self::assertResponseIsSuccessful();
|
||||
|
||||
$username = 'Test_user'.uniqid();
|
||||
$password = 'Password1234!';
|
||||
$password = 'Password_1234!';
|
||||
|
||||
// Fill in the form and submit it
|
||||
|
||||
@@ -99,7 +99,7 @@ final class UserControllerTest extends WebTestCase
|
||||
{
|
||||
$client = $this->getClientAuthenticatedAsAdmin();
|
||||
$crawler = $client->request('GET', "/fr/admin/user/{$userId}/edit_password");
|
||||
$newPassword = '1234Password!';
|
||||
$newPassword = '1234_Password!';
|
||||
|
||||
$form = $crawler->selectButton('Changer le mot de passe')->form([
|
||||
'chill_mainbundle_user_password[new_password][first]' => $newPassword,
|
||||
|
@@ -0,0 +1,173 @@
|
||||
<?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,6 +11,9 @@ declare(strict_types=1);
|
||||
|
||||
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\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
@@ -148,8 +151,11 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
|
||||
$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'];
|
||||
yield [[], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
|
||||
'abstain because there is no workflow'];
|
||||
|
||||
yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(),
|
||||
'force deny because the user is not present as a dest user'];
|
||||
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
@@ -171,6 +177,9 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
|
||||
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
|
||||
'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();
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$dto->futureDestUsers[] = $user = new User();
|
||||
@@ -232,6 +241,13 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
|
||||
|
||||
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'];
|
||||
}
|
||||
|
||||
public function testNoWorkflow(): void
|
||||
@@ -253,7 +269,217 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
|
||||
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
||||
$entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn($entityWorkflows);
|
||||
|
||||
return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable()));
|
||||
$repository = $this->prophesize(EntityWorkflowAttachmentRepository::class);
|
||||
$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
|
||||
@@ -261,10 +487,13 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
|
||||
$builder = new DefinitionBuilder();
|
||||
$builder
|
||||
->setInitialPlaces(['initial'])
|
||||
->addPlaces(['initial', 'test', 'final_positive', 'final_negative'])
|
||||
->addPlaces(['initial', 'test', 'sent_external', 'final_positive', 'final_negative'])
|
||||
->setMetadataStore(
|
||||
new InMemoryMetadataStore(
|
||||
placesMetadata: [
|
||||
'sent_external' => [
|
||||
'isSentExternal' => true,
|
||||
],
|
||||
'final_positive' => [
|
||||
'isFinal' => true,
|
||||
'isFinalPositive' => true,
|
||||
|
@@ -11,8 +11,11 @@ declare(strict_types=1);
|
||||
|
||||
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\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
@@ -23,6 +26,7 @@ use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Mime\Address;
|
||||
@@ -39,24 +43,54 @@ class PostSendExternalMessageHandlerTest extends TestCase
|
||||
public function testSendMessageHappyScenario(): void
|
||||
{
|
||||
$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->futureDestineeEmails = ['external@example.com'];
|
||||
$dto->futureDestineeThirdParties = [(new ThirdParty())->setEmail('3party@example.com')];
|
||||
$entityWorkflow->setStep('send_external', $dto, 'to_send_external', new \DateTimeImmutable(), new User());
|
||||
|
||||
// Repository returns our workflow
|
||||
$repository = $this->prophesize(EntityWorkflowRepository::class);
|
||||
$repository->find(1)->willReturn($entityWorkflow);
|
||||
|
||||
// Mailer must send to both recipients
|
||||
$mailer = $this->prophesize(MailerInterface::class);
|
||||
$mailer->send(Argument::that($this->buildCheckAddressCallback('3party@example.com')))->shouldBeCalledOnce();
|
||||
$mailer->send(Argument::that($this->buildCheckAddressCallback('external@example.com')))->shouldBeCalledOnce();
|
||||
|
||||
// Workflow manager and handler
|
||||
$workflowHandler = $this->prophesize(EntityWorkflowHandlerInterface::class);
|
||||
$workflowHandler->getEntityTitle($entityWorkflow, Argument::any())->willReturn('title');
|
||||
$workflowManager = $this->prophesize(EntityWorkflowManager::class);
|
||||
$workflowManager->getHandler($entityWorkflow)->willReturn($workflowHandler->reveal());
|
||||
|
||||
$handler = new PostSendExternalMessageHandler($repository->reveal(), $mailer->reveal(), $workflowManager->reveal());
|
||||
// Associated stored object for the workflow
|
||||
$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'));
|
||||
|
||||
|
@@ -11,9 +11,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Workflow\Helper;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
@@ -58,21 +61,39 @@ class WorkflowRelatedEntityPermissionHelper
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||
private readonly EntityWorkflowAttachmentRepository $entityWorkflowAttachmentRepository,
|
||||
private readonly Registry $registry,
|
||||
private readonly ClockInterface $clock,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param object $entity The entity may be an
|
||||
*
|
||||
* @return 'FORCE_GRANT'|'FORCE_DENIED'|'ABSTAIN'
|
||||
*/
|
||||
public function isAllowedByWorkflowForReadOperation(object $entity): string
|
||||
{
|
||||
if ($entity instanceof StoredObject) {
|
||||
$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)) {
|
||||
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
|
||||
// the signature last state
|
||||
foreach ($entityWorkflows as $workflow) {
|
||||
@@ -100,28 +121,45 @@ class WorkflowRelatedEntityPermissionHelper
|
||||
*/
|
||||
public function isAllowedByWorkflowForWriteOperation(object $entity): string
|
||||
{
|
||||
if ($entity instanceof StoredObject) {
|
||||
$attachments = $this->entityWorkflowAttachmentRepository->findByStoredObject($entity);
|
||||
$entityWorkflows = array_map(static fn (EntityWorkflowAttachment $attachment) => $attachment->getEntityWorkflow(), $attachments);
|
||||
$isAttached = true;
|
||||
} else {
|
||||
$entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
|
||||
$runningWorkflows = [];
|
||||
$isAttached = false;
|
||||
}
|
||||
|
||||
// if a workflow is finalized positive, we are not allowed to edit to document any more
|
||||
if ([] === $entityWorkflows) {
|
||||
return self::ABSTAIN;
|
||||
}
|
||||
|
||||
// if a workflow is finalized positive, anyone is allowed to edit the document anymore
|
||||
foreach ($entityWorkflows as $entityWorkflow) {
|
||||
if ($entityWorkflow->isFinal()) {
|
||||
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||
$marking = $workflow->getMarkingStore()->getMarking($entityWorkflow);
|
||||
foreach ($marking->getPlaces() as $place => $int) {
|
||||
$placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
|
||||
if (true === ($placeMetadata['isFinalPositive'] ?? false)) {
|
||||
// the workflow is final, and final positive, so we stop here.
|
||||
if (
|
||||
($entityWorkflow->isFinal() && ($placeMetadata['isFinalPositive'] ?? false))
|
||||
|| ($placeMetadata['isSentExternal'] ?? false)
|
||||
) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// if there is a signature on a **running workflow**, no one can edit the workflow any more
|
||||
$runningWorkflows = array_filter($entityWorkflows, fn (EntityWorkflow $ew) => !$ew->isFinal());
|
||||
|
||||
// if there is a signature on a **running workflow**, no one is allowed edit the workflow anymore
|
||||
if (!$isAttached) {
|
||||
foreach ($runningWorkflows as $entityWorkflow) {
|
||||
foreach ($entityWorkflow->getSteps() as $step) {
|
||||
foreach ($step->getSignatures() as $signature) {
|
||||
@@ -131,15 +169,20 @@ class WorkflowRelatedEntityPermissionHelper
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// allow only the users involved
|
||||
if ($this->isUserInvolvedInAWorkflow($runningWorkflows)) {
|
||||
return self::FORCE_GRANT;
|
||||
}
|
||||
|
||||
if ($isAttached) {
|
||||
return self::ABSTAIN;
|
||||
}
|
||||
|
||||
return self::FORCE_DENIED;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<EntityWorkflow> $entityWorkflows
|
||||
*/
|
||||
|
@@ -11,9 +11,14 @@ declare(strict_types=1);
|
||||
|
||||
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\Repository\Workflow\EntityWorkflowRepository;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
|
||||
@@ -25,6 +30,8 @@ final readonly class PostSendExternalMessageHandler implements MessageHandlerInt
|
||||
private EntityWorkflowRepository $entityWorkflowRepository,
|
||||
private MailerInterface $mailer,
|
||||
private EntityWorkflowManager $workflowManager,
|
||||
private StoredObjectToPdfConverter $storedObjectToPdfConverter,
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
public function __invoke(PostSendExternalMessage $message): void
|
||||
@@ -35,11 +42,38 @@ final readonly class PostSendExternalMessageHandler implements MessageHandlerInt
|
||||
throw new UnrecoverableMessageHandlingException(sprintf('Entity workflow with id %d not found', $message->entityWorkflowId));
|
||||
}
|
||||
|
||||
$this->convertToPdf($entityWorkflow, $message->lang);
|
||||
|
||||
foreach ($entityWorkflow->getCurrentStep()->getSends() as $send) {
|
||||
$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
|
||||
{
|
||||
$entityWorkflow = $send->getEntityWorkflowStep()->getEntityWorkflow();
|
||||
|
@@ -22,6 +22,7 @@ use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
use Symfony\Component\Workflow\Transition;
|
||||
|
||||
/**
|
||||
* Handles state changes for signature steps within a workflow.
|
||||
@@ -50,8 +51,10 @@ class SignatureStepStateChanger
|
||||
*
|
||||
* @param EntityWorkflowStepSignature $signature the signature entity to be marked as signed
|
||||
* @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): void
|
||||
public function markSignatureAsSigned(EntityWorkflowStepSignature $signature, ?int $atIndex): string
|
||||
{
|
||||
$this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
|
||||
|
||||
@@ -60,7 +63,14 @@ class SignatureStepStateChanger
|
||||
->setZoneSignatureIndex($atIndex)
|
||||
->setStateDate($this->clock->now());
|
||||
$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()));
|
||||
if (null === $transition) {
|
||||
return $signature->getStep()->getEntityWorkflow()->getStep();
|
||||
}
|
||||
|
||||
return $transition->getTos()[0];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,8 +81,10 @@ class SignatureStepStateChanger
|
||||
*
|
||||
* This method updates the signature state to 'canceled' and logs the action.
|
||||
* 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): void
|
||||
public function markSignatureAsCanceled(EntityWorkflowStepSignature $signature): string
|
||||
{
|
||||
$this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
|
||||
|
||||
@@ -80,7 +92,15 @@ class SignatureStepStateChanger
|
||||
->setState(EntityWorkflowSignatureStateEnum::CANCELED)
|
||||
->setStateDate($this->clock->now());
|
||||
$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()));
|
||||
if (null === $transition) {
|
||||
return $signature->getStep()->getEntityWorkflow()->getStep();
|
||||
}
|
||||
|
||||
return $transition->getTos()[0];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,8 +113,10 @@ class SignatureStepStateChanger
|
||||
* a state change has occurred.
|
||||
*
|
||||
* @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): void
|
||||
public function markSignatureAsRejected(EntityWorkflowStepSignature $signature): string
|
||||
{
|
||||
$this->entityManager->refresh($signature, LockMode::PESSIMISTIC_WRITE);
|
||||
|
||||
@@ -102,7 +124,16 @@ class SignatureStepStateChanger
|
||||
->setState(EntityWorkflowSignatureStateEnum::REJECTED)
|
||||
->setStateDate($this->clock->now());
|
||||
$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()));
|
||||
|
||||
if (null === $transition) {
|
||||
return $signature->getStep()->getEntityWorkflow()->getStep();
|
||||
}
|
||||
|
||||
return $transition->getTos()[0];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,10 +148,35 @@ class SignatureStepStateChanger
|
||||
{
|
||||
$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())) {
|
||||
$this->logger->info(self::LOG_PREFIX.'This is not the last signature, skipping transition to another place', ['signatureId' => $signature->getId()]);
|
||||
|
||||
return;
|
||||
return ['transition' => null, 'futureUser' => null];
|
||||
}
|
||||
|
||||
$this->logger->debug(self::LOG_PREFIX.'Continuing the process to find a transition', ['signatureId' => $signature->getId()]);
|
||||
@@ -144,7 +200,7 @@ class SignatureStepStateChanger
|
||||
if (null === $transition) {
|
||||
$this->logger->info(self::LOG_PREFIX.'The transition is not configured, will not apply a transition', ['signatureId' => $signature->getId()]);
|
||||
|
||||
return;
|
||||
return ['transition' => null, 'futureUser' => null];
|
||||
}
|
||||
|
||||
if ('person' === $signature->getSignerKind()) {
|
||||
@@ -156,19 +212,16 @@ class SignatureStepStateChanger
|
||||
if (null === $futureUser) {
|
||||
$this->logger->info(self::LOG_PREFIX.'No previous user, will not apply a transition', ['signatureId' => $signature->getId()]);
|
||||
|
||||
return;
|
||||
return ['transition' => null, 'futureUser' => null];
|
||||
}
|
||||
|
||||
$transitionDto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$transitionDto->futureDestUsers[] = $futureUser;
|
||||
foreach ($workflow->getDefinition()->getTransitions() as $transitionObj) {
|
||||
if ($transitionObj->getName() === $transition) {
|
||||
return ['transition' => $transitionObj, 'futureUser' => $futureUser];
|
||||
}
|
||||
}
|
||||
|
||||
$workflow->apply($entityWorkflow, $transition, [
|
||||
'context' => $transitionDto,
|
||||
'transitionAt' => $this->clock->now(),
|
||||
'transition' => $transition,
|
||||
]);
|
||||
|
||||
$this->logger->info(self::LOG_PREFIX.'Transition automatically applied', ['signatureId' => $signature->getId()]);
|
||||
throw new \RuntimeException('Transition not found');
|
||||
}
|
||||
|
||||
private function getPreviousSender(EntityWorkflowStep $entityWorkflowStep): ?User
|
||||
|
@@ -965,6 +965,31 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$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:
|
||||
get:
|
||||
tags:
|
||||
|
@@ -120,5 +120,8 @@ module.exports = function (encore, entries) {
|
||||
"vue_onthefly",
|
||||
__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
|
||||
Profile successfully updated!: Votre profil a été mis à jour!
|
||||
no job: Pas de métier assigné
|
||||
no scope: Pas de cercle assigné
|
||||
no scope: Pas de service assigné
|
||||
notification_preferences: Préférences pour mes notifications
|
||||
|
||||
user_group:
|
||||
@@ -102,9 +102,9 @@ createdAt: Créé le
|
||||
createdBy: Créé par
|
||||
|
||||
#elements used in software
|
||||
centers: centres
|
||||
Centers: Centres
|
||||
center: centre
|
||||
centers: territoires
|
||||
Centers: Territoires
|
||||
center: territoire
|
||||
comment: commentaire
|
||||
Comment: Commentaire
|
||||
Comments: Commentaires
|
||||
@@ -227,12 +227,12 @@ Location Menu: Localisations et types de localisation
|
||||
Management of location: Gestion des localisations et types de localisation
|
||||
|
||||
#admin section for center's administration
|
||||
Create a new center: Créer un nouveau centre
|
||||
Center list: Liste des centres
|
||||
Center edit: Édition d'un centre
|
||||
Center creation: Création d'un centre
|
||||
New center: Nouveau centre
|
||||
Center: Centre
|
||||
Create a new center: Créer une nouveau territoire
|
||||
Center list: Liste des territoires
|
||||
Center edit: Édition d'un territoire
|
||||
Center creation: Création d'un territoire
|
||||
New center: Nouveau territoire
|
||||
Center: Territoire
|
||||
|
||||
#admin section for permissions group
|
||||
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%'
|
||||
Role: Rôle
|
||||
Choose amongst roles: Choisir un rôle
|
||||
Choose amongst scopes: Choisir un cercle
|
||||
Choose amongst scopes: Choisir un service
|
||||
Add permission: Ajouter les permissions
|
||||
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%' on circle '%scope%' has been removed: Le rôle "%role%" sur le cercle "%scope%" 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
|
||||
Unclassified: Non classifié
|
||||
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 cercle.
|
||||
The role does not need scope: Ce rôle ne nécessite pas de cercle !
|
||||
Help to pick role and scope: Certains rôles ne nécessitent pas de service.
|
||||
The role need scope: Ce rôle nécessite un service.
|
||||
The role does not need scope: Ce rôle ne nécessite pas de service !
|
||||
|
||||
#admin section for users
|
||||
User configuration: Gestion des utilisateurs
|
||||
@@ -270,7 +270,7 @@ Grant new permissions: 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 where removed.: Les permissions ont été enlevées.
|
||||
Center & groups: Centre et groupes
|
||||
Center & groups: Territoire et groupes
|
||||
User %username%: Utilisateur %username%
|
||||
Add a new user: Ajouter un nouvel utilisateur
|
||||
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
|
||||
Flags: Drapeaux
|
||||
Main location: Localisation principale
|
||||
Main scope: Cercle
|
||||
Main center: Centre
|
||||
Main scope: Service
|
||||
Main center: Territoire
|
||||
user job: Métier de l'utilisateur
|
||||
Job: Métier
|
||||
Jobs: Métiers
|
||||
Choose a main center: Choisir un centre
|
||||
Choose a main scope: Choisir un cercle
|
||||
Choose a main center: Choisir un territoire
|
||||
Choose a main scope: Choisir un service
|
||||
choose a job: Choisir un métier
|
||||
choose a location: Choisir une localisation
|
||||
|
||||
@@ -302,12 +302,12 @@ Current location successfully updated: Localisation actuelle mise à jour
|
||||
Pick a location: Choisir un lieu
|
||||
|
||||
#admin section for circles (old: scopes)
|
||||
List circles: Cercles
|
||||
New circle: Nouveau cercle
|
||||
Circle: Cercle
|
||||
Circle edit: Modification du cercle
|
||||
Circle creation: Création d'un cercle
|
||||
Create a new circle: Créer un nouveau cercle
|
||||
List circles: Services
|
||||
New circle: Nouveau service
|
||||
Circle: Service
|
||||
Circle edit: Modification du service
|
||||
Circle creation: Création d'un service
|
||||
Create a new circle: Créer un nouveau service
|
||||
|
||||
#admin section for location
|
||||
Location: Localisation
|
||||
@@ -347,9 +347,9 @@ Country list: Liste des pays
|
||||
Country code: Code du pays
|
||||
|
||||
# circles / scopes
|
||||
Choose the circle: Choisir le cercle
|
||||
Scope: Cercle
|
||||
Scopes: Cercles
|
||||
Choose the circle: Choisir le service
|
||||
Scope: Service
|
||||
Scopes: Services
|
||||
|
||||
#export
|
||||
|
||||
@@ -357,14 +357,14 @@ Scopes: Cercles
|
||||
Exports list: Liste des exports
|
||||
Create an export: Créer un export
|
||||
#export creation step 'center' : pick a center
|
||||
Pick centers: Choisir les centres
|
||||
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 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 centres choisis.
|
||||
Pick centers: Choisir les territoires
|
||||
Pick a center: Choisir un territoire
|
||||
The export will contains only data from the picked centers.: L'export ne contiendra que les données des 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 territoires choisis.
|
||||
Go to export options: Vers la préparation de l'export
|
||||
Pick aggregated centers: Regroupement de centres
|
||||
uncheck all centers: Désélectionner tous les centres
|
||||
check all centers: Sélectionner tous les centres
|
||||
Pick aggregated centers: Regroupement de territoires
|
||||
uncheck all centers: Désélectionner tous les territoires
|
||||
check all centers: Sélectionner tous les territoires
|
||||
# export creation step 'export' : choose aggregators, filtering and formatter
|
||||
Formatter: Mise en forme
|
||||
Choose the formatter: Choisissez le format d'export voulu.
|
||||
@@ -510,10 +510,10 @@ crud:
|
||||
title_edit: Modifier un regroupement
|
||||
center:
|
||||
index:
|
||||
title: Liste des centres
|
||||
add_new: Ajouter un centre
|
||||
title_new: Nouveau centre
|
||||
title_edit: Modifier un centre
|
||||
title: Liste des territoires
|
||||
add_new: Ajouter un territoire
|
||||
title_new: Nouveau territoire
|
||||
title_edit: Modifier un territoire
|
||||
news_item:
|
||||
index:
|
||||
title: Liste des actualités
|
||||
@@ -666,10 +666,17 @@ workflow:
|
||||
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_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:
|
||||
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 all steps: Recevoir une notification à chaque étape
|
||||
CHILL_MAIN_WORKFLOW_APPLY_ALL_TRANSITION: Appliquer les transitions sur tous les workflows
|
||||
@@ -853,7 +860,7 @@ absence:
|
||||
admin:
|
||||
users:
|
||||
export_list_csv: Liste des utilisateurs (format CSV)
|
||||
export_permissions_csv: Association utilisateurs - groupes de permissions - centre (format CSV)
|
||||
export_permissions_csv: Association utilisateurs - groupes de permissions - territoire (format CSV)
|
||||
export:
|
||||
id: Identifiant
|
||||
username: Nom d'utilisateur
|
||||
@@ -863,8 +870,8 @@ admin:
|
||||
civility_abbreviation: Abbréviation civilité
|
||||
civility_name: Civilité
|
||||
label: Label
|
||||
mainCenter_id: Identifiant centre principal
|
||||
mainCenter_name: Centre principal
|
||||
mainCenter_id: Identifiant territoire principal
|
||||
mainCenter_name: Territoire principal
|
||||
mainScope_id: Identifiant service principal
|
||||
mainScope_name: Service principal
|
||||
userJob_id: Identifiant métier
|
||||
@@ -874,8 +881,8 @@ admin:
|
||||
mainLocation_id: Identifiant localisation principale
|
||||
mainLocation_name: Localisation principale
|
||||
absenceStart: Absent à partir du
|
||||
center_id: Identifiant du centre
|
||||
center_name: Centre
|
||||
center_id: Identifiant du territoire
|
||||
center_name: Territoire
|
||||
permissionsGroup_id: Identifiant du groupe de permissions
|
||||
permissionsGroup_name: Groupe de permissions
|
||||
job_scope_histories:
|
||||
|
@@ -1,15 +1,15 @@
|
||||
# role_scope constraint
|
||||
# scope presence
|
||||
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 cercle.
|
||||
The role "%role%" require to be associated with a scope.: Le rôle "%role%" doit être associé à un service.
|
||||
The role "%role%" should not be associated with a scope.: Le rôle "%role%" ne doit pas être associé à un service.
|
||||
"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 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 cercle.
|
||||
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.
|
||||
|
||||
#UserCircleConsistency
|
||||
"{{ username }} is not allowed to see entities published in this circle": "{{ username }} n'est pas autorisé à voir l'élément publié dans ce cercle."
|
||||
"{{ username }} is not allowed to see entities published in this circle": "{{ username }} n'est pas autorisé à voir l'élément publié dans ce service."
|
||||
|
||||
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",
|
||||
lastName: "Nom",
|
||||
birthdate: "Date de naissance",
|
||||
center: "Centre",
|
||||
center: "Territoire",
|
||||
phonenumber: "Téléphone",
|
||||
mobilenumber: "Mobile",
|
||||
altNames: "Autres noms",
|
||||
|
@@ -6,7 +6,7 @@
|
||||
:id="evaluation.id"
|
||||
:templates="templates"
|
||||
:preventDefaultMoveToGenerate="true"
|
||||
@go-to-generate-document="$emit('submitBeforeGenerate', $event)"
|
||||
@go-to-generate-document="submitBeforeGenerate"
|
||||
>
|
||||
<template v-slot:title>
|
||||
<label class="col-form-label">{{
|
||||
@@ -22,7 +22,7 @@
|
||||
<li>
|
||||
<drop-file-modal
|
||||
:allow-remove="false"
|
||||
@add-document="$emit('addDocument', $event)"
|
||||
@add-document="emit('addDocument', $event)"
|
||||
></drop-file-modal>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -39,9 +39,34 @@ import {
|
||||
EVALUATION_GENERATE_A_DOCUMENT,
|
||||
trans,
|
||||
} from "translator";
|
||||
import { buildLink } from "ChillDocGeneratorAssets/lib/document-generator";
|
||||
import { useStore } from "vuex";
|
||||
|
||||
defineProps(["evaluation", "templates"]);
|
||||
defineEmits(["addDocument", "submitBeforeGenerate"]);
|
||||
const store = useStore();
|
||||
|
||||
const props = defineProps(["evaluation", "templates"]);
|
||||
const emit = defineEmits(["addDocument"]);
|
||||
|
||||
async function submitBeforeGenerate({ template }) {
|
||||
const callback = (data) => {
|
||||
let evaluationId = data.accompanyingPeriodWorkEvaluations.find(
|
||||
(e) => e.key === props.evaluation.key,
|
||||
).id;
|
||||
|
||||
window.location.assign(
|
||||
buildLink(
|
||||
template,
|
||||
evaluationId,
|
||||
"Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluation",
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
return store.dispatch("submit", callback).catch((e) => {
|
||||
console.log(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@@ -58,7 +58,7 @@
|
||||
:preventDefaultMoveToGenerate="true"
|
||||
:goToGenerateWorkflowPayload="{ doc: d }"
|
||||
@go-to-generate-workflow="
|
||||
$emit('goToGenerateWorkflow', $event)
|
||||
goToGenerateWorkflowEvaluationDocument
|
||||
"
|
||||
></list-workflow-modal>
|
||||
</li>
|
||||
@@ -95,10 +95,9 @@
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@click="
|
||||
$emit(
|
||||
'goToGenerateNotification',
|
||||
goToGenerateDocumentNotification(
|
||||
d,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
"
|
||||
>
|
||||
@@ -113,8 +112,7 @@
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@click="
|
||||
$emit(
|
||||
'goToGenerateNotification',
|
||||
goToGenerateDocumentNotification(
|
||||
d,
|
||||
false,
|
||||
)
|
||||
@@ -150,15 +148,35 @@
|
||||
"
|
||||
></document-action-buttons-group>
|
||||
</li>
|
||||
<!--replace document-->
|
||||
<li
|
||||
v-if="
|
||||
Number.isInteger(d.id) &&
|
||||
d.storedObject._permissions.canEdit
|
||||
"
|
||||
>
|
||||
<drop-file-modal
|
||||
:existing-doc="d.storedObject"
|
||||
:allow-remove="false"
|
||||
@add-document="
|
||||
(arg) =>
|
||||
replaceDocument(
|
||||
d,
|
||||
arg.stored_object,
|
||||
arg.stored_object_version,
|
||||
)
|
||||
"
|
||||
></drop-file-modal>
|
||||
</li>
|
||||
<li v-if="Number.isInteger(d.id)">
|
||||
<div class="duplicate-dropdown">
|
||||
<button
|
||||
class="btn btn-edit dropdown-toggle"
|
||||
class="btn btn-outline-primary dropdown-toggle"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
{{ trans(EVALUATION_DOCUMENT_EDIT) }}
|
||||
<i class="bi bi-lightning-fill"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<!--delete-->
|
||||
@@ -180,27 +198,6 @@
|
||||
}}
|
||||
</a>
|
||||
</li>
|
||||
<!--replace document-->
|
||||
<li
|
||||
v-if="
|
||||
d.storedObject._permissions
|
||||
.canEdit
|
||||
"
|
||||
>
|
||||
<drop-file-modal
|
||||
:existing-doc="d.storedObject"
|
||||
:allow-remove="false"
|
||||
@add-document="
|
||||
(arg) =>
|
||||
$emit(
|
||||
'replaceDocument',
|
||||
d,
|
||||
arg.stored_object,
|
||||
arg.stored_object_version,
|
||||
)
|
||||
"
|
||||
></drop-file-modal>
|
||||
</li>
|
||||
<!--duplicate document-->
|
||||
<li>
|
||||
<a
|
||||
@@ -300,35 +297,45 @@ import {
|
||||
EVALUATION_DOCUMENTS,
|
||||
EVALUATION_DOCUMENT_MOVE,
|
||||
EVALUATION_DOCUMENT_DELETE,
|
||||
EVALUATION_DOCUMENT_EDIT,
|
||||
EVALUATION_DOCUMENT_DUPLICATE_HERE,
|
||||
EVALUATION_DOCUMENT_DUPLICATE_TO_OTHER_EVALUATION,
|
||||
trans,
|
||||
} from "translator";
|
||||
import { ref, watch } from "vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import AccompanyingPeriodWorkSelectorModal from "ChillPersonAssets/vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkSelectorModal.vue";
|
||||
import { buildLinkCreate } from "ChillMainAssets/lib/entity-workflow/api";
|
||||
import { buildLinkCreate as buildLinkCreateNotification } from "ChillMainAssets/lib/entity-notification/api";
|
||||
import { useStore } from "vuex";
|
||||
|
||||
defineProps([
|
||||
const props = defineProps([
|
||||
"documents",
|
||||
"docAnchorId",
|
||||
"accompanyingPeriodId",
|
||||
"accompanyingPeriodWorkId",
|
||||
"evaluation",
|
||||
]);
|
||||
const emit = defineEmits([
|
||||
"inputDocumentTitle",
|
||||
"removeDocument",
|
||||
"duplicateDocument",
|
||||
"statusDocumentChanged",
|
||||
"goToGenerateWorkflow",
|
||||
"goToGenerateNotification",
|
||||
"duplicateDocumentToWork",
|
||||
]);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const showAccompanyingPeriodSelector = ref(false);
|
||||
const selectedEvaluation = ref(null);
|
||||
const selectedDocumentToDuplicate = ref(null);
|
||||
const selectedDocumentToMove = ref(null);
|
||||
|
||||
const AmIRefferer = computed(() => {
|
||||
return !(
|
||||
store.state.work.accompanyingPeriod.user &&
|
||||
store.state.me &&
|
||||
store.state.work.accompanyingPeriod.user.id !== store.state.me.id
|
||||
);
|
||||
});
|
||||
|
||||
const prepareDocumentDuplicationToWork = (d) => {
|
||||
selectedDocumentToDuplicate.value = d;
|
||||
/** ensure selectedDocumentToMove is null */
|
||||
@@ -358,4 +365,91 @@ watch(selectedEvaluation, (val) => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function goToGenerateWorkflowEvaluationDocument({
|
||||
workflowName,
|
||||
payload,
|
||||
}) {
|
||||
const callback = (data) => {
|
||||
let evaluation = data.accompanyingPeriodWorkEvaluations.find(
|
||||
(e) => e.key === props.evaluation.key,
|
||||
);
|
||||
let updatedDocument = evaluation.documents.find(
|
||||
(d) => d.key === payload.doc.key,
|
||||
);
|
||||
window.location.assign(
|
||||
buildLinkCreate(
|
||||
workflowName,
|
||||
"Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument",
|
||||
updatedDocument.id,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
return store.dispatch("submit", callback).catch((e) => {
|
||||
console.log(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces a document in the store with a new document.
|
||||
*
|
||||
* @param {Object} oldDocument - The document to be replaced.
|
||||
* @param {StoredObject} storedObject - The stored object of the new document.
|
||||
* @param {StoredObjectVersion} storedObjectVersion - The new version of the document
|
||||
* @return {void}
|
||||
*/
|
||||
async function replaceDocument(oldDocument, storedObject, storedObjectVersion) {
|
||||
let document = {
|
||||
type: "accompanying_period_work_evaluation_document",
|
||||
storedObject: storedObject,
|
||||
title: oldDocument.title,
|
||||
};
|
||||
|
||||
return store.commit("replaceDocument", {
|
||||
key: props.evaluation.key,
|
||||
document,
|
||||
oldDocument: oldDocument,
|
||||
stored_object_version: storedObjectVersion,
|
||||
});
|
||||
}
|
||||
|
||||
async function goToGenerateDocumentNotification(document, tos) {
|
||||
const callback = (data) => {
|
||||
let evaluation = data.accompanyingPeriodWorkEvaluations.find(
|
||||
(e) => e.key === props.evaluation.key,
|
||||
);
|
||||
let updatedDocument = evaluation.documents.find(
|
||||
(d) => d.key === document.key,
|
||||
);
|
||||
window.location.assign(
|
||||
buildLinkCreateNotification(
|
||||
"Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument",
|
||||
updatedDocument.id,
|
||||
tos === true
|
||||
? store.state.work.accompanyingPeriod.user?.id
|
||||
: null,
|
||||
window.location.pathname +
|
||||
window.location.search +
|
||||
window.location.hash,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
return store.dispatch("submit", callback).catch((e) => {
|
||||
console.log(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
async function submitBeforeLeaveToEditor() {
|
||||
console.log("submit beore edit 2");
|
||||
// empty callback
|
||||
const callback = () => null;
|
||||
return store.dispatch("submit", callback).catch((e) => {
|
||||
console.log(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
@@ -24,8 +24,8 @@
|
||||
v-if="evaluation.documents.length > 0"
|
||||
:documents="evaluation.documents"
|
||||
:docAnchorId="docAnchorId"
|
||||
:evaluation="evaluation"
|
||||
:accompanyingPeriodId="store.state.work.accompanyingPeriod.id"
|
||||
:accompanying-period-work-id="store.state.work.id"
|
||||
@inputDocumentTitle="onInputDocumentTitle"
|
||||
@removeDocument="removeDocument"
|
||||
@duplicateDocument="duplicateDocument"
|
||||
@@ -34,7 +34,6 @@
|
||||
"
|
||||
@move-document-to-evaluation="moveDocumentToEvaluation"
|
||||
@statusDocumentChanged="onStatusDocumentChanged"
|
||||
@goToGenerateWorkflow="goToGenerateWorkflowEvaluationDocument"
|
||||
@goToGenerateNotification="goToGenerateDocumentNotification"
|
||||
/>
|
||||
|
||||
@@ -42,7 +41,6 @@
|
||||
:evaluation="evaluation"
|
||||
:templates="getTemplatesAvailables"
|
||||
@addDocument="addDocument"
|
||||
@submitBeforeGenerate="submitBeforeGenerate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -290,29 +288,6 @@ function onStatusDocumentChanged(newStatus) {
|
||||
});
|
||||
}
|
||||
|
||||
function goToGenerateWorkflowEvaluationDocument({ workflowName, payload }) {
|
||||
const callback = (data) => {
|
||||
let evaluation = data.accompanyingPeriodWorkEvaluations.find(
|
||||
(e) => e.key === props.evaluation.key,
|
||||
);
|
||||
let updatedDocument = evaluation.documents.find(
|
||||
(d) => d.key === payload.doc.key,
|
||||
);
|
||||
window.location.assign(
|
||||
buildLinkCreate(
|
||||
workflowName,
|
||||
"Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument",
|
||||
updatedDocument.id,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
store.dispatch("submit", callback).catch((e) => {
|
||||
console.log(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
function goToGenerateDocumentNotification(document, tos) {
|
||||
const callback = (data) => {
|
||||
let evaluation = data.accompanyingPeriodWorkEvaluations.find(
|
||||
|
@@ -50,8 +50,8 @@ const visMessages = {
|
||||
return "Né·e le";
|
||||
}
|
||||
},
|
||||
center_id: "Identifiant du centre",
|
||||
center_type: "Type de centre",
|
||||
center_id: "Identifiant du territoire",
|
||||
center_type: "Type de territoire",
|
||||
center_name: "Territoire", // vendée
|
||||
phonenumber: "Téléphone",
|
||||
mobilenumber: "Mobile",
|
||||
|
@@ -30,11 +30,7 @@
|
||||
>
|
||||
<template #header>
|
||||
<h3>
|
||||
{{
|
||||
trans(
|
||||
ACPW_DUPLICATE_SELECT_ACCOMPANYING_PERIOD_WORK,
|
||||
)
|
||||
}}
|
||||
{{ getModalTitle() }}
|
||||
</h3>
|
||||
</template>
|
||||
|
||||
@@ -73,6 +69,7 @@ import { AccompanyingPeriodWork } from "../../../types";
|
||||
import {
|
||||
trans,
|
||||
ACPW_DUPLICATE_SELECT_ACCOMPANYING_PERIOD_WORK,
|
||||
ACPW_DUPLICATE_SELECT_AN_EVALUATION,
|
||||
CONFIRM,
|
||||
} from "translator";
|
||||
import { fetchResults } from "ChillMainAssets/lib/api/apiMethods";
|
||||
@@ -97,6 +94,11 @@ const emit = defineEmits<{
|
||||
"update:selectedEvaluation": [evaluation: AccompanyingPeriodWorkEvaluation];
|
||||
}>();
|
||||
|
||||
const getModalTitle = () =>
|
||||
evaluations.value.length > 0
|
||||
? trans(ACPW_DUPLICATE_SELECT_AN_EVALUATION)
|
||||
: trans(ACPW_DUPLICATE_SELECT_ACCOMPANYING_PERIOD_WORK);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.accompanyingPeriodId) {
|
||||
getAccompanyingPeriodWorks(parseInt(props.accompanyingPeriodId));
|
||||
@@ -106,6 +108,7 @@ onMounted(() => {
|
||||
|
||||
showModal.value = true;
|
||||
});
|
||||
|
||||
const getAccompanyingPeriodWorks = async (periodId: number) => {
|
||||
const url = `/api/1.0/person/accompanying-course/${periodId}/works.json`;
|
||||
|
||||
|
@@ -464,7 +464,7 @@ export default {
|
||||
this.errors.push("Le genre doit être renseigné");
|
||||
}
|
||||
if (this.showCenters && this.person.center === null) {
|
||||
this.errors.push("Le centre doit être renseigné");
|
||||
this.errors.push("Le territoire doit être renseigné");
|
||||
}
|
||||
},
|
||||
loadData() {
|
||||
|
@@ -25,8 +25,8 @@ const personMessages = {
|
||||
return "Né·e le";
|
||||
}
|
||||
},
|
||||
center_id: "Identifiant du centre",
|
||||
center_type: "Type de centre",
|
||||
center_id: "Identifiant du territoire",
|
||||
center_type: "Type de territoire",
|
||||
center_name: "Territoire", // vendée
|
||||
phonenumber: "Téléphone",
|
||||
mobilenumber: "Mobile",
|
||||
@@ -53,8 +53,8 @@ const personMessages = {
|
||||
"Un nouveau ménage va être créé. L'usager sera membre de ce ménage.",
|
||||
},
|
||||
center: {
|
||||
placeholder: "Choisissez un centre",
|
||||
title: "Centre",
|
||||
placeholder: "Choisissez un territoire",
|
||||
title: "territoire",
|
||||
},
|
||||
},
|
||||
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
|
||||
Address valid at this date: Addresse valide à 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 centre, 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 territoire, l'adresse, le ménage, sera calculé à cette date.
|
||||
List duplicates: Liste des 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
|
||||
@@ -786,8 +786,8 @@ evaluation:
|
||||
duplicate: Dupliquer
|
||||
duplicate_here: Dupliquer ici
|
||||
duplicate_to_other_evaluation: Dupliquer vers une autre évaluation
|
||||
duplicate_success: Le document d'évaluation a été dupliquer
|
||||
move_success: Le document d'évaluation a été déplacer
|
||||
duplicate_success: Le document d'évaluation a été dupliqué
|
||||
move_success: Le document d'évaluation a été déplacé
|
||||
|
||||
|
||||
goal:
|
||||
@@ -1110,9 +1110,9 @@ export:
|
||||
Group course by household composition: Grouper les usagers par composition familiale
|
||||
Calc date: Date de calcul de la composition du ménage
|
||||
by_center:
|
||||
title: Grouper les usagers par centre
|
||||
at_date: Date de calcul du centre
|
||||
center: Centre de l'usager
|
||||
title: Grouper les usagers par territoire
|
||||
at_date: Date de calcul du territoire
|
||||
center: Territoire de l'usager
|
||||
by_postal_code:
|
||||
title: Grouper les usagers par code postal de l'adresse
|
||||
at_date: Date de calcul de l'adresse
|
||||
@@ -1437,7 +1437,7 @@ export:
|
||||
acpParticipantPersons: Usagers concernés
|
||||
acpParticipantPersonsIds: Usagers concernés (identifiants)
|
||||
duration: Durée du parcours (en jours)
|
||||
centers: Centres des usagers
|
||||
centers: Territoires des usagers
|
||||
|
||||
eval:
|
||||
List of evaluations: Liste des évaluations
|
||||
@@ -1543,7 +1543,8 @@ entity_display_title:
|
||||
acpw_duplicate:
|
||||
title: Fusionner les actions d'accompagnement
|
||||
description: Cette fusion conservera la date de début la plus ancienne, la date de fin la plus récente, toutes les évaluations, documents et workflows. Les agents traitants seront additionnés ainsi que les tiers intervenants. Les commentaires seront mis l'un à la suite de l'autre.
|
||||
Select accompanying period work: Selectionner un action d'accompagnement
|
||||
Select accompanying period work: Sélectionner une action d'accompagnement
|
||||
Select an evaluation: Sélectionner une évaluation
|
||||
Assign duplicate: Désigner un action d'accompagnement doublon
|
||||
Accompanying period work to delete: Action d'accompagnement à supprimer
|
||||
Accompanying period work to delete explanation: Cet action d'accompagnement sera supprimé.
|
||||
|
@@ -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.
|
||||
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 centre est requis
|
||||
A center is required: Un territoire est requis
|
||||
|
||||
#export list
|
||||
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'
|
||||
Details: Détails
|
||||
Person: Usager
|
||||
Scope: Cercle
|
||||
Scope: Service
|
||||
Date: Date
|
||||
User: Utilisateur
|
||||
'Report type': 'Type de rapport'
|
||||
|
@@ -4,7 +4,7 @@ Tasks: "Tâches"
|
||||
Title: Titre
|
||||
Description: Description
|
||||
Assignee: "Personne assignée"
|
||||
Scope: Cercle
|
||||
Scope: Service
|
||||
"Start date": "Date de début"
|
||||
"End date": "Date d'échéance"
|
||||
"Warning date": "Date d'avertissement"
|
||||
@@ -106,7 +106,7 @@ My tasks over deadline: Mes tâches à échéance dépassée
|
||||
#transition page
|
||||
Apply transition on task <em>%title%</em>: Appliquer la transition sur la tâche <em>%title%</em>
|
||||
|
||||
All centers: Tous les centres
|
||||
All centers: Tous les territoires
|
||||
|
||||
# ROLES
|
||||
CHILL_TASK_TASK_CREATE: Ajouter une tâche
|
||||
|
@@ -73,8 +73,8 @@ No acronym given: Aucun sigle renseigné
|
||||
No phone given: Aucun téléphone renseigné
|
||||
No email given: Aucune adresse courriel renseignée
|
||||
|
||||
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 centre
|
||||
The party is visible in those centers: Le tiers est visible dans ces territoires
|
||||
The party is not visible in any center: Le tiers n'est associé à aucun territoire
|
||||
No third parties: Aucun tiers
|
||||
Any third party selected: Aucun tiers sélectionné
|
||||
|
||||
|
Reference in New Issue
Block a user