mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-15 03:04:58 +00:00
Compare commits
194 Commits
signature-
...
v3.4.1
Author | SHA1 | Date | |
---|---|---|---|
8a2272f93b
|
|||
9ef884349a
|
|||
5acf9432d6
|
|||
4fdb722dc6 | |||
e113e3dce5 | |||
6536662aba | |||
4127ce1d97 | |||
b327f65ef8 | |||
0f1604817b | |||
63fc4f1089 | |||
ba3fe6af8c | |||
bc4c2c1471 | |||
3f381c207d | |||
df30ca2c4f | |||
2573c32160 | |||
38886cd0b6 | |||
875d3293d2 | |||
16d5f121db | |||
10999a2077 | |||
128f8b8852
|
|||
6ca4b91e1e
|
|||
3a1947df9e
|
|||
9012e68b70 | |||
04b2def8a5 | |||
39b918e7eb | |||
4be6c09d4d
|
|||
9a44cf060f
|
|||
723ca8db6a
|
|||
f04ef3c3e3 | |||
b5f1f3153f
|
|||
03fe9a6d86
|
|||
a312b45777
|
|||
5d5150faa7
|
|||
9b661c3b8f
|
|||
f90fae4e14 | |||
b7e27536bd
|
|||
887f3e0aa2 | |||
829fb669fe
|
|||
a8660ecdb2 | |||
903a87c589
|
|||
aad10cc61f
|
|||
c99dda0126
|
|||
94f9ebd726
|
|||
5ad11041e0
|
|||
d50b169ab8
|
|||
ba2d8663f1
|
|||
d6c55c830b
|
|||
937caa878e
|
|||
21ec3121ec | |||
261d47a8a4
|
|||
6453237340
|
|||
79621e8ab7
|
|||
bfa58177e0
|
|||
ddf73e1a48
|
|||
c3cc6c8353
|
|||
3ec0d26001
|
|||
64d91e2afe
|
|||
5339d4f5d9
|
|||
0439c29305
|
|||
8e34f6962a
|
|||
e5148f603b
|
|||
e2e24090ab | |||
b6c141a785
|
|||
db4d7669f1
|
|||
9526d016c6 | |||
b2f6dbbe30
|
|||
5447ad2961
|
|||
d2b3ee0a2f | |||
66b87358c8 | |||
83f0044eba | |||
ac353ec3bc
|
|||
7aca08c89e
|
|||
4d53c8a295 | |||
1ac9d32565 | |||
63c2578012 | |||
884b3684fe | |||
456d29e605 | |||
8cb2bb1ef4 | |||
cc7e9235b5 | |||
973ffcbffa | |||
8c3de682d6 | |||
e71c2f162c | |||
43b70fd773
|
|||
5c0a383909
|
|||
4dc2348893
|
|||
32459e6092 | |||
1e02fed32b | |||
2c3818258a | |||
64f3b40694 | |||
|
76458cf375 | ||
|
5259ea71a1 | ||
1cadc71d5a | |||
2b45a51f57 | |||
4c66adee86 | |||
6c8fd99cd1 | |||
e886387f17 | |||
c79f030310 | |||
a648fd09b0 | |||
1bd5e6d582 | |||
80940a7b19 | |||
7541238c1e | |||
34748dca76 | |||
12bb264eb5 | |||
ac3ac432e1 | |||
a00f47c312 | |||
b503f58089 | |||
5629a0c124 | |||
|
3bc6595f58 | ||
989fdad561
|
|||
d7174cdb95
|
|||
182e2fc3af
|
|||
7df5a22b14 | |||
f750cfecac
|
|||
bf85e9bb71
|
|||
97729de66d
|
|||
4e0a421a03
|
|||
00408b91a9 | |||
d04f9ae9ff | |||
086f391dc9 | |||
06cbfdd0c3 | |||
f1844ae02b | |||
73b0dd6009 | |||
4d8bcc5a5a | |||
5dfa5e1e7f | |||
588f02cdf4 | |||
30b66d5806 | |||
5786759daa | |||
0c1c1cbf8b | |||
9741794f7a | |||
5ca558bba3 | |||
9d05f2ac2b | |||
566c40dd84 | |||
0d2e0b4e91 | |||
30ebd00693 | |||
8b1d73356f | |||
ddfaa2861e | |||
9416a19d85 | |||
34bbee2031 | |||
7f1764658a | |||
43c3cc26ea | |||
74593a7d28 | |||
e629dbf994 | |||
8e02db6c85 | |||
7183d9a3b1 | |||
70335a6360 | |||
fa64f44cf1 | |||
f34c94fd65 | |||
73d80af80a | |||
363cbc8a76 | |||
9f3893243e | |||
7520d746e8 | |||
052e09cf64 | |||
77ece243c0 | |||
a47c8d916b | |||
5fce9ee9fb | |||
47d954fe9f | |||
d9dc2d1f4e | |||
37cfc035a3 | |||
2eb686ffdb | |||
34e2a26d1e | |||
789c977aba | |||
0ba93ec7c6 | |||
b9d2f5efa3 | |||
567c01f395 | |||
67a6eb17db | |||
b78f0980f5 | |||
f428afc7ca | |||
18899d665d | |||
59f9ac25ba | |||
2da6b746fb | |||
11f75bf6f1 | |||
a1db1a1d65 | |||
bdfdabe10e | |||
843a2a4b5b | |||
6781fdbd9b | |||
e6102d339b | |||
06cb3ddcd1 | |||
05d56c6eeb | |||
23e7f4a120 | |||
3eeb105913 | |||
726cdb385f | |||
406eba80d2 | |||
236e8117d4 | |||
e6bfcddae2 | |||
d61c090cee | |||
43dd94dad6 | |||
f7f8319749 | |||
376ce59917 | |||
06d6227d0e | |||
de914f4f17 | |||
e831cb1656 | |||
94875d83b3 | |||
8e30873001 | |||
be8901a5c4
|
@@ -1,8 +0,0 @@
|
||||
kind: Feature
|
||||
body: |-
|
||||
Electronic signature
|
||||
|
||||
Implementation of the electronic signature for documents within chill.
|
||||
time: 2024-06-14T15:32:36.875891692+02:00
|
||||
custom:
|
||||
Issue: ""
|
@@ -1,7 +0,0 @@
|
||||
kind: Feature
|
||||
body: The behavoir of the voters for stored objects is adjusted so as to limit edit
|
||||
and delete possibilities to users related to the activity, social action or workflow
|
||||
entity.
|
||||
time: 2024-06-14T15:35:37.582159301+02:00
|
||||
custom:
|
||||
Issue: "286"
|
@@ -1,5 +0,0 @@
|
||||
kind: Feature
|
||||
body: Metadata form added for person signatures
|
||||
time: 2024-07-18T15:12:33.8134266+02:00
|
||||
custom:
|
||||
Issue: "288"
|
@@ -1,6 +0,0 @@
|
||||
kind: Fixed
|
||||
body: Show only the current referrer in the page "show" for an accompanying period
|
||||
workf
|
||||
time: 2024-09-16T15:18:43.017401122+02:00
|
||||
custom:
|
||||
Issue: "308"
|
6
.changes/v3.1.1.md
Normal file
6
.changes/v3.1.1.md
Normal file
@@ -0,0 +1,6 @@
|
||||
## v3.1.1 - 2024-10-01
|
||||
### Fixed
|
||||
* ([#308](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/308)) Show only the current referrer in the page "show" for an accompanying period workf
|
||||
* ([#309](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/309)) Correctly compute the grouping by referrer aggregator
|
||||
|
||||
* Fixed typing of custom field long choice and custom field group
|
3
.changes/v3.2.0.md
Normal file
3
.changes/v3.2.0.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## v3.2.0 - 2024-10-30
|
||||
### Feature
|
||||
* Introduce a gender entity
|
4
.changes/v3.2.1.md
Normal file
4
.changes/v3.2.1.md
Normal file
@@ -0,0 +1,4 @@
|
||||
## v3.2.1 - 2024-10-31
|
||||
### Fixed
|
||||
* Add the possibility of unknown to the gender entity
|
||||
* Fix the fusion of person doubles by excluding accompanyingPeriod work entities to be deleted. They are moved instead.
|
3
.changes/v3.2.2.md
Normal file
3
.changes/v3.2.2.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## v3.2.2 - 2024-10-31
|
||||
### Fixed
|
||||
* Fix gender translation for unknown
|
4
.changes/v3.2.3.md
Normal file
4
.changes/v3.2.3.md
Normal file
@@ -0,0 +1,4 @@
|
||||
## v3.2.3 - 2024-11-05
|
||||
### Fixed
|
||||
* ([#315](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/315)) Fix display of accompanying period work referrers. Only current referrers should be displayed.
|
||||
Fix color of Chill footer
|
3
.changes/v3.2.4.md
Normal file
3
.changes/v3.2.4.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## v3.2.4 - 2024-11-06
|
||||
### Fixed
|
||||
* Fix compilation of chill assets
|
13
.changes/v3.3.0.md
Normal file
13
.changes/v3.3.0.md
Normal file
@@ -0,0 +1,13 @@
|
||||
## v3.3.0 - 2024-11-20
|
||||
### Feature
|
||||
* Electronic signature
|
||||
|
||||
Implementation of the electronic signature for documents within chill.
|
||||
* ([#286](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/286)) The behavoir of the voters for stored objects is adjusted so as to limit edit and delete possibilities to users related to the activity, social action or workflow entity.
|
||||
* ([#288](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/288)) Metadata form added for person signatures
|
||||
* Add a signature step in workflow, which allow to apply an electronic signature on documents
|
||||
* Keep an history of each version of a stored object.
|
||||
* Add a "send external" step in workflow, which allow to send stored objects and other elements to remote people, by sending them a public url
|
||||
### Fixed
|
||||
* Adjust household list export to include households even if their address is NULL
|
||||
* Remove validation of date string on deathDate
|
4
.changes/v3.4.0.md
Normal file
4
.changes/v3.4.0.md
Normal file
@@ -0,0 +1,4 @@
|
||||
## v3.4.0 - 2024-11-20
|
||||
### Feature
|
||||
* ([#314](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/314)) Admin: improve document type admin form with a select field for related class.
|
||||
Admin: Allow administrator to assign multiple group centers in one go to a user.
|
3
.changes/v3.4.1.md
Normal file
3
.changes/v3.4.1.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## v3.4.1 - 2024-11-22
|
||||
### Fixed
|
||||
* Set the workflow's title to notification content and subject
|
72
CHANGELOG.md
72
CHANGELOG.md
@@ -6,6 +6,58 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
|
||||
|
||||
## v3.4.1 - 2024-11-22
|
||||
### Fixed
|
||||
* Set the workflow's title to notification content and subject
|
||||
|
||||
## v3.4.0 - 2024-11-20
|
||||
### Feature
|
||||
* ([#314](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/314)) Admin: improve document type admin form with a select field for related class.
|
||||
Admin: Allow administrator to assign multiple group centers in one go to a user.
|
||||
|
||||
## v3.3.0 - 2024-11-20
|
||||
### Feature
|
||||
* Electronic signature
|
||||
|
||||
Implementation of the electronic signature for documents within chill.
|
||||
* ([#286](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/286)) The behavoir of the voters for stored objects is adjusted so as to limit edit and delete possibilities to users related to the activity, social action or workflow entity.
|
||||
* ([#288](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/288)) Metadata form added for person signatures
|
||||
* Add a signature step in workflow, which allow to apply an electronic signature on documents
|
||||
* Keep an history of each version of a stored object.
|
||||
* Add a "send external" step in workflow, which allow to send stored objects and other elements to remote people, by sending them a public url
|
||||
### Fixed
|
||||
* Adjust household list export to include households even if their address is NULL
|
||||
* Remove validation of date string on deathDate
|
||||
|
||||
## v3.2.4 - 2024-11-06
|
||||
### Fixed
|
||||
* Fix compilation of chill assets
|
||||
|
||||
## v3.2.3 - 2024-11-05
|
||||
### Fixed
|
||||
* ([#315](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/315)) Fix display of accompanying period work referrers. Only current referrers should be displayed.
|
||||
Fix color of Chill footer
|
||||
|
||||
## v3.2.2 - 2024-10-31
|
||||
### Fixed
|
||||
* Fix gender translation for unknown
|
||||
|
||||
## v3.2.1 - 2024-10-31
|
||||
### Fixed
|
||||
* Add the possibility of unknown to the gender entity
|
||||
* Fix the fusion of person doubles by excluding accompanyingPeriod work entities to be deleted. They are moved instead.
|
||||
|
||||
## v3.2.0 - 2024-10-30
|
||||
### Feature
|
||||
* Introduce a gender entity
|
||||
|
||||
## v3.1.1 - 2024-10-01
|
||||
### Fixed
|
||||
* ([#308](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/308)) Show only the current referrer in the page "show" for an accompanying period workf
|
||||
* ([#309](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/309)) Correctly compute the grouping by referrer aggregator
|
||||
|
||||
* Fixed typing of custom field long choice and custom field group
|
||||
|
||||
## v3.1.0 - 2024-08-30
|
||||
### Feature
|
||||
* Add export aggregator to aggregate activities by household + filter persons that are not part of an accompanyingperiod during a certain timeframe.
|
||||
@@ -20,8 +72,14 @@ and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
### Feature
|
||||
* ([#306](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/306)) When a document is converted or downloaded in the browser, this document is removed from the browser memory after 45s. Future click on the button re-download the document.
|
||||
|
||||
## v2.23.0 - 2024-07-19 & 2024-07-23
|
||||
## v2.23.0 - 2024-07-23 & 2024-07-19
|
||||
### Feature
|
||||
* ([#221](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/221)) [DX] move async-upload-bundle features into chill-bundles
|
||||
* Add job bundle (module emploi)
|
||||
* Upgrade import of address list to the last version of compiled addresses of belgian-best-address
|
||||
|
||||
* Upgrade CKEditor and refactor configuration with use of typescript
|
||||
|
||||
* ([#123](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/123)) Add a button to duplicate calendar ranges from a week to another one
|
||||
* [admin] filter users by active / inactive in the admin user's list
|
||||
* ([#273](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/273)) Add the possibility to mark all notifications as read
|
||||
@@ -31,6 +89,8 @@ and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
* Do not update the "createdAt" column when importing postal code which does not change
|
||||
* Display filename on file upload within the UI interface
|
||||
### Fixed
|
||||
* Fix resolving of centers for an household, which will fix in turn the access control
|
||||
* Resolved type hinting error in activity list export
|
||||
* ([#271](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/271)) Take into account the acp closing date in the acp works date filter
|
||||
|
||||
### Traduction française des principaux changements
|
||||
@@ -43,16 +103,6 @@ and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
- Agrandit l'icône du type de fichier dans l'interface de dépôt de fichier;
|
||||
- correction: tient compte de la date de fermeture du parcours dans les filtres sur les actions d'accompagnement.
|
||||
|
||||
* ([#221](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/221)) [DX] move async-upload-bundle features into chill-bundles
|
||||
* Add job bundle (module emploi)
|
||||
* Upgrade import of address list to the last version of compiled addresses of belgian-best-address
|
||||
|
||||
* Upgrade CKEditor and refactor configuration with use of typescript
|
||||
|
||||
### Fixed
|
||||
* Fix resolving of centers for an household, which will fix in turn the access control
|
||||
* Resolved type hinting error in activity list export
|
||||
|
||||
## v2.22.2 - 2024-07-03
|
||||
### Fixed
|
||||
* Remove scope required for event participation stats
|
||||
|
@@ -59,7 +59,8 @@
|
||||
"vue-i18n": "^9.1.6",
|
||||
"vue-multiselect": "3.0.0-alpha.2",
|
||||
"vue-toast-notification": "^3.1.2",
|
||||
"vuex": "^4.0.0"
|
||||
"vuex": "^4.0.0",
|
||||
"bootstrap-icons": "^1.11.3"
|
||||
},
|
||||
"browserslist": [
|
||||
"Firefox ESR"
|
||||
|
@@ -16,7 +16,7 @@ use Chill\ActivityBundle\Repository\ActivityRepository;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
|
||||
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
|
||||
@@ -24,7 +24,7 @@ class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
|
||||
public function __construct(
|
||||
private readonly ActivityRepository $repository,
|
||||
Security $security,
|
||||
WorkflowStoredObjectPermissionHelper $workflowDocumentService,
|
||||
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
|
||||
) {
|
||||
parent::__construct($security, $workflowDocumentService);
|
||||
}
|
||||
|
@@ -42,8 +42,8 @@ class CustomFieldLongChoice extends AbstractCustomField
|
||||
$translatableStringHelper = $this->translatableStringHelper;
|
||||
$builder->add($customField->getSlug(), Select2ChoiceType::class, [
|
||||
'choices' => $entries,
|
||||
'choice_label' => static fn (Option $option) => $translatableStringHelper->localize($option->getText()),
|
||||
'choice_value' => static fn (Option $key): ?int => null === $key ? null : $key->getId(),
|
||||
'choice_label' => static fn (?Option $option) => $translatableStringHelper->localize($option->getText()),
|
||||
'choice_value' => static fn (?Option $key): ?int => $key?->getId(),
|
||||
'multiple' => false,
|
||||
'expanded' => false,
|
||||
'required' => $customField->isRequired(),
|
||||
|
@@ -46,11 +46,8 @@ class CustomFieldsGroup
|
||||
#[ORM\GeneratedValue(strategy: 'AUTO')]
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON)]
|
||||
private $name;
|
||||
private array|string $name;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON)]
|
||||
private array $options = [];
|
||||
@@ -181,7 +178,7 @@ class CustomFieldsGroup
|
||||
*
|
||||
* @return CustomFieldsGroup
|
||||
*/
|
||||
public function setName($name)
|
||||
public function setName(array|string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
|
@@ -15,11 +15,14 @@ use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessa
|
||||
use Chill\DocStoreBundle\Service\Signature\PDFPage;
|
||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectToPdfConverter;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||
use Chill\MainBundle\Templating\Entity\ChillEntityRenderManagerInterface;
|
||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowStepSignatureVoter;
|
||||
use Chill\MainBundle\Templating\Entity\UserRender;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -38,6 +41,8 @@ class SignatureRequestController
|
||||
private readonly ChillEntityRenderManagerInterface $entityRender,
|
||||
private readonly NormalizerInterface $normalizer,
|
||||
private readonly Security $security,
|
||||
private readonly StoredObjectToPdfConverter $converter,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
#[Route('/api/1.0/document/workflow/{id}/signature-request', name: 'chill_docstore_signature_request')]
|
||||
@@ -52,11 +57,17 @@ class SignatureRequestController
|
||||
if (EntityWorkflowSignatureStateEnum::PENDING !== $signature->getState()) {
|
||||
return new JsonResponse([], status: Response::HTTP_CONFLICT);
|
||||
}
|
||||
|
||||
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
|
||||
$content = $this->storedObjectManager->read($storedObject);
|
||||
|
||||
$data = \json_decode((string) $request->getContent(), true, 512, JSON_THROW_ON_ERROR); // TODO parse payload: json_decode ou, mieux, dataTransfertObject
|
||||
if ('application/pdf' !== $storedObject->getType()) {
|
||||
[$storedObject, $storedObjectVersion, $content] = $this->converter->addConvertedVersion($storedObject, $request->getLocale(), includeConvertedContent: true);
|
||||
$this->entityManager->persist($storedObjectVersion);
|
||||
$this->entityManager->flush();
|
||||
} else {
|
||||
$content = $this->storedObjectManager->read($storedObject);
|
||||
}
|
||||
|
||||
$data = \json_decode((string) $request->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
||||
$zone = new PDFSignatureZone(
|
||||
$data['zone']['index'],
|
||||
$data['zone']['x'],
|
||||
@@ -75,6 +86,7 @@ class SignatureRequestController
|
||||
// options for user render
|
||||
'absence' => false,
|
||||
'main_scope' => false,
|
||||
UserRender::SPLIT_LINE_BEFORE_CHARACTER => 30,
|
||||
// options for person render
|
||||
'addAge' => false,
|
||||
]),
|
||||
|
@@ -18,6 +18,7 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table('chill_doc.accompanyingcourse_document')]
|
||||
#[ORM\UniqueConstraint(name: 'acc_course_document_unique_stored_object', columns: ['object_id'])]
|
||||
class AccompanyingCourseDocument extends Document implements HasScopesInterface, HasCentersInterface
|
||||
{
|
||||
#[ORM\ManyToOne(targetEntity: AccompanyingPeriod::class)]
|
||||
|
@@ -40,6 +40,7 @@ class Document implements TrackCreationInterface, TrackUpdateInterface
|
||||
#[Assert\Valid]
|
||||
#[Assert\NotNull(message: 'Upload a document')]
|
||||
#[ORM\ManyToOne(targetEntity: StoredObject::class, cascade: ['persist'])]
|
||||
#[ORM\JoinColumn(name: 'object_id', referencedColumnName: 'id')]
|
||||
private ?StoredObject $object = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: DocGeneratorTemplate::class)]
|
||||
|
@@ -19,6 +19,7 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table('chill_doc.person_document')]
|
||||
#[ORM\UniqueConstraint(name: 'person_document_unique_stored_object', columns: ['object_id'])]
|
||||
class PersonDocument extends Document implements HasCenterInterface, HasScopeInterface
|
||||
{
|
||||
#[ORM\Id]
|
||||
|
@@ -17,15 +17,23 @@ use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class DocumentCategoryType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly TranslatorInterface $translator) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$bundles = [
|
||||
'chill-doc-store' => 'chill-doc-store',
|
||||
];
|
||||
|
||||
$documentClasses = [
|
||||
$this->translator->trans('Accompanying period document') => \Chill\DocStoreBundle\Entity\AccompanyingCourseDocument::class,
|
||||
$this->translator->trans('Person document') => \Chill\DocStoreBundle\Entity\PersonDocument::class,
|
||||
];
|
||||
|
||||
$builder
|
||||
->add('bundleId', ChoiceType::class, [
|
||||
'choices' => $bundles,
|
||||
@@ -34,7 +42,10 @@ class DocumentCategoryType extends AbstractType
|
||||
->add('idInsideBundle', null, [
|
||||
'disabled' => true,
|
||||
])
|
||||
->add('documentClass', null, [
|
||||
->add('documentClass', ChoiceType::class, [
|
||||
'choices' => $documentClasses,
|
||||
'expanded' => false,
|
||||
'required' => true,
|
||||
'disabled' => false,
|
||||
])
|
||||
->add('name', TranslatableStringFormType::class);
|
||||
|
@@ -130,3 +130,12 @@ export interface CheckSignature {
|
||||
}
|
||||
|
||||
export type CanvasEvent = "select" | "add";
|
||||
|
||||
export interface ZoomLevel {
|
||||
id: number;
|
||||
zoom: number;
|
||||
label: {
|
||||
fr?: string,
|
||||
nl?: string
|
||||
};
|
||||
}
|
@@ -28,9 +28,21 @@
|
||||
</teleport>
|
||||
<div class="col-12 m-auto">
|
||||
<div class="row justify-content-center border-bottom pdf-tools d-md-none">
|
||||
<div v-if="pageCount > 1" class="col text-center turn-page">
|
||||
<div class="col text-center turn-page">
|
||||
<select
|
||||
class="form-select form-select-sm"
|
||||
id="zoomSelect"
|
||||
v-model="zoomLevel"
|
||||
@change="setZoomLevel(zoomLevel)"
|
||||
>
|
||||
<option value="" selected disabled>Zoom</option>
|
||||
<option v-for="z in zoomLevels" :value="z.zoom" :key="z.id">
|
||||
{{ z.label.fr }}
|
||||
</option>
|
||||
</select>
|
||||
<template v-if="pageCount > 1">
|
||||
<button
|
||||
class="btn btn-light btn-sm"
|
||||
class="btn btn-light btn-xs p-1"
|
||||
:disabled="page <= 1"
|
||||
@click="turnPage(-1)"
|
||||
>
|
||||
@@ -38,14 +50,18 @@
|
||||
</button>
|
||||
<span>{{ page }}/{{ pageCount }}</span>
|
||||
<button
|
||||
class="btn btn-light btn-sm"
|
||||
class="btn btn-light btn-xs p-1"
|
||||
:disabled="page >= pageCount"
|
||||
@click="turnPage(1)"
|
||||
>
|
||||
❯
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="signature.zones.length > 1" class="col-3 p-0">
|
||||
<div
|
||||
v-if="signature.zones.length > 1"
|
||||
class="col-5 p-0 text-center turnSignature"
|
||||
>
|
||||
<button
|
||||
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
|
||||
class="btn btn-light btn-sm"
|
||||
@@ -53,8 +69,7 @@
|
||||
>
|
||||
{{ $t("last_zone") }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="signature.zones.length > 1" class="col-3 p-0">
|
||||
<span>|</span>
|
||||
<button
|
||||
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
|
||||
class="btn btn-light btn-sm"
|
||||
@@ -63,7 +78,7 @@
|
||||
{{ $t("next_zone") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col text-end p-0">
|
||||
<div class="col text-end" v-if="signedState !== 'signed'">
|
||||
<button
|
||||
class="btn btn-misc btn-sm"
|
||||
:hidden="!userSignatureZone"
|
||||
@@ -81,23 +96,38 @@
|
||||
>
|
||||
{{ $t("cancel") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-1" v-if="signedState !== 'signed'">
|
||||
<button
|
||||
class="btn btn-create btn-sm"
|
||||
:class="{ active: canvasEvent === 'add' }"
|
||||
<button v-if="userSignatureZone === null"
|
||||
:class="{ btn: true, 'btn-sm': true, 'btn-create': canvasEvent !== 'add', 'btn-chill-green': canvasEvent === 'add', active: canvasEvent === 'add' }"
|
||||
@click="toggleAddZone()"
|
||||
:title="$t('add_sign_zone')"
|
||||
></button>
|
||||
>
|
||||
<template v-if="canvasEvent === 'add'">
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="row justify-content-center border-bottom pdf-tools d-none d-md-flex"
|
||||
>
|
||||
<div v-if="pageCount > 1" class="col-2 text-center turn-page p-0">
|
||||
<div class="col-3 text-center turn-page ps-3">
|
||||
<select
|
||||
class="form-select form-select-sm"
|
||||
id="zoomSelect"
|
||||
v-model="zoomLevel"
|
||||
@change="setZoomLevel(zoomLevel)"
|
||||
>
|
||||
<option value="" selected disabled>Zoom</option>
|
||||
<option v-for="z in zoomLevels" :value="z.zoom" :key="z.id">
|
||||
{{ z.label.fr }}
|
||||
</option>
|
||||
</select>
|
||||
<template v-if="pageCount > 1">
|
||||
<button
|
||||
class="btn btn-light btn-sm"
|
||||
class="btn btn-light btn-xs p-1"
|
||||
:disabled="page <= 1"
|
||||
@click="turnPage(-1)"
|
||||
>
|
||||
@@ -105,16 +135,17 @@
|
||||
</button>
|
||||
<span>{{ page }} / {{ pageCount }}</span>
|
||||
<button
|
||||
class="btn btn-light btn-sm"
|
||||
class="btn btn-light btn-xs p-1"
|
||||
:disabled="page >= pageCount"
|
||||
@click="turnPage(1)"
|
||||
>
|
||||
❯
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-if="signature.zones.length > 1 && signedState !== 'signed'"
|
||||
class="col text-end d-xl-none"
|
||||
class="col-4 d-xl-none text-center turnSignature p-0"
|
||||
>
|
||||
<button
|
||||
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
|
||||
@@ -123,11 +154,7 @@
|
||||
>
|
||||
{{ $t("last_zone") }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="signature.zones.length > 1 && signedState !== 'signed'"
|
||||
class="col text-start d-xl-none"
|
||||
>
|
||||
<span>|</span>
|
||||
<button
|
||||
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
|
||||
class="btn btn-light btn-sm"
|
||||
@@ -138,7 +165,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="signature.zones.length > 1 && signedState !== 'signed'"
|
||||
class="col text-end d-none d-xl-flex p-0"
|
||||
class="col-4 d-none d-xl-flex p-0 text-center turnSignature"
|
||||
>
|
||||
<button
|
||||
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
|
||||
@@ -147,11 +174,7 @@
|
||||
>
|
||||
{{ $t("last_sign_zone") }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="signature.zones.length > 1 && signedState !== 'signed'"
|
||||
class="col text-start d-none d-xl-flex p-0"
|
||||
>
|
||||
<span>|</span>
|
||||
<button
|
||||
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
|
||||
class="btn btn-light btn-sm"
|
||||
@@ -160,7 +183,7 @@
|
||||
{{ $t("next_sign_zone") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col text-end p-0" v-if="signedState !== 'signed'">
|
||||
<div class="col text-end" v-if="signedState !== 'signed'">
|
||||
<button
|
||||
class="btn btn-misc btn-sm"
|
||||
:hidden="!userSignatureZone"
|
||||
@@ -177,29 +200,43 @@
|
||||
>
|
||||
{{ $t("cancel") }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="col text-end p-0 pe-2 pe-xxl-4"
|
||||
v-if="signedState !== 'signed'"
|
||||
>
|
||||
<button
|
||||
class="btn btn-create btn-sm"
|
||||
:class="{ active: canvasEvent === 'add' }"
|
||||
<button v-if="userSignatureZone === null"
|
||||
:class="{ btn: true, 'btn-sm': true, 'btn-create': canvasEvent !== 'add', 'btn-chill-green': canvasEvent === 'add', active: canvasEvent === 'add' }"
|
||||
@click="toggleAddZone()"
|
||||
:title="$t('add_sign_zone')"
|
||||
>
|
||||
<template v-if="canvasEvent !== 'add'">
|
||||
{{ $t("add_zone") }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t("click_on_document")}}
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-12 col-lg-9 m-auto my-5 text-center">
|
||||
<canvas class="m-auto" id="canvas"></canvas>
|
||||
<div class="col-xs-12 col-md-12 col-lg-9 m-auto my-5 text-center" :class="{onAddZone: canvasEvent === 'add'}">
|
||||
<canvas class="m-auto" id="canvas" ></canvas>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-md-12 col-lg-9 m-auto p-4" id="action-buttons">
|
||||
<div class="row">
|
||||
<div class="col-4" v-if="signedState !== 'signed'">
|
||||
<div class="col d-flex">
|
||||
<a
|
||||
class="btn btn-cancel"
|
||||
v-if="signedState !== 'signed'"
|
||||
:href="getReturnPath()"
|
||||
>
|
||||
{{ $t("cancel") }}
|
||||
</a>
|
||||
<a class="btn btn-misc" v-else :href="getReturnPath()">
|
||||
{{ $t("return") }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col text-end" v-if="signedState !== 'signed'">
|
||||
<button
|
||||
class="btn btn-action me-2"
|
||||
:disabled="!userSignatureZone"
|
||||
@@ -209,18 +246,6 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-4" v-else></div>
|
||||
<div class="col-8 d-flex justify-content-end">
|
||||
<a
|
||||
class="btn btn-delete"
|
||||
v-if="signedState !== 'signed'"
|
||||
:href="getReturnPath()"
|
||||
>
|
||||
{{ $t("cancel_signing") }}
|
||||
</a>
|
||||
<a class="btn btn-misc" v-else :href="getReturnPath()">
|
||||
{{ $t("return") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -235,6 +260,7 @@ import {
|
||||
Signature,
|
||||
SignatureZone,
|
||||
SignedState,
|
||||
ZoomLevel,
|
||||
} from "../../types";
|
||||
import { makeFetch } from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
||||
import * as pdfjsLib from "pdfjs-dist";
|
||||
@@ -251,7 +277,7 @@ console.log(PdfWorker); // incredible but this is needed
|
||||
// pdfjsLib.GlobalWorkerOptions.workerSrc = PdfWorker;
|
||||
|
||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
||||
import { download_and_decrypt_doc } from "../StoredObjectButton/helpers";
|
||||
import {download_and_decrypt_doc, download_doc_as_pdf} from "../StoredObjectButton/helpers";
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = "pdfjs-dist/build/pdf.worker.mjs";
|
||||
|
||||
@@ -262,6 +288,52 @@ const canvasEvent: Ref<CanvasEvent> = ref("select");
|
||||
const signedState: Ref<SignedState> = ref("pending");
|
||||
const page: Ref<number> = ref(1);
|
||||
const pageCount: Ref<number> = ref(0);
|
||||
const zoom: Ref<number> = ref(1);
|
||||
let zoomLevel = "";
|
||||
const zoomLevels: Ref<ZoomLevel[]> = ref([
|
||||
{
|
||||
id: 0,
|
||||
zoom: 0.75,
|
||||
label: {
|
||||
fr: "75%",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
zoom: zoom.value,
|
||||
label: {
|
||||
fr: "100%",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
zoom: 1.25,
|
||||
label: {
|
||||
fr: "125%",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
zoom: 1.5,
|
||||
label: {
|
||||
fr: "150%",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
zoom: 2,
|
||||
label: {
|
||||
fr: "200%",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
zoom: 3,
|
||||
label: {
|
||||
fr: "300%",
|
||||
},
|
||||
},
|
||||
]);
|
||||
let userSignatureZone: Ref<null | SignatureZone> = ref(null);
|
||||
let pdf = {} as PDFDocumentProxy;
|
||||
|
||||
@@ -275,17 +347,21 @@ const $toast = useToast();
|
||||
|
||||
const signature = window.signature;
|
||||
|
||||
console.log(signature);
|
||||
const setZoomLevel = (zoomLevel: string) => {
|
||||
zoom.value = Number.parseFloat(zoomLevel);
|
||||
setPage(page.value);
|
||||
setTimeout(() => drawAllZones(page.value), 200);
|
||||
};
|
||||
|
||||
const mountPdf = async (url: string) => {
|
||||
const loadingTask = pdfjsLib.getDocument(url);
|
||||
const mountPdf = async (doc: ArrayBuffer) => {
|
||||
const loadingTask = pdfjsLib.getDocument(doc);
|
||||
pdf = await loadingTask.promise;
|
||||
pageCount.value = pdf.numPages;
|
||||
await setPage(page.value);
|
||||
};
|
||||
|
||||
const getRenderContext = (pdfPage: PDFPageProxy) => {
|
||||
const scale = 1;
|
||||
const scale = 1 * zoom.value;
|
||||
const viewport = pdfPage.getViewport({ scale });
|
||||
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
||||
const context = canvas.getContext("2d") as CanvasRenderingContext2D;
|
||||
@@ -309,15 +385,13 @@ const init = () => downloadAndOpen().then(initPdf);
|
||||
async function downloadAndOpen(): Promise<Blob> {
|
||||
let raw;
|
||||
try {
|
||||
raw = await download_and_decrypt_doc(
|
||||
signature.storedObject,
|
||||
signature.storedObject.currentVersion
|
||||
);
|
||||
raw = await download_doc_as_pdf(signature.storedObject);
|
||||
} catch (e) {
|
||||
console.error("error while downloading and decrypting document", e);
|
||||
throw e;
|
||||
}
|
||||
await mountPdf(URL.createObjectURL(raw));
|
||||
const doc = await raw.arrayBuffer();
|
||||
await mountPdf(doc);
|
||||
return raw;
|
||||
}
|
||||
|
||||
@@ -342,12 +416,12 @@ const hitSignature = (
|
||||
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) < xy[0] &&
|
||||
xy[0] <
|
||||
scaleXToCanvas(zone.x + zone.width, canvasWidth, zone.PDFPage.width) &&
|
||||
zone.PDFPage.height -
|
||||
zone.PDFPage.height * zoom.value -
|
||||
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) <
|
||||
xy[1] &&
|
||||
xy[1] <
|
||||
scaleYToCanvas(zone.height - zone.y, canvasHeight, zone.PDFPage.height) +
|
||||
zone.PDFPage.height;
|
||||
zone.PDFPage.height * zoom.value;
|
||||
|
||||
const selectZone = (z: SignatureZone, canvas: HTMLCanvasElement) => {
|
||||
userSignatureZone.value = z;
|
||||
@@ -424,19 +498,19 @@ const drawZone = (
|
||||
ctx.lineJoin = "bevel";
|
||||
ctx.strokeRect(
|
||||
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width),
|
||||
zone.PDFPage.height -
|
||||
zone.PDFPage.height * zoom.value -
|
||||
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height),
|
||||
scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width),
|
||||
scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height)
|
||||
);
|
||||
ctx.font = "bold 16px serif";
|
||||
ctx.font = `bold ${16 * zoom.value}px serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillStyle = "black";
|
||||
const xText =
|
||||
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) +
|
||||
scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width) / 2;
|
||||
const yText =
|
||||
zone.PDFPage.height -
|
||||
zone.PDFPage.height * zoom.value -
|
||||
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) +
|
||||
scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height) / 2;
|
||||
if (userSignatureZone.value?.index === zone.index) {
|
||||
@@ -444,8 +518,8 @@ const drawZone = (
|
||||
ctx.fillText("Signer ici", xText, yText);
|
||||
} else {
|
||||
ctx.fillStyle = unselectedBlue;
|
||||
ctx.fillText("Choisir cette", xText, yText - 12);
|
||||
ctx.fillText("zone de signature", xText, yText + 12);
|
||||
ctx.fillText("Choisir cette", xText, yText - 12 * zoom.value);
|
||||
ctx.fillText("zone de signature", xText, yText + 12 * zoom.value);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -607,6 +681,15 @@ init();
|
||||
#canvas {
|
||||
box-shadow: 0 20px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.onAddZone {
|
||||
cursor: not-allowed;
|
||||
|
||||
#canvas {
|
||||
cursor: copy;
|
||||
}
|
||||
}
|
||||
|
||||
div#action-buttons {
|
||||
position: sticky;
|
||||
bottom: 0px;
|
||||
@@ -615,16 +698,29 @@ div#action-buttons {
|
||||
}
|
||||
div.pdf-tools {
|
||||
background-color: #f3f3f3;
|
||||
font-size: 0.8rem;
|
||||
font-size: 0.6rem;
|
||||
button {
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
div.turnSignature {
|
||||
span {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1400px) {
|
||||
// background: none;
|
||||
// border: none !important;
|
||||
}
|
||||
}
|
||||
div.turn-page {
|
||||
display: flex;
|
||||
span {
|
||||
font-size: 0.8rem;
|
||||
margin: 0 0.4rem;
|
||||
font-size: 0.75rem;
|
||||
margin: auto 0.4rem;
|
||||
}
|
||||
select {
|
||||
width: 5rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
div.signature-modal-body {
|
||||
|
@@ -12,10 +12,10 @@ const appMessages = {
|
||||
sign: 'Signer',
|
||||
choose_another_signature: 'Choisir une autre zone',
|
||||
cancel: 'Annuler',
|
||||
cancel_signing: 'Refuser de signer',
|
||||
last_sign_zone: 'Zone de signature précédente',
|
||||
next_sign_zone: 'Zone de signature suivante',
|
||||
add_sign_zone: 'Ajouter une zone de signature',
|
||||
click_on_document: 'Cliquer sur le document',
|
||||
last_zone: 'Zone précédente',
|
||||
next_zone: 'Zone suivante',
|
||||
add_zone: 'Ajouter une zone',
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<i class="fa fa-download"></i>
|
||||
<template v-if="displayActionStringInButton">Télécharger</template>
|
||||
</a>
|
||||
<a v-else :class="props.classes" target="_blank" :type="props.storedObject.type" :download="buildDocumentName()" :href="state.href_url" ref="open_button" title="Ouvrir">
|
||||
<a v-else :class="props.classes" target="_blank" :type="props.atVersion.type" :download="buildDocumentName()" :href="state.href_url" ref="open_button" title="Ouvrir">
|
||||
<i class="fa fa-external-link"></i>
|
||||
<template v-if="displayActionStringInButton">Ouvrir</template>
|
||||
</a>
|
||||
|
@@ -55,7 +55,7 @@ const classes = computed<{row: true, 'row-hover': true, 'blinking-1': boolean, '
|
||||
<restore-version-button :stored-object-version="props.version" @restore-version="onRestore"></restore-version-button>
|
||||
</li>
|
||||
<li>
|
||||
<download-button :stored-object="storedObject" :at-version="version" :classes="{btn: true, 'btn-outline-primary': true}" :display-action-string-in-button="false"></download-button>
|
||||
<download-button :stored-object="storedObject" :at-version="version" :classes="{btn: true, 'btn-outline-primary': true, 'btn-sm': true}" :display-action-string-in-button="false"></download-button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@@ -197,6 +197,32 @@ async function download_and_decrypt_doc(storedObject: StoredObject, atVersion: n
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the stored object as a pdf.
|
||||
*
|
||||
* If the document is already in a pdf on the server side, the document is retrieved "as is" from the usual
|
||||
* storage.
|
||||
*/
|
||||
async function download_doc_as_pdf(storedObject: StoredObject): Promise<Blob>
|
||||
{
|
||||
if (null === storedObject.currentVersion) {
|
||||
throw new Error("the stored object does not count any version");
|
||||
}
|
||||
|
||||
if (storedObject.currentVersion?.type === 'application/pdf') {
|
||||
return download_and_decrypt_doc(storedObject, storedObject.currentVersion);
|
||||
}
|
||||
|
||||
const convertLink = build_convert_link(storedObject.uuid);
|
||||
const response = await fetch(convertLink);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Could not convert the document: " + response.status);
|
||||
}
|
||||
|
||||
return response.blob();
|
||||
}
|
||||
|
||||
async function is_object_ready(storedObject: StoredObject): Promise<StoredObjectStatusChange>
|
||||
{
|
||||
const new_status_response = await window
|
||||
@@ -214,6 +240,7 @@ export {
|
||||
build_wopi_editor_link,
|
||||
download_and_decrypt_doc,
|
||||
download_doc,
|
||||
download_doc_as_pdf,
|
||||
is_extension_editable,
|
||||
is_extension_viewable,
|
||||
is_object_ready,
|
||||
|
@@ -8,7 +8,7 @@
|
||||
<table class="table table-bordered border-dark align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'Creator bundle id' | trans }}</th>
|
||||
{# <th>{{ 'Creator bundle id' | trans }}</th>#}
|
||||
<th>{{ 'Internal id inside creator bundle' | trans }}</th>
|
||||
<th>{{ 'Document class' | trans }}</th>
|
||||
<th>{{ 'Name' | trans }}</th>
|
||||
@@ -18,7 +18,7 @@
|
||||
<tbody>
|
||||
{% for document_category in document_categories %}
|
||||
<tr>
|
||||
<td>{{ document_category.bundleId }}</td>
|
||||
{# <td>{{ document_category.bundleId }}</td>#}
|
||||
<td>{{ document_category.idInsideBundle }}</td>
|
||||
<td>{{ document_category.documentClass }}</td>
|
||||
<td>{{ document_category.name | localize_translatable_string}}</td>
|
||||
|
@@ -15,7 +15,7 @@ use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
|
||||
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
@@ -34,7 +34,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
|
||||
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly ?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null,
|
||||
private readonly ?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
|
||||
) {}
|
||||
|
||||
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool
|
||||
@@ -46,24 +46,27 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
|
||||
|
||||
public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool
|
||||
{
|
||||
// Retrieve the related accompanying course document
|
||||
// Retrieve the related entity
|
||||
$entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);
|
||||
|
||||
// Determine the attribute to pass to AccompanyingCourseDocumentVoter
|
||||
// Determine the attribute to pass to the voter for argument
|
||||
$voterAttribute = $this->attributeToRole($attribute);
|
||||
|
||||
if (false === $this->security->isGranted($voterAttribute, $entity)) {
|
||||
return false;
|
||||
$regularPermission = $this->security->isGranted($voterAttribute, $entity);
|
||||
|
||||
if (!$this->canBeAssociatedWithWorkflow()) {
|
||||
return $regularPermission;
|
||||
}
|
||||
|
||||
if (StoredObjectRoleEnum::SEE !== $attribute && $this->canBeAssociatedWithWorkflow()) {
|
||||
if (null === $this->workflowDocumentService) {
|
||||
throw new \LogicException('Provide a workflow document service');
|
||||
}
|
||||
$workflowPermission = match ($attribute) {
|
||||
StoredObjectRoleEnum::SEE => $this->workflowDocumentService->isAllowedByWorkflowForReadOperation($entity),
|
||||
StoredObjectRoleEnum::EDIT => $this->workflowDocumentService->isAllowedByWorkflowForWriteOperation($entity),
|
||||
};
|
||||
|
||||
return $this->workflowDocumentService->notBlockedByWorkflow($entity);
|
||||
}
|
||||
|
||||
return true;
|
||||
return match ($workflowPermission) {
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_GRANT => true,
|
||||
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED => false,
|
||||
WorkflowRelatedEntityPermissionHelper::ABSTAIN => $regularPermission,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@ use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredObjectVoter
|
||||
@@ -24,7 +24,7 @@ final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredOb
|
||||
public function __construct(
|
||||
private readonly AccompanyingCourseDocumentRepository $repository,
|
||||
Security $security,
|
||||
WorkflowStoredObjectPermissionHelper $workflowDocumentService,
|
||||
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
|
||||
) {
|
||||
parent::__construct($security, $workflowDocumentService);
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
|
||||
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter
|
||||
@@ -24,7 +24,7 @@ class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter
|
||||
public function __construct(
|
||||
private readonly PersonDocumentRepository $repository,
|
||||
Security $security,
|
||||
WorkflowStoredObjectPermissionHelper $workflowDocumentService,
|
||||
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
|
||||
) {
|
||||
parent::__construct($security, $workflowDocumentService);
|
||||
}
|
||||
|
@@ -17,15 +17,30 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\WopiBundle\Service\WopiConverter;
|
||||
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||
|
||||
class PDFSignatureZoneAvailable
|
||||
class PDFSignatureZoneAvailable implements LocaleAwareInterface
|
||||
{
|
||||
private string $locale;
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||
private readonly PDFSignatureZoneParser $pdfSignatureZoneParser,
|
||||
private readonly StoredObjectManagerInterface $storedObjectManager,
|
||||
private readonly WopiConverter $converter,
|
||||
) {}
|
||||
|
||||
public function setLocale(string $locale)
|
||||
{
|
||||
$this->locale = $locale;
|
||||
}
|
||||
|
||||
public function getLocale()
|
||||
{
|
||||
return $this->locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<PDFSignatureZone>
|
||||
*/
|
||||
@@ -38,10 +53,16 @@ class PDFSignatureZoneAvailable
|
||||
}
|
||||
|
||||
if ('application/pdf' !== $storedObject->getType()) {
|
||||
throw new \RuntimeException('Only PDF documents are supported');
|
||||
$content = $this->converter->convert($this->getLocale(), $this->storedObjectManager->read($storedObject), $storedObject->getType());
|
||||
} else {
|
||||
$content = $this->storedObjectManager->read($storedObject);
|
||||
}
|
||||
|
||||
$zones = $this->pdfSignatureZoneParser->findSignatureZones($this->storedObjectManager->read($storedObject));
|
||||
$zones = $this->pdfSignatureZoneParser->findSignatureZones($content);
|
||||
|
||||
// free some memory as soon as possible...
|
||||
unset($content);
|
||||
|
||||
$signatureZonesIndexes = array_map(
|
||||
fn (EntityWorkflowStepSignature $step) => $step->getZoneSignatureIndex(),
|
||||
$this->collectSignaturesInUse($entityWorkflow)
|
||||
|
@@ -39,13 +39,13 @@ class StoredObjectToPdfConverter
|
||||
* @param string $lang the language for the conversion context
|
||||
* @param string $convertTo The target format for the conversion. Default is 'pdf'.
|
||||
*
|
||||
* @return array{0: StoredObjectPointInTime, 1: StoredObjectVersion} contains the point in time before conversion and the new version of the stored object
|
||||
* @return array{0: StoredObjectPointInTime, 1: StoredObjectVersion, 2?: string} contains the point in time before conversion and the new version of the stored object. The converted content is included in the response if $includeConvertedContent is true
|
||||
*
|
||||
* @throws \UnexpectedValueException if the preferred mime type for the conversion is not found
|
||||
* @throws \RuntimeException if the conversion or storage of the new version fails
|
||||
* @throws StoredObjectManagerException
|
||||
*/
|
||||
public function addConvertedVersion(StoredObject $storedObject, string $lang, $convertTo = 'pdf'): array
|
||||
public function addConvertedVersion(StoredObject $storedObject, string $lang, $convertTo = 'pdf', bool $includeConvertedContent = false): array
|
||||
{
|
||||
$newMimeType = $this->mimeTypes->getMimeTypes($convertTo)[0] ?? null;
|
||||
|
||||
@@ -70,6 +70,11 @@ class StoredObjectToPdfConverter
|
||||
$pointInTime = new StoredObjectPointInTime($currentVersion, StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION);
|
||||
$version = $this->storedObjectManager->write($storedObject, $converted, $newMimeType);
|
||||
|
||||
if (!$includeConvertedContent) {
|
||||
return [$pointInTime, $version];
|
||||
}
|
||||
|
||||
return [$pointInTime, $version, $converted];
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Service;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
|
||||
class WorkflowStoredObjectPermissionHelper
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||
private readonly Registry $registry,
|
||||
) {}
|
||||
|
||||
public function notBlockedByWorkflow(object $entity): bool
|
||||
{
|
||||
$entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
|
||||
$currentUser = $this->security->getUser();
|
||||
|
||||
foreach ($entityWorkflows as $entityWorkflow) {
|
||||
if ($entityWorkflow->isFinal()) {
|
||||
|
||||
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||
$marking = $workflow->getMarkingStore()->getMarking($entityWorkflow);
|
||||
foreach ($marking->getPlaces() as $place => $active) {
|
||||
$metadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
|
||||
if ($metadata['isFinalPositive'] ?? true) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!$entityWorkflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// as soon as there is one signatured applyied, we are not able to
|
||||
// edit the document any more
|
||||
foreach ($entityWorkflow->getSteps() as $step) {
|
||||
foreach ($step->getSignatures() as $signature) {
|
||||
if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -15,10 +15,11 @@ use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
|
||||
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
/**
|
||||
@@ -28,26 +29,21 @@ use Symfony\Component\Security\Core\Security;
|
||||
*/
|
||||
class AbstractStoredObjectVoterTest extends TestCase
|
||||
{
|
||||
private AssociatedEntityToStoredObjectInterface $repository;
|
||||
private Security $security;
|
||||
private WorkflowStoredObjectPermissionHelper $workflowDocumentService;
|
||||
use ProphecyTrait;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->repository = $this->createMock(AssociatedEntityToStoredObjectInterface::class);
|
||||
$this->security = $this->createMock(Security::class);
|
||||
$this->workflowDocumentService = $this->createMock(WorkflowStoredObjectPermissionHelper::class);
|
||||
}
|
||||
|
||||
private function buildStoredObjectVoter(bool $canBeAssociatedWithWorkflow, AssociatedEntityToStoredObjectInterface $repository, Security $security, ?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null): AbstractStoredObjectVoter
|
||||
{
|
||||
private function buildStoredObjectVoter(
|
||||
bool $canBeAssociatedWithWorkflow,
|
||||
AssociatedEntityToStoredObjectInterface $repository,
|
||||
Security $security,
|
||||
?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
|
||||
): AbstractStoredObjectVoter {
|
||||
// Anonymous class extending the abstract class
|
||||
return new class ($canBeAssociatedWithWorkflow, $repository, $security, $workflowDocumentService) extends AbstractStoredObjectVoter {
|
||||
public function __construct(
|
||||
private readonly bool $canBeAssociatedWithWorkflow,
|
||||
private readonly AssociatedEntityToStoredObjectInterface $repository,
|
||||
Security $security,
|
||||
?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null,
|
||||
?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
|
||||
) {
|
||||
parent::__construct($security, $workflowDocumentService);
|
||||
}
|
||||
@@ -74,95 +70,89 @@ class AbstractStoredObjectVoterTest extends TestCase
|
||||
};
|
||||
}
|
||||
|
||||
private function setupMockObjects(): array
|
||||
{
|
||||
$user = new User();
|
||||
$token = $this->createMock(TokenInterface::class);
|
||||
$subject = new StoredObject();
|
||||
$entity = new \stdClass();
|
||||
|
||||
return [$user, $token, $subject, $entity];
|
||||
}
|
||||
|
||||
private function setupMocksForVoteOnAttribute(User $user, TokenInterface $token, bool $isGrantedForEntity, object $entity, bool $workflowAllowed): void
|
||||
{
|
||||
// Set up token to return user
|
||||
$token->method('getUser')->willReturn($user);
|
||||
|
||||
// Mock the return of an AccompanyingCourseDocument by the repository
|
||||
$this->repository->method('findAssociatedEntityToStoredObject')->willReturn($entity);
|
||||
|
||||
// Mock scenario where user is allowed to see_details of the AccompanyingCourseDocument
|
||||
$this->security->method('isGranted')->willReturn($isGrantedForEntity);
|
||||
|
||||
// Mock case where user is blocked or not by workflow
|
||||
$this->workflowDocumentService->method('notBlockedByWorkflow')->willReturn($workflowAllowed);
|
||||
}
|
||||
|
||||
public function testSupportsOnAttribute(): void
|
||||
{
|
||||
[$user, $token, $subject, $entity] = $this->setupMockObjects();
|
||||
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(new \stdClass()), $this->prophesize(Security::class)->reveal(), null);
|
||||
|
||||
// Setup mocks for voteOnAttribute method
|
||||
$this->setupMocksForVoteOnAttribute($user, $token, true, $entity, true);
|
||||
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
|
||||
self::assertTrue($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject()));
|
||||
|
||||
self::assertTrue($voter->supports(StoredObjectRoleEnum::SEE, $subject));
|
||||
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(new User()), $this->prophesize(Security::class)->reveal(), null);
|
||||
|
||||
self::assertFalse($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject()));
|
||||
|
||||
$voter = $this->buildStoredObjectVoter(false, new DummyRepository(null), $this->prophesize(Security::class)->reveal(), null);
|
||||
|
||||
self::assertFalse($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject()));
|
||||
}
|
||||
|
||||
public function testVoteOnAttributeAllowedAndWorkflowAllowed(): void
|
||||
{
|
||||
[$user, $token, $subject, $entity] = $this->setupMockObjects();
|
||||
/**
|
||||
* @dataProvider dataProviderVoteOnAttribute
|
||||
*/
|
||||
public function testVoteOnAttribute(
|
||||
StoredObjectRoleEnum $attribute,
|
||||
bool $expected,
|
||||
bool $canBeAssociatedWithWorkflow,
|
||||
bool $isGrantedRegularPermission,
|
||||
?string $isGrantedWorkflowPermissionRead,
|
||||
?string $isGrantedWorkflowPermissionWrite,
|
||||
string $message,
|
||||
): void {
|
||||
$storedObject = new StoredObject();
|
||||
$dummyRepository = new DummyRepository($related = new \stdClass());
|
||||
$token = new UsernamePasswordToken(new User(), 'dummy');
|
||||
|
||||
// Setup mocks for voteOnAttribute method
|
||||
$this->setupMocksForVoteOnAttribute($user, $token, true, $entity, true);
|
||||
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
|
||||
|
||||
// The voteOnAttribute method should return True when workflow is allowed
|
||||
self::assertTrue($voter->voteOnAttribute(StoredObjectRoleEnum::SEE, $subject, $token));
|
||||
$workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class);
|
||||
if (null !== $isGrantedWorkflowPermissionRead) {
|
||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
|
||||
->willReturn($isGrantedWorkflowPermissionRead)->shouldBeCalled();
|
||||
} else {
|
||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)->shouldNotBeCalled();
|
||||
}
|
||||
|
||||
public function testVoteOnAttributeNotAllowed(): void
|
||||
{
|
||||
[$user, $token, $subject, $entity] = $this->setupMockObjects();
|
||||
|
||||
// Setup mocks for voteOnAttribute method where isGranted() returns false
|
||||
$this->setupMocksForVoteOnAttribute($user, $token, false, $entity, true);
|
||||
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
|
||||
|
||||
// The voteOnAttribute method should return True when workflow is allowed
|
||||
self::assertFalse($voter->voteOnAttribute(StoredObjectRoleEnum::SEE, $subject, $token));
|
||||
if (null !== $isGrantedWorkflowPermissionWrite) {
|
||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related)
|
||||
->willReturn($isGrantedWorkflowPermissionWrite)->shouldBeCalled();
|
||||
} else {
|
||||
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related)->shouldNotBeCalled();
|
||||
}
|
||||
|
||||
public function testVoteOnAttributeAllowedWorkflowNotAllowed(): void
|
||||
{
|
||||
[$user, $token, $subject, $entity] = $this->setupMockObjects();
|
||||
|
||||
// Setup mocks for voteOnAttribute method
|
||||
$this->setupMocksForVoteOnAttribute($user, $token, true, $entity, false);
|
||||
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
|
||||
|
||||
// Test voteOnAttribute method
|
||||
$attribute = StoredObjectRoleEnum::EDIT;
|
||||
$result = $voter->voteOnAttribute($attribute, $subject, $token);
|
||||
|
||||
// Assert that access is denied when workflow is not allowed
|
||||
$this->assertFalse($result);
|
||||
$voter = $this->buildStoredObjectVoter($canBeAssociatedWithWorkflow, $dummyRepository, $security->reveal(), $workflowRelatedEntityPermissionHelper->reveal());
|
||||
self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message);
|
||||
}
|
||||
|
||||
public function testVoteOnAttributeAllowedWorkflowAllowedToSeeDocument(): void
|
||||
public static function dataProviderVoteOnAttribute(): iterable
|
||||
{
|
||||
[$user, $token, $subject, $entity] = $this->setupMockObjects();
|
||||
// not associated on a workflow
|
||||
yield [StoredObjectRoleEnum::SEE, true, false, true, null, null, 'not associated on a workflow, granted by regular access, must not rely on helper'];
|
||||
yield [StoredObjectRoleEnum::SEE, false, false, false, null, null, 'not associated on a workflow, denied by regular access, must not rely on helper'];
|
||||
|
||||
// Setup mocks for voteOnAttribute method
|
||||
$this->setupMocksForVoteOnAttribute($user, $token, true, $entity, false);
|
||||
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
|
||||
// associated on a workflow, read operation
|
||||
yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be granted'];
|
||||
yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelper::ABSTAIN, null, 'associated on a workflow, read by regular, abstain by workflow, ask for read, should be granted'];
|
||||
yield [StoredObjectRoleEnum::SEE, false, true, true, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be denied'];
|
||||
yield [StoredObjectRoleEnum::SEE, true, true, false, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be granted'];
|
||||
yield [StoredObjectRoleEnum::SEE, false, true, false, WorkflowRelatedEntityPermissionHelper::ABSTAIN, null, 'associated on a workflow, denied read by regular, abstain by workflow, ask for read, should be granted'];
|
||||
yield [StoredObjectRoleEnum::SEE, false, true, false, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be denied'];
|
||||
|
||||
// Test voteOnAttribute method
|
||||
$attribute = StoredObjectRoleEnum::SEE;
|
||||
$result = $voter->voteOnAttribute($attribute, $subject, $token);
|
||||
|
||||
// Assert that access is denied when workflow is not allowed
|
||||
$this->assertTrue($result);
|
||||
// association on a workflow, write operation
|
||||
yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be granted'];
|
||||
yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelper::ABSTAIN, 'associated on a workflow, write by regular, abstain by workflow, ask for write, should be granted'];
|
||||
yield [StoredObjectRoleEnum::EDIT, false, true, true, null, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be denied'];
|
||||
yield [StoredObjectRoleEnum::EDIT, true, true, false, null, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be granted'];
|
||||
yield [StoredObjectRoleEnum::EDIT, false, true, false, null, WorkflowRelatedEntityPermissionHelper::ABSTAIN, 'associated on a workflow, denied write by regular, abstain by workflow, ask for write, should be granted'];
|
||||
yield [StoredObjectRoleEnum::EDIT, false, true, false, null, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be denied'];
|
||||
}
|
||||
}
|
||||
|
||||
class DummyRepository implements AssociatedEntityToStoredObjectInterface
|
||||
{
|
||||
public function __construct(private readonly ?object $relatedEntity) {}
|
||||
|
||||
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object
|
||||
{
|
||||
return $this->relatedEntity;
|
||||
}
|
||||
}
|
||||
|
@@ -22,6 +22,7 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\WopiBundle\Service\WopiConverter;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
@@ -65,6 +66,7 @@ class PDFSignatureZoneAvailableTest extends TestCase
|
||||
$entityWorkflowManager->reveal(),
|
||||
$parser->reveal(),
|
||||
$storedObjectManager->reveal(),
|
||||
$this->prophesize(WopiConverter::class)->reveal(),
|
||||
);
|
||||
|
||||
$actual = $filter->getAvailableSignatureZones($entityWorkflow);
|
||||
|
@@ -1,161 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Service;
|
||||
|
||||
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
|
||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
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\Workflow;
|
||||
use Symfony\Component\Workflow\WorkflowInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class WorkflowStoredObjectPermissionHelperTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
/**
|
||||
* @dataProvider provideDataNotBlockByWorkflow
|
||||
*/
|
||||
public function testNotBlockByWorkflow(EntityWorkflow $entityWorkflow, User $user, bool $expected, string $message): void
|
||||
{
|
||||
// all entities must have this workflow name, so we are ok to set it here
|
||||
$entityWorkflow->setWorkflowName('dummy');
|
||||
$object = new \stdClass();
|
||||
$helper = $this->buildHelper($object, $entityWorkflow, $user);
|
||||
|
||||
self::assertEquals($expected, $helper->notBlockedByWorkflow($entityWorkflow), $message);
|
||||
}
|
||||
|
||||
private function buildHelper(object $relatedEntity, EntityWorkflow $entityWorkflow, User $user): WorkflowStoredObjectPermissionHelper
|
||||
{
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->getUser()->willReturn($user);
|
||||
|
||||
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
||||
$entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn([$entityWorkflow]);
|
||||
|
||||
return new WorkflowStoredObjectPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $this->buildRegistry());
|
||||
}
|
||||
|
||||
public static function provideDataNotBlockByWorkflow(): iterable
|
||||
{
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable());
|
||||
|
||||
yield [$entityWorkflow, new User(), false, 'blocked 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(), $user);
|
||||
|
||||
yield [$entityWorkflow, $user, true, 'allowed because the user is present as a dest user'];
|
||||
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$dto->futureDestUsers[] = $user = new User();
|
||||
$entityWorkflow->setStep('final_positive', $dto, 'to_final_positive', new \DateTimeImmutable(), $user);
|
||||
$entityWorkflow->getCurrentStep()->setIsFinal(true);
|
||||
|
||||
yield [$entityWorkflow, $user, false, 'blocked because the step is final, and final positive'];
|
||||
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$dto->futureDestUsers[] = $user = new User();
|
||||
$entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable(), $user);
|
||||
$entityWorkflow->getCurrentStep()->setIsFinal(true);
|
||||
|
||||
yield [$entityWorkflow, $user, true, 'allowed because the step is final, and final negative'];
|
||||
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$dto->futureDestUsers[] = $user = new User();
|
||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user);
|
||||
$step = $entityWorkflow->getCurrentStep();
|
||||
new EntityWorkflowStepSignature($step, new Person());
|
||||
|
||||
yield [$entityWorkflow, $user, true, 'allow, a signature is present but still pending'];
|
||||
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$dto->futureDestUsers[] = $user = new User();
|
||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user);
|
||||
$step = $entityWorkflow->getCurrentStep();
|
||||
$signature = new EntityWorkflowStepSignature($step, new Person());
|
||||
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED);
|
||||
|
||||
yield [$entityWorkflow, $user, false, 'blocked, a signature is present and signed'];
|
||||
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$dto->futureDestUsers[] = $user = new User();
|
||||
$entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable(), $user);
|
||||
$step = $entityWorkflow->getCurrentStep();
|
||||
$signature = new EntityWorkflowStepSignature($step, new Person());
|
||||
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED);
|
||||
|
||||
yield [$entityWorkflow, $user, false, 'blocked, a signature is present and signed, although the workflow is final negative'];
|
||||
|
||||
}
|
||||
|
||||
private static function buildRegistry(): Registry
|
||||
{
|
||||
$builder = new DefinitionBuilder();
|
||||
$builder
|
||||
->setInitialPlaces(['initial'])
|
||||
->addPlaces(['initial', 'test', 'final_positive', 'final_negative'])
|
||||
->setMetadataStore(
|
||||
new InMemoryMetadataStore(
|
||||
placesMetadata: [
|
||||
'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;
|
||||
}
|
||||
}
|
@@ -1,208 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Workflow;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTime;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectToPdfConverter;
|
||||
use Chill\DocStoreBundle\Workflow\ConvertToPdfBeforeSignatureStepEventSubscriber;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
|
||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
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 ConvertToPdfBeforeSignatureStepEventSubscriberTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testConvertToPdfBeforeSignatureStepEventSubscriberToSignature(): void
|
||||
{
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$storedObject = new StoredObject();
|
||||
$previousVersion = $storedObject->registerVersion();
|
||||
|
||||
$converter = $this->prophesize(StoredObjectToPdfConverter::class);
|
||||
$converter->addConvertedVersion($storedObject, 'fr', 'pdf')
|
||||
->shouldBeCalledOnce()
|
||||
->will(function ($args) {
|
||||
/** @var StoredObject $storedObject */
|
||||
$storedObject = $args[0];
|
||||
|
||||
$pointInTime = new StoredObjectPointInTime($storedObject->getCurrentVersion(), StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION);
|
||||
$newVersion = $storedObject->registerVersion(filename: 'next');
|
||||
|
||||
return [$pointInTime, $newVersion];
|
||||
});
|
||||
|
||||
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
||||
$entityWorkflowManager->getAssociatedStoredObject($entityWorkflow)->willReturn($storedObject);
|
||||
|
||||
$request = new Request();
|
||||
$request->setLocale('fr');
|
||||
$stack = new RequestStack();
|
||||
$stack->push($request);
|
||||
|
||||
$eventSubscriber = new ConvertToPdfBeforeSignatureStepEventSubscriber($entityWorkflowManager->reveal(), $converter->reveal(), $stack);
|
||||
|
||||
$registry = $this->buildRegistry($eventSubscriber);
|
||||
$workflow = $registry->get($entityWorkflow, 'dummy');
|
||||
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$workflow->apply($entityWorkflow, 'to_signature', ['context' => $dto, 'transition' => 'to_signature', 'transitionAt' => new \DateTimeImmutable('now')]);
|
||||
|
||||
self::assertEquals('signature', $entityWorkflow->getStep());
|
||||
self::assertNotSame($previousVersion, $storedObject->getCurrentVersion());
|
||||
self::assertTrue($previousVersion->hasPointInTimes());
|
||||
self::assertCount(2, $storedObject->getVersions());
|
||||
self::assertEquals('next', $storedObject->getCurrentVersion()->getFilename());
|
||||
}
|
||||
|
||||
public function testConvertToPdfBeforeSignatureStepEventSubscriberToNotASignatureStep(): void
|
||||
{
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$storedObject = new StoredObject();
|
||||
$previousVersion = $storedObject->registerVersion();
|
||||
|
||||
$converter = $this->prophesize(StoredObjectToPdfConverter::class);
|
||||
$converter->addConvertedVersion($storedObject, 'fr', 'pdf')
|
||||
->shouldNotBeCalled();
|
||||
|
||||
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
||||
$entityWorkflowManager->getAssociatedStoredObject($entityWorkflow)->willReturn($storedObject);
|
||||
|
||||
$request = new Request();
|
||||
$request->setLocale('fr');
|
||||
$stack = new RequestStack();
|
||||
$stack->push($request);
|
||||
|
||||
$eventSubscriber = new ConvertToPdfBeforeSignatureStepEventSubscriber($entityWorkflowManager->reveal(), $converter->reveal(), $stack);
|
||||
|
||||
$registry = $this->buildRegistry($eventSubscriber);
|
||||
$workflow = $registry->get($entityWorkflow, 'dummy');
|
||||
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$workflow->apply($entityWorkflow, 'to_something', ['context' => $dto, 'transition' => 'to_something', 'transitionAt' => new \DateTimeImmutable('now')]);
|
||||
|
||||
self::assertEquals('something', $entityWorkflow->getStep());
|
||||
self::assertSame($previousVersion, $storedObject->getCurrentVersion());
|
||||
self::assertFalse($previousVersion->hasPointInTimes());
|
||||
self::assertCount(1, $storedObject->getVersions());
|
||||
}
|
||||
|
||||
public function testConvertToPdfBeforeSignatureStepEventSubscriberToSignatureAlreadyAPdf(): void
|
||||
{
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$storedObject = new StoredObject();
|
||||
$previousVersion = $storedObject->registerVersion(type: 'application/pdf');
|
||||
|
||||
$converter = $this->prophesize(StoredObjectToPdfConverter::class);
|
||||
$converter->addConvertedVersion($storedObject, 'fr', 'pdf')
|
||||
->shouldNotBeCalled();
|
||||
|
||||
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
||||
$entityWorkflowManager->getAssociatedStoredObject($entityWorkflow)->willReturn($storedObject);
|
||||
|
||||
$request = new Request();
|
||||
$request->setLocale('fr');
|
||||
$stack = new RequestStack();
|
||||
$stack->push($request);
|
||||
|
||||
$eventSubscriber = new ConvertToPdfBeforeSignatureStepEventSubscriber($entityWorkflowManager->reveal(), $converter->reveal(), $stack);
|
||||
|
||||
$registry = $this->buildRegistry($eventSubscriber);
|
||||
$workflow = $registry->get($entityWorkflow, 'dummy');
|
||||
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$workflow->apply($entityWorkflow, 'to_signature', ['context' => $dto, 'transition' => 'to_signature', 'transitionAt' => new \DateTimeImmutable('now')]);
|
||||
|
||||
self::assertEquals('signature', $entityWorkflow->getStep());
|
||||
self::assertSame($previousVersion, $storedObject->getCurrentVersion());
|
||||
self::assertFalse($previousVersion->hasPointInTimes());
|
||||
self::assertCount(1, $storedObject->getVersions());
|
||||
}
|
||||
|
||||
public function testConvertToPdfBeforeSignatureStepEventSubscriberToSignatureWithNoStoredObject(): void
|
||||
{
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
|
||||
$converter = $this->prophesize(StoredObjectToPdfConverter::class);
|
||||
$converter->addConvertedVersion(Argument::type(StoredObject::class), 'fr', 'pdf')
|
||||
->shouldNotBeCalled();
|
||||
|
||||
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
||||
$entityWorkflowManager->getAssociatedStoredObject($entityWorkflow)->willReturn(null);
|
||||
|
||||
$request = new Request();
|
||||
$request->setLocale('fr');
|
||||
$stack = new RequestStack();
|
||||
$stack->push($request);
|
||||
|
||||
$eventSubscriber = new ConvertToPdfBeforeSignatureStepEventSubscriber($entityWorkflowManager->reveal(), $converter->reveal(), $stack);
|
||||
|
||||
$registry = $this->buildRegistry($eventSubscriber);
|
||||
$workflow = $registry->get($entityWorkflow, 'dummy');
|
||||
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$workflow->apply($entityWorkflow, 'to_signature', ['context' => $dto, 'transition' => 'to_signature', 'transitionAt' => new \DateTimeImmutable('now')]);
|
||||
|
||||
self::assertEquals('signature', $entityWorkflow->getStep());
|
||||
}
|
||||
|
||||
private function buildRegistry(EventSubscriberInterface $eventSubscriber): Registry
|
||||
{
|
||||
$builder = new DefinitionBuilder();
|
||||
$builder
|
||||
->setInitialPlaces('initial')
|
||||
->addPlaces(['initial', 'signature', 'something'])
|
||||
->addTransition(new Transition('to_something', 'initial', 'something'))
|
||||
->addTransition(new Transition('to_signature', 'initial', 'signature'));
|
||||
|
||||
$metadataStore = new InMemoryMetadataStore([], ['signature' => ['isSignature' => ['user']]]);
|
||||
$builder->setMetadataStore($metadataStore);
|
||||
|
||||
$eventDispatcher = new EventDispatcher();
|
||||
$eventDispatcher->addSubscriber($eventSubscriber);
|
||||
|
||||
$workflow = new Workflow($builder->build(), new EntityWorkflowMarkingStore(), $eventDispatcher, 'dummy');
|
||||
|
||||
$supports = new class () implements WorkflowSupportStrategyInterface {
|
||||
public function supports(WorkflowInterface $workflow, object $subject): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
$registry = new Registry();
|
||||
$registry->addWorkflow($workflow, $supports);
|
||||
|
||||
return $registry;
|
||||
}
|
||||
}
|
@@ -1,75 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Workflow;
|
||||
|
||||
use Chill\DocStoreBundle\Service\StoredObjectToPdfConverter;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Workflow\Event\CompletedEvent;
|
||||
use Symfony\Component\Workflow\WorkflowEvents;
|
||||
|
||||
/**
|
||||
* Event subscriber to convert objects to PDF when the document reach a signature step.
|
||||
*/
|
||||
class ConvertToPdfBeforeSignatureStepEventSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||
private readonly StoredObjectToPdfConverter $storedObjectToPdfConverter,
|
||||
private readonly RequestStack $requestStack,
|
||||
) {}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
WorkflowEvents::COMPLETED => 'convertToPdfBeforeSignatureStepEvent',
|
||||
];
|
||||
}
|
||||
|
||||
public function convertToPdfBeforeSignatureStepEvent(CompletedEvent $event): void
|
||||
{
|
||||
$entityWorkflow = $event->getSubject();
|
||||
if (!$entityWorkflow instanceof EntityWorkflow) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tos = $event->getTransition()->getTos();
|
||||
$workflow = $event->getWorkflow();
|
||||
$metadataStore = $workflow->getMetadataStore();
|
||||
|
||||
foreach ($tos as $to) {
|
||||
$metadata = $metadataStore->getPlaceMetadata($to);
|
||||
if (array_key_exists('isSignature', $metadata) && 0 < count($metadata['isSignature'])) {
|
||||
$this->convertToPdf($entityWorkflow);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function convertToPdf(EntityWorkflow $entityWorkflow): void
|
||||
{
|
||||
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
|
||||
|
||||
if (null === $storedObject) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ('application/pdf' === $storedObject->getCurrentVersion()->getType()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->storedObjectToPdfConverter->addConvertedVersion($storedObject, $this->requestStack->getCurrentRequest()->getLocale(), 'pdf');
|
||||
}
|
||||
}
|
@@ -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\Migrations\DocStore;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20241118151618 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Force no duplicated object_id within person_document and accompanyingcourse_document';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql(<<<'SQL'
|
||||
WITH ranked AS (
|
||||
SELECT id, rank() OVER (PARTITION BY object_id ORDER BY id ASC) FROM chill_doc.accompanyingcourse_document
|
||||
)
|
||||
DELETE FROM chill_doc.accompanyingcourse_document WHERE id IN (SELECT id FROM ranked where "rank" <> 1)
|
||||
SQL);
|
||||
$this->addSql('CREATE UNIQUE INDEX acc_course_document_unique_stored_object ON chill_doc.accompanyingcourse_document (object_id)');
|
||||
$this->addSql('CREATE UNIQUE INDEX person_document_unique_stored_object ON chill_doc.person_document (object_id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX acc_course_document_unique_stored_object');
|
||||
$this->addSql('DROP INDEX person_document_unique_stored_object');
|
||||
}
|
||||
}
|
@@ -74,6 +74,8 @@ no records found:
|
||||
Create new category: Créer une nouvelle catégorie
|
||||
Back to the category list: Retour à la liste
|
||||
Create new DocumentCategory: Créer une nouvelle catégorie de document
|
||||
Accompanying period document: Document de parcours d'accompagnement
|
||||
Person document: Document de personne
|
||||
|
||||
# WOPI EDIT
|
||||
online_edit_document: Éditer en ligne
|
||||
|
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\EventBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* Class Status.
|
||||
@@ -36,6 +37,7 @@ class Status
|
||||
private $name;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: EventType::class, inversedBy: 'statuses')]
|
||||
#[Assert\NotNull(message: 'An event status must be linked to an event type.')]
|
||||
private ?EventType $type = null;
|
||||
|
||||
/**
|
||||
|
@@ -1,10 +1,10 @@
|
||||
{% extends "@ChillEvent/Admin/index.html.twig" %}
|
||||
{% extends '@ChillMain/Admin/layoutWithVerticalMenu.html.twig' %}
|
||||
|
||||
{% block admin_content -%}
|
||||
|
||||
<h1>{{ 'EventType list'|trans }}</h1>
|
||||
|
||||
<table class="records_list">
|
||||
<table class="table table-bordered border-dark align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'Id'|trans }}</th>
|
||||
|
@@ -1,10 +1,10 @@
|
||||
{% extends "@ChillEvent/Admin/index.html.twig" %}
|
||||
{% extends '@ChillMain/Admin/layoutWithVerticalMenu.html.twig' %}
|
||||
|
||||
{% block admin_content -%}
|
||||
|
||||
<h1>{{ 'Role list'|trans }}</h1>
|
||||
|
||||
<table class="records_list">
|
||||
<table class="table table-bordered border-dark align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'Id'|trans }}</th>
|
||||
|
@@ -1,10 +1,10 @@
|
||||
{% extends "@ChillEvent/Admin/index.html.twig" %}
|
||||
{% extends '@ChillMain/Admin/layoutWithVerticalMenu.html.twig' %}
|
||||
|
||||
{% block admin_content -%}
|
||||
|
||||
<h1>{{ 'Status list'|trans }}</h1>
|
||||
|
||||
<table class="records_list">
|
||||
<table class="table table-bordered border-dark align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'Id'|trans }}</th>
|
||||
|
@@ -14,7 +14,7 @@ namespace Chill\EventBundle\Security\Authorization;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
|
||||
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Chill\EventBundle\Entity\Event;
|
||||
use Chill\EventBundle\Repository\EventRepository;
|
||||
use Chill\EventBundle\Security\EventVoter;
|
||||
@@ -25,7 +25,7 @@ class EventStoredObjectVoter extends AbstractStoredObjectVoter
|
||||
public function __construct(
|
||||
private readonly EventRepository $repository,
|
||||
Security $security,
|
||||
WorkflowStoredObjectPermissionHelper $workflowDocumentService,
|
||||
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
|
||||
) {
|
||||
parent::__construct($security, $workflowDocumentService);
|
||||
}
|
||||
|
@@ -1,19 +0,0 @@
|
||||
footer.footer {
|
||||
padding: 0;
|
||||
background-color: white;
|
||||
border-top: 1px solid grey;
|
||||
div.sponsors {
|
||||
p {
|
||||
padding-bottom: 10px;
|
||||
color: #000;
|
||||
font-size: 16px;
|
||||
}
|
||||
background-color: white;
|
||||
padding: 2em 0;
|
||||
img {
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1 +0,0 @@
|
||||
require('./csconnectes.scss');
|
||||
|
@@ -5,8 +5,7 @@ module.exports = function(encore, chillEntries)
|
||||
personal_situation_edit_file = __dirname + '/Resources/public/module/personal_situation/index.js',
|
||||
cv_edit_file = __dirname + '/Resources/public/module/cv_edit/index.js',
|
||||
immersion_edit_file = __dirname + '/Resources/public/module/immersion_edit/index.js',
|
||||
images = __dirname + '/Resources/public/images/index.js',
|
||||
sass_styles = __dirname + '/Resources/public/sass/index.js'
|
||||
images = __dirname + '/Resources/public/images/index.js'
|
||||
;
|
||||
|
||||
encore.addEntry('dispositifs_edit', dispositif_edit_file);
|
||||
@@ -15,6 +14,4 @@ module.exports = function(encore, chillEntries)
|
||||
encore.addEntry('images', images);
|
||||
encore.addEntry('cs_cv', cv_edit_file);
|
||||
|
||||
chillEntries.push(sass_styles);
|
||||
|
||||
};
|
||||
|
@@ -0,0 +1,32 @@
|
||||
<?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\CRUD\Controller\ApiController;
|
||||
use Chill\MainBundle\Pagination\PaginatorInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class GenderApiController extends ApiController
|
||||
{
|
||||
protected function customizeQuery(string $action, Request $request, $query): void
|
||||
{
|
||||
$query
|
||||
->andWhere(
|
||||
$query->expr()->eq('e.active', "'TRUE'")
|
||||
);
|
||||
}
|
||||
|
||||
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator, $_format)
|
||||
{
|
||||
return $query->addOrderBy('e.order', 'ASC');
|
||||
}
|
||||
}
|
26
src/Bundle/ChillMainBundle/Controller/GenderController.php
Normal file
26
src/Bundle/ChillMainBundle/Controller/GenderController.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?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\CRUD\Controller\CRUDController;
|
||||
use Chill\MainBundle\Pagination\PaginatorInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class GenderController extends CRUDController
|
||||
{
|
||||
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
|
||||
{
|
||||
$query->addOrderBy('e.order', 'ASC');
|
||||
|
||||
return parent::orderQuery($action, $query, $request, $paginator);
|
||||
}
|
||||
}
|
@@ -12,7 +12,9 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\CRUD\Controller\CRUDController;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\GroupCenter;
|
||||
use Chill\MainBundle\Entity\PermissionsGroup;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Form\Type\ComposedGroupCenterType;
|
||||
use Chill\MainBundle\Form\UserCurrentLocationType;
|
||||
@@ -64,10 +66,14 @@ class UserController extends CRUDController
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isValid()) {
|
||||
$groupCenter = $this->getPersistedGroupCenter(
|
||||
$form[self::FORM_GROUP_CENTER_COMPOSED]->getData()
|
||||
);
|
||||
|
||||
$formData = $form[self::FORM_GROUP_CENTER_COMPOSED]->getData();
|
||||
$selectedCenters = $formData['center'];
|
||||
|
||||
foreach ($selectedCenters as $center) {
|
||||
$groupCenter = $this->getPersistedGroupCenter($center, $formData['permissionsgroup']);
|
||||
$user->addGroupCenter($groupCenter);
|
||||
}
|
||||
|
||||
if (0 === $this->validator->validate($user)->count()) {
|
||||
$em->flush();
|
||||
@@ -419,17 +425,21 @@ class UserController extends CRUDController
|
||||
}
|
||||
}
|
||||
|
||||
private function getPersistedGroupCenter(GroupCenter $groupCenter)
|
||||
private function getPersistedGroupCenter(Center $center, PermissionsGroup $permissionsGroup)
|
||||
{
|
||||
$em = $this->managerRegistry->getManager();
|
||||
|
||||
$groupCenterManaged = $em->getRepository(GroupCenter::class)
|
||||
->findOneBy([
|
||||
'center' => $groupCenter->getCenter(),
|
||||
'permissionsGroup' => $groupCenter->getPermissionsGroup(),
|
||||
'center' => $center,
|
||||
'permissionsGroup' => $permissionsGroup,
|
||||
]);
|
||||
|
||||
if (!$groupCenterManaged) {
|
||||
$groupCenter = new GroupCenter();
|
||||
$groupCenter->setCenter($center);
|
||||
$groupCenter->setPermissionsGroup($permissionsGroup);
|
||||
|
||||
$em->persist($groupCenter);
|
||||
|
||||
return $groupCenter;
|
||||
|
63
src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadGenders.php
Normal file
63
src/Bundle/ChillMainBundle/DataFixtures/ORM/LoadGenders.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?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\DataFixtures\ORM;
|
||||
|
||||
use Chill\MainBundle\Entity\Gender;
|
||||
use Chill\MainBundle\Entity\GenderEnum;
|
||||
use Chill\MainBundle\Entity\GenderIconEnum;
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
|
||||
class LoadGenders extends AbstractFixture implements OrderedFixtureInterface
|
||||
{
|
||||
private array $genders = [
|
||||
[
|
||||
'label' => ['en' => 'man', 'fr' => 'homme'],
|
||||
'genderTranslation' => GenderEnum::MALE,
|
||||
'icon' => GenderIconEnum::MALE,
|
||||
],
|
||||
[
|
||||
'label' => ['en' => 'woman', 'fr' => 'femme'],
|
||||
'genderTranslation' => GenderEnum::FEMALE,
|
||||
'icon' => GenderIconEnum::FEMALE,
|
||||
],
|
||||
[
|
||||
'label' => ['en' => 'neutral', 'fr' => 'neutre'],
|
||||
'genderTranslation' => GenderEnum::NEUTRAL,
|
||||
'icon' => GenderIconEnum::NEUTRAL,
|
||||
],
|
||||
];
|
||||
|
||||
public function getOrder()
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
{
|
||||
echo "loading genders... \n";
|
||||
|
||||
foreach ($this->genders as $g) {
|
||||
echo $g['label']['fr'].' ';
|
||||
$new_g = new Gender();
|
||||
$new_g->setGenderTranslation($g['genderTranslation']);
|
||||
$new_g->setLabel($g['label']);
|
||||
$new_g->setIcon($g['icon']);
|
||||
|
||||
$this->addReference('g_'.$g['genderTranslation']->value, $new_g);
|
||||
$manager->persist($new_g);
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
}
|
@@ -17,6 +17,8 @@ use Chill\MainBundle\Controller\CivilityApiController;
|
||||
use Chill\MainBundle\Controller\CivilityController;
|
||||
use Chill\MainBundle\Controller\CountryApiController;
|
||||
use Chill\MainBundle\Controller\CountryController;
|
||||
use Chill\MainBundle\Controller\GenderApiController;
|
||||
use Chill\MainBundle\Controller\GenderController;
|
||||
use Chill\MainBundle\Controller\GeographicalUnitApiController;
|
||||
use Chill\MainBundle\Controller\LanguageController;
|
||||
use Chill\MainBundle\Controller\LocationController;
|
||||
@@ -54,6 +56,7 @@ use Chill\MainBundle\Doctrine\Type\PointType;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\Civility;
|
||||
use Chill\MainBundle\Entity\Country;
|
||||
use Chill\MainBundle\Entity\Gender;
|
||||
use Chill\MainBundle\Entity\GeographicalUnitLayer;
|
||||
use Chill\MainBundle\Entity\Language;
|
||||
use Chill\MainBundle\Entity\Location;
|
||||
@@ -66,6 +69,7 @@ use Chill\MainBundle\Entity\UserJob;
|
||||
use Chill\MainBundle\Form\CenterType;
|
||||
use Chill\MainBundle\Form\CivilityType;
|
||||
use Chill\MainBundle\Form\CountryType;
|
||||
use Chill\MainBundle\Form\GenderType;
|
||||
use Chill\MainBundle\Form\LanguageType;
|
||||
use Chill\MainBundle\Form\LocationFormType;
|
||||
use Chill\MainBundle\Form\LocationTypeType;
|
||||
@@ -511,6 +515,28 @@ class ChillMainExtension extends Extension implements
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'class' => Gender::class,
|
||||
'name' => 'main_gender',
|
||||
'base_path' => '/admin/main/gender',
|
||||
'base_role' => 'ROLE_ADMIN',
|
||||
'form_class' => GenderType::class,
|
||||
'controller' => GenderController::class,
|
||||
'actions' => [
|
||||
'index' => [
|
||||
'role' => 'ROLE_ADMIN',
|
||||
'template' => '@ChillMain/Gender/index.html.twig',
|
||||
],
|
||||
'new' => [
|
||||
'role' => 'ROLE_ADMIN',
|
||||
'template' => '@ChillMain/Gender/new.html.twig',
|
||||
],
|
||||
'edit' => [
|
||||
'role' => 'ROLE_ADMIN',
|
||||
'template' => '@ChillMain/Gender/edit.html.twig',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'class' => Language::class,
|
||||
'name' => 'main_language',
|
||||
@@ -814,6 +840,21 @@ class ChillMainExtension extends Extension implements
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'class' => Gender::class,
|
||||
'name' => 'gender',
|
||||
'base_path' => '/api/1.0/main/gender',
|
||||
'base_role' => 'ROLE_USER',
|
||||
'controller' => GenderApiController::class,
|
||||
'actions' => [
|
||||
'_index' => [
|
||||
'methods' => [
|
||||
Request::METHOD_GET => true,
|
||||
Request::METHOD_HEAD => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'class' => GeographicalUnitLayer::class,
|
||||
'controller' => GeographicalUnitApiController::class,
|
||||
|
104
src/Bundle/ChillMainBundle/Entity/Gender.php
Normal file
104
src/Bundle/ChillMainBundle/Entity/Gender.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?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\Entity;
|
||||
|
||||
use Chill\MainBundle\Repository\GenderRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['chill_main_gender' => Gender::class])]
|
||||
#[ORM\Entity(repositoryClass: GenderRepository::class)]
|
||||
#[ORM\Table(name: 'chill_main_gender')]
|
||||
class Gender
|
||||
{
|
||||
#[Serializer\Groups(['read'])]
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
|
||||
private ?int $id = null;
|
||||
|
||||
#[Serializer\Groups(['read'])]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON)]
|
||||
private array $label = [];
|
||||
|
||||
#[Serializer\Groups(['read'])]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN)]
|
||||
private bool $active = true;
|
||||
|
||||
#[Assert\NotNull(message: 'You must choose a gender translation')]
|
||||
#[Serializer\Groups(['read'])]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, enumType: GenderEnum::class)]
|
||||
private GenderEnum $genderTranslation;
|
||||
|
||||
#[Serializer\Groups(['read'])]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, enumType: GenderIconEnum::class)]
|
||||
private GenderIconEnum $icon;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::FLOAT, name: 'ordering', nullable: true, options: ['default' => '0.0'])]
|
||||
private float $order = 0;
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getLabel(): array
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function setLabel(array $label): void
|
||||
{
|
||||
$this->label = $label;
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->active;
|
||||
}
|
||||
|
||||
public function setActive(bool $active): void
|
||||
{
|
||||
$this->active = $active;
|
||||
}
|
||||
|
||||
public function getGenderTranslation(): GenderEnum
|
||||
{
|
||||
return $this->genderTranslation;
|
||||
}
|
||||
|
||||
public function setGenderTranslation(GenderEnum $genderTranslation): void
|
||||
{
|
||||
$this->genderTranslation = $genderTranslation;
|
||||
}
|
||||
|
||||
public function getIcon(): GenderIconEnum
|
||||
{
|
||||
return $this->icon;
|
||||
}
|
||||
|
||||
public function setIcon(GenderIconEnum $icon): void
|
||||
{
|
||||
$this->icon = $icon;
|
||||
}
|
||||
|
||||
public function getOrder(): float
|
||||
{
|
||||
return $this->order;
|
||||
}
|
||||
|
||||
public function setOrder(float $order): void
|
||||
{
|
||||
$this->order = $order;
|
||||
}
|
||||
}
|
20
src/Bundle/ChillMainBundle/Entity/GenderEnum.php
Normal file
20
src/Bundle/ChillMainBundle/Entity/GenderEnum.php
Normal file
@@ -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\MainBundle\Entity;
|
||||
|
||||
enum GenderEnum: string
|
||||
{
|
||||
case MALE = 'man';
|
||||
case FEMALE = 'woman';
|
||||
case NEUTRAL = 'neutral';
|
||||
case UNKNOWN = 'unknown';
|
||||
}
|
22
src/Bundle/ChillMainBundle/Entity/GenderIconEnum.php
Normal file
22
src/Bundle/ChillMainBundle/Entity/GenderIconEnum.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?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\Entity;
|
||||
|
||||
enum GenderIconEnum: string
|
||||
{
|
||||
case MALE = 'bi bi-gender-male';
|
||||
case FEMALE = 'bi bi-gender-female';
|
||||
case NEUTRAL = 'bi bi-gender-neuter';
|
||||
case AMBIGUOUS = 'bi bi-gender-ambiguous';
|
||||
case TRANS = 'bi bi-gender-trans';
|
||||
case UNKNOWN = 'bi bi-question';
|
||||
}
|
@@ -243,6 +243,9 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
||||
throw new \RuntimeException();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Selectable<int, EntityWorkflowStep>&Collection<int, EntityWorkflowStep>
|
||||
*/
|
||||
public function getSteps(): Collection&Selectable
|
||||
{
|
||||
return $this->steps;
|
||||
@@ -431,6 +434,7 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
||||
$previousStep = $this->getCurrentStep();
|
||||
|
||||
$previousStep
|
||||
->setComment($transitionContextDTO->comment)
|
||||
->setTransitionAfter($transition)
|
||||
->setTransitionAt($transitionAt)
|
||||
->setTransitionBy($byUser);
|
||||
|
@@ -56,7 +56,7 @@ class EntityWorkflowSend implements TrackCreationInterface
|
||||
/**
|
||||
* @var Collection<int, EntityWorkflowSendView>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: EntityWorkflowSendView::class, mappedBy: 'send')]
|
||||
#[ORM\OneToMany(mappedBy: 'send', targetEntity: EntityWorkflowSendView::class, cascade: ['remove'])]
|
||||
private Collection $views;
|
||||
|
||||
public function __construct(
|
||||
|
@@ -66,7 +66,7 @@ class EntityWorkflowStep
|
||||
/**
|
||||
* @var Collection <int, EntityWorkflowStepSignature>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepSignature::class, cascade: ['persist'], orphanRemoval: true)]
|
||||
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepSignature::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
private Collection $signatures;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: EntityWorkflow::class, inversedBy: 'steps')]
|
||||
@@ -115,7 +115,7 @@ class EntityWorkflowStep
|
||||
/**
|
||||
* @var Collection<int, EntityWorkflowSend>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'entityWorkflowStep', targetEntity: EntityWorkflowSend::class, cascade: ['persist'], orphanRemoval: true)]
|
||||
#[ORM\OneToMany(mappedBy: 'entityWorkflowStep', targetEntity: EntityWorkflowSend::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
private Collection $sends;
|
||||
|
||||
public function __construct()
|
||||
|
64
src/Bundle/ChillMainBundle/Form/GenderType.php
Normal file
64
src/Bundle/ChillMainBundle/Form/GenderType.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?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\Form;
|
||||
|
||||
use Chill\MainBundle\Entity\Gender;
|
||||
use Chill\MainBundle\Entity\GenderEnum;
|
||||
use Chill\MainBundle\Entity\GenderIconEnum;
|
||||
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EnumType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class GenderType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('label', TranslatableStringFormType::class, [
|
||||
'required' => true,
|
||||
])
|
||||
->add('icon', EnumType::class, [
|
||||
'class' => GenderIconEnum::class,
|
||||
'choices' => GenderIconEnum::cases(),
|
||||
'expanded' => true,
|
||||
'multiple' => false,
|
||||
'mapped' => true,
|
||||
'choice_label' => fn (GenderIconEnum $enum) => '<i class="'.strtolower($enum->value).'"></i>',
|
||||
'choice_value' => fn (?GenderIconEnum $enum) => null !== $enum ? $enum->value : null,
|
||||
'label' => 'gender.admin.Select gender icon',
|
||||
'label_html' => true,
|
||||
])
|
||||
->add('genderTranslation', EnumType::class, [
|
||||
'class' => GenderEnum::class,
|
||||
'choice_label' => fn (GenderEnum $enum) => $enum->value,
|
||||
'label' => 'gender.admin.Select gender translation',
|
||||
])
|
||||
->add('active', ChoiceType::class, [
|
||||
'choices' => [
|
||||
'Active' => true,
|
||||
'Inactive' => false,
|
||||
],
|
||||
])
|
||||
->add('order', NumberType::class);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Gender::class,
|
||||
]);
|
||||
}
|
||||
}
|
@@ -13,36 +13,30 @@ namespace Chill\MainBundle\Form\Type;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\PermissionsGroup;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Chill\MainBundle\Repository\CenterRepository;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ComposedGroupCenterType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly CenterRepository $centerRepository) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$centers = $this->centerRepository->findActive();
|
||||
|
||||
$builder->add('permissionsgroup', EntityType::class, [
|
||||
'class' => PermissionsGroup::class,
|
||||
'choice_label' => static fn (PermissionsGroup $group) => $group->getName(),
|
||||
])->add('center', EntityType::class, [
|
||||
'class' => Center::class,
|
||||
'query_builder' => static function (EntityRepository $er) {
|
||||
$qb = $er->createQueryBuilder('c');
|
||||
$qb->where($qb->expr()->eq('c.isActive', 'TRUE'))
|
||||
->orderBy('c.name', 'ASC');
|
||||
|
||||
return $qb;
|
||||
},
|
||||
])->add('center', ChoiceType::class, [
|
||||
'choices' => $centers,
|
||||
'choice_label' => fn (Center $center) => $center->getName(),
|
||||
'multiple' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefault('data_class', \Chill\MainBundle\Entity\GroupCenter::class);
|
||||
}
|
||||
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return 'composed_groupcenter';
|
||||
|
@@ -39,6 +39,7 @@ class UserGroupType extends AbstractType
|
||||
'label' => 'user_group.Email',
|
||||
'help' => 'user_group.EmailHelp',
|
||||
'empty_data' => '',
|
||||
'required' => false,
|
||||
])
|
||||
->add('excludeKey', TextType::class, [
|
||||
'label' => 'user_group.ExcludeKey',
|
||||
|
47
src/Bundle/ChillMainBundle/Repository/GenderRepository.php
Normal file
47
src/Bundle/ChillMainBundle/Repository/GenderRepository.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?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\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\Gender;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Gender>
|
||||
*/
|
||||
class GenderRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Gender::class);
|
||||
}
|
||||
|
||||
public function findByActiveOrdered(): array
|
||||
{
|
||||
return $this->createQueryBuilder('g')
|
||||
->select('g')
|
||||
->where('g.active = True')
|
||||
->orderBy('g.order', 'ASC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
public function findByGenderTranslation($gender): array
|
||||
{
|
||||
return $this->createQueryBuilder('g')
|
||||
->select('g')
|
||||
->where('g.genderTranslation = :gender')
|
||||
->setParameter('gender', $gender)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
}
|
@@ -10,6 +10,7 @@ import Modal from 'bootstrap/js/dist/modal';
|
||||
import Collapse from 'bootstrap/js/src/collapse';
|
||||
import Carousel from 'bootstrap/js/src/carousel';
|
||||
import Popover from 'bootstrap/js/src/popover';
|
||||
import 'bootstrap-icons/font/bootstrap-icons.css';
|
||||
|
||||
//
|
||||
// Carousel: ACHeaderSlider is a small slider used in banner of AccompanyingCourse Section
|
||||
|
@@ -25,6 +25,10 @@ window.addEventListener('DOMContentLoaded', function() {
|
||||
for (let transition of froms) {
|
||||
for (let input of transition.querySelectorAll('input')) {
|
||||
if (input.checked) {
|
||||
if ('1' === input.dataset.toFinal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('1' === input.dataset.isSentExternal) {
|
||||
return false;
|
||||
}
|
||||
@@ -151,6 +155,7 @@ window.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
toggle_callback: function (c, dir) {
|
||||
for (let div of c) {
|
||||
|
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<i :class="gender.icon"></i>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
const props = defineProps({
|
||||
gender: Object
|
||||
})
|
||||
|
||||
</script>
|
@@ -1,20 +1,13 @@
|
||||
<template>
|
||||
|
||||
<button v-if="hasWorkflow"
|
||||
class="btn btn-primary"
|
||||
@click="openModal">
|
||||
<b>{{ countWorkflows }}</b>
|
||||
<template v-if="countWorkflows > 1">{{ $t('workflows') }}</template>
|
||||
<template v-else>{{ $t('workflow') }}</template>
|
||||
</button>
|
||||
|
||||
<pick-workflow v-else-if="allowCreate"
|
||||
<pick-workflow
|
||||
:relatedEntityClass="this.relatedEntityClass"
|
||||
:relatedEntityId="this.relatedEntityId"
|
||||
:workflowsAvailables="workflowsAvailables"
|
||||
:preventDefaultMoveToGenerate="this.$props.preventDefaultMoveToGenerate"
|
||||
:goToGenerateWorkflowPayload="this.goToGenerateWorkflowPayload"
|
||||
:countExistingWorkflows="countWorkflows"
|
||||
@go-to-generate-workflow="goToGenerateWorkflow"
|
||||
@click-open-list="openModal"
|
||||
></pick-workflow>
|
||||
|
||||
<teleport to="body">
|
||||
@@ -39,6 +32,8 @@
|
||||
:workflowsAvailables="workflowsAvailables"
|
||||
:preventDefaultMoveToGenerate="this.$props.preventDefaultMoveToGenerate"
|
||||
:goToGenerateWorkflowPayload="this.goToGenerateWorkflowPayload"
|
||||
:countExistingWorkflows="countWorkflows"
|
||||
:embedded-within-list-modal="true"
|
||||
@go-to-generate-workflow="this.goToGenerateWorkflow"
|
||||
></pick-workflow>
|
||||
</template>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<template v-if="props.workflowsAvailables.length >= 1">
|
||||
<div class="dropdown d-grid gap-2">
|
||||
<div v-if="countExistingWorkflows == 0 || embeddedWithinListModal" class="dropdown d-grid gap-2">
|
||||
<button class="btn btn-primary dropdown-toggle" type="button" id="createWorkflowButton" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Créer un workflow
|
||||
</button>
|
||||
@@ -10,6 +10,38 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="btn-group">
|
||||
<button @click="emit('clickOpenList')" class="btn btn-primary">
|
||||
<template v-if="countExistingWorkflows === 1">
|
||||
1 workflow associé
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ countExistingWorkflows }} workflows associés
|
||||
</template>
|
||||
</button>
|
||||
<button class="btn btn-primary dropdown-toggle dropdown-toggle-split" type="button" id="createWorkflowButton" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<span class="visually-hidden">Liste des workflows disponibles</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="createWorkflowButton">
|
||||
<li v-for="w in props.workflowsAvailables" :key="w.name">
|
||||
<button class="dropdown-item" type="button" @click.prevent="goToGenerateWorkflow($event, w.name)">{{ w.text }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="countExistingWorkflows > 0" class="dropdown d-grid gap-2">
|
||||
<button @click="emit('clickOpenList')" class="btn btn-primary" type="button" id="createWorkflowButton" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<template v-if="countExistingWorkflows === 1">
|
||||
1 workflow associé
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ countExistingWorkflows }} workflows associés
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -30,12 +62,18 @@ interface PickWorkflowConfig {
|
||||
workflowsAvailables: WorkflowAvailable[];
|
||||
preventDefaultMoveToGenerate: boolean;
|
||||
goToGenerateWorkflowPayload: object;
|
||||
countExistingWorkflows: number;
|
||||
/**
|
||||
* if true, this button will not present a splitted button
|
||||
*/
|
||||
embeddedWithinListModal: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<PickWorkflowConfig>(), {preventDefaultMoveToGenerate: false, goToGenerateWorkflowPayload: {}});
|
||||
const props = withDefaults(defineProps<PickWorkflowConfig>(), {preventDefaultMoveToGenerate: false, goToGenerateWorkflowPayload: {}, allowCreateWorkflow: false});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'goToGenerateWorkflow', {event: MouseEvent, workflowName: string, isLinkValid: boolean, link: string, payload: object}): void;
|
||||
(e: 'clickOpenList'): void;
|
||||
}>();
|
||||
|
||||
const makeLink = (workflowName: string): string => buildLinkCreate(workflowName, props.relatedEntityClass, props.relatedEntityId);
|
||||
|
@@ -38,7 +38,9 @@ const messages = {
|
||||
person: "Usager",
|
||||
birthday: {
|
||||
man: "Né le",
|
||||
woman: "Née le"
|
||||
woman: "Née le",
|
||||
neutral: "Né·e le",
|
||||
unknown: "Né·e le",
|
||||
},
|
||||
deathdate: "Date de décès",
|
||||
household_without_address: "Le ménage de l'usager est sans adresse",
|
||||
|
@@ -0,0 +1,75 @@
|
||||
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
|
||||
|
||||
{% block title %}
|
||||
{% include('@ChillMain/CRUD/_edit_title.html.twig') %}
|
||||
{% endblock %}
|
||||
|
||||
{% form_theme form _self %}
|
||||
|
||||
{% block _gender_icon_widget %}
|
||||
{% for child in form %}
|
||||
<div class="form-check">
|
||||
<input
|
||||
type="radio"
|
||||
id="{{ child.vars.id }}"
|
||||
name="{{ child.vars.full_name }}"
|
||||
value="{{ child.vars.value }}"
|
||||
{% if child.vars.checked %}checked="checked"{% endif %}
|
||||
/>
|
||||
|
||||
<label for="{{ child.vars.id }}">{{ child.vars.label|raw }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
{% set formId = crudMainFormId|default('crud_main_form') %}
|
||||
|
||||
{% block crud_content_header %}
|
||||
<h1 class="mb-5">{{ ('crud.'~crud_name~'.title_edit')|trans }}</h1>
|
||||
{% endblock crud_content_header %}
|
||||
|
||||
{% block crud_content_form %}
|
||||
{{ form_start(form, { 'attr' : { 'id': formId } }) }}
|
||||
|
||||
{{ form_row(form.label) }}
|
||||
{{ form_row(form.genderTranslation) }}
|
||||
{{ form_row(form.icon) }}
|
||||
{{ form_row(form.active) }}
|
||||
{{ form_row(form.order) }}
|
||||
|
||||
{{ form_end(form) }}
|
||||
{% block crud_content_after_form %}{% endblock %}
|
||||
|
||||
{% block crud_content_form_actions %}
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
{% block content_form_actions_back %}
|
||||
<li class="cancel">
|
||||
{# <a class="btn btn-cancel" href="{{ chill_return_path_or('chill_crud_'~crud_name~'_index') }}">#}
|
||||
{# {{ 'Cancel'|trans }}#}
|
||||
{# </a>#}
|
||||
</li>
|
||||
{% endblock %}
|
||||
{% block content_form_actions_before %}{% endblock %}
|
||||
{% block content_form_actions_delete %}
|
||||
{% if chill_crud_action_exists(crud_name, 'delete') %}
|
||||
{% if is_granted(chill_crud_config('role', crud_name, 'delete'), entity) %}
|
||||
<li class="">
|
||||
<a class="btn btn-small btn-delete" href="{{ chill_path_add_return_path('chill_crud_'~crud_name~'_delete', { 'id': entity.id }) }}"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock content_form_actions_delete %}
|
||||
{% block content_form_actions_save_and_close %}
|
||||
<li class="">
|
||||
<button type="submit" name="submit" value="save-and-close" class="btn btn-update" form="{{ formId }}">
|
||||
{{ 'crud.edit.save_and_close'|trans }}
|
||||
</button>
|
||||
</li>
|
||||
{% endblock %}
|
||||
{% block content_form_actions_after %}{% endblock %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
||||
{% endblock admin_content %}
|
@@ -0,0 +1,46 @@
|
||||
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
|
||||
|
||||
{% block admin_content %}
|
||||
{% embed '@ChillMain/CRUD/_index.html.twig' %}
|
||||
{% block table_entities_thead_tr %}
|
||||
<th>id</th>
|
||||
<th>{{ 'label'|trans }}</th>
|
||||
<th>{{ 'icon'|trans }}</th>
|
||||
<th>{{ 'gender.genderTranslation'|trans }}</th>
|
||||
<th>{{ 'active'|trans }}</th>
|
||||
<th>{{ 'ordering'|trans }}</th>
|
||||
<th></th>
|
||||
{% endblock %}
|
||||
{% block table_entities_tbody %}
|
||||
{% for entity in entities %}
|
||||
<tr>
|
||||
<td>{{ entity.id }}</td>
|
||||
<td>{{ entity.label|localize_translatable_string }}</td>
|
||||
<td>{{ entity.icon|chill_entity_render_box }}</td>
|
||||
<td>{{ entity.genderTranslation.value }}</td>
|
||||
<td style="text-align:center;">
|
||||
{%- if entity.active -%}
|
||||
<i class="fa fa-check-square-o"></i>
|
||||
{%- else -%}
|
||||
<i class="fa fa-square-o"></i>
|
||||
{%- endif -%}
|
||||
</td>
|
||||
<td>{{ entity.order }}</td>
|
||||
<td>
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_crud_main_gender_edit', { 'id': entity.id}) }}" class="btn btn-sm btn-edit btn-mini"></a>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block actions_before %}
|
||||
<li class='cancel'>
|
||||
<a href="{{ path('chill_main_admin_central') }}" class="btn btn-cancel">{{'Back to the admin'|trans}}</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
{% endblock %}
|
@@ -0,0 +1,79 @@
|
||||
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
|
||||
|
||||
{% block title %}
|
||||
{% include('@ChillMain/CRUD/_new_title.html.twig') %}
|
||||
{% endblock %}
|
||||
|
||||
{% form_theme form _self %}
|
||||
|
||||
{% block _gender_icon_widget %}
|
||||
{% for child in form %}
|
||||
<div class="form-check">
|
||||
<input
|
||||
type="radio"
|
||||
id="{{ child.vars.id }}"
|
||||
name="{{ child.vars.full_name }}"
|
||||
value="{{ child.vars.value }}"
|
||||
{% if child.vars.checked %}checked="checked"{% endif %}
|
||||
/>
|
||||
|
||||
<label for="{{ child.vars.id }}">{{ child.vars.label|raw }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
{% set formId = crudMainFormId|default('crud_main_form') %}
|
||||
|
||||
{% block crud_content_header %}
|
||||
<h1>{{ ('crud.' ~ crud_name ~ '.title_new')|trans({'%crud_name%' : crud_name }) }}</h1>
|
||||
{% endblock crud_content_header %}
|
||||
|
||||
{% block crud_content_form %}
|
||||
{{ form_start(form, { 'attr' : { 'id': formId } }) }}
|
||||
{{ form_row(form.label) }}
|
||||
{{ form_row(form.genderTranslation) }}
|
||||
{{ form_row(form.icon) }}
|
||||
{{ form_row(form.active) }}
|
||||
{{ form_row(form.order) }}
|
||||
{{ form_end(form) }}
|
||||
|
||||
{% block crud_content_after_form %}{% endblock %}
|
||||
|
||||
{% block crud_content_form_actions %}
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
{% block content_form_actions_back %}
|
||||
<li class="cancel">
|
||||
<a class="btn btn-cancel" href="{{ chill_return_path_or('chill_crud_'~crud_name~'_index') }}">
|
||||
{{ 'Cancel'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
{% block content_form_actions_save_and_close %}
|
||||
<li class="">
|
||||
<button type="submit" name="submit" value="save-and-close" class="btn btn-create" form="{{ formId }}">
|
||||
{{ 'crud.new.save_and_close'|trans }}
|
||||
</button>
|
||||
</li>
|
||||
{% endblock %}
|
||||
{% block content_form_actions_save_and_show %}
|
||||
<li class="">
|
||||
<button type="submit" name="submit" value="save-and-show" class="btn btn-create" form="{{ formId }}">
|
||||
{{ 'crud.new.save_and_show'|trans }}
|
||||
</button>
|
||||
</li>
|
||||
{% endblock %}
|
||||
{% block content_form_actions_save_and_new %}
|
||||
<li class="">
|
||||
<button type="submit" name="submit" value="save-and-new" class="btn btn-create" form="{{ formId }}">
|
||||
{{ 'crud.new.save_and_new'|trans }}
|
||||
</button>
|
||||
</li>
|
||||
{% endblock %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
|
||||
{% endblock admin_content %}
|
@@ -42,7 +42,7 @@
|
||||
<div class="item-col" style="width: inherit;">
|
||||
<div>
|
||||
{%- if step.transitionBy is not null -%}
|
||||
{{ step.transitionBy|chill_entity_render_box({'at_date': step.transitionAt}) }}
|
||||
<span class="badge-user">{{ step.transitionBy|chill_entity_render_box({'at_date': step.transitionAt}) }}</span>
|
||||
{% else %}
|
||||
<span class="chill-no-data-statement">{{ 'workflow.Automated transition'|trans }}</span>
|
||||
{%- endif -%}
|
||||
@@ -74,7 +74,7 @@
|
||||
{% if not loop.last and step.signatures|length > 0 %}
|
||||
<div class="separator">
|
||||
<div>
|
||||
<p><b>{{ 'workflow.signature_required_title'|trans({'nb_signatures': step.signatures|length}) }} :</b></p>
|
||||
<p><b>{{ 'workflow.signatures_title'|trans({'nb_signatures': step.signatures|length}) }} :</b></p>
|
||||
<div>
|
||||
{{ include('@ChillMain/Workflow/_signature_list.html.twig', {'signatures': step.signatures, is_small: true }) }}
|
||||
</div>
|
||||
@@ -105,7 +105,7 @@
|
||||
<p><b>{{ 'workflow.Users put in Cc'|trans }} : </b></p>
|
||||
<ul>
|
||||
{% for u in step.ccUser %}
|
||||
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li>
|
||||
<li><span class="badge-user">{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</span></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
@@ -123,14 +123,14 @@
|
||||
<p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }} :</b></p>
|
||||
<ul>
|
||||
{% for u in step.destUserByAccessKey %}
|
||||
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li>
|
||||
<li><span class="badge-user">{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</span></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if step.signatures|length > 0 %}
|
||||
<p><b>{{ 'workflow.signature_required_title'|trans({'nb_signatures': step.signatures|length}) }} :</b></p>
|
||||
<p><b>{{ 'workflow.signatures_title'|trans({'nb_signatures': step.signatures|length}) }} :</b></p>
|
||||
{{ include('@ChillMain/Workflow/_signature_list.html.twig', {'signatures': step.signatures, is_small: true }) }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
@@ -1,11 +1,15 @@
|
||||
<h2>
|
||||
{% if handler is not null %}
|
||||
<h2>
|
||||
{{ 'workflow_'|trans }}
|
||||
</h2>
|
||||
</h2>
|
||||
|
||||
<div class="item-row col" style="display: block;">
|
||||
<div class="item-row col" style="display: block;">
|
||||
{% include handler.template(entity_workflow) with handler.templateData(entity_workflow)|merge({
|
||||
'description': true,
|
||||
'breadcrumb': true,
|
||||
'add_classes': ''
|
||||
}) %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<h2>{{ 'workflow.deleted_title'|trans }}</h2>
|
||||
{% endif %}
|
||||
|
@@ -37,7 +37,7 @@
|
||||
<div class="mb-5">
|
||||
<h2>{{ handler.entityTitle(entity_workflow) }}</h2>
|
||||
|
||||
{{ macro.breadcrumb({'entity_workflow': entity_workflow}) }}
|
||||
{{ macro.breadcrumb(entity_workflow) }}
|
||||
{% if entity_workflow.isOnHoldAtCurrentStep %}
|
||||
<span class="badge bg-success rounded-pill" title="{{ 'workflow.On hold'|trans|escape('html_attr') }}">{{ 'workflow.On hold'|trans }}</span>
|
||||
{% endif %}
|
||||
|
@@ -68,7 +68,7 @@
|
||||
|
||||
</button>
|
||||
<div>
|
||||
{{ macro.breadcrumb(l) }}
|
||||
{{ macro.breadcrumb(l.entity_workflow) }}
|
||||
{% if l.entity_workflow.isOnHoldAtCurrentStep %}
|
||||
<span class="badge bg-success rounded-pill" title="{{ 'workflow.On hold'|trans|escape('html_attr') }}">{{ 'workflow.On hold'|trans }}</span>
|
||||
{% endif %}
|
||||
|
@@ -9,11 +9,13 @@
|
||||
<span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span>
|
||||
<b>{{ step.previous.transitionAt|format_datetime('short', 'short') }}</b>
|
||||
</li>
|
||||
{% if step.destUser|length > 0 %}
|
||||
{% if step.destUser|length > 0 or step.destUserGroups|length > 0 %}
|
||||
<li>
|
||||
<span class="item-key">{{ 'workflow.For'|trans ~ ' : ' }}</span>
|
||||
<b>
|
||||
{% for d in step.destUser %}{{ d|chill_entity_render_string({'at_date': step.previous.transitionAt}) }}{% if not loop.last %}, {% endif %}{% endfor %}
|
||||
{% for d in step.destUser %}<span class="badge-user">{{ d|chill_entity_render_string({'at_date': step.previous.transitionAt}) }}</span>{% if not loop.last %}, {% endif -%}{% endfor -%}
|
||||
{%- if step.destUser|length > 0 and step.destUserGroups|length > 0 %}, {% endif -%}
|
||||
{%- for d in step.destUserGroups %}{{ d|chill_entity_render_box }}{% if not loop.last %}, {% endif %}{% endfor -%}
|
||||
</b>
|
||||
</li>
|
||||
{% endif %}
|
||||
@@ -50,10 +52,10 @@
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro breadcrumb(_ctx) %}
|
||||
{% macro breadcrumb(entity_workflow) %}
|
||||
<div class="breadcrumb">
|
||||
{% for step in _ctx.entity_workflow.stepsChained %}
|
||||
{% set labels = workflow_metadata(_ctx.entity_workflow, 'label', step.currentStep, _ctx.entity_workflow.workflowName) %}
|
||||
{% for step in entity_workflow.stepsChained %}
|
||||
{% set labels = workflow_metadata(entity_workflow, 'label', step.currentStep, entity_workflow.workflowName) %}
|
||||
{% set label = labels is null ? step.currentStep : labels|localize_translatable_string %}
|
||||
{% set popTitle = _self.popoverTitle(step) %}
|
||||
{% set popContent = _self.popoverContent(step) %}
|
||||
|
@@ -1,11 +1,14 @@
|
||||
{{ dest.label }},
|
||||
|
||||
Un suivi "{{ workflow.text }}" a atteint une nouvelle étape: {{ workflow.text }}
|
||||
{%- if is_dest %}
|
||||
|
||||
Titre du workflow: "{{ title }}".
|
||||
{% if is_dest %}
|
||||
|
||||
Vous êtes invités à valider cette étape au plus tôt.
|
||||
{% endif %}
|
||||
|
||||
|
||||
Vous pouvez visualiser le workflow sur cette page:
|
||||
|
||||
{{ absolute_url(path('chill_main_workflow_show', {'id': entity_workflow.id, '_locale': 'fr'})) }}
|
||||
|
@@ -2,8 +2,14 @@ Chers membres du groupe {{ user_group.label|localize_translatable_string }},
|
||||
|
||||
Un suivi "{{ workflow.text }}" a atteint une nouvelle étape: {{ place.text }}
|
||||
|
||||
Vous pouvez visualiser le workflow sur cette page:
|
||||
Titre du workflow: "{{ title }}".
|
||||
|
||||
{{ absolute_url(path('chill_main_workflow_show', {'id': entity_workflow.id, '_locale': 'fr'})) }}
|
||||
Vous êtes invité·e à valider cette étape. Pour obtenir un accès, vous pouvez cliquer sur le lien suivant:
|
||||
|
||||
{{ absolute_url(path('chill_main_workflow_grant_access_by_key', {'id': entity_workflow.currentStep.id, '_locale': 'fr', 'accessKey': entity_workflow.currentStep.accessKey})) }}
|
||||
|
||||
Dès que vous aurez cliqué une fois sur le lien, vous serez autorisé à valider cette étape.
|
||||
|
||||
Notez que vous devez disposer d'un compte utilisateur valide dans Chill.
|
||||
|
||||
Cordialement,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{%- if is_dest -%}
|
||||
Un suivi {{ workflow.text }} demande votre attention
|
||||
Un suivi {{ workflow.text }} demande votre attention: {{ title }}
|
||||
{%- else -%}
|
||||
Un suivi {{ workflow.text }} a atteint une nouvelle étape: {{ place.text }}
|
||||
Un suivi {{ workflow.text }} a atteint une nouvelle étape: {{ place.text }}: {{ title }}
|
||||
{%- endif -%}
|
||||
|
@@ -40,4 +40,15 @@ final readonly class ChillUrlGenerator implements ChillUrlGeneratorInterface
|
||||
|
||||
return $this->urlGenerator->generate($name, $parameters, $referenceType);
|
||||
}
|
||||
|
||||
public function forwardReturnPath(string $name, array $parameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string
|
||||
{
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
|
||||
if ($request->query->has('returnPath')) {
|
||||
return $this->urlGenerator->generate($name, [...$parameters, 'returnPath' => $request->query->get('returnPath')], $referenceType);
|
||||
}
|
||||
|
||||
return $this->urlGenerator->generate($name, $parameters, $referenceType);
|
||||
}
|
||||
}
|
||||
|
@@ -32,4 +32,9 @@ interface ChillUrlGeneratorInterface
|
||||
* Get the return path or, if any, generate an url.
|
||||
*/
|
||||
public function returnPathOr(string $name, array $parameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string;
|
||||
|
||||
/**
|
||||
* Return a new URL, with the same return path as the existing one. If any, no return path is forwarded.
|
||||
*/
|
||||
public function forwardReturnPath(string $name, array $parameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string;
|
||||
}
|
||||
|
@@ -62,12 +62,12 @@ abstract class AbstractSearch implements SearchInterface
|
||||
$recomposed .= ' '.$term.':';
|
||||
$containsSpace = str_contains((string) $terms[$term], ' ');
|
||||
|
||||
if ($containsSpace) {
|
||||
if ($containsSpace || is_numeric($terms[$term])) {
|
||||
$recomposed .= '"';
|
||||
}
|
||||
$recomposed .= (false === mb_stristr(' ', (string) $terms[$term])) ? $terms[$term] : '('.$terms[$term].')';
|
||||
|
||||
if ($containsSpace) {
|
||||
if ($containsSpace || is_numeric($terms[$term])) {
|
||||
$recomposed .= '"';
|
||||
}
|
||||
}
|
||||
|
@@ -17,6 +17,7 @@ use Chill\MainBundle\Workflow\Helper\DuplicateEntityWorkflowFinder;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
|
||||
class EntityWorkflowVoter extends Voter
|
||||
{
|
||||
@@ -32,6 +33,7 @@ class EntityWorkflowVoter extends Voter
|
||||
private readonly EntityWorkflowManager $manager,
|
||||
private readonly Security $security,
|
||||
private readonly DuplicateEntityWorkflowFinder $duplicateEntityWorkflowFinder,
|
||||
private readonly Registry $registry,
|
||||
) {}
|
||||
|
||||
protected function supports($attribute, $subject)
|
||||
@@ -83,7 +85,25 @@ class EntityWorkflowVoter extends Voter
|
||||
return false;
|
||||
|
||||
case self::DELETE:
|
||||
return 'initial' === $subject->getStep();
|
||||
if ('initial' === $subject->getStep()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$subject->isFinal()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// the entity workflow is finalized. We check if the final is not positive. If yes, we can
|
||||
// delete the entity
|
||||
$workflow = $this->registry->get($subject, $subject->getWorkflowName());
|
||||
foreach ($workflow->getMarking($subject)->getPlaces() as $place => $key) {
|
||||
$metadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
|
||||
if (false === ($metadata['isFinalPositive'] ?? true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
case self::SHOW_ENTITY_LINK:
|
||||
if ('initial' === $subject->getStep()) {
|
||||
|
@@ -74,12 +74,14 @@ class DateNormalizer implements ContextAwareNormalizerInterface, DenormalizerInt
|
||||
$formatterLong = \IntlDateFormatter::create(
|
||||
$locale,
|
||||
\IntlDateFormatter::LONG,
|
||||
$hasTime ? \IntlDateFormatter::SHORT : \IntlDateFormatter::NONE
|
||||
$hasTime ? \IntlDateFormatter::SHORT : \IntlDateFormatter::NONE,
|
||||
$date->getTimezone(),
|
||||
);
|
||||
$formatterShort = \IntlDateFormatter::create(
|
||||
$locale,
|
||||
\IntlDateFormatter::SHORT,
|
||||
$hasTime ? \IntlDateFormatter::SHORT : \IntlDateFormatter::NONE
|
||||
$hasTime ? \IntlDateFormatter::SHORT : \IntlDateFormatter::NONE,
|
||||
$date->getTimezone(),
|
||||
);
|
||||
|
||||
return [
|
||||
|
@@ -37,6 +37,10 @@ class CancelStaleWorkflowCronJob implements CronJobInterface
|
||||
|
||||
public function canRun(?CronJobExecution $cronJobExecution): bool
|
||||
{
|
||||
if (null === $cronJobExecution) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->clock->now() >= $cronJobExecution->getLastEnd()->add(new \DateInterval('P1D'));
|
||||
}
|
||||
|
||||
|
@@ -56,9 +56,32 @@ class ChillTwigRoutingHelper extends AbstractExtension
|
||||
new TwigFunction('chill_return_path_or', $this->getReturnPathOr(...), ['is_safe_callback' => $this->isUrlGenerationSafe(...)]),
|
||||
new TwigFunction('chill_path_add_return_path', $this->getPathAddReturnPath(...), ['is_safe_callback' => $this->isUrlGenerationSafe(...)]),
|
||||
new TwigFunction('chill_path_forward_return_path', $this->getPathForwardReturnPath(...), ['is_safe_callback' => $this->isUrlGenerationSafe(...)]),
|
||||
new TwigFunction('chill_path_force_return_path', $this->getPathForceReturnPath(...), ['is_safe_callback' => $this->isUrlGenerationSafe(...)]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a URL with a forced returnPath parameter.
|
||||
*
|
||||
* @param string $forcePath the forced path to return to
|
||||
* @param string $name the name of the route
|
||||
* @param array $parameters additional parameters for the URL
|
||||
* @param bool $relative whether the URL should be relative
|
||||
* @param string|null $label optional label for the return path
|
||||
*
|
||||
* @return string the generated URL
|
||||
*/
|
||||
public function getPathForceReturnPath(string $forcePath, string $name, array $parameters = [], $relative = false, $label = null): string
|
||||
{
|
||||
$params = [...$parameters, 'returnPath' => $forcePath];
|
||||
|
||||
if (null !== $label) {
|
||||
$params['returnPathLabel'] = $label;
|
||||
}
|
||||
|
||||
return $this->originalExtension->getPath($name, $params, $relative);
|
||||
}
|
||||
|
||||
public function getLabelReturnPath($default)
|
||||
{
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
|
@@ -35,7 +35,7 @@ interface ChillEntityRenderInterface
|
||||
*
|
||||
* @phpstan-pure
|
||||
*/
|
||||
public function renderBox($entity, array $options): string;
|
||||
public function renderBox(mixed $entity, array $options): string;
|
||||
|
||||
/**
|
||||
* Return the entity as a string.
|
||||
@@ -46,7 +46,7 @@ interface ChillEntityRenderInterface
|
||||
*
|
||||
* @phpstan-pure
|
||||
*/
|
||||
public function renderString($entity, array $options): string;
|
||||
public function renderString(mixed $entity, array $options): string;
|
||||
|
||||
/**
|
||||
* Return true if the class support this object for the given options.
|
||||
|
@@ -0,0 +1,35 @@
|
||||
<?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\Templating\Entity;
|
||||
|
||||
use Chill\MainBundle\Entity\GenderIconEnum;
|
||||
|
||||
/**
|
||||
* @implements ChillEntityRenderInterface<GenderIconEnum>
|
||||
*/
|
||||
final readonly class ChillGenderIconRender implements ChillEntityRenderInterface
|
||||
{
|
||||
public function renderBox($icon, array $options): string
|
||||
{
|
||||
return '<i class="'.htmlspecialchars($icon->value, ENT_QUOTES, 'UTF-8').'"></i>';
|
||||
}
|
||||
|
||||
public function renderString($icon, array $options): string
|
||||
{
|
||||
return $icon->value;
|
||||
}
|
||||
|
||||
public function supports($icon, array $options): bool
|
||||
{
|
||||
return $icon instanceof GenderIconEnum;
|
||||
}
|
||||
}
|
@@ -13,8 +13,6 @@ namespace Chill\MainBundle\Templating\Entity;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use DateTime;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Twig\Error\LoaderError;
|
||||
@@ -26,11 +24,17 @@ use Twig\Error\SyntaxError;
|
||||
*/
|
||||
class UserRender implements ChillEntityRenderInterface
|
||||
{
|
||||
public const SPLIT_LINE_BEFORE_CHARACTER = 'split_lines_before_characters';
|
||||
final public const DEFAULT_OPTIONS = [
|
||||
'main_scope' => true,
|
||||
'user_job' => true,
|
||||
'absence' => true,
|
||||
'at_date' => null, // instanceof DateTimeInterface
|
||||
/*
|
||||
* when set, the jobs and service will be splitted in multiple lines. The line will be splitted
|
||||
* before the given character. Only for renderString, renderBox is not concerned.
|
||||
*/
|
||||
self::SPLIT_LINE_BEFORE_CHARACTER => null,
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
@@ -65,8 +69,6 @@ class UserRender implements ChillEntityRenderInterface
|
||||
{
|
||||
$opts = \array_merge(self::DEFAULT_OPTIONS, $options);
|
||||
|
||||
// $immutableAtDate = $opts['at_date'] instanceOf DateTime ? DateTimeImmutable::createFromMutable($opts['at_date']) : $opts['at_date'];
|
||||
|
||||
if (null === $opts['at_date']) {
|
||||
$opts['at_date'] = $this->clock->now();
|
||||
} elseif ($opts['at_date'] instanceof \DateTime) {
|
||||
@@ -89,6 +91,28 @@ class UserRender implements ChillEntityRenderInterface
|
||||
$str .= ' ('.$this->translator->trans('absence.Absent').')';
|
||||
}
|
||||
|
||||
if (null !== $opts[self::SPLIT_LINE_BEFORE_CHARACTER]) {
|
||||
if (!is_int($opts[self::SPLIT_LINE_BEFORE_CHARACTER])) {
|
||||
throw new \InvalidArgumentException('Only integer for option split_lines_before_characters is allowed');
|
||||
}
|
||||
|
||||
$characterPerLine = $opts[self::SPLIT_LINE_BEFORE_CHARACTER];
|
||||
$exploded = explode(' ', $str);
|
||||
$charOnLine = 0;
|
||||
$str = '';
|
||||
foreach ($exploded as $word) {
|
||||
if ($charOnLine + strlen($word) > $characterPerLine) {
|
||||
$str .= "\n";
|
||||
$charOnLine = 0;
|
||||
}
|
||||
if ($charOnLine > 0) {
|
||||
$str .= ' ';
|
||||
}
|
||||
$str .= $word;
|
||||
$charOnLine += strlen($word);
|
||||
}
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,194 @@
|
||||
<?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\EntityWorkflowVoter;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
|
||||
use Chill\MainBundle\Workflow\EventSubscriber\EntityWorkflowTransitionEventSubscriber;
|
||||
use Chill\MainBundle\Workflow\Helper\DuplicateEntityWorkflowFinder;
|
||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
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 EntityWorkflowVoterTest extends TestCase
|
||||
{
|
||||
public function testVoteDeleteEntityWorkflowForInitialPlace(): void
|
||||
{
|
||||
$entityWorkflow = $this->buildEntityWorkflow();
|
||||
|
||||
$voter = $this->buildVoter();
|
||||
$token = $this->buildToken();
|
||||
|
||||
$actual = $voter->vote($token, $entityWorkflow, [EntityWorkflowVoter::DELETE]);
|
||||
|
||||
self::assertEquals(Voter::ACCESS_GRANTED, $actual);
|
||||
}
|
||||
|
||||
public function testVoteDeleteEntityWorkflowForSomeOherPlace(): void
|
||||
{
|
||||
$entityWorkflow = $this->buildEntityWorkflow();
|
||||
$registry = $this->buildRegistry();
|
||||
$workflow = $registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$dto->futureDestUsers = [new User()];
|
||||
$workflow->apply($entityWorkflow, 'move_to_in_between', ['context' => $dto, 'transition' => 'move_to_in_between', 'transitionAt' => new \DateTimeImmutable()]);
|
||||
|
||||
assert('in_between' === $entityWorkflow->getStep(), 'we ensure that the workflow is well transitionned');
|
||||
|
||||
$voter = $this->buildVoter();
|
||||
$token = $this->buildToken();
|
||||
|
||||
$actual = $voter->vote($token, $entityWorkflow, [EntityWorkflowVoter::DELETE]);
|
||||
|
||||
self::assertEquals(Voter::ACCESS_DENIED, $actual);
|
||||
}
|
||||
|
||||
public function testVoteDeleteEntityWorkflowForFinalPositive(): void
|
||||
{
|
||||
$entityWorkflow = $this->buildEntityWorkflow();
|
||||
$registry = $this->buildRegistry();
|
||||
$workflow = $registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$dto->futureDestUsers = [new User()];
|
||||
$workflow->apply($entityWorkflow, 'move_to_final_positive', ['context' => $dto, 'transition' => 'move_to_final_positive', 'transitionAt' => new \DateTimeImmutable()]);
|
||||
|
||||
assert('final_positive' === $entityWorkflow->getStep(), 'we ensure that the workflow is well transitionned');
|
||||
|
||||
$voter = $this->buildVoter();
|
||||
$token = $this->buildToken();
|
||||
|
||||
$actual = $voter->vote($token, $entityWorkflow, [EntityWorkflowVoter::DELETE]);
|
||||
|
||||
self::assertEquals(Voter::ACCESS_DENIED, $actual);
|
||||
}
|
||||
|
||||
public function testVoteDeleteEntityWorkflowForFinalNegative(): void
|
||||
{
|
||||
$entityWorkflow = $this->buildEntityWorkflow();
|
||||
$registry = $this->buildRegistry();
|
||||
$workflow = $registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$dto->futureDestUsers = [new User()];
|
||||
$workflow->apply($entityWorkflow, 'move_to_final_negative', ['context' => $dto, 'transition' => 'move_to_final_negative', 'transitionAt' => new \DateTimeImmutable()]);
|
||||
|
||||
$voter = $this->buildVoter();
|
||||
$token = $this->buildToken();
|
||||
|
||||
$actual = $voter->vote($token, $entityWorkflow, [EntityWorkflowVoter::DELETE]);
|
||||
|
||||
self::assertEquals(Voter::ACCESS_GRANTED, $actual);
|
||||
}
|
||||
|
||||
private function buildToken(): TokenInterface
|
||||
{
|
||||
return new UsernamePasswordToken($user = new User(), 'main', $user->getRoles());
|
||||
}
|
||||
|
||||
private function buildVoter(): EntityWorkflowVoter
|
||||
{
|
||||
$manager = $this->createMock(EntityWorkflowManager::class);
|
||||
$security = $this->createMock(Security::class);
|
||||
$duplicateEntityWorkflowFind = $this->createMock(DuplicateEntityWorkflowFinder::class);
|
||||
|
||||
return new EntityWorkflowVoter(
|
||||
$manager,
|
||||
$security,
|
||||
$duplicateEntityWorkflowFind,
|
||||
$this->buildRegistry()
|
||||
);
|
||||
}
|
||||
|
||||
private function buildEntityWorkflow(): EntityWorkflow
|
||||
{
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$entityWorkflow->setWorkflowName('dummy');
|
||||
$entityWorkflow->setRelatedEntityId(1)->setRelatedEntityClass(\stdClass::class);
|
||||
|
||||
return $entityWorkflow;
|
||||
}
|
||||
|
||||
private function buildRegistry(): Registry
|
||||
{
|
||||
$security = $this->createMock(Security::class);
|
||||
$security->method('getUser')->willReturn(new User());
|
||||
|
||||
$builder = new DefinitionBuilder();
|
||||
$builder->addPlaces(['initial', 'in_between', 'final_positive', 'final_negative']);
|
||||
|
||||
$metadataStore = new InMemoryMetadataStore(
|
||||
placesMetadata: [
|
||||
'final_positive' => [
|
||||
'isFinal' => true,
|
||||
'isFinalPositive' => true,
|
||||
],
|
||||
'final_negative' => [
|
||||
'isFinal' => true,
|
||||
'isFinalPositive' => false,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->setMetadataStore($metadataStore);
|
||||
|
||||
$transitions = [
|
||||
new Transition('move_to_in_between', 'initial', 'in_between'),
|
||||
new Transition('move_to_final_positive', 'initial', 'final_positive'),
|
||||
new Transition('move_to_final_negative', 'initial', 'final_negative'),
|
||||
];
|
||||
|
||||
foreach ($transitions as $transition) {
|
||||
$builder->addTransition($transition);
|
||||
}
|
||||
|
||||
$definition = $builder->build();
|
||||
|
||||
$eventSubscriber = new EventDispatcher();
|
||||
$eventSubscriber->addSubscriber(
|
||||
new EntityWorkflowTransitionEventSubscriber(
|
||||
new NullLogger(),
|
||||
$security
|
||||
)
|
||||
);
|
||||
|
||||
$registry = new Registry();
|
||||
$workflow = new Workflow($definition, new EntityWorkflowMarkingStore(), $eventSubscriber, name: 'dummy');
|
||||
|
||||
$registry->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface {
|
||||
public function supports(WorkflowInterface $workflow, $subject): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return $registry;
|
||||
}
|
||||
}
|
@@ -104,6 +104,11 @@ class CancelStaleWorkflowCronJobTest extends TestCase
|
||||
(new CronJobExecution('last-canceled-workflow-id'))->setLastEnd(new \DateTimeImmutable('2023-12-31 00:00:01', new \DateTimeZone('+00:00'))),
|
||||
false,
|
||||
];
|
||||
|
||||
yield [
|
||||
null,
|
||||
true,
|
||||
];
|
||||
}
|
||||
|
||||
private function buildMessageBus(bool $expectDispatchAtLeastOnce = false): MessageBusInterface
|
||||
|
@@ -35,7 +35,6 @@ class UserRenderTest extends TestCase
|
||||
public function testRenderUserWithJobAndScopeAtCertainDate(): void
|
||||
{
|
||||
// Create a user with a certain user job
|
||||
|
||||
$user = new User();
|
||||
$userJobA = new UserJob();
|
||||
$scopeA = new Scope();
|
||||
@@ -106,4 +105,52 @@ class UserRenderTest extends TestCase
|
||||
$expectedStringC = 'BOB ISLA (directrice) (service B)';
|
||||
$this->assertEquals($expectedStringC, $renderer->renderString($user, $optionsNoDate));
|
||||
}
|
||||
|
||||
public function testRenderStringWithSplitLines(): void
|
||||
{
|
||||
|
||||
// Create a user with a certain user job
|
||||
$user = new User();
|
||||
$userJobA = new UserJob();
|
||||
$scopeA = new Scope();
|
||||
|
||||
$userJobA->setLabel(['fr' => 'assistant social en maison de service accompagné'])
|
||||
->setActive(true);
|
||||
$scopeA->setName(['fr' => 'service de l\'assistant professionnel']);
|
||||
$user->setLabel('Robert Van Zorrizzeen Gorikke');
|
||||
|
||||
$userJobHistoryA = (new User\UserJobHistory())
|
||||
->setUser($user)
|
||||
->setJob($userJobA)
|
||||
->setStartDate(new \DateTimeImmutable('2023-11-01 12:00:00'))
|
||||
->setEndDate(new \DateTimeImmutable('2023-11-30 00:00:00'));
|
||||
|
||||
$userScopeHistoryA = (new User\UserScopeHistory())
|
||||
->setUser($user)
|
||||
->setScope($scopeA)
|
||||
->setStartDate(new \DateTimeImmutable('2023-11-01 12:00:00'))
|
||||
->setEndDate(new \DateTimeImmutable('2023-11-30 00:00:00'));
|
||||
|
||||
$user->getUserJobHistories()->add($userJobHistoryA);
|
||||
$user->getUserScopeHistories()->add($userScopeHistoryA);
|
||||
|
||||
// Create renderer
|
||||
$translatableStringHelperMock = $this->prophesize(TranslatableStringHelperInterface::class);
|
||||
$translatableStringHelperMock->localize(Argument::type('array'))->will(fn ($args) => $args[0]['fr']);
|
||||
|
||||
$engineMock = $this->createMock(Environment::class);
|
||||
$translatorMock = $this->createMock(TranslatorInterface::class);
|
||||
$clock = new MockClock(new \DateTimeImmutable('2023-11-15 12:00:00'));
|
||||
|
||||
$renderer = new UserRender($translatableStringHelperMock->reveal(), $engineMock, $translatorMock, $clock);
|
||||
|
||||
$actual = $renderer->renderString($user, ['split_lines_before_characters' => 30]);
|
||||
self::assertEquals(<<<'STR'
|
||||
Robert Van Zorrizzeen Gorikke
|
||||
(assistant social en maison de
|
||||
service accompagné) (service de
|
||||
l'assistant professionnel)
|
||||
STR, $actual);
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,135 @@
|
||||
<?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\Workflow\EventSubscriber;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
|
||||
use Chill\MainBundle\Workflow\EventSubscriber\BlockSignatureOnRelatedEntityWithoutAnyStoredObjectGuard;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
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 BlockSignatureOnRelatedEntityWithoutAnyStoredObjectGuardTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testAllowedForSignatureOnRelatedEntityWithStoredObject(): void
|
||||
{
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$entityWorkflow->setWorkflowName('dummy')->setRelatedEntityClass('with_stored_object');
|
||||
|
||||
$registry = $this->buildRegistry();
|
||||
$workflow = $registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||
|
||||
$list = $workflow->buildTransitionBlockerList($entityWorkflow, 'to_signature');
|
||||
|
||||
self::assertCount(0, $list);
|
||||
}
|
||||
|
||||
public function testBlockForSignatureOnRelatedEntityWithoutStoredObject(): void
|
||||
{
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$entityWorkflow->setWorkflowName('dummy')->setRelatedEntityClass('no_stored_object');
|
||||
|
||||
$registry = $this->buildRegistry();
|
||||
$workflow = $registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||
|
||||
$list = $workflow->buildTransitionBlockerList($entityWorkflow, 'to_signature');
|
||||
|
||||
self::assertCount(1, $list);
|
||||
self::assertTrue($list->has('e8e28caa-a106-11ef-97e8-f3919e8b5c8a'));
|
||||
}
|
||||
|
||||
public function testAllowedForNoSignatureOnRelatedEntityWithoutStoredObject(): void
|
||||
{
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$entityWorkflow->setWorkflowName('dummy')->setRelatedEntityClass('no_stored_object');
|
||||
|
||||
$registry = $this->buildRegistry();
|
||||
$workflow = $registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||
|
||||
$list = $workflow->buildTransitionBlockerList($entityWorkflow, 'to_no_signature');
|
||||
|
||||
self::assertTrue($list->isEmpty());
|
||||
}
|
||||
|
||||
public function testAllowedForNoSignatureOnRelatedEntityWithStoredObject(): void
|
||||
{
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$entityWorkflow->setWorkflowName('dummy')->setRelatedEntityClass('no_stored_object');
|
||||
|
||||
$registry = $this->buildRegistry();
|
||||
$workflow = $registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||
|
||||
$list = $workflow->buildTransitionBlockerList($entityWorkflow, 'to_no_signature');
|
||||
|
||||
self::assertTrue($list->isEmpty());
|
||||
}
|
||||
|
||||
private function buildRegistry(): Registry
|
||||
{
|
||||
$definitionBuilder = new DefinitionBuilder();
|
||||
$definitionBuilder
|
||||
->addPlaces(['initial', 'signature', 'no_signature'])
|
||||
->addTransition(
|
||||
new Transition('to_signature', 'initial', 'signature')
|
||||
)
|
||||
->addTransition(
|
||||
new Transition('to_no_signature', 'initial', 'no_signature')
|
||||
)
|
||||
->setMetadataStore(
|
||||
new InMemoryMetadataStore(
|
||||
placesMetadata: ['signature' => ['isSignature' => ['person']]]
|
||||
)
|
||||
);
|
||||
|
||||
$workflow = new Workflow($definitionBuilder->build(), new EntityWorkflowMarkingStore(), $eventDispatcher = new EventDispatcher(), name: 'dummy');
|
||||
$registry = new Registry();
|
||||
$registry->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface {
|
||||
public function supports(WorkflowInterface $workflow, object $subject): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
||||
$entityWorkflowManager->canAssociateStoredObject(Argument::type(EntityWorkflow::class))->will(
|
||||
function ($args): bool {
|
||||
/** @var EntityWorkflow $entityWorkflow */
|
||||
$entityWorkflow = $args[0];
|
||||
|
||||
return 'with_stored_object' === $entityWorkflow->getRelatedEntityClass();
|
||||
}
|
||||
);
|
||||
$eventSubscriber = new BlockSignatureOnRelatedEntityWithoutAnyStoredObjectGuard(
|
||||
$entityWorkflowManager->reveal()
|
||||
);
|
||||
|
||||
$eventDispatcher->addSubscriber($eventSubscriber);
|
||||
|
||||
return $registry;
|
||||
}
|
||||
}
|
@@ -16,6 +16,8 @@ use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\EventSubscriber\NotificationOnTransition;
|
||||
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
@@ -91,12 +93,18 @@ final class NotificationOnTransitionTest extends TestCase
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->getUser()->willReturn($currentUser);
|
||||
|
||||
$entityWorkflowHandler = $this->prophesize(EntityWorkflowHandlerInterface::class);
|
||||
$entityWorkflowHandler->getEntityTitle($entityWorkflow)->willReturn('workflow title');
|
||||
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
||||
$entityWorkflowManager->getHandler($entityWorkflow)->willReturn($entityWorkflowHandler->reveal());
|
||||
|
||||
$notificationOnTransition = new NotificationOnTransition(
|
||||
$em->reveal(),
|
||||
$engine->reveal(),
|
||||
$extractor->reveal(),
|
||||
$security->reveal(),
|
||||
$registry->reveal()
|
||||
$registry->reveal(),
|
||||
$entityWorkflowManager->reveal(),
|
||||
);
|
||||
|
||||
$event = new Event($entityWorkflow, new Marking(), new Transition('dummy_transition', ['from_state'], ['to_state']), $workflow);
|
||||
|
@@ -14,10 +14,13 @@ namespace Chill\MainBundle\Tests\Workflow\EventSubscriber;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
|
||||
use Chill\MainBundle\Workflow\EventSubscriber\NotificationToUserGroupsOnTransition;
|
||||
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
|
||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
@@ -45,20 +48,20 @@ class NotificationToUserGroupsOnTransitionTest extends KernelTestCase
|
||||
use ProphecyTrait;
|
||||
private Environment $twig;
|
||||
private BodyRendererInterface $bodyRenderer;
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->twig = self::getContainer()->get('twig');
|
||||
$this->bodyRenderer = self::getContainer()->get(BodyRendererInterface::class);
|
||||
$this->em = self::getContainer()->get('doctrine.orm.entity_manager');
|
||||
}
|
||||
|
||||
public function testOnCompletedSendNotificationToUserGroupWithEmailAddress(): void
|
||||
{
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$reflection = new \ReflectionClass($entityWorkflow);
|
||||
$idProperty = $reflection->getProperty('id');
|
||||
$idProperty->setValue($entityWorkflow, 1);
|
||||
$this->em->persist($entityWorkflow);
|
||||
|
||||
$entityWorkflow->setWorkflowName('dummy');
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
@@ -81,7 +84,12 @@ class NotificationToUserGroupsOnTransitionTest extends KernelTestCase
|
||||
$metadataExtractor->buildArrayPresentationForWorkflow(Argument::type(Workflow::class))->willReturn(['name' => 'dummy', 'text' => 'Dummy Workflow']);
|
||||
$metadataExtractor->buildArrayPresentationForPlace($entityWorkflow)->willReturn(['name' => 'to_one', 'text' => 'Dummy Place']);
|
||||
|
||||
$registry = $this->buildRegistryWithEventSubscriber($mailer->reveal(), $metadataExtractor->reveal());
|
||||
$entityWorkflowHandler = $this->prophesize(EntityWorkflowHandlerInterface::class);
|
||||
$entityWorkflowHandler->getEntityTitle($entityWorkflow)->willReturn('My title');
|
||||
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
||||
$entityWorkflowManager->getHandler($entityWorkflow)->willReturn($entityWorkflowHandler->reveal());
|
||||
|
||||
$registry = $this->buildRegistryWithEventSubscriber($mailer->reveal(), $metadataExtractor->reveal(), $entityWorkflowManager->reveal());
|
||||
$workflow = $registry->get($entityWorkflow, 'dummy');
|
||||
|
||||
$workflow->apply($entityWorkflow, 'to_one', ['context' => $dto, 'transition' => 'to_one', 'transitionAt' => new \DateTimeImmutable(), 'byUser' => new User()]);
|
||||
@@ -105,13 +113,18 @@ class NotificationToUserGroupsOnTransitionTest extends KernelTestCase
|
||||
$metadataExtractor->buildArrayPresentationForWorkflow(Argument::type(Workflow::class))->willReturn(['name' => 'dummy', 'text' => 'Dummy Workflow']);
|
||||
$metadataExtractor->buildArrayPresentationForPlace($entityWorkflow)->willReturn(['name' => 'to_one', 'text' => 'Dummy Place']);
|
||||
|
||||
$registry = $this->buildRegistryWithEventSubscriber($mailer->reveal(), $metadataExtractor->reveal());
|
||||
$entityWorkflowHandler = $this->prophesize(EntityWorkflowHandlerInterface::class);
|
||||
$entityWorkflowHandler->getEntityTitle($entityWorkflow)->willReturn('My title');
|
||||
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
||||
$entityWorkflowManager->getHandler($entityWorkflow)->willReturn($entityWorkflowHandler->reveal());
|
||||
|
||||
$registry = $this->buildRegistryWithEventSubscriber($mailer->reveal(), $metadataExtractor->reveal(), $entityWorkflowManager->reveal());
|
||||
$workflow = $registry->get($entityWorkflow, 'dummy');
|
||||
|
||||
$workflow->apply($entityWorkflow, 'to_one', ['context' => $dto, 'transition' => 'to_one', 'transitionAt' => new \DateTimeImmutable(), 'byUser' => new User()]);
|
||||
}
|
||||
|
||||
public function buildRegistryWithEventSubscriber(MailerInterface $mailer, MetadataExtractor $metadataExtractor): Registry
|
||||
private function buildRegistryWithEventSubscriber(MailerInterface $mailer, MetadataExtractor $metadataExtractor, EntityWorkflowManager $entityWorkflowManager): Registry
|
||||
{
|
||||
$builder = new DefinitionBuilder();
|
||||
$builder
|
||||
@@ -133,7 +146,7 @@ class NotificationToUserGroupsOnTransitionTest extends KernelTestCase
|
||||
}
|
||||
});
|
||||
|
||||
$notificationEventSubscriber = new NotificationToUserGroupsOnTransition($this->twig, $metadataExtractor, $registry, $mailer);
|
||||
$notificationEventSubscriber = new NotificationToUserGroupsOnTransition($this->twig, $metadataExtractor, $registry, $mailer, $this->em, $entityWorkflowManager);
|
||||
$eventDispatcher->addSubscriber($notificationEventSubscriber);
|
||||
|
||||
return $registry;
|
||||
|
@@ -0,0 +1,291 @@
|
||||
<?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\Workflow\Helper;
|
||||
|
||||
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
|
||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
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\Workflow;
|
||||
use Symfony\Component\Workflow\WorkflowInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class WorkflowRelatedEntityPermissionHelperTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
/**
|
||||
* @dataProvider provideDataAllowedByWorkflowReadOperation
|
||||
*
|
||||
* @param list<EntityWorkflow> $entityWorkflows
|
||||
*/
|
||||
public function testAllowedByWorkflowRead(
|
||||
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->buildHelper($entityWorkflows, $user, $atDate);
|
||||
|
||||
self::assertEquals($expected, $helper->isAllowedByWorkflowForReadOperation(new \stdClass()), $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideDataAllowedByWorkflowWriteOperation
|
||||
*
|
||||
* @param list<EntityWorkflow> $entityWorkflows
|
||||
*/
|
||||
public function testAllowedByWorkflowWrite(
|
||||
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->buildHelper($entityWorkflows, $user, $atDate);
|
||||
|
||||
self::assertEquals($expected, $helper->isAllowedByWorkflowForWriteOperation(new \stdClass()), $message);
|
||||
}
|
||||
|
||||
public function testNoWorkflow(): void
|
||||
{
|
||||
$helper = $this->buildHelper([], new User(), null);
|
||||
|
||||
self::assertEquals(WorkflowRelatedEntityPermissionHelper::ABSTAIN, $helper->isAllowedByWorkflowForWriteOperation(new \stdClass()));
|
||||
self::assertEquals(WorkflowRelatedEntityPermissionHelper::ABSTAIN, $helper->isAllowedByWorkflowForReadOperation(new \stdClass()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<EntityWorkflow> $entityWorkflows
|
||||
*/
|
||||
private function buildHelper(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'))->willReturn($entityWorkflows);
|
||||
|
||||
return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable()));
|
||||
}
|
||||
|
||||
public static function provideDataAllowedByWorkflowReadOperation(): 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::FORCE_GRANT, new \DateTimeImmutable(),
|
||||
'force grant because there is a signature for person'];
|
||||
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$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('2024-01-01T12:00:00'));
|
||||
|
||||
yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable('2024-01-10T12:00:00'),
|
||||
'abstain because there is a signature for person, already signed, and for a long time ago'];
|
||||
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$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('2024-01-01T12:00:00'));
|
||||
|
||||
yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
||||
new \DateTimeImmutable('2024-01-01T12:30:00'),
|
||||
'force grant because there is a signature for person, already signed, a short time ago'];
|
||||
}
|
||||
|
||||
public static function provideDataAllowedByWorkflowWriteOperation(): 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->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], $user, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(),
|
||||
'force denied because there is a signature'];
|
||||
|
||||
$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], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
|
||||
'force grant: there is a signature, but still pending'];
|
||||
|
||||
$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'];
|
||||
}
|
||||
|
||||
private static function buildRegistry(): Registry
|
||||
{
|
||||
$builder = new DefinitionBuilder();
|
||||
$builder
|
||||
->setInitialPlaces(['initial'])
|
||||
->addPlaces(['initial', 'test', 'final_positive', 'final_negative'])
|
||||
->setMetadataStore(
|
||||
new InMemoryMetadataStore(
|
||||
placesMetadata: [
|
||||
'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;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user