mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-27 17:15:02 +00:00
Compare commits
19 Commits
signature-
...
manage-tra
Author | SHA1 | Date | |
---|---|---|---|
1961bf36e2 | |||
52f06e2142 | |||
6ded75119c | |||
89fa10cede | |||
b0b1a28f50 | |||
760f74b386 | |||
d82a3af063 | |||
7edd644963 | |||
077163a774 | |||
9c963a2122 | |||
c1c632dcb0 | |||
9732b80298 | |||
c602c27b39 | |||
41f13e29e0 | |||
611261c863 | |||
5921404712 | |||
23d882d4cd | |||
155066be13 | |||
a5329c5d69 |
@@ -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"
|
|
7
.changes/unreleased/Feature-20241118-150627.yaml
Normal file
7
.changes/unreleased/Feature-20241118-150627.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
kind: Feature
|
||||||
|
body: "Implementation of new translation management with one source of truth for both
|
||||||
|
twig and vue component templates using YAML files. \nDuplicate translation keys
|
||||||
|
can also be detected with new command."
|
||||||
|
time: 2024-11-18T15:06:27.929549251+01:00
|
||||||
|
custom:
|
||||||
|
Issue: ""
|
@@ -1,5 +0,0 @@
|
|||||||
## v3.0.0 - 2024-08-26
|
|
||||||
### Fixed
|
|
||||||
* Fix delete action for accompanying periods in draft state
|
|
||||||
* Fix connection to azure when making an calendar event in chill
|
|
||||||
* CollectionType js fixes for remove button and adding multiple entries
|
|
@@ -138,4 +138,4 @@ release:
|
|||||||
- echo "running release_job"
|
- echo "running release_job"
|
||||||
release:
|
release:
|
||||||
tag_name: '$CI_COMMIT_TAG'
|
tag_name: '$CI_COMMIT_TAG'
|
||||||
description: "./.changes/$CI_COMMIT_TAG.md"
|
description: "./.changes/v$CI_COMMIT_TAG.md"
|
||||||
|
@@ -6,12 +6,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
|||||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||||
|
|
||||||
|
|
||||||
## v3.0.0 - 2024-08-26
|
|
||||||
### Fixed
|
|
||||||
* Fix delete action for accompanying periods in draft state
|
|
||||||
* Fix connection to azure when making an calendar event in chill
|
|
||||||
* CollectionType js fixes for remove button and adding multiple entries
|
|
||||||
|
|
||||||
## v2.23.0 - 2024-07-23
|
## v2.23.0 - 2024-07-23
|
||||||
### Feature
|
### Feature
|
||||||
* ([#221](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/221)) [DX] move async-upload-bundle features into chill-bundles
|
* ([#221](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/221)) [DX] move async-upload-bundle features into chill-bundles
|
||||||
|
@@ -31,7 +31,6 @@
|
|||||||
"phpoffice/phpspreadsheet": "^1.16",
|
"phpoffice/phpspreadsheet": "^1.16",
|
||||||
"ramsey/uuid-doctrine": "^1.7",
|
"ramsey/uuid-doctrine": "^1.7",
|
||||||
"sensio/framework-extra-bundle": "^5.5",
|
"sensio/framework-extra-bundle": "^5.5",
|
||||||
"smalot/pdfparser": "^2.10",
|
|
||||||
"spomky-labs/base64url": "^2.0",
|
"spomky-labs/base64url": "^2.0",
|
||||||
"symfony/asset": "^5.4",
|
"symfony/asset": "^5.4",
|
||||||
"symfony/browser-kit": "^5.4",
|
"symfony/browser-kit": "^5.4",
|
||||||
@@ -66,10 +65,12 @@
|
|||||||
"symfony/security-guard": "^5.4",
|
"symfony/security-guard": "^5.4",
|
||||||
"symfony/security-http": "^5.4",
|
"symfony/security-http": "^5.4",
|
||||||
"symfony/serializer": "^5.4",
|
"symfony/serializer": "^5.4",
|
||||||
|
"symfony/stimulus-bundle": "^2.19",
|
||||||
"symfony/string": "^5.4",
|
"symfony/string": "^5.4",
|
||||||
"symfony/templating": "^5.4",
|
"symfony/templating": "^5.4",
|
||||||
"symfony/translation": "^5.4",
|
"symfony/translation": "^5.4",
|
||||||
"symfony/twig-bundle": "^5.4",
|
"symfony/twig-bundle": "^5.4",
|
||||||
|
"symfony/ux-translator": "^2.19",
|
||||||
"symfony/validator": "^5.4",
|
"symfony/validator": "^5.4",
|
||||||
"symfony/webpack-encore-bundle": "^1.11",
|
"symfony/webpack-encore-bundle": "^1.11",
|
||||||
"symfony/workflow": "^5.4",
|
"symfony/workflow": "^5.4",
|
||||||
|
31
docs/source/development/translations.rst
Normal file
31
docs/source/development/translations.rst
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
Translations
|
||||||
|
*************
|
||||||
|
|
||||||
|
Translator-UX: one source of truth
|
||||||
|
==================================
|
||||||
|
|
||||||
|
The Translator-ux integration streamlines the process of managing and using translation keys dynamically in our views, whether they be in twig or vue components. The goal is to have one source of truth
|
||||||
|
for all translations and avoid having to add translation keys in the YAML files as well as in i18ns files.
|
||||||
|
|
||||||
|
To add new translation keys, you can define them in your translation YAML files. Running `symfony console cache:clear` will subsequently update the compiled translation keys which can then also be imported and
|
||||||
|
used within any vue component. For use within a twig template they can be leveraged by using the |trans function.
|
||||||
|
Within vue components you will have to import the translation keys you require and then they can be used in the template with the trans() function.
|
||||||
|
|
||||||
|
It is advisable, before adding a translation key to do a search on the existing translation keys of the translation you require. An IDE will allow you to do so easily.
|
||||||
|
However to avoid the creation of duplicate translation keys a command also exists to detect them. We also strongly advise you to use this command as explained below.
|
||||||
|
|
||||||
|
Detect duplicates command
|
||||||
|
=========================
|
||||||
|
|
||||||
|
The DetectTranslationDuplicatesCommand `chill:detect-duplicate-translations` is a Symfony console command designed to identify duplicate translations across YAML files in a project.
|
||||||
|
It checks for repeated translation values linked to different keys within a specified locale.
|
||||||
|
The command accepts two main options:
|
||||||
|
|
||||||
|
1. `--locale`: to specify the language locale to check (defaulting to 'en')
|
||||||
|
2. `--exclude-namespaces`: to list namespaces to ignore during the check.
|
||||||
|
3. [optional] `--verify-hash`: can be used to ensure that the hash of current duplicates matches a given expected value,
|
||||||
|
aiding in maintaining translation integrity.
|
||||||
|
|
||||||
|
When duplicates are detected, they are displayed in a table format, listing the repeated translations alongside the keys where they are found.
|
||||||
|
If a mismatch occurs between the computed and expected hash values, an error message is displayed to signal a potential issue in translation consistency.
|
||||||
|
This command is useful for maintaining clean and consistent translations, avoiding redundancy in your YAML files.
|
@@ -53,7 +53,6 @@
|
|||||||
"marked": "^12.0.2",
|
"marked": "^12.0.2",
|
||||||
"masonry-layout": "^4.2.2",
|
"masonry-layout": "^4.2.2",
|
||||||
"mime": "^4.0.0",
|
"mime": "^4.0.0",
|
||||||
"pdfjs-dist": "^4.3.136",
|
|
||||||
"swagger-ui": "^4.15.5",
|
"swagger-ui": "^4.15.5",
|
||||||
"vis-network": "^9.1.0",
|
"vis-network": "^9.1.0",
|
||||||
"vue": "^3.2.37",
|
"vue": "^3.2.37",
|
||||||
|
@@ -69,8 +69,9 @@ return static function (RectorConfig $rectorConfig): void {
|
|||||||
|
|
||||||
// skip some path...
|
// skip some path...
|
||||||
$rectorConfig->skip([
|
$rectorConfig->skip([
|
||||||
// waiting for fixing this bug: https://github.com/rectorphp/rector-doctrine/issues/342
|
// we must adapt service definition
|
||||||
\Rector\Doctrine\CodeQuality\Rector\Property\ImproveDoctrineCollectionDocTypeInEntityRector::class,
|
\Rector\Symfony\Symfony28\Rector\MethodCall\GetToConstructorInjectionRector::class,
|
||||||
|
\Rector\Symfony\Symfony34\Rector\Closure\ContainerGetNameToTypeInTestsRector::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$rectorConfig->ruleWithConfiguration(AnnotationToAttributeRector::class, [
|
$rectorConfig->ruleWithConfiguration(AnnotationToAttributeRector::class, [
|
||||||
|
@@ -80,7 +80,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
|||||||
private \DateTime $date;
|
private \DateTime $date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, StoredObject>
|
* @var Collection<StoredObject>
|
||||||
*/
|
*/
|
||||||
#[Assert\Valid(traverse: true)]
|
#[Assert\Valid(traverse: true)]
|
||||||
#[ORM\ManyToMany(targetEntity: StoredObject::class, cascade: ['persist'])]
|
#[ORM\ManyToMany(targetEntity: StoredObject::class, cascade: ['persist'])]
|
||||||
@@ -107,7 +107,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
|||||||
private ?Person $person = null;
|
private ?Person $person = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, \Chill\PersonBundle\Entity\Person>
|
* @var Collection<Person>
|
||||||
*/
|
*/
|
||||||
#[Groups(['read', 'docgen:read'])]
|
#[Groups(['read', 'docgen:read'])]
|
||||||
#[ORM\ManyToMany(targetEntity: Person::class)]
|
#[ORM\ManyToMany(targetEntity: Person::class)]
|
||||||
@@ -117,7 +117,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
|||||||
private PrivateCommentEmbeddable $privateComment;
|
private PrivateCommentEmbeddable $privateComment;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, ActivityReason>
|
* @var Collection<ActivityReason>
|
||||||
*/
|
*/
|
||||||
#[Groups(['docgen:read'])]
|
#[Groups(['docgen:read'])]
|
||||||
#[ORM\ManyToMany(targetEntity: ActivityReason::class)]
|
#[ORM\ManyToMany(targetEntity: ActivityReason::class)]
|
||||||
@@ -132,7 +132,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
|||||||
private string $sentReceived = '';
|
private string $sentReceived = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, \Chill\PersonBundle\Entity\SocialWork\SocialAction>
|
* @var Collection<SocialAction>
|
||||||
*/
|
*/
|
||||||
#[Groups(['read', 'docgen:read'])]
|
#[Groups(['read', 'docgen:read'])]
|
||||||
#[ORM\ManyToMany(targetEntity: SocialAction::class)]
|
#[ORM\ManyToMany(targetEntity: SocialAction::class)]
|
||||||
@@ -140,7 +140,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
|||||||
private Collection $socialActions;
|
private Collection $socialActions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, SocialIssue>
|
* @var Collection<SocialIssue>
|
||||||
*/
|
*/
|
||||||
#[Groups(['read', 'docgen:read'])]
|
#[Groups(['read', 'docgen:read'])]
|
||||||
#[ORM\ManyToMany(targetEntity: SocialIssue::class)]
|
#[ORM\ManyToMany(targetEntity: SocialIssue::class)]
|
||||||
@@ -148,7 +148,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
|||||||
private Collection $socialIssues;
|
private Collection $socialIssues;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, ThirdParty>
|
* @var Collection<ThirdParty>
|
||||||
*/
|
*/
|
||||||
#[Groups(['read', 'docgen:read'])]
|
#[Groups(['read', 'docgen:read'])]
|
||||||
#[ORM\ManyToMany(targetEntity: ThirdParty::class)]
|
#[ORM\ManyToMany(targetEntity: ThirdParty::class)]
|
||||||
@@ -162,7 +162,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
|||||||
private ?User $user = null;
|
private ?User $user = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, User>
|
* @var Collection<User>
|
||||||
*/
|
*/
|
||||||
#[Groups(['read', 'docgen:read'])]
|
#[Groups(['read', 'docgen:read'])]
|
||||||
#[ORM\ManyToMany(targetEntity: User::class)]
|
#[ORM\ManyToMany(targetEntity: User::class)]
|
||||||
|
@@ -40,9 +40,9 @@ class ActivityReasonCategory implements \Stringable
|
|||||||
/**
|
/**
|
||||||
* Array of ActivityReason.
|
* Array of ActivityReason.
|
||||||
*
|
*
|
||||||
* @var Collection<int, ActivityReason>
|
* @var Collection<ActivityReason>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(mappedBy: 'category', targetEntity: ActivityReason::class)]
|
#[ORM\OneToMany(targetEntity: ActivityReason::class, mappedBy: 'category')]
|
||||||
private Collection $reasons;
|
private Collection $reasons;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -12,8 +12,6 @@ declare(strict_types=1);
|
|||||||
namespace Chill\ActivityBundle\Repository;
|
namespace Chill\ActivityBundle\Repository;
|
||||||
|
|
||||||
use Chill\ActivityBundle\Entity\Activity;
|
use Chill\ActivityBundle\Entity\Activity;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
|
||||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
@@ -25,7 +23,7 @@ use Doctrine\Persistence\ManagerRegistry;
|
|||||||
* @method Activity[] findAll()
|
* @method Activity[] findAll()
|
||||||
* @method Activity[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
* @method Activity[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||||
*/
|
*/
|
||||||
class ActivityRepository extends ServiceEntityRepository implements AssociatedEntityToStoredObjectInterface
|
class ActivityRepository extends ServiceEntityRepository
|
||||||
{
|
{
|
||||||
public function __construct(ManagerRegistry $registry)
|
public function __construct(ManagerRegistry $registry)
|
||||||
{
|
{
|
||||||
@@ -99,16 +97,4 @@ class ActivityRepository extends ServiceEntityRepository implements AssociatedEn
|
|||||||
|
|
||||||
return $qb->getQuery()->getResult();
|
return $qb->getQuery()->getResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?Activity
|
|
||||||
{
|
|
||||||
$qb = $this->createQueryBuilder('a');
|
|
||||||
$query = $qb
|
|
||||||
->leftJoin('a.documents', 'ad')
|
|
||||||
->where('ad.id = :storedObjectId')
|
|
||||||
->setParameter('storedObjectId', $storedObject->getId())
|
|
||||||
->getQuery();
|
|
||||||
|
|
||||||
return $query->getOneOrNullResult();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -22,8 +22,8 @@
|
|||||||
<ul class="record_actions">
|
<ul class="record_actions">
|
||||||
<li class="add-persons">
|
<li class="add-persons">
|
||||||
<add-persons
|
<add-persons
|
||||||
buttonTitle="activity.add_persons"
|
:buttonTitle="trans(ACTIVITY_ADD_PERSONS)"
|
||||||
modalTitle="activity.add_persons"
|
:modalTitle="trans(ACTIVITY_ADD_PERSONS)"
|
||||||
v-bind:key="addPersons.key"
|
v-bind:key="addPersons.key"
|
||||||
v-bind:options="addPersonsOptions"
|
v-bind:options="addPersonsOptions"
|
||||||
@addNewPersons="addNewPersons"
|
@addNewPersons="addNewPersons"
|
||||||
@@ -40,6 +40,20 @@ import { mapState, mapGetters } from 'vuex';
|
|||||||
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue';
|
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue';
|
||||||
import PersonsBloc from './ConcernedGroups/PersonsBloc.vue';
|
import PersonsBloc from './ConcernedGroups/PersonsBloc.vue';
|
||||||
import PersonText from 'ChillPersonAssets/vuejs/_components/Entity/PersonText.vue';
|
import PersonText from 'ChillPersonAssets/vuejs/_components/Entity/PersonText.vue';
|
||||||
|
import {
|
||||||
|
ACTIVITY_BLOC_PERSONS,
|
||||||
|
ACTIVITY_BLOC_PERSONS_ASSOCIATED,
|
||||||
|
ACTIVITY_BLOC_PERSONS_NOT_ASSOCIATED,
|
||||||
|
ACTIVITY_BLOC_THIRDPARTY,
|
||||||
|
ACTIVITY_BLOC_USERS,
|
||||||
|
ACTIVITY_ADD_PERSONS,
|
||||||
|
ACTIVITY_LOCATION,
|
||||||
|
ACTIVITY_CHOOSE_LOCATION,
|
||||||
|
MULTISELECT_SELECT_LABEL,
|
||||||
|
MULTISELECT_DESELECT_LABEL,
|
||||||
|
MULTISELECT_SELECTED_LABEL,
|
||||||
|
trans,
|
||||||
|
} from "../../../../../../../../../../../assets/translator";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ConcernedGroups",
|
name: "ConcernedGroups",
|
||||||
@@ -48,16 +62,22 @@ export default {
|
|||||||
PersonsBloc,
|
PersonsBloc,
|
||||||
PersonText
|
PersonText
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
trans,
|
||||||
|
ACTIVITY_ADD_PERSONS
|
||||||
|
};
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
personsBlocs: [
|
personsBlocs: [
|
||||||
{ key: 'persons',
|
{ key: 'persons',
|
||||||
title: 'activity.bloc_persons',
|
title: trans(ACTIVITY_BLOC_PERSONS),
|
||||||
persons: [],
|
persons: [],
|
||||||
included: false
|
included: false
|
||||||
},
|
},
|
||||||
{ key: 'personsAssociated',
|
{ key: 'personsAssociated',
|
||||||
title: 'activity.bloc_persons_associated',
|
title: trans(ACTIVITY_BLOC_PERSONS_ASSOCIATED),
|
||||||
persons: [],
|
persons: [],
|
||||||
included: window.activity ? window.activity.activityType.personsVisible !== 0 : true
|
included: window.activity ? window.activity.activityType.personsVisible !== 0 : true
|
||||||
},
|
},
|
||||||
@@ -67,12 +87,12 @@ export default {
|
|||||||
included: window.activity ? window.activity.activityType.personsVisible !== 0 : true
|
included: window.activity ? window.activity.activityType.personsVisible !== 0 : true
|
||||||
},
|
},
|
||||||
{ key: 'thirdparty',
|
{ key: 'thirdparty',
|
||||||
title: 'activity.bloc_thirdparty',
|
title: trans(ACTIVITY_BLOC_THIRDPARTY),
|
||||||
persons: [],
|
persons: [],
|
||||||
included: window.activity ? window.activity.activityType.thirdPartiesVisible !== 0 : true
|
included: window.activity ? window.activity.activityType.thirdPartiesVisible !== 0 : true
|
||||||
},
|
},
|
||||||
{ key: 'users',
|
{ key: 'users',
|
||||||
title: 'activity.bloc_users',
|
title: trans(ACTIVITY_BLOC_USERS),
|
||||||
persons: [],
|
persons: [],
|
||||||
included: window.activity ? window.activity.activityType.usersVisible !== 0 : true
|
included: window.activity ? window.activity.activityType.usersVisible !== 0 : true
|
||||||
},
|
},
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
<teleport to="#location">
|
<teleport to="#location">
|
||||||
<div class="mb-3 row">
|
<div class="mb-3 row">
|
||||||
<label :class="locationClassList">
|
<label :class="locationClassList">
|
||||||
{{ $t("activity.location") }}
|
{{ trans(ACTIVITY_LOCATION) }}
|
||||||
</label>
|
</label>
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
<VueMultiselect
|
<VueMultiselect
|
||||||
@@ -13,11 +13,11 @@
|
|||||||
open-direction="top"
|
open-direction="top"
|
||||||
:multiple="false"
|
:multiple="false"
|
||||||
:searchable="true"
|
:searchable="true"
|
||||||
:placeholder="$t('activity.choose_location')"
|
:placeholder="trans(ACTIVITY_CHOOSE_LOCATION)"
|
||||||
:custom-label="customLabel"
|
:custom-label="customLabel"
|
||||||
:select-label="$t('multiselect.select_label')"
|
:select-label="trans(MULTISELECT_SELECT_LABEL)"
|
||||||
:deselect-label="$t('multiselect.deselect_label')"
|
:deselect-label="trans(MULTISELECT_DESELECT_LABEL)"
|
||||||
:selected-label="$t('multiselect.selected_label')"
|
:selected-label="trans(MULTISELECT_SELECTED_LABEL)"
|
||||||
:options="availableLocations"
|
:options="availableLocations"
|
||||||
group-values="locations"
|
group-values="locations"
|
||||||
group-label="locationGroup"
|
group-label="locationGroup"
|
||||||
@@ -34,6 +34,14 @@
|
|||||||
import { mapState, mapGetters } from "vuex";
|
import { mapState, mapGetters } from "vuex";
|
||||||
import VueMultiselect from "vue-multiselect";
|
import VueMultiselect from "vue-multiselect";
|
||||||
import NewLocation from "./Location/NewLocation.vue";
|
import NewLocation from "./Location/NewLocation.vue";
|
||||||
|
import {
|
||||||
|
trans,
|
||||||
|
ACTIVITY_LOCATION,
|
||||||
|
ACTIVITY_CHOOSE_LOCATION,
|
||||||
|
MULTISELECT_SELECT_LABEL,
|
||||||
|
MULTISELECT_DESELECT_LABEL,
|
||||||
|
MULTISELECT_SELECTED_LABEL
|
||||||
|
} from '../../../../../../../../../../../assets/translator'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Location",
|
name: "Location",
|
||||||
@@ -41,6 +49,16 @@ export default {
|
|||||||
NewLocation,
|
NewLocation,
|
||||||
VueMultiselect,
|
VueMultiselect,
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
trans,
|
||||||
|
ACTIVITY_LOCATION,
|
||||||
|
ACTIVITY_CHOOSE_LOCATION,
|
||||||
|
MULTISELECT_SELECT_LABEL,
|
||||||
|
MULTISELECT_DESELECT_LABEL,
|
||||||
|
MULTISELECT_SELECTED_LABEL
|
||||||
|
};
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
locationClassList:
|
locationClassList:
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
<ul class="record_actions">
|
<ul class="record_actions">
|
||||||
<li>
|
<li>
|
||||||
<a class="btn btn-sm btn-create" @click="openModal">
|
<a class="btn btn-sm btn-create" @click="openModal">
|
||||||
{{ $t('activity.create_new_location') }}
|
{{ trans(ACTIVITY_CREATE_NEW_LOCATION) }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
@close="modal.showModal = false">
|
@close="modal.showModal = false">
|
||||||
|
|
||||||
<template v-slot:header>
|
<template v-slot:header>
|
||||||
<h3 class="modal-title">{{ $t('activity.create_new_location') }}</h3>
|
<h3 class="modal-title">{{ trans(ACTIVITY_CREATE_NEW_LOCATION) }}</h3>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:body>
|
<template v-slot:body>
|
||||||
<form>
|
<form>
|
||||||
@@ -26,17 +26,17 @@
|
|||||||
|
|
||||||
<div class="form-floating mb-3">
|
<div class="form-floating mb-3">
|
||||||
<select class="form-select form-select-lg" id="type" required v-model="selectType">
|
<select class="form-select form-select-lg" id="type" required v-model="selectType">
|
||||||
<option selected disabled value="">{{ $t('activity.choose_location_type') }}</option>
|
<option selected disabled value="">{{ trans(ACTIVITY_CHOOSE_LOCATION_TYPE) }}</option>
|
||||||
<option v-for="t in locationTypes" :value="t" :key="t.id">
|
<option v-for="t in locationTypes" :value="t" :key="t.id">
|
||||||
{{ t.title.fr }}
|
{{ t.title.fr }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<label>{{ $t('activity.location_fields.type') }}</label>
|
<label>{{ trans(ACTIVITY_LOCATION_FIELDS_TYPE) }}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-floating mb-3">
|
<div class="form-floating mb-3">
|
||||||
<input class="form-control form-control-lg" id="name" v-model="inputName" placeholder />
|
<input class="form-control form-control-lg" id="name" v-model="inputName" placeholder />
|
||||||
<label for="name">{{ $t('activity.location_fields.name') }}</label>
|
<label for="name">{{ trans(ACTIVITY_LOCATION_FIELDS_NAME) }}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<add-address
|
<add-address
|
||||||
@@ -49,15 +49,15 @@
|
|||||||
|
|
||||||
<div class="form-floating mb-3" v-if="showContactData">
|
<div class="form-floating mb-3" v-if="showContactData">
|
||||||
<input class="form-control form-control-lg" id="phonenumber1" v-model="inputPhonenumber1" placeholder />
|
<input class="form-control form-control-lg" id="phonenumber1" v-model="inputPhonenumber1" placeholder />
|
||||||
<label for="phonenumber1">{{ $t('activity.location_fields.phonenumber1') }}</label>
|
<label for="phonenumber1">{{ trans(ACTIVITY_LOCATION_FIELDS_PHONENUMBER1) }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-floating mb-3" v-if="hasPhonenumber1">
|
<div class="form-floating mb-3" v-if="hasPhonenumber1">
|
||||||
<input class="form-control form-control-lg" id="phonenumber2" v-model="inputPhonenumber2" placeholder />
|
<input class="form-control form-control-lg" id="phonenumber2" v-model="inputPhonenumber2" placeholder />
|
||||||
<label for="phonenumber2">{{ $t('activity.location_fields.phonenumber2') }}</label>
|
<label for="phonenumber2">{{ trans(ACTIVITY_LOCATION_FIELDS_PHONENUMBER2) }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-floating mb-3" v-if="showContactData">
|
<div class="form-floating mb-3" v-if="showContactData">
|
||||||
<input class="form-control form-control-lg" id="email" v-model="inputEmail" placeholder />
|
<input class="form-control form-control-lg" id="email" v-model="inputEmail" placeholder />
|
||||||
<label for="email">{{ $t('activity.location_fields.email') }}</label>
|
<label for="email">{{ trans(ACTIVITY_LOCATION_FIELDS_EMAIL) }}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
<button class="btn btn-save"
|
<button class="btn btn-save"
|
||||||
@click.prevent="saveNewLocation"
|
@click.prevent="saveNewLocation"
|
||||||
>
|
>
|
||||||
{{ $t('action.save') }}
|
{{ trans(SAVE) }}
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -81,6 +81,13 @@ import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress.vue"
|
|||||||
import { mapState } from "vuex";
|
import { mapState } from "vuex";
|
||||||
import { getLocationTypes } from "../../api";
|
import { getLocationTypes } from "../../api";
|
||||||
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
|
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
|
||||||
|
import {
|
||||||
|
SAVE,
|
||||||
|
ACTIVITY_LOCATION_FIELDS_EMAIL, ACTIVITY_LOCATION_FIELDS_PHONENUMBER1,
|
||||||
|
ACTIVITY_LOCATION_FIELDS_PHONENUMBER2, ACTIVITY_LOCATION_FIELDS_NAME,
|
||||||
|
ACTIVITY_LOCATION_FIELDS_TYPE, ACTIVITY_CHOOSE_LOCATION_TYPE, ACTIVITY_CREATE_NEW_LOCATION,
|
||||||
|
trans
|
||||||
|
} from "../../../../../../../../../../../../assets/translator";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "NewLocation",
|
name: "NewLocation",
|
||||||
@@ -88,6 +95,19 @@ export default {
|
|||||||
Modal,
|
Modal,
|
||||||
AddAddress,
|
AddAddress,
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
trans,
|
||||||
|
SAVE,
|
||||||
|
ACTIVITY_LOCATION_FIELDS_EMAIL,
|
||||||
|
ACTIVITY_LOCATION_FIELDS_PHONENUMBER1,
|
||||||
|
ACTIVITY_LOCATION_FIELDS_PHONENUMBER2,
|
||||||
|
ACTIVITY_LOCATION_FIELDS_NAME,
|
||||||
|
ACTIVITY_LOCATION_FIELDS_TYPE,
|
||||||
|
ACTIVITY_CHOOSE_LOCATION_TYPE,
|
||||||
|
ACTIVITY_CREATE_NEW_LOCATION,
|
||||||
|
};
|
||||||
|
},
|
||||||
props: ['availableLocations'],
|
props: ['availableLocations'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<div class="mb-3 row">
|
<div class="mb-3 row">
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<label :class="socialIssuesClassList">{{ $t('activity.social_issues') }}</label>
|
<label :class="socialIssuesClassList">{{ trans(ACTIVITY_SOCIAL_ISSUES) }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
:allow-empty="true"
|
:allow-empty="true"
|
||||||
:show-labels="false"
|
:show-labels="false"
|
||||||
:loading="issueIsLoading"
|
:loading="issueIsLoading"
|
||||||
:placeholder="$t('activity.choose_other_social_issue')"
|
:placeholder="trans(ACTIVITY_CHOOSE_OTHER_SOCIAL_ISSUE)"
|
||||||
:options="socialIssuesOther"
|
:options="socialIssuesOther"
|
||||||
@select="addIssueInList">
|
@select="addIssueInList">
|
||||||
</VueMultiselect>
|
</VueMultiselect>
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
<div class="mb-3 row">
|
<div class="mb-3 row">
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<label :class="socialActionsClassList">{{ $t('activity.social_actions') }}</label>
|
<label :class="socialActionsClassList">{{ trans(ACTIVITY_SOCIAL_ACTIONS) }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span v-else-if="socialIssuesSelected.length === 0" class="inline-choice chill-no-data-statement mt-3">
|
<span v-else-if="socialIssuesSelected.length === 0" class="inline-choice chill-no-data-statement mt-3">
|
||||||
{{ $t('activity.select_first_a_social_issue') }}
|
{{ trans(ACTIVITY_SELECT_FIRST_A_SOCIAL_ISSUE) }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<template v-else-if="socialActionsList.length > 0">
|
<template v-else-if="socialActionsList.length > 0">
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<span v-else-if="actionAreLoaded && socialActionsList.length === 0" class="inline-choice chill-no-data-statement mt-3">
|
<span v-else-if="actionAreLoaded && socialActionsList.length === 0" class="inline-choice chill-no-data-statement mt-3">
|
||||||
{{ $t('activity.social_action_list_empty') }}
|
{{ trans(ACTIVITY_SOCIAL_ACTION_LIST_EMPTY) }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|
||||||
@@ -81,6 +81,11 @@ import VueMultiselect from 'vue-multiselect';
|
|||||||
import CheckSocialIssue from './SocialIssuesAcc/CheckSocialIssue.vue';
|
import CheckSocialIssue from './SocialIssuesAcc/CheckSocialIssue.vue';
|
||||||
import CheckSocialAction from './SocialIssuesAcc/CheckSocialAction.vue';
|
import CheckSocialAction from './SocialIssuesAcc/CheckSocialAction.vue';
|
||||||
import { getSocialIssues, getSocialActionByIssue } from '../api.js';
|
import { getSocialIssues, getSocialActionByIssue } from '../api.js';
|
||||||
|
import {
|
||||||
|
ACTIVITY_SOCIAL_ACTION_LIST_EMPTY,
|
||||||
|
ACTIVITY_SELECT_FIRST_A_SOCIAL_ISSUE, ACTIVITY_SOCIAL_ACTIONS,
|
||||||
|
ACTIVITY_SOCIAL_ISSUES, ACTIVITY_CHOOSE_OTHER_SOCIAL_ISSUE, trans
|
||||||
|
} from "../../../../../../../../../../../assets/translator";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "SocialIssuesAcc",
|
name: "SocialIssuesAcc",
|
||||||
@@ -89,6 +94,16 @@ export default {
|
|||||||
CheckSocialAction,
|
CheckSocialAction,
|
||||||
VueMultiselect
|
VueMultiselect
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
trans,
|
||||||
|
ACTIVITY_SOCIAL_ACTION_LIST_EMPTY,
|
||||||
|
ACTIVITY_SELECT_FIRST_A_SOCIAL_ISSUE,
|
||||||
|
ACTIVITY_SOCIAL_ACTIONS,
|
||||||
|
ACTIVITY_SOCIAL_ISSUES,
|
||||||
|
ACTIVITY_CHOOSE_OTHER_SOCIAL_ISSUE
|
||||||
|
};
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
issueIsLoading: false,
|
issueIsLoading: false,
|
||||||
|
@@ -1,54 +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\ActivityBundle\Security\Authorization;
|
|
||||||
|
|
||||||
use Chill\ActivityBundle\Entity\Activity;
|
|
||||||
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 Symfony\Component\Security\Core\Security;
|
|
||||||
|
|
||||||
class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly ActivityRepository $repository,
|
|
||||||
Security $security,
|
|
||||||
WorkflowStoredObjectPermissionHelper $workflowDocumentService
|
|
||||||
) {
|
|
||||||
parent::__construct($security, $workflowDocumentService);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
|
||||||
{
|
|
||||||
return $this->repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getClass(): string
|
|
||||||
{
|
|
||||||
return Activity::class;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
|
|
||||||
{
|
|
||||||
return match ($attribute) {
|
|
||||||
StoredObjectRoleEnum::EDIT => ActivityVoter::UPDATE,
|
|
||||||
StoredObjectRoleEnum::SEE => ActivityVoter::SEE_DETAILS,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function canBeAssociatedWithWorkflow(): bool
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -101,6 +101,31 @@ activity:
|
|||||||
Insert a document: Insérer un document
|
Insert a document: Insérer un document
|
||||||
Remove a document: Supprimer le document
|
Remove a document: Supprimer le document
|
||||||
comment: Commentaire
|
comment: Commentaire
|
||||||
|
errors: Le formulaire contient des erreurs
|
||||||
|
social_issues: Problématiques sociales
|
||||||
|
choose_other_social_issue: Ajouter une autre problématique sociale...
|
||||||
|
social_actions: Actions d'accompagnement
|
||||||
|
select_first_a_social_issue: Sélectionnez d'abord une problématique sociale
|
||||||
|
social_action_list_empty: Aucune action sociale disponible
|
||||||
|
add_persons: Ajouter des personnes concernées
|
||||||
|
bloc_persons: Usagers
|
||||||
|
bloc_persons_associated: Usagers du parcours
|
||||||
|
bloc_persons_not_associated: Tiers non-pro.
|
||||||
|
bloc_thirdparty: Tiers professionnels
|
||||||
|
bloc_users: T(M)S
|
||||||
|
location: Localisation
|
||||||
|
choose_location: Choisissez une localisation
|
||||||
|
choose_location_type: Choisissez un type de localisation
|
||||||
|
create_new_location: Créer une nouvelle localisation
|
||||||
|
location_fields:
|
||||||
|
name: Nom
|
||||||
|
type: Type
|
||||||
|
phonenumber1: Téléphone
|
||||||
|
phonenumber2: Autre téléphone
|
||||||
|
email: Adresse courriel
|
||||||
|
create_address: Créer une adresse
|
||||||
|
edit_address: Modifier l'adresse
|
||||||
|
|
||||||
No documents: Aucun document
|
No documents: Aucun document
|
||||||
|
|
||||||
# activity filter in list page
|
# activity filter in list page
|
||||||
|
@@ -22,9 +22,9 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
|||||||
class AsideActivityCategory
|
class AsideActivityCategory
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, AsideActivityCategory>
|
* @var Collection<AsideActivityCategory>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: AsideActivityCategory::class)]
|
#[ORM\OneToMany(targetEntity: AsideActivityCategory::class, mappedBy: 'parent')]
|
||||||
private Collection $children;
|
private Collection $children;
|
||||||
|
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
|
@@ -103,7 +103,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
|
|||||||
private int $dateTimeVersion = 0;
|
private int $dateTimeVersion = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, \Chill\CalendarBundle\Entity\CalendarDoc>
|
* @var Collection<CalendarDoc>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(mappedBy: 'calendar', targetEntity: CalendarDoc::class, orphanRemoval: true)]
|
#[ORM\OneToMany(mappedBy: 'calendar', targetEntity: CalendarDoc::class, orphanRemoval: true)]
|
||||||
private Collection $documents;
|
private Collection $documents;
|
||||||
@@ -120,7 +120,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
|
|||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var \Doctrine\Common\Collections\Collection<int, \Chill\CalendarBundle\Entity\Invite>&Selectable
|
* @var Collection&Selectable<int, Invite>
|
||||||
*/
|
*/
|
||||||
#[Serializer\Groups(['read', 'docgen:read'])]
|
#[Serializer\Groups(['read', 'docgen:read'])]
|
||||||
#[ORM\OneToMany(mappedBy: 'calendar', targetEntity: Invite::class, cascade: ['persist', 'remove', 'merge', 'detach'], orphanRemoval: true)]
|
#[ORM\OneToMany(mappedBy: 'calendar', targetEntity: Invite::class, cascade: ['persist', 'remove', 'merge', 'detach'], orphanRemoval: true)]
|
||||||
@@ -143,7 +143,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
|
|||||||
private ?Person $person = null;
|
private ?Person $person = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, Person>
|
* @var Collection<Person>
|
||||||
*/
|
*/
|
||||||
#[Serializer\Groups(['calendar:read', 'read', 'calendar:light', 'docgen:read'])]
|
#[Serializer\Groups(['calendar:read', 'read', 'calendar:light', 'docgen:read'])]
|
||||||
#[Assert\Count(min: 1, minMessage: 'calendar.At least {{ limit }} person is required.')]
|
#[Assert\Count(min: 1, minMessage: 'calendar.At least {{ limit }} person is required.')]
|
||||||
@@ -157,7 +157,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
|
|||||||
private PrivateCommentEmbeddable $privateComment;
|
private PrivateCommentEmbeddable $privateComment;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, ThirdParty>
|
* @var Collection<ThirdParty>
|
||||||
*/
|
*/
|
||||||
#[Serializer\Groups(['calendar:read', 'read', 'calendar:light', 'docgen:read'])]
|
#[Serializer\Groups(['calendar:read', 'read', 'calendar:light', 'docgen:read'])]
|
||||||
#[ORM\ManyToMany(targetEntity: ThirdParty::class)]
|
#[ORM\ManyToMany(targetEntity: ThirdParty::class)]
|
||||||
|
@@ -47,7 +47,7 @@ final class CalendarContextTest extends TestCase
|
|||||||
{
|
{
|
||||||
$expected =
|
$expected =
|
||||||
[
|
[
|
||||||
'trackDatetime' => true,
|
'track_datetime' => true,
|
||||||
'askMainPerson' => true,
|
'askMainPerson' => true,
|
||||||
'mainPersonLabel' => 'docgen.calendar.Destinee',
|
'mainPersonLabel' => 'docgen.calendar.Destinee',
|
||||||
'askThirdParty' => false,
|
'askThirdParty' => false,
|
||||||
@@ -61,7 +61,7 @@ final class CalendarContextTest extends TestCase
|
|||||||
{
|
{
|
||||||
$expected =
|
$expected =
|
||||||
[
|
[
|
||||||
'trackDatetime' => true,
|
'track_datetime' => true,
|
||||||
'askMainPerson' => true,
|
'askMainPerson' => true,
|
||||||
'mainPersonLabel' => 'docgen.calendar.Destinee',
|
'mainPersonLabel' => 'docgen.calendar.Destinee',
|
||||||
'askThirdParty' => false,
|
'askThirdParty' => false,
|
||||||
|
@@ -23,9 +23,9 @@ class Option
|
|||||||
private bool $active = true;
|
private bool $active = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, Option>
|
* @var Collection<Option>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: Option::class)]
|
#[ORM\OneToMany(targetEntity: Option::class, mappedBy: 'parent')]
|
||||||
private Collection $children;
|
private Collection $children;
|
||||||
|
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
|
@@ -32,9 +32,9 @@ class CustomFieldsGroup
|
|||||||
* The custom fields of the group.
|
* The custom fields of the group.
|
||||||
* The custom fields are asc-ordered regarding to their property "ordering".
|
* The custom fields are asc-ordered regarding to their property "ordering".
|
||||||
*
|
*
|
||||||
* @var Collection<int, CustomField>
|
* @var Collection<CustomField>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(mappedBy: 'customFieldGroup', targetEntity: CustomField::class)]
|
#[ORM\OneToMany(targetEntity: CustomField::class, mappedBy: 'customFieldGroup')]
|
||||||
#[ORM\OrderBy(['ordering' => \Doctrine\Common\Collections\Criteria::ASC])]
|
#[ORM\OrderBy(['ordering' => \Doctrine\Common\Collections\Criteria::ASC])]
|
||||||
private Collection $customFields;
|
private Collection $customFields;
|
||||||
|
|
||||||
|
@@ -89,7 +89,6 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf
|
|||||||
$g = new SignedUrlPost(
|
$g = new SignedUrlPost(
|
||||||
$url = $this->generateUrl($object_name),
|
$url = $this->generateUrl($object_name),
|
||||||
$expires,
|
$expires,
|
||||||
$object_name,
|
|
||||||
$this->max_post_file_size,
|
$this->max_post_file_size,
|
||||||
$max_file_count,
|
$max_file_count,
|
||||||
$submit_delay,
|
$submit_delay,
|
||||||
@@ -128,7 +127,7 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf
|
|||||||
];
|
];
|
||||||
$url = $url.'?'.\http_build_query($args);
|
$url = $url.'?'.\http_build_query($args);
|
||||||
|
|
||||||
$signature = new SignedUrl(strtoupper($method), $url, $expires, $object_name);
|
$signature = new SignedUrl(strtoupper($method), $url, $expires);
|
||||||
|
|
||||||
$this->event_dispatcher->dispatch(
|
$this->event_dispatcher->dispatch(
|
||||||
new TempUrlGenerateEvent($signature)
|
new TempUrlGenerateEvent($signature)
|
||||||
|
@@ -21,8 +21,6 @@ readonly class SignedUrl
|
|||||||
#[Serializer\Groups(['read'])]
|
#[Serializer\Groups(['read'])]
|
||||||
public string $url,
|
public string $url,
|
||||||
public \DateTimeImmutable $expires,
|
public \DateTimeImmutable $expires,
|
||||||
#[Serializer\Groups(['read'])]
|
|
||||||
public string $object_name,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Serializer\Groups(['read'])]
|
#[Serializer\Groups(['read'])]
|
||||||
|
@@ -18,7 +18,6 @@ readonly class SignedUrlPost extends SignedUrl
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
string $url,
|
string $url,
|
||||||
\DateTimeImmutable $expires,
|
\DateTimeImmutable $expires,
|
||||||
string $object_name,
|
|
||||||
#[Serializer\Groups(['read'])]
|
#[Serializer\Groups(['read'])]
|
||||||
public int $max_file_size,
|
public int $max_file_size,
|
||||||
#[Serializer\Groups(['read'])]
|
#[Serializer\Groups(['read'])]
|
||||||
@@ -32,6 +31,6 @@ readonly class SignedUrlPost extends SignedUrl
|
|||||||
#[Serializer\Groups(['read'])]
|
#[Serializer\Groups(['read'])]
|
||||||
public string $signature,
|
public string $signature,
|
||||||
) {
|
) {
|
||||||
parent::__construct('POST', $url, $expires, $object_name);
|
parent::__construct('POST', $url, $expires);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -26,8 +26,6 @@ use Symfony\Component\HttpFoundation\Request;
|
|||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneParser;
|
|
||||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class DocumentPersonController.
|
* Class DocumentPersonController.
|
||||||
@@ -42,8 +40,6 @@ class DocumentPersonController extends AbstractController
|
|||||||
protected TranslatorInterface $translator,
|
protected TranslatorInterface $translator,
|
||||||
protected EventDispatcherInterface $eventDispatcher,
|
protected EventDispatcherInterface $eventDispatcher,
|
||||||
protected AuthorizationHelper $authorizationHelper,
|
protected AuthorizationHelper $authorizationHelper,
|
||||||
protected PDFSignatureZoneParser $PDFSignatureZoneParser,
|
|
||||||
protected StoredObjectManagerInterface $storedObjectManagerInterface,
|
|
||||||
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry
|
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -201,36 +197,4 @@ class DocumentPersonController extends AbstractController
|
|||||||
['document' => $document, 'person' => $person]
|
['document' => $document, 'person' => $person]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route(path: '/{id}/signature', name: 'person_document_signature', methods: 'GET')]
|
|
||||||
public function signature(Person $person, PersonDocument $document): Response
|
|
||||||
{
|
|
||||||
$this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person);
|
|
||||||
$this->denyAccessUnlessGranted('CHILL_PERSON_DOCUMENT_SEE', $document);
|
|
||||||
|
|
||||||
$event = new PrivacyEvent($person, [
|
|
||||||
'element_class' => PersonDocument::class,
|
|
||||||
'element_id' => $document->getId(),
|
|
||||||
'action' => 'show',
|
|
||||||
]);
|
|
||||||
$this->eventDispatcher->dispatch($event, PrivacyEvent::PERSON_PRIVACY_EVENT);
|
|
||||||
|
|
||||||
$storedObject = $document->getObject();
|
|
||||||
$content = $this->storedObjectManagerInterface->read($storedObject);
|
|
||||||
$zones = $this->PDFSignatureZoneParser->findSignatureZones($content);
|
|
||||||
|
|
||||||
$signature = [];
|
|
||||||
$signature['id'] = 1;
|
|
||||||
$signature['storedObject'] = [ // TEMP
|
|
||||||
'filename' => $storedObject->getFilename(),
|
|
||||||
'iv' => $storedObject->getIv(),
|
|
||||||
'keyInfos' => $storedObject->getKeyInfos(),
|
|
||||||
];
|
|
||||||
$signature['zones'] = $zones;
|
|
||||||
|
|
||||||
return $this->render(
|
|
||||||
'@ChillDocStore/PersonDocument/signature.html.twig',
|
|
||||||
['document' => $document, 'person' => $person, 'signature' => $signature]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,67 +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\Controller;
|
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessage;
|
|
||||||
use Chill\DocStoreBundle\Service\Signature\PDFPage;
|
|
||||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
|
|
||||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
|
||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
|
||||||
|
|
||||||
class SignatureRequestController
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly MessageBusInterface $messageBus,
|
|
||||||
private readonly StoredObjectManagerInterface $storedObjectManager,
|
|
||||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
#[Route('/api/1.0/document/workflow/{id}/signature-request', name: 'chill_docstore_signature_request')]
|
|
||||||
public function processSignature(EntityWorkflowStepSignature $signature, Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
|
|
||||||
$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
|
|
||||||
$zone = new PDFSignatureZone(
|
|
||||||
$data['zone']['index'],
|
|
||||||
$data['zone']['x'],
|
|
||||||
$data['zone']['y'],
|
|
||||||
$data['zone']['height'],
|
|
||||||
$data['zone']['width'],
|
|
||||||
new PDFPage($data['zone']['PDFPage']['index'], $data['zone']['PDFPage']['width'], $data['zone']['PDFPage']['height'])
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->messageBus->dispatch(new RequestPdfSignMessage(
|
|
||||||
$signature->getId(),
|
|
||||||
$zone,
|
|
||||||
$data['zone']['index'],
|
|
||||||
'test signature', // reason (string)
|
|
||||||
'Mme Caroline Diallo', // signerText (string)
|
|
||||||
$content
|
|
||||||
));
|
|
||||||
|
|
||||||
return new JsonResponse(null, JsonResponse::HTTP_OK, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/api/1.0/document/workflow/{id}/check-signature', name: 'chill_docstore_check_signature')]
|
|
||||||
public function checkSignature(EntityWorkflowStepSignature $signature): JsonResponse
|
|
||||||
{
|
|
||||||
return new JsonResponse($signature->getState(), JsonResponse::HTTP_OK, []);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -14,7 +14,6 @@ namespace Chill\DocStoreBundle\DependencyInjection;
|
|||||||
use Chill\DocStoreBundle\Controller\StoredObjectApiController;
|
use Chill\DocStoreBundle\Controller\StoredObjectApiController;
|
||||||
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
|
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
|
||||||
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
|
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
|
||||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
|
|
||||||
use Symfony\Component\Config\FileLocator;
|
use Symfony\Component\Config\FileLocator;
|
||||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||||
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
|
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
|
||||||
@@ -36,8 +35,6 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
|
|||||||
|
|
||||||
$container->setParameter('chill_doc_store', $config);
|
$container->setParameter('chill_doc_store', $config);
|
||||||
|
|
||||||
$container->registerForAutoconfiguration(StoredObjectVoterInterface::class)->addTag('stored_object_voter');
|
|
||||||
|
|
||||||
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
|
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
|
||||||
$loader->load('services.yaml');
|
$loader->load('services.yaml');
|
||||||
$loader->load('services/controller.yaml');
|
$loader->load('services/controller.yaml');
|
||||||
@@ -45,7 +42,6 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
|
|||||||
$loader->load('services/fixtures.yaml');
|
$loader->load('services/fixtures.yaml');
|
||||||
$loader->load('services/form.yaml');
|
$loader->load('services/form.yaml');
|
||||||
$loader->load('services/templating.yaml');
|
$loader->load('services/templating.yaml');
|
||||||
$loader->load('services/security.yaml');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function prepend(ContainerBuilder $container)
|
public function prepend(ContainerBuilder $container)
|
||||||
|
@@ -12,14 +12,13 @@ declare(strict_types=1);
|
|||||||
namespace Chill\DocStoreBundle\Repository;
|
namespace Chill\DocStoreBundle\Repository;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Doctrine\Persistence\ObjectRepository;
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
|
||||||
class AccompanyingCourseDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface
|
class AccompanyingCourseDocumentRepository implements ObjectRepository
|
||||||
{
|
{
|
||||||
private readonly EntityRepository $repository;
|
private readonly EntityRepository $repository;
|
||||||
|
|
||||||
@@ -46,16 +45,6 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository, Associat
|
|||||||
return $qb->getQuery()->getSingleScalarResult();
|
return $qb->getQuery()->getSingleScalarResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object
|
|
||||||
{
|
|
||||||
$qb = $this->repository->createQueryBuilder('d');
|
|
||||||
$query = $qb->where('d.object = :storedObject')
|
|
||||||
->setParameter('storedObject', $storedObject)
|
|
||||||
->getQuery();
|
|
||||||
|
|
||||||
return $query->getOneOrNullResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function find($id): ?AccompanyingCourseDocument
|
public function find($id): ?AccompanyingCourseDocument
|
||||||
{
|
{
|
||||||
return $this->repository->find($id);
|
return $this->repository->find($id);
|
||||||
@@ -66,7 +55,7 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository, Associat
|
|||||||
return $this->repository->findAll();
|
return $this->repository->findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
|
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null)
|
||||||
{
|
{
|
||||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||||
}
|
}
|
||||||
@@ -76,7 +65,7 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository, Associat
|
|||||||
return $this->findOneBy($criteria);
|
return $this->findOneBy($criteria);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getClassName(): string
|
public function getClassName()
|
||||||
{
|
{
|
||||||
return AccompanyingCourseDocument::class;
|
return AccompanyingCourseDocument::class;
|
||||||
}
|
}
|
||||||
|
@@ -1,19 +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\Repository;
|
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
|
||||||
|
|
||||||
interface AssociatedEntityToStoredObjectInterface
|
|
||||||
{
|
|
||||||
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object;
|
|
||||||
}
|
|
@@ -12,7 +12,6 @@ declare(strict_types=1);
|
|||||||
namespace Chill\DocStoreBundle\Repository;
|
namespace Chill\DocStoreBundle\Repository;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\PersonDocument;
|
use Chill\DocStoreBundle\Entity\PersonDocument;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
use Doctrine\Persistence\ObjectRepository;
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
@@ -20,7 +19,7 @@ use Doctrine\Persistence\ObjectRepository;
|
|||||||
/**
|
/**
|
||||||
* @template ObjectRepository<PersonDocument::class>
|
* @template ObjectRepository<PersonDocument::class>
|
||||||
*/
|
*/
|
||||||
readonly class PersonDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface
|
readonly class PersonDocumentRepository implements ObjectRepository
|
||||||
{
|
{
|
||||||
private EntityRepository $repository;
|
private EntityRepository $repository;
|
||||||
|
|
||||||
@@ -54,14 +53,4 @@ readonly class PersonDocumentRepository implements ObjectRepository, AssociatedE
|
|||||||
{
|
{
|
||||||
return PersonDocument::class;
|
return PersonDocument::class;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object
|
|
||||||
{
|
|
||||||
$qb = $this->repository->createQueryBuilder('d');
|
|
||||||
$query = $qb->where('d.object = :storedObject')
|
|
||||||
->setParameter('storedObject', $storedObject)
|
|
||||||
->getQuery();
|
|
||||||
|
|
||||||
return $query->getOneOrNullResult();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -62,26 +62,3 @@ export interface PostStoreObjectSignature {
|
|||||||
signature: string,
|
signature: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PDFPage {
|
|
||||||
index: number,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
}
|
|
||||||
export interface SignatureZone {
|
|
||||||
index: number | null,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
PDFPage: PDFPage,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Signature {
|
|
||||||
id: number,
|
|
||||||
storedObject: StoredObject,
|
|
||||||
zones: SignatureZone[],
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SignedState = 'pending' | 'signed' | 'rejected' | 'canceled' | 'error';
|
|
||||||
|
|
||||||
export type CanvasEvent = 'select' | 'add';
|
|
@@ -1,559 +0,0 @@
|
|||||||
<template>
|
|
||||||
<teleport to="body">
|
|
||||||
<modal v-if="modalOpen" @close="modalOpen = false">
|
|
||||||
<template v-slot:header>
|
|
||||||
<h2>{{ $t("signature_confirmation") }}</h2>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body>
|
|
||||||
<div class="signature-modal-body text-center" v-if="loading">
|
|
||||||
<p>{{ $t("electronic_signature_in_progress") }}</p>
|
|
||||||
<div class="loading">
|
|
||||||
<i
|
|
||||||
class="fa fa-circle-o-notch fa-spin fa-3x"
|
|
||||||
:title="$t('loading')"
|
|
||||||
></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="signature-modal-body text-center" v-else>
|
|
||||||
<p>{{ $t("you_are_going_to_sign") }}</p>
|
|
||||||
<p>{{ $t("are_you_sure") }}</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-slot:footer>
|
|
||||||
<button class="btn btn-action" @click.prevent="confirmSign">
|
|
||||||
{{ $t("yes") }}
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</modal>
|
|
||||||
</teleport>
|
|
||||||
<div class="col-12">
|
|
||||||
<div
|
|
||||||
class="row justify-content-center mb-2"
|
|
||||||
v-if="signature.zones.length > 1"
|
|
||||||
>
|
|
||||||
<div class="col-4 gap-2 d-grid">
|
|
||||||
<button
|
|
||||||
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
|
|
||||||
class="btn btn-light btn-sm"
|
|
||||||
@click="turnSignature(-1)"
|
|
||||||
>
|
|
||||||
{{ $t("last_sign_zone") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="col-4 gap-2 d-grid">
|
|
||||||
<button
|
|
||||||
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
|
|
||||||
class="btn btn-light btn-sm"
|
|
||||||
@click="turnSignature(1)"
|
|
||||||
>
|
|
||||||
{{ $t("next_sign_zone") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="turn-page"
|
|
||||||
class="row justify-content-center mb-2"
|
|
||||||
v-if="pageCount > 1"
|
|
||||||
>
|
|
||||||
<div class="col-6-sm col-3-md text-center">
|
|
||||||
<button
|
|
||||||
class="btn btn-light btn-sm"
|
|
||||||
:disabled="page <= 1"
|
|
||||||
@click="turnPage(-1)"
|
|
||||||
>
|
|
||||||
❮
|
|
||||||
</button>
|
|
||||||
<span>page {{ page }} / {{ pageCount }}</span>
|
|
||||||
<button
|
|
||||||
class="btn btn-light btn-sm"
|
|
||||||
:disabled="page >= pageCount"
|
|
||||||
@click="turnPage(1)"
|
|
||||||
>
|
|
||||||
❯
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="multiPage" class="col-12 text-center">
|
|
||||||
<canvas
|
|
||||||
v-for="p in pageCount"
|
|
||||||
:key="p"
|
|
||||||
class="m-auto"
|
|
||||||
:id="`canvas-${p}`"
|
|
||||||
></canvas>
|
|
||||||
</div>
|
|
||||||
<div v-else class="col-12 text-center">
|
|
||||||
<canvas class="m-auto" :id="canvas"></canvas>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 p-4" id="action-buttons" v-if="signedState !== 'signed'">
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-12 d-flex justify-content-end">
|
|
||||||
<div class="col-4 col-xl-3 gap-2 d-grid">
|
|
||||||
<button
|
|
||||||
v-if="adding"
|
|
||||||
class="btn btn-misc btn-cancel me-2 btn-sm"
|
|
||||||
@click="removeNewZone()"
|
|
||||||
>
|
|
||||||
{{ $t("remove_sign_zone") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="col-4 gap-2 d-grid">
|
|
||||||
<button
|
|
||||||
class="btn btn-create btn-sm"
|
|
||||||
:class="{ active: canvasEvent === 'add' }"
|
|
||||||
@click="toggleAddZone()"
|
|
||||||
>
|
|
||||||
{{ $t("add_sign_zone") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-4">
|
|
||||||
<button
|
|
||||||
class="btn btn-action me-2"
|
|
||||||
:disabled="!userSignatureZone"
|
|
||||||
@click="sign"
|
|
||||||
>
|
|
||||||
{{ $t("sign") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="col-8 d-flex justify-content-end">
|
|
||||||
<button
|
|
||||||
class="btn btn-misc me-2"
|
|
||||||
:hidden="!userSignatureZone"
|
|
||||||
@click="undoSign"
|
|
||||||
v-if="signature.zones.length > 1"
|
|
||||||
>
|
|
||||||
{{ $t("choose_another_signature") }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-misc me-2"
|
|
||||||
:hidden="!userSignatureZone"
|
|
||||||
@click="undoSign"
|
|
||||||
v-else
|
|
||||||
>
|
|
||||||
{{ $t("cancel") }}
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-delete" @click="undoSign">
|
|
||||||
{{ $t("cancel_signing") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, Ref, reactive } from "vue";
|
|
||||||
import { useToast } from "vue-toast-notification";
|
|
||||||
import "vue-toast-notification/dist/theme-sugar.css";
|
|
||||||
import {
|
|
||||||
CanvasEvent,
|
|
||||||
Signature,
|
|
||||||
SignatureZone,
|
|
||||||
SignedState,
|
|
||||||
} from "../../types";
|
|
||||||
import { makeFetch } from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
|
||||||
import * as pdfjsLib from "pdfjs-dist";
|
|
||||||
import {
|
|
||||||
PDFDocumentProxy,
|
|
||||||
PDFPageProxy,
|
|
||||||
} from "pdfjs-dist/types/src/display/api";
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import * as PdfWorker from "pdfjs-dist/build/pdf.worker.mjs";
|
|
||||||
console.log(PdfWorker); // incredible but this is needed
|
|
||||||
|
|
||||||
// import { PdfWorker } from 'pdfjs-dist/build/pdf.worker.mjs'
|
|
||||||
// pdfjsLib.GlobalWorkerOptions.workerSrc = PdfWorker;
|
|
||||||
|
|
||||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
|
||||||
import {
|
|
||||||
build_download_info_link,
|
|
||||||
download_and_decrypt_doc,
|
|
||||||
} from "../StoredObjectButton/helpers";
|
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = "pdfjs-dist/build/pdf.worker.mjs";
|
|
||||||
|
|
||||||
const multiPage: Ref<boolean> = ref(true);
|
|
||||||
const modalOpen: Ref<boolean> = ref(false);
|
|
||||||
const loading: Ref<boolean> = ref(false);
|
|
||||||
const adding: Ref<boolean> = ref(false);
|
|
||||||
const canvasEvent: Ref<CanvasEvent> = ref("select");
|
|
||||||
const signedState: Ref<SignedState> = ref("pending");
|
|
||||||
const page: Ref<number> = ref(1);
|
|
||||||
const pageCount: Ref<number> = ref(0);
|
|
||||||
let userSignatureZone: Ref<null | SignatureZone> = ref(null);
|
|
||||||
let pdf = {} as PDFDocumentProxy;
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
signature: Signature;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const $toast = useToast();
|
|
||||||
|
|
||||||
const signature = window.signature;
|
|
||||||
const urlInfo = build_download_info_link(signature.storedObject.filename);
|
|
||||||
|
|
||||||
console.log(signature);
|
|
||||||
|
|
||||||
const mountPdf = async (url: string) => {
|
|
||||||
const loadingTask = pdfjsLib.getDocument(url);
|
|
||||||
pdf = await loadingTask.promise;
|
|
||||||
pageCount.value = pdf.numPages;
|
|
||||||
if (multiPage.value) {
|
|
||||||
await setAllPages();
|
|
||||||
} else {
|
|
||||||
await setPage(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRenderContext = (pdfPage: PDFPageProxy) => {
|
|
||||||
const scale = 1;
|
|
||||||
const viewport = pdfPage.getViewport({ scale });
|
|
||||||
let canvas;
|
|
||||||
if (multiPage.value) {
|
|
||||||
canvas = document.getElementById(
|
|
||||||
`canvas-${pdfPage.pageNumber}`
|
|
||||||
) as HTMLCanvasElement;
|
|
||||||
} else {
|
|
||||||
canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
|
||||||
}
|
|
||||||
const context = canvas.getContext("2d") as CanvasRenderingContext2D;
|
|
||||||
canvas.height = viewport.height;
|
|
||||||
canvas.width = viewport.width;
|
|
||||||
|
|
||||||
return {
|
|
||||||
canvasContext: context,
|
|
||||||
viewport: viewport,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const setAllPages = async () =>
|
|
||||||
Array.from(Array(pageCount.value).keys()).map((p) => setPage(p + 1));
|
|
||||||
|
|
||||||
const setPage = async (page: number) => {
|
|
||||||
const pdfPage = await pdf.getPage(page);
|
|
||||||
const renderContext = getRenderContext(pdfPage);
|
|
||||||
await pdfPage.render(renderContext);
|
|
||||||
};
|
|
||||||
|
|
||||||
async function downloadAndOpen(): Promise<Blob> {
|
|
||||||
let raw;
|
|
||||||
try {
|
|
||||||
raw = await download_and_decrypt_doc(
|
|
||||||
urlInfo,
|
|
||||||
signature.storedObject.keyInfos,
|
|
||||||
new Uint8Array(signature.storedObject.iv)
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("error while downloading and decrypting document", e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
await mountPdf(URL.createObjectURL(raw));
|
|
||||||
initPdf();
|
|
||||||
return raw;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initPdf = () => {
|
|
||||||
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
|
||||||
canvas.addEventListener("pointerup", canvasClick, false);
|
|
||||||
setTimeout(() => addZones(page.value), 800);
|
|
||||||
};
|
|
||||||
|
|
||||||
const scaleXToCanvas = (x: number, canvasWidth: number, PDFWidth: number) =>
|
|
||||||
Math.round((x * canvasWidth) / PDFWidth);
|
|
||||||
|
|
||||||
const scaleYToCanvas = (h: number, canvasHeight: number, PDFHeight: number) =>
|
|
||||||
Math.round((h * canvasHeight) / PDFHeight);
|
|
||||||
|
|
||||||
const hitSignature = (
|
|
||||||
zone: SignatureZone,
|
|
||||||
xy: number[],
|
|
||||||
canvasWidth: number,
|
|
||||||
canvasHeight: number
|
|
||||||
) =>
|
|
||||||
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) < xy[0] &&
|
|
||||||
xy[0] <
|
|
||||||
scaleXToCanvas(zone.x + zone.width, canvasWidth, zone.PDFPage.width) &&
|
|
||||||
zone.PDFPage.height -
|
|
||||||
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) <
|
|
||||||
xy[1] &&
|
|
||||||
xy[1] <
|
|
||||||
scaleYToCanvas(zone.height - zone.y, canvasHeight, zone.PDFPage.height) +
|
|
||||||
zone.PDFPage.height;
|
|
||||||
|
|
||||||
const selectZone = (z: SignatureZone, canvas: HTMLCanvasElement) => {
|
|
||||||
userSignatureZone.value = z;
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
if (ctx) {
|
|
||||||
setPage(page.value);
|
|
||||||
setTimeout(() => drawZone(z, ctx, canvas.width, canvas.height), 200);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectZoneOnCanvas = (e: PointerEvent, canvas: HTMLCanvasElement) =>
|
|
||||||
signature.zones
|
|
||||||
.filter((z) => z.PDFPage.index + 1 === page.value)
|
|
||||||
.map((z) => {
|
|
||||||
if (
|
|
||||||
hitSignature(z, [e.offsetX, e.offsetY], canvas.width, canvas.height)
|
|
||||||
) {
|
|
||||||
if (userSignatureZone.value === null) {
|
|
||||||
selectZone(z, canvas);
|
|
||||||
} else {
|
|
||||||
if (userSignatureZone.value.index === z.index) {
|
|
||||||
sign();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const canvasClick = (e: PointerEvent) => {
|
|
||||||
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
|
|
||||||
canvasEvent.value === "select"
|
|
||||||
? selectZoneOnCanvas(e, canvas)
|
|
||||||
: addZoneOnCanvas(e, canvas);
|
|
||||||
};
|
|
||||||
|
|
||||||
const turnPage = async (upOrDown: number) => {
|
|
||||||
userSignatureZone.value = null;
|
|
||||||
page.value = page.value + upOrDown;
|
|
||||||
await setPage(page.value);
|
|
||||||
setTimeout(() => addZones(page.value), 200);
|
|
||||||
};
|
|
||||||
|
|
||||||
const turnSignature = async (upOrDown: number) => {
|
|
||||||
let zoneIndex = userSignatureZone.value?.index ?? -1;
|
|
||||||
if (zoneIndex < -1) {
|
|
||||||
zoneIndex = -1;
|
|
||||||
}
|
|
||||||
if (zoneIndex < signature.zones.length) {
|
|
||||||
zoneIndex = zoneIndex + upOrDown;
|
|
||||||
} else {
|
|
||||||
zoneIndex = 0;
|
|
||||||
}
|
|
||||||
let currentZone = signature.zones[zoneIndex];
|
|
||||||
if (currentZone) {
|
|
||||||
page.value = currentZone.PDFPage.index + 1;
|
|
||||||
userSignatureZone.value = currentZone;
|
|
||||||
const canvas = document.querySelectorAll("canvas")[0];
|
|
||||||
selectZone(currentZone, canvas);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const drawZone = (
|
|
||||||
zone: SignatureZone,
|
|
||||||
ctx: CanvasRenderingContext2D,
|
|
||||||
canvasWidth: number,
|
|
||||||
canvasHeight: number
|
|
||||||
) => {
|
|
||||||
const unselectedBlue = "#007bff";
|
|
||||||
const selectedBlue = "#034286";
|
|
||||||
ctx.strokeStyle =
|
|
||||||
userSignatureZone.value?.index === zone.index
|
|
||||||
? selectedBlue
|
|
||||||
: unselectedBlue;
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.lineJoin = "bevel";
|
|
||||||
ctx.strokeRect(
|
|
||||||
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width),
|
|
||||||
zone.PDFPage.height -
|
|
||||||
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.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 -
|
|
||||||
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) +
|
|
||||||
scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height) / 2;
|
|
||||||
if (userSignatureZone.value?.index === zone.index) {
|
|
||||||
ctx.fillStyle = selectedBlue;
|
|
||||||
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.strokeStyle = "#c6c6c6"; // halo
|
|
||||||
// ctx.strokeText("Choisir cette", xText, yText - 12);
|
|
||||||
// ctx.strokeText("zone de signature", xText, yText + 12);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addZones = (page: number) => {
|
|
||||||
const canvas = document.querySelectorAll("canvas")[0];
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
if (ctx) {
|
|
||||||
signature.zones
|
|
||||||
.filter((z) => z.PDFPage.index + 1 === page)
|
|
||||||
.map((z) => drawZone(z, ctx, canvas.width, canvas.height));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkSignature = () => {
|
|
||||||
const url = `/api/1.0/document/workflow/${signature.id}/check-signature`;
|
|
||||||
return makeFetch("GET", url)
|
|
||||||
.then((r) => {
|
|
||||||
signedState.value = r as SignedState;
|
|
||||||
checkForReady();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
signedState.value = "error";
|
|
||||||
console.log("Error while checking the signature", error);
|
|
||||||
$toast.error(
|
|
||||||
`Erreur lors de la vérification de la signature: ${error.txt}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const maxTryForReady = 60; //2 minutes for trying to sign
|
|
||||||
let tryForReady = 0;
|
|
||||||
|
|
||||||
const stopTrySigning = () => {
|
|
||||||
loading.value = false;
|
|
||||||
modalOpen.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkForReady = () => {
|
|
||||||
if (tryForReady > maxTryForReady) {
|
|
||||||
stopTrySigning();
|
|
||||||
tryForReady = 0;
|
|
||||||
console.log("Reached the maximum number of tentative to try signing");
|
|
||||||
$toast.error(
|
|
||||||
"Le nombre maximum de tentatives pour essayer de signer est atteint"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (signedState.value === "rejected") {
|
|
||||||
stopTrySigning();
|
|
||||||
console.log("Signature rejected by the server");
|
|
||||||
$toast.error("Signature rejetée par le serveur");
|
|
||||||
}
|
|
||||||
if (signedState.value === "canceled") {
|
|
||||||
stopTrySigning();
|
|
||||||
console.log("Signature canceled");
|
|
||||||
$toast.error("Signature annulée");
|
|
||||||
}
|
|
||||||
if (signedState.value === "pending") {
|
|
||||||
tryForReady = tryForReady + 1;
|
|
||||||
setTimeout(() => checkSignature(), 2000);
|
|
||||||
} else {
|
|
||||||
stopTrySigning();
|
|
||||||
if (signedState.value === "signed") {
|
|
||||||
userSignatureZone.value = null;
|
|
||||||
downloadAndOpen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const sign = () => (modalOpen.value = true);
|
|
||||||
|
|
||||||
const confirmSign = () => {
|
|
||||||
loading.value = true;
|
|
||||||
const url = `/api/1.0/document/workflow/${signature.id}/signature-request`;
|
|
||||||
const body = {
|
|
||||||
storedObject: signature.storedObject,
|
|
||||||
zone: userSignatureZone.value,
|
|
||||||
};
|
|
||||||
makeFetch("POST", url, body)
|
|
||||||
.then((r) => {
|
|
||||||
checkForReady();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.log("Error while posting the signature", error);
|
|
||||||
stopTrySigning();
|
|
||||||
$toast.error(
|
|
||||||
`Erreur lors de la soumission de la signature: ${error.txt}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const undoSign = async () => {
|
|
||||||
signature.zones = signature.zones.filter((z) => z.index !== null);
|
|
||||||
await setPage(page.value);
|
|
||||||
setTimeout(() => addZones(page.value), 200);
|
|
||||||
userSignatureZone.value = null;
|
|
||||||
adding.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleAddZone = () => {
|
|
||||||
canvasEvent.value === "select"
|
|
||||||
? (canvasEvent.value = "add")
|
|
||||||
: (canvasEvent.value = "select");
|
|
||||||
};
|
|
||||||
|
|
||||||
const addZoneOnCanvas = (e: PointerEvent, canvas: HTMLCanvasElement) => {
|
|
||||||
const BOX_WIDTH = 180;
|
|
||||||
const BOX_HEIGHT = 90;
|
|
||||||
const PDFPageHeight = canvas.height;
|
|
||||||
const PDFPageWidth = canvas.width;
|
|
||||||
|
|
||||||
const x = e.offsetX;
|
|
||||||
const y = e.offsetY;
|
|
||||||
const newZone: SignatureZone = {
|
|
||||||
index: null,
|
|
||||||
x:
|
|
||||||
scaleXToCanvas(x, canvas.width, PDFPageWidth) -
|
|
||||||
scaleXToCanvas(BOX_WIDTH / 2, canvas.width, PDFPageWidth),
|
|
||||||
y:
|
|
||||||
PDFPageHeight -
|
|
||||||
scaleYToCanvas(y, canvas.height, PDFPageHeight) +
|
|
||||||
scaleYToCanvas(BOX_HEIGHT / 2, canvas.height, PDFPageHeight),
|
|
||||||
width: BOX_WIDTH,
|
|
||||||
height: BOX_HEIGHT,
|
|
||||||
PDFPage: {
|
|
||||||
index: page.value - 1,
|
|
||||||
width: PDFPageWidth,
|
|
||||||
height: PDFPageHeight,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
signature.zones.push(newZone);
|
|
||||||
|
|
||||||
setTimeout(() => addZones(page.value), 200);
|
|
||||||
canvasEvent.value = "select";
|
|
||||||
adding.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeNewZone = async () => {
|
|
||||||
signature.zones = signature.zones.filter((z) => z.index !== null);
|
|
||||||
userSignatureZone.value = null;
|
|
||||||
await setPage(page.value);
|
|
||||||
setTimeout(() => addZones(page.value), 200);
|
|
||||||
canvasEvent.value = "select";
|
|
||||||
adding.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
downloadAndOpen();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
canvas {
|
|
||||||
box-shadow: 0 20px 20px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
div#action-buttons {
|
|
||||||
position: sticky;
|
|
||||||
bottom: 0px;
|
|
||||||
background-color: white;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
div#turn-page {
|
|
||||||
span {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
margin: 0 0.4rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div.signature-modal-body {
|
|
||||||
height: 8rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@@ -1,32 +0,0 @@
|
|||||||
import { createApp } from "vue";
|
|
||||||
// @ts-ignore
|
|
||||||
import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
|
|
||||||
import App from "./App.vue";
|
|
||||||
|
|
||||||
const appMessages = {
|
|
||||||
fr: {
|
|
||||||
yes: 'Oui',
|
|
||||||
are_you_sure: 'Êtes-vous sûr·e?',
|
|
||||||
you_are_going_to_sign: 'Vous allez signer le document',
|
|
||||||
signature_confirmation: 'Confirmation de la signature',
|
|
||||||
sign: 'Signer',
|
|
||||||
choose_another_signature: 'Choisir une autre zone de signature',
|
|
||||||
cancel: 'Annuler',
|
|
||||||
cancel_signing: 'Refuser de signer',
|
|
||||||
last_sign_zone: 'Zone de signature précédente',
|
|
||||||
next_sign_zone: 'Zone de signature suivante',
|
|
||||||
electronic_signature_in_progress: 'Signature électronique en cours...',
|
|
||||||
loading: 'Chargement...',
|
|
||||||
add_sign_zone: 'Ajouter une zone de signature',
|
|
||||||
remove_sign_zone: 'Enlever la zone',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const i18n = _createI18n(appMessages);
|
|
||||||
|
|
||||||
const app = createApp({
|
|
||||||
template: `<app></app>`,
|
|
||||||
})
|
|
||||||
.use(i18n)
|
|
||||||
.component("app", App)
|
|
||||||
.mount("#document-signature");
|
|
@@ -71,7 +71,7 @@
|
|||||||
</li>
|
</li>
|
||||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
|
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
|
||||||
<li>
|
<li>
|
||||||
{{ document.object|chill_document_button_group(document.title) }}
|
{{ document.object|chill_document_button_group(document.title, is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document)) }}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a>
|
<a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a>
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
{% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %}
|
{% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %}
|
||||||
<li>
|
<li>
|
||||||
{{ document.object|chill_document_button_group(document.title) }}
|
{{ document.object|chill_document_button_group(document.title, is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document)) }}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ path('person_document_show', {'person': person.id, 'id': document.id}) }}" class="btn btn-show"></a>
|
<a href="{{ path('person_document_show', {'person': person.id, 'id': document.id}) }}" class="btn btn-show"></a>
|
||||||
|
@@ -1,38 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<link rel="shortcut icon" href="{{ asset('build/images/favicon.ico') }}" type="image/x-icon">
|
|
||||||
<title>Signature</title>
|
|
||||||
|
|
||||||
{{ encore_entry_link_tags('mod_bootstrap') }}
|
|
||||||
{{ encore_entry_link_tags('mod_forkawesome') }}
|
|
||||||
{{ encore_entry_link_tags('chill') }}
|
|
||||||
{{ encore_entry_link_tags('vue_document_signature') }}
|
|
||||||
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
{% block js %}
|
|
||||||
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
|
|
||||||
<script type="text/javascript">
|
|
||||||
window.signature = {{ signature|json_encode|raw }};
|
|
||||||
</script>
|
|
||||||
{{ encore_entry_script_tags('vue_document_signature') }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
<div class="content" id="content">
|
|
||||||
<div class="container-xxl">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-12 col-md-12 col-lg-9 my-5 m-auto">
|
|
||||||
<h4>{{ 'Document %title%' | trans({ '%title%': document.title }) }}</h4>
|
|
||||||
<div class="row" id="document-signature"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@@ -70,12 +70,12 @@ class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements Prov
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function supports($attribute, $subject): bool
|
protected function supports($attribute, $subject): bool
|
||||||
{
|
{
|
||||||
return $this->voterHelper->supports($attribute, $subject);
|
return $this->voterHelper->supports($attribute, $subject);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||||
{
|
{
|
||||||
if (!$token->getUser() instanceof User) {
|
if (!$token->getUser() instanceof User) {
|
||||||
return false;
|
return false;
|
||||||
|
@@ -12,7 +12,6 @@ declare(strict_types=1);
|
|||||||
namespace Chill\DocStoreBundle\Security\Authorization;
|
namespace Chill\DocStoreBundle\Security\Authorization;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||||
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
|
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
@@ -23,7 +22,6 @@ final class AsyncUploadVoter extends Voter
|
|||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Security $security,
|
private readonly Security $security,
|
||||||
private readonly StoredObjectRepository $storedObjectRepository
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
protected function supports($attribute, $subject): bool
|
protected function supports($attribute, $subject): bool
|
||||||
@@ -34,16 +32,10 @@ final class AsyncUploadVoter extends Voter
|
|||||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||||
{
|
{
|
||||||
/** @var SignedUrl $subject */
|
/** @var SignedUrl $subject */
|
||||||
if (!in_array($subject->method, ['POST', 'GET', 'HEAD', 'PUT'], true)) {
|
if (!in_array($subject->method, ['POST', 'GET', 'HEAD'], true)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$storedObject = $this->storedObjectRepository->findOneBy(['filename' => $subject->object_name]);
|
return $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN');
|
||||||
|
|
||||||
return match ($subject->method) {
|
|
||||||
'GET', 'HEAD' => $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject),
|
|
||||||
'PUT' => $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject),
|
|
||||||
'POST' => $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN')
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -12,10 +12,9 @@ declare(strict_types=1);
|
|||||||
namespace Chill\DocStoreBundle\Security\Authorization;
|
namespace Chill\DocStoreBundle\Security\Authorization;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Psr\Log\LoggerInterface;
|
use Chill\DocStoreBundle\Security\Guard\DavTokenAuthenticationEventSubscriber;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Voter for the content of a stored object.
|
* Voter for the content of a stored object.
|
||||||
@@ -24,10 +23,6 @@ use Symfony\Component\Security\Core\Security;
|
|||||||
*/
|
*/
|
||||||
class StoredObjectVoter extends Voter
|
class StoredObjectVoter extends Voter
|
||||||
{
|
{
|
||||||
public const LOG_PREFIX = '[stored object voter] ';
|
|
||||||
|
|
||||||
public function __construct(private readonly Security $security, private readonly iterable $storedObjectVoters, private readonly LoggerInterface $logger) {}
|
|
||||||
|
|
||||||
protected function supports($attribute, $subject): bool
|
protected function supports($attribute, $subject): bool
|
||||||
{
|
{
|
||||||
return StoredObjectRoleEnum::tryFrom($attribute) instanceof StoredObjectRoleEnum
|
return StoredObjectRoleEnum::tryFrom($attribute) instanceof StoredObjectRoleEnum
|
||||||
@@ -37,28 +32,24 @@ class StoredObjectVoter extends Voter
|
|||||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||||
{
|
{
|
||||||
/** @var StoredObject $subject */
|
/** @var StoredObject $subject */
|
||||||
$attributeAsEnum = StoredObjectRoleEnum::from($attribute);
|
if (
|
||||||
|
!$token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)
|
||||||
// Loop through context-specific voters
|
|| $subject->getUuid()->toString() !== $token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)
|
||||||
foreach ($this->storedObjectVoters as $storedObjectVoter) {
|
) {
|
||||||
if ($storedObjectVoter->supports($attributeAsEnum, $subject)) {
|
|
||||||
$grant = $storedObjectVoter->voteOnAttribute($attributeAsEnum, $subject, $token);
|
|
||||||
|
|
||||||
if (false === $grant) {
|
|
||||||
$this->logger->debug(self::LOG_PREFIX.'deny access by storedObjectVoter', ['stored_object_voter' => $storedObjectVoter::class]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $grant;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// User role-based fallback
|
|
||||||
if ($this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN')) {
|
|
||||||
// TODO: this maybe considered as a security issue, as all authenticated users can reach a stored object which
|
|
||||||
// is potentially detached from an existing entity.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$askedRole = StoredObjectRoleEnum::from($attribute);
|
||||||
|
$tokenRoleAuthorization =
|
||||||
|
$token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS);
|
||||||
|
|
||||||
|
return match ($askedRole) {
|
||||||
|
StoredObjectRoleEnum::SEE => StoredObjectRoleEnum::EDIT === $tokenRoleAuthorization || StoredObjectRoleEnum::SEE === $tokenRoleAuthorization,
|
||||||
|
StoredObjectRoleEnum::EDIT => StoredObjectRoleEnum::EDIT === $tokenRoleAuthorization
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,69 +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\Security\Authorization\StoredObjectVoter;
|
|
||||||
|
|
||||||
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 Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
|
|
||||||
abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
|
|
||||||
{
|
|
||||||
abstract protected function getRepository(): AssociatedEntityToStoredObjectInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return class-string
|
|
||||||
*/
|
|
||||||
abstract protected function getClass(): string;
|
|
||||||
|
|
||||||
abstract protected function attributeToRole(StoredObjectRoleEnum $attribute): string;
|
|
||||||
|
|
||||||
abstract protected function canBeAssociatedWithWorkflow(): bool;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
private readonly Security $security,
|
|
||||||
private readonly ?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool
|
|
||||||
{
|
|
||||||
$class = $this->getClass();
|
|
||||||
|
|
||||||
return $this->getRepository()->findAssociatedEntityToStoredObject($subject) instanceof $class;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool
|
|
||||||
{
|
|
||||||
// Retrieve the related accompanying course document
|
|
||||||
$entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);
|
|
||||||
|
|
||||||
// Determine the attribute to pass to AccompanyingCourseDocumentVoter
|
|
||||||
$voterAttribute = $this->attributeToRole($attribute);
|
|
||||||
|
|
||||||
if (false === $this->security->isGranted($voterAttribute, $entity)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StoredObjectRoleEnum::SEE !== $attribute && $this->canBeAssociatedWithWorkflow()) {
|
|
||||||
if (null === $this->workflowDocumentService) {
|
|
||||||
throw new \LogicException('Provide a workflow document service');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->workflowDocumentService->notBlockedByWorkflow($entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,54 +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\Security\Authorization\StoredObjectVoter;
|
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
|
||||||
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 Symfony\Component\Security\Core\Security;
|
|
||||||
|
|
||||||
final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredObjectVoter
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly AccompanyingCourseDocumentRepository $repository,
|
|
||||||
Security $security,
|
|
||||||
WorkflowStoredObjectPermissionHelper $workflowDocumentService
|
|
||||||
) {
|
|
||||||
parent::__construct($security, $workflowDocumentService);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
|
||||||
{
|
|
||||||
return $this->repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
|
|
||||||
{
|
|
||||||
return match ($attribute) {
|
|
||||||
StoredObjectRoleEnum::EDIT => AccompanyingCourseDocumentVoter::UPDATE,
|
|
||||||
StoredObjectRoleEnum::SEE => AccompanyingCourseDocumentVoter::SEE_DETAILS,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getClass(): string
|
|
||||||
{
|
|
||||||
return AccompanyingCourseDocument::class;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function canBeAssociatedWithWorkflow(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,54 +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\Security\Authorization\StoredObjectVoter;
|
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\PersonDocument;
|
|
||||||
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 Symfony\Component\Security\Core\Security;
|
|
||||||
|
|
||||||
class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly PersonDocumentRepository $repository,
|
|
||||||
Security $security,
|
|
||||||
WorkflowStoredObjectPermissionHelper $workflowDocumentService
|
|
||||||
) {
|
|
||||||
parent::__construct($security, $workflowDocumentService);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
|
||||||
{
|
|
||||||
return $this->repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getClass(): string
|
|
||||||
{
|
|
||||||
return PersonDocument::class;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
|
|
||||||
{
|
|
||||||
return match ($attribute) {
|
|
||||||
StoredObjectRoleEnum::EDIT => PersonDocumentVoter::UPDATE,
|
|
||||||
StoredObjectRoleEnum::SEE => PersonDocumentVoter::SEE_DETAILS,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function canBeAssociatedWithWorkflow(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,22 +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\Security\Authorization;
|
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
|
||||||
|
|
||||||
interface StoredObjectVoterInterface
|
|
||||||
{
|
|
||||||
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool;
|
|
||||||
|
|
||||||
public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool;
|
|
||||||
}
|
|
@@ -15,7 +15,6 @@ use Chill\DocStoreBundle\Entity\StoredObject;
|
|||||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
|
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
|
||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
@@ -33,8 +32,7 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa
|
|||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly JWTDavTokenProviderInterface $JWTDavTokenProvider,
|
private readonly JWTDavTokenProviderInterface $JWTDavTokenProvider,
|
||||||
private readonly UrlGeneratorInterface $urlGenerator,
|
private readonly UrlGeneratorInterface $urlGenerator
|
||||||
private readonly Security $security
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function normalize($object, ?string $format = null, array $context = [])
|
public function normalize($object, ?string $format = null, array $context = [])
|
||||||
@@ -57,13 +55,13 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa
|
|||||||
// deprecated property
|
// deprecated property
|
||||||
$datas['creationDate'] = $datas['createdAt'];
|
$datas['creationDate'] = $datas['createdAt'];
|
||||||
|
|
||||||
$canSee = $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $object);
|
$canDavSee = in_array(self::ADD_DAV_SEE_LINK_CONTEXT, $context['groups'] ?? [], true);
|
||||||
$canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $object);
|
$canDavEdit = in_array(self::ADD_DAV_EDIT_LINK_CONTEXT, $context['groups'] ?? [], true);
|
||||||
|
|
||||||
if ($canSee || $canEdit) {
|
if ($canDavSee || $canDavEdit) {
|
||||||
$accessToken = $this->JWTDavTokenProvider->createToken(
|
$accessToken = $this->JWTDavTokenProvider->createToken(
|
||||||
$object,
|
$object,
|
||||||
$canEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE
|
$canDavEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE
|
||||||
);
|
);
|
||||||
|
|
||||||
$datas['_links'] = [
|
$datas['_links'] = [
|
||||||
|
@@ -1,23 +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\Signature\Driver\BaseSigner;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Message which is received when a pdf is signed.
|
|
||||||
*/
|
|
||||||
final readonly class PdfSignedMessage
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public readonly int $signatureId,
|
|
||||||
public readonly string $content
|
|
||||||
) {}
|
|
||||||
}
|
|
@@ -1,61 +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\Signature\Driver\BaseSigner;
|
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
|
||||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepSignatureRepository;
|
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
use Symfony\Component\Clock\ClockInterface;
|
|
||||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
|
||||||
|
|
||||||
final readonly class PdfSignedMessageHandler implements MessageHandlerInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* log prefix.
|
|
||||||
*/
|
|
||||||
private const P = '[pdf signed message] ';
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
private LoggerInterface $logger,
|
|
||||||
private EntityWorkflowManager $entityWorkflowManager,
|
|
||||||
private StoredObjectManagerInterface $storedObjectManager,
|
|
||||||
private EntityWorkflowStepSignatureRepository $entityWorkflowStepSignatureRepository,
|
|
||||||
private EntityManagerInterface $entityManager,
|
|
||||||
private ClockInterface $clock,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function __invoke(PdfSignedMessage $message): void
|
|
||||||
{
|
|
||||||
$this->logger->info(self::P.'a message is received', ['signaturedId' => $message->signatureId]);
|
|
||||||
|
|
||||||
$signature = $this->entityWorkflowStepSignatureRepository->find($message->signatureId);
|
|
||||||
|
|
||||||
if (null === $signature) {
|
|
||||||
throw new \RuntimeException('no signature found');
|
|
||||||
}
|
|
||||||
|
|
||||||
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($signature->getStep()->getEntityWorkflow());
|
|
||||||
|
|
||||||
if (null === $storedObject) {
|
|
||||||
throw new \RuntimeException('no stored object found');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->storedObjectManager->write($storedObject, $message->content);
|
|
||||||
|
|
||||||
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate($this->clock->now());
|
|
||||||
$this->entityManager->flush();
|
|
||||||
$this->entityManager->clear();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,66 +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\Signature\Driver\BaseSigner;
|
|
||||||
|
|
||||||
use Symfony\Component\Messenger\Envelope;
|
|
||||||
use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
|
|
||||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode (and requeue) @see{PdfSignedMessage}, which comes from an external producer.
|
|
||||||
*/
|
|
||||||
final readonly class PdfSignedMessageSerializer implements SerializerInterface
|
|
||||||
{
|
|
||||||
public function decode(array $encodedEnvelope): Envelope
|
|
||||||
{
|
|
||||||
$body = $encodedEnvelope['body'];
|
|
||||||
|
|
||||||
try {
|
|
||||||
$decoded = json_decode((string) $body, true, 512, JSON_THROW_ON_ERROR);
|
|
||||||
} catch (\JsonException $e) {
|
|
||||||
throw new MessageDecodingFailedException('Could not deserialize message', previous: $e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!array_key_exists('signatureId', $decoded) || !array_key_exists('content', $decoded)) {
|
|
||||||
throw new MessageDecodingFailedException('Could not find expected keys: signatureId or content');
|
|
||||||
}
|
|
||||||
|
|
||||||
$content = base64_decode((string) $decoded['content'], true);
|
|
||||||
|
|
||||||
if (false === $content) {
|
|
||||||
throw new MessageDecodingFailedException('Invalid character found in the base64 encoded content');
|
|
||||||
}
|
|
||||||
|
|
||||||
$message = new PdfSignedMessage($decoded['signatureId'], $content);
|
|
||||||
|
|
||||||
return new Envelope($message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function encode(Envelope $envelope): array
|
|
||||||
{
|
|
||||||
$message = $envelope->getMessage();
|
|
||||||
|
|
||||||
if (!$message instanceof PdfSignedMessage) {
|
|
||||||
throw new MessageDecodingFailedException('Expected a PdfSignedMessage');
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = [
|
|
||||||
'signatureId' => $message->signatureId,
|
|
||||||
'content' => base64_encode($message->content),
|
|
||||||
];
|
|
||||||
|
|
||||||
return [
|
|
||||||
'body' => json_encode($data, JSON_THROW_ON_ERROR),
|
|
||||||
'headers' => [],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,29 +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\Signature\Driver\BaseSigner;
|
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Message which is sent when we request a signature on a pdf.
|
|
||||||
*/
|
|
||||||
final readonly class RequestPdfSignMessage
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public int $signatureId,
|
|
||||||
public PDFSignatureZone $PDFSignatureZone,
|
|
||||||
public int $signatureZoneIndex,
|
|
||||||
public string $reason,
|
|
||||||
public string $signerText,
|
|
||||||
public string $content,
|
|
||||||
) {}
|
|
||||||
}
|
|
@@ -1,105 +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\Signature\Driver\BaseSigner;
|
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
|
|
||||||
use Symfony\Component\Messenger\Envelope;
|
|
||||||
use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
|
|
||||||
use Symfony\Component\Messenger\Stamp\NonSendableStampInterface;
|
|
||||||
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serialize a RequestPdfSignMessage, for external consumer.
|
|
||||||
*/
|
|
||||||
final readonly class RequestPdfSignMessageSerializer implements SerializerInterface
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private NormalizerInterface $normalizer,
|
|
||||||
private DenormalizerInterface $denormalizer,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function decode(array $encodedEnvelope): Envelope
|
|
||||||
{
|
|
||||||
$body = $encodedEnvelope['body'];
|
|
||||||
$headers = $encodedEnvelope['headers'];
|
|
||||||
|
|
||||||
if (RequestPdfSignMessage::class !== ($headers['Message'] ?? null)) {
|
|
||||||
throw new MessageDecodingFailedException('serializer does not support this message');
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = json_decode((string) $body, true);
|
|
||||||
|
|
||||||
$zoneSignature = $this->denormalizer->denormalize($data['signatureZone'], PDFSignatureZone::class, 'json', [
|
|
||||||
AbstractNormalizer::GROUPS => ['write'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$content = base64_decode((string) $data['content'], true);
|
|
||||||
|
|
||||||
if (false === $content) {
|
|
||||||
throw new MessageDecodingFailedException('the content could not be converted from base64 encoding');
|
|
||||||
}
|
|
||||||
|
|
||||||
$message = new RequestPdfSignMessage(
|
|
||||||
$data['signatureId'],
|
|
||||||
$zoneSignature,
|
|
||||||
$data['signatureZoneIndex'],
|
|
||||||
$data['reason'],
|
|
||||||
$data['signerText'],
|
|
||||||
$content,
|
|
||||||
);
|
|
||||||
|
|
||||||
// in case of redelivery, unserialize any stamps
|
|
||||||
$stamps = [];
|
|
||||||
if (isset($headers['stamps'])) {
|
|
||||||
$stamps = unserialize($headers['stamps']);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Envelope($message, $stamps);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function encode(Envelope $envelope): array
|
|
||||||
{
|
|
||||||
$message = $envelope->getMessage();
|
|
||||||
|
|
||||||
if (!$message instanceof RequestPdfSignMessage) {
|
|
||||||
throw new MessageDecodingFailedException('Message is not a RequestPdfSignMessage');
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = [
|
|
||||||
'signatureId' => $message->signatureId,
|
|
||||||
'signatureZoneIndex' => $message->signatureZoneIndex,
|
|
||||||
'signatureZone' => $this->normalizer->normalize($message->PDFSignatureZone, 'json', [AbstractNormalizer::GROUPS => ['read']]),
|
|
||||||
'reason' => $message->reason,
|
|
||||||
'signerText' => $message->signerText,
|
|
||||||
'content' => base64_encode($message->content),
|
|
||||||
];
|
|
||||||
|
|
||||||
$allStamps = [];
|
|
||||||
foreach ($envelope->all() as $stamp) {
|
|
||||||
if ($stamp instanceof NonSendableStampInterface) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$allStamps = [...$allStamps, ...$stamp];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'body' => json_encode($data, JSON_THROW_ON_ERROR, 512),
|
|
||||||
'headers' => [
|
|
||||||
'stamps' => serialize($allStamps),
|
|
||||||
'Message' => RequestPdfSignMessage::class,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,33 +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\Signature;
|
|
||||||
|
|
||||||
use Symfony\Component\Serializer\Annotation\Groups;
|
|
||||||
|
|
||||||
final readonly class PDFPage
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
#[Groups(['read'])]
|
|
||||||
public int $index,
|
|
||||||
#[Groups(['read'])]
|
|
||||||
public float $width,
|
|
||||||
#[Groups(['read'])]
|
|
||||||
public float $height,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function equals(self $page): bool
|
|
||||||
{
|
|
||||||
return $page->index === $this->index
|
|
||||||
&& round($page->width, 2) === round($this->width, 2)
|
|
||||||
&& round($page->height, 2) === round($this->height, 2);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,43 +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\Signature;
|
|
||||||
|
|
||||||
use Symfony\Component\Serializer\Annotation\Groups;
|
|
||||||
|
|
||||||
final readonly class PDFSignatureZone
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
#[Groups(['read'])]
|
|
||||||
public int $index,
|
|
||||||
#[Groups(['read'])]
|
|
||||||
public float $x,
|
|
||||||
#[Groups(['read'])]
|
|
||||||
public float $y,
|
|
||||||
#[Groups(['read'])]
|
|
||||||
public float $height,
|
|
||||||
#[Groups(['read'])]
|
|
||||||
public float $width,
|
|
||||||
#[Groups(['read'])]
|
|
||||||
public PDFPage $PDFPage,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function equals(self $other): bool
|
|
||||||
{
|
|
||||||
return
|
|
||||||
$this->index == $other->index
|
|
||||||
&& $this->x == $other->x
|
|
||||||
&& $this->y == $other->y
|
|
||||||
&& $this->height == $other->height
|
|
||||||
&& $this->width == $other->width
|
|
||||||
&& $this->PDFPage->equals($other->PDFPage);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,60 +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\Signature;
|
|
||||||
|
|
||||||
use Smalot\PdfParser\Parser;
|
|
||||||
|
|
||||||
class PDFSignatureZoneParser
|
|
||||||
{
|
|
||||||
public const ZONE_SIGNATURE_START = 'signature_zone';
|
|
||||||
|
|
||||||
private readonly Parser $parser;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
public float $defaultHeight = 90.0,
|
|
||||||
public float $defaultWidth = 180.0,
|
|
||||||
) {
|
|
||||||
$this->parser = new Parser();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<PDFSignatureZone>
|
|
||||||
*/
|
|
||||||
public function findSignatureZones(string $fileContent): array
|
|
||||||
{
|
|
||||||
$pdf = $this->parser->parseContent($fileContent);
|
|
||||||
$zones = [];
|
|
||||||
|
|
||||||
$defaults = $pdf->getObjectsByType('Pages');
|
|
||||||
$defaultPage = reset($defaults);
|
|
||||||
$defaultPageDetails = $defaultPage->getDetails();
|
|
||||||
$zoneIndex = 0;
|
|
||||||
|
|
||||||
foreach ($pdf->getPages() as $index => $page) {
|
|
||||||
$details = $page->getDetails();
|
|
||||||
$pdfPage = new PDFPage(
|
|
||||||
$index,
|
|
||||||
(float) ($details['MediaBox'][2] ?? $defaultPageDetails['MediaBox'][2]),
|
|
||||||
(float) ($details['MediaBox'][3] ?? $defaultPageDetails['MediaBox'][3]),
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach ($page->getDataTm() as $dataTm) {
|
|
||||||
if (str_starts_with((string) $dataTm[1], self::ZONE_SIGNATURE_START)) {
|
|
||||||
$zones[] = new PDFSignatureZone($zoneIndex, (float) $dataTm[0][4], (float) $dataTm[0][5], $this->defaultHeight, $this->defaultWidth, $pdfPage);
|
|
||||||
++$zoneIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $zones;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,38 +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\Workflow\EntityWorkflowManager;
|
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
|
|
||||||
class WorkflowStoredObjectPermissionHelper
|
|
||||||
{
|
|
||||||
public function __construct(private readonly Security $security, private readonly EntityWorkflowManager $entityWorkflowManager) {}
|
|
||||||
|
|
||||||
public function notBlockedByWorkflow(object $entity): bool
|
|
||||||
{
|
|
||||||
$workflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
|
|
||||||
$currentUser = $this->security->getUser();
|
|
||||||
|
|
||||||
foreach ($workflows as $workflow) {
|
|
||||||
if ($workflow->isFinal()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$workflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -16,7 +16,6 @@ use Chill\DocStoreBundle\Entity\StoredObject;
|
|||||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
|
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
|
||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
@@ -129,7 +128,6 @@ final readonly class WopiEditTwigExtensionRuntime implements RuntimeExtensionInt
|
|||||||
private NormalizerInterface $normalizer,
|
private NormalizerInterface $normalizer,
|
||||||
private JWTDavTokenProviderInterface $davTokenProvider,
|
private JWTDavTokenProviderInterface $davTokenProvider,
|
||||||
private UrlGeneratorInterface $urlGenerator,
|
private UrlGeneratorInterface $urlGenerator,
|
||||||
private Security $security,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -150,10 +148,8 @@ final readonly class WopiEditTwigExtensionRuntime implements RuntimeExtensionInt
|
|||||||
* @throws \Twig\Error\RuntimeError
|
* @throws \Twig\Error\RuntimeError
|
||||||
* @throws \Twig\Error\SyntaxError
|
* @throws \Twig\Error\SyntaxError
|
||||||
*/
|
*/
|
||||||
public function renderButtonGroup(Environment $environment, StoredObject $document, ?string $title = null, bool $showEditButtons = true, array $options = []): string
|
public function renderButtonGroup(Environment $environment, StoredObject $document, ?string $title = null, bool $canEdit = true, array $options = []): string
|
||||||
{
|
{
|
||||||
$canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $document) && $showEditButtons;
|
|
||||||
|
|
||||||
$accessToken = $this->davTokenProvider->createToken(
|
$accessToken = $this->davTokenProvider->createToken(
|
||||||
$document,
|
$document,
|
||||||
$canEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE
|
$canEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE
|
||||||
|
@@ -122,8 +122,7 @@ class TempUrlOpenstackGeneratorTest extends TestCase
|
|||||||
$signedUrl = new SignedUrl(
|
$signedUrl = new SignedUrl(
|
||||||
'GET',
|
'GET',
|
||||||
'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543',
|
'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543',
|
||||||
\DateTimeImmutable::createFromFormat('U', '1702043543'),
|
\DateTimeImmutable::createFromFormat('U', '1702043543')
|
||||||
$objectName
|
|
||||||
);
|
);
|
||||||
|
|
||||||
foreach ($baseUrls as $baseUrl) {
|
foreach ($baseUrls as $baseUrl) {
|
||||||
@@ -154,7 +153,6 @@ class TempUrlOpenstackGeneratorTest extends TestCase
|
|||||||
$signedUrl = new SignedUrlPost(
|
$signedUrl = new SignedUrlPost(
|
||||||
'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543',
|
'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543',
|
||||||
\DateTimeImmutable::createFromFormat('U', '1702043543'),
|
\DateTimeImmutable::createFromFormat('U', '1702043543'),
|
||||||
$objectName,
|
|
||||||
150,
|
150,
|
||||||
1,
|
1,
|
||||||
1800,
|
1800,
|
||||||
|
@@ -35,7 +35,7 @@ class AsyncUploadExtensionTest extends KernelTestCase
|
|||||||
{
|
{
|
||||||
$generator = $this->prophesize(TempUrlGeneratorInterface::class);
|
$generator = $this->prophesize(TempUrlGeneratorInterface::class);
|
||||||
$generator->generate(Argument::in(['GET', 'POST']), Argument::type('string'), Argument::any())
|
$generator->generate(Argument::in(['GET', 'POST']), Argument::type('string'), Argument::any())
|
||||||
->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours'), $args[1]));
|
->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours')));
|
||||||
|
|
||||||
$urlGenerator = $this->prophesize(UrlGeneratorInterface::class);
|
$urlGenerator = $this->prophesize(UrlGeneratorInterface::class);
|
||||||
$urlGenerator->generate('async_upload.generate_url', Argument::type('array'))
|
$urlGenerator->generate('async_upload.generate_url', Argument::type('array'))
|
||||||
|
@@ -73,7 +73,6 @@ class AsyncUploadControllerTest extends TestCase
|
|||||||
return new SignedUrlPost(
|
return new SignedUrlPost(
|
||||||
'https://object.store.example',
|
'https://object.store.example',
|
||||||
new \DateTimeImmutable('1 hour'),
|
new \DateTimeImmutable('1 hour'),
|
||||||
'abc',
|
|
||||||
150,
|
150,
|
||||||
1,
|
1,
|
||||||
1800,
|
1800,
|
||||||
@@ -88,8 +87,7 @@ class AsyncUploadControllerTest extends TestCase
|
|||||||
return new SignedUrl(
|
return new SignedUrl(
|
||||||
$method,
|
$method,
|
||||||
'https://object.store.example',
|
'https://object.store.example',
|
||||||
new \DateTimeImmutable('1 hour'),
|
new \DateTimeImmutable('1 hour')
|
||||||
$object_name
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -23,7 +23,6 @@ use Prophecy\PhpUnit\ProphecyTrait;
|
|||||||
use Symfony\Component\Form\PreloadedExtension;
|
use Symfony\Component\Form\PreloadedExtension;
|
||||||
use Symfony\Component\Form\Test\TypeTestCase;
|
use Symfony\Component\Form\Test\TypeTestCase;
|
||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
use Symfony\Component\Serializer\Encoder\JsonEncoder;
|
use Symfony\Component\Serializer\Encoder\JsonEncoder;
|
||||||
use Symfony\Component\Serializer\Serializer;
|
use Symfony\Component\Serializer\Serializer;
|
||||||
|
|
||||||
@@ -81,15 +80,11 @@ class StoredObjectTypeTest extends TypeTestCase
|
|||||||
$urlGenerator->generate('chill_docstore_dav_document_get', Argument::type('array'), UrlGeneratorInterface::ABSOLUTE_URL)
|
$urlGenerator->generate('chill_docstore_dav_document_get', Argument::type('array'), UrlGeneratorInterface::ABSOLUTE_URL)
|
||||||
->willReturn('http://url/fake');
|
->willReturn('http://url/fake');
|
||||||
|
|
||||||
$security = $this->prophesize(Security::class);
|
|
||||||
$security->isGranted(Argument::cetera())->willReturn(true);
|
|
||||||
|
|
||||||
$serializer = new Serializer(
|
$serializer = new Serializer(
|
||||||
[
|
[
|
||||||
new StoredObjectNormalizer(
|
new StoredObjectNormalizer(
|
||||||
$jwtTokenProvider->reveal(),
|
$jwtTokenProvider->reveal(),
|
||||||
$urlGenerator->reveal(),
|
$urlGenerator->reveal(),
|
||||||
$security->reveal()
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
@@ -1,168 +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\Security\Authorization;
|
|
||||||
|
|
||||||
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 PHPUnit\Framework\TestCase;
|
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @coversNothing
|
|
||||||
*/
|
|
||||||
class AbstractStoredObjectVoterTest extends TestCase
|
|
||||||
{
|
|
||||||
private AssociatedEntityToStoredObjectInterface $repository;
|
|
||||||
private Security $security;
|
|
||||||
private WorkflowStoredObjectPermissionHelper $workflowDocumentService;
|
|
||||||
|
|
||||||
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
|
|
||||||
{
|
|
||||||
// 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
|
|
||||||
) {
|
|
||||||
parent::__construct($security, $workflowDocumentService);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function attributeToRole($attribute): string
|
|
||||||
{
|
|
||||||
return 'SOME_ROLE';
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
|
||||||
{
|
|
||||||
return $this->repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getClass(): string
|
|
||||||
{
|
|
||||||
return \stdClass::class;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function canBeAssociatedWithWorkflow(): bool
|
|
||||||
{
|
|
||||||
return $this->canBeAssociatedWithWorkflow;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
// 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, $subject));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testVoteOnAttributeAllowedAndWorkflowAllowed(): void
|
|
||||||
{
|
|
||||||
[$user, $token, $subject, $entity] = $this->setupMockObjects();
|
|
||||||
|
|
||||||
// Setup mocks for voteOnAttribute method
|
|
||||||
$this->setupMocksForVoteOnAttribute($user, $token, true, $entity, true);
|
|
||||||
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
|
|
||||||
|
|
||||||
// The voteOnAttribute method should return True when workflow is allowed
|
|
||||||
self::assertTrue($voter->voteOnAttribute(StoredObjectRoleEnum::SEE, $subject, $token));
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testVoteOnAttributeAllowedWorkflowAllowedToSeeDocument(): 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::SEE;
|
|
||||||
$result = $voter->voteOnAttribute($attribute, $subject, $token);
|
|
||||||
|
|
||||||
// Assert that access is denied when workflow is not allowed
|
|
||||||
$this->assertTrue($result);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -14,14 +14,11 @@ namespace Chill\DocStoreBundle\Tests\Security\Authorization;
|
|||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter;
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter;
|
||||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
|
use Chill\DocStoreBundle\Security\Guard\DavTokenAuthenticationEventSubscriber;
|
||||||
use Chill\MainBundle\Entity\User;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Psr\Log\NullLogger;
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
|
||||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
@@ -30,93 +27,97 @@ use Symfony\Component\Security\Core\Security;
|
|||||||
*/
|
*/
|
||||||
class StoredObjectVoterTest extends TestCase
|
class StoredObjectVoterTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dataProvider provideDataVote
|
* @dataProvider provideDataVote
|
||||||
*/
|
*/
|
||||||
public function testVote(array $storedObjectVotersDefinition, object $subject, string $attribute, bool $fallbackSecurityExpected, bool $securityIsGrantedResult, mixed $expected): void
|
public function testVote(TokenInterface $token, ?object $subject, string $attribute, mixed $expected): void
|
||||||
{
|
{
|
||||||
$storedObjectVoters = array_map(fn (array $definition) => $this->buildStoredObjectVoter($definition[0], $definition[1], $definition[2]), $storedObjectVotersDefinition);
|
$voter = new StoredObjectVoter();
|
||||||
$token = new UsernamePasswordToken(new User(), 'chill_main', ['ROLE_USER']);
|
|
||||||
|
|
||||||
$security = $this->createMock(Security::class);
|
|
||||||
$security->expects($fallbackSecurityExpected ? $this->atLeastOnce() : $this->never())
|
|
||||||
->method('isGranted')
|
|
||||||
->with($this->logicalOr($this->identicalTo('ROLE_USER'), $this->identicalTo('ROLE_ADMIN')))
|
|
||||||
->willReturn($securityIsGrantedResult);
|
|
||||||
|
|
||||||
$voter = new StoredObjectVoter($security, $storedObjectVoters, new NullLogger());
|
|
||||||
|
|
||||||
self::assertEquals($expected, $voter->vote($token, $subject, [$attribute]));
|
self::assertEquals($expected, $voter->vote($token, $subject, [$attribute]));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildStoredObjectVoter(bool $supportsIsCalled, bool $supports, bool $voteOnAttribute): StoredObjectVoterInterface
|
public function provideDataVote(): iterable
|
||||||
{
|
|
||||||
$storedObjectVoter = $this->createMock(StoredObjectVoterInterface::class);
|
|
||||||
$storedObjectVoter->expects($supportsIsCalled ? $this->once() : $this->never())->method('supports')
|
|
||||||
->with(self::isInstanceOf(StoredObjectRoleEnum::class), $this->isInstanceOf(StoredObject::class))
|
|
||||||
->willReturn($supports);
|
|
||||||
$storedObjectVoter->expects($supportsIsCalled && $supports ? $this->once() : $this->never())->method('voteOnAttribute')
|
|
||||||
->with(self::isInstanceOf(StoredObjectRoleEnum::class), $this->isInstanceOf(StoredObject::class), $this->isInstanceOf(TokenInterface::class))
|
|
||||||
->willReturn($voteOnAttribute);
|
|
||||||
|
|
||||||
return $storedObjectVoter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function provideDataVote(): iterable
|
|
||||||
{
|
{
|
||||||
yield [
|
yield [
|
||||||
// we try with something else than a SToredObject, the voter should abstain
|
$this->buildToken(StoredObjectRoleEnum::EDIT, new StoredObject()),
|
||||||
[[false, false, false]],
|
|
||||||
new \stdClass(),
|
new \stdClass(),
|
||||||
'SOMETHING',
|
'SOMETHING',
|
||||||
false,
|
|
||||||
false,
|
|
||||||
VoterInterface::ACCESS_ABSTAIN,
|
VoterInterface::ACCESS_ABSTAIN,
|
||||||
];
|
];
|
||||||
|
|
||||||
yield [
|
yield [
|
||||||
// we try with an unsupported attribute, the voter must abstain
|
$this->buildToken(StoredObjectRoleEnum::EDIT, $so = new StoredObject()),
|
||||||
[[false, false, false]],
|
$so,
|
||||||
new StoredObject(),
|
|
||||||
'SOMETHING',
|
'SOMETHING',
|
||||||
false,
|
|
||||||
false,
|
|
||||||
VoterInterface::ACCESS_ABSTAIN,
|
VoterInterface::ACCESS_ABSTAIN,
|
||||||
];
|
];
|
||||||
|
|
||||||
yield [
|
yield [
|
||||||
// happy scenario: there is a role voter
|
$this->buildToken(StoredObjectRoleEnum::EDIT, $so = new StoredObject()),
|
||||||
[[true, true, true]],
|
$so,
|
||||||
new StoredObject(),
|
|
||||||
StoredObjectRoleEnum::SEE->value,
|
StoredObjectRoleEnum::SEE->value,
|
||||||
false,
|
|
||||||
false,
|
|
||||||
VoterInterface::ACCESS_GRANTED,
|
VoterInterface::ACCESS_GRANTED,
|
||||||
];
|
];
|
||||||
|
|
||||||
yield [
|
yield [
|
||||||
// there is a role voter, but not allowed to see the stored object
|
$this->buildToken(StoredObjectRoleEnum::EDIT, $so = new StoredObject()),
|
||||||
[[true, true, false]],
|
$so,
|
||||||
new StoredObject(),
|
StoredObjectRoleEnum::EDIT->value,
|
||||||
StoredObjectRoleEnum::SEE->value,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
VoterInterface::ACCESS_DENIED,
|
|
||||||
];
|
|
||||||
yield [
|
|
||||||
// there is no role voter, fallback to security, which does not grant access
|
|
||||||
[[true, false, false]],
|
|
||||||
new StoredObject(),
|
|
||||||
StoredObjectRoleEnum::SEE->value,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
VoterInterface::ACCESS_DENIED,
|
|
||||||
];
|
|
||||||
yield [
|
|
||||||
// there is no role voter, fallback to security, which does grant access
|
|
||||||
[[true, false, false]],
|
|
||||||
new StoredObject(),
|
|
||||||
StoredObjectRoleEnum::SEE->value,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
VoterInterface::ACCESS_GRANTED,
|
VoterInterface::ACCESS_GRANTED,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
$this->buildToken(StoredObjectRoleEnum::SEE, $so = new StoredObject()),
|
||||||
|
$so,
|
||||||
|
StoredObjectRoleEnum::EDIT->value,
|
||||||
|
VoterInterface::ACCESS_DENIED,
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
$this->buildToken(StoredObjectRoleEnum::SEE, $so = new StoredObject()),
|
||||||
|
$so,
|
||||||
|
StoredObjectRoleEnum::SEE->value,
|
||||||
|
VoterInterface::ACCESS_GRANTED,
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
$this->buildToken(null, null),
|
||||||
|
new StoredObject(),
|
||||||
|
StoredObjectRoleEnum::SEE->value,
|
||||||
|
VoterInterface::ACCESS_DENIED,
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
$this->buildToken(null, null),
|
||||||
|
new StoredObject(),
|
||||||
|
StoredObjectRoleEnum::SEE->value,
|
||||||
|
VoterInterface::ACCESS_DENIED,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildToken(?StoredObjectRoleEnum $storedObjectRoleEnum = null, ?StoredObject $storedObject = null): TokenInterface
|
||||||
|
{
|
||||||
|
$token = $this->prophesize(TokenInterface::class);
|
||||||
|
|
||||||
|
if (null !== $storedObjectRoleEnum) {
|
||||||
|
$token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willReturn(true);
|
||||||
|
$token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willReturn($storedObjectRoleEnum);
|
||||||
|
} else {
|
||||||
|
$token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willReturn(false);
|
||||||
|
$token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willThrow(new \InvalidArgumentException());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $storedObject) {
|
||||||
|
$token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willReturn(true);
|
||||||
|
$token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willReturn($storedObject->getUuid()->toString());
|
||||||
|
} else {
|
||||||
|
$token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willReturn(false);
|
||||||
|
$token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willThrow(new \InvalidArgumentException());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $token->reveal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -38,8 +38,7 @@ class SignedUrlNormalizerTest extends KernelTestCase
|
|||||||
$signedUrl = new SignedUrl(
|
$signedUrl = new SignedUrl(
|
||||||
'GET',
|
'GET',
|
||||||
'https://object.store.example/container/object',
|
'https://object.store.example/container/object',
|
||||||
\DateTimeImmutable::createFromFormat('U', '1700000'),
|
\DateTimeImmutable::createFromFormat('U', '1700000')
|
||||||
'object'
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$actual = self::$normalizer->normalize($signedUrl, 'json', [AbstractNormalizer::GROUPS => ['read']]);
|
$actual = self::$normalizer->normalize($signedUrl, 'json', [AbstractNormalizer::GROUPS => ['read']]);
|
||||||
@@ -49,7 +48,6 @@ class SignedUrlNormalizerTest extends KernelTestCase
|
|||||||
'method' => 'GET',
|
'method' => 'GET',
|
||||||
'expires' => 1_700_000,
|
'expires' => 1_700_000,
|
||||||
'url' => 'https://object.store.example/container/object',
|
'url' => 'https://object.store.example/container/object',
|
||||||
'object_name' => 'object',
|
|
||||||
],
|
],
|
||||||
$actual
|
$actual
|
||||||
);
|
);
|
||||||
|
@@ -38,7 +38,6 @@ class SignedUrlPostNormalizerTest extends KernelTestCase
|
|||||||
$signedUrl = new SignedUrlPost(
|
$signedUrl = new SignedUrlPost(
|
||||||
'https://object.store.example/container/object',
|
'https://object.store.example/container/object',
|
||||||
\DateTimeImmutable::createFromFormat('U', '1700000'),
|
\DateTimeImmutable::createFromFormat('U', '1700000'),
|
||||||
'abc',
|
|
||||||
15000,
|
15000,
|
||||||
1,
|
1,
|
||||||
180,
|
180,
|
||||||
@@ -60,7 +59,6 @@ class SignedUrlPostNormalizerTest extends KernelTestCase
|
|||||||
'method' => 'POST',
|
'method' => 'POST',
|
||||||
'expires' => 1_700_000,
|
'expires' => 1_700_000,
|
||||||
'url' => 'https://object.store.example/container/object',
|
'url' => 'https://object.store.example/container/object',
|
||||||
'object_name' => 'abc',
|
|
||||||
],
|
],
|
||||||
$actual
|
$actual
|
||||||
);
|
);
|
||||||
|
@@ -1,99 +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\Signature\Driver\BaseSigner;
|
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
|
||||||
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\PdfSignedMessage;
|
|
||||||
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\PdfSignedMessageHandler;
|
|
||||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
|
||||||
use Chill\MainBundle\Entity\User;
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
|
||||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepSignatureRepository;
|
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
|
||||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
|
||||||
use Chill\PersonBundle\Entity\Person;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Psr\Log\NullLogger;
|
|
||||||
use Symfony\Component\Clock\MockClock;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @coversNothing
|
|
||||||
*/
|
|
||||||
class PdfSignedMessageHandlerTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testThatObjectIsWrittenInStoredObjectManagerHappyScenario(): void
|
|
||||||
{
|
|
||||||
// a dummy stored object
|
|
||||||
$storedObject = new StoredObject();
|
|
||||||
// build the associated EntityWorkflow, with one step with a person signature
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futurePersonSignatures[] = new Person();
|
|
||||||
$entityWorkflow->setStep('new_step', $dto, 'new_transition', new \DateTimeImmutable(), new User());
|
|
||||||
$step = $entityWorkflow->getCurrentStep();
|
|
||||||
$signature = $step->getSignatures()->first();
|
|
||||||
|
|
||||||
$handler = new PdfSignedMessageHandler(
|
|
||||||
new NullLogger(),
|
|
||||||
$this->buildEntityWorkflowManager($storedObject),
|
|
||||||
$this->buildStoredObjectManager($storedObject, $expectedContent = '1234'),
|
|
||||||
$this->buildSignatureRepository($signature),
|
|
||||||
$this->buildEntityManager(true),
|
|
||||||
new MockClock('now'),
|
|
||||||
);
|
|
||||||
|
|
||||||
// we simply call the handler. The mocked StoredObjectManager will check that the "write" method is invoked once
|
|
||||||
// with the content "1234"
|
|
||||||
$handler(new PdfSignedMessage(10, $expectedContent));
|
|
||||||
|
|
||||||
self::assertEquals('signed', $signature->getState()->value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildSignatureRepository(EntityWorkflowStepSignature $signature): EntityWorkflowStepSignatureRepository
|
|
||||||
{
|
|
||||||
$entityWorkflowStepSignatureRepository = $this->createMock(EntityWorkflowStepSignatureRepository::class);
|
|
||||||
$entityWorkflowStepSignatureRepository->method('find')->with($this->isType('int'))->willReturn($signature);
|
|
||||||
|
|
||||||
return $entityWorkflowStepSignatureRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildEntityWorkflowManager(?StoredObject $associatedStoredObject): EntityWorkflowManager
|
|
||||||
{
|
|
||||||
$entityWorkflowManager = $this->createMock(EntityWorkflowManager::class);
|
|
||||||
$entityWorkflowManager->method('getAssociatedStoredObject')->willReturn($associatedStoredObject);
|
|
||||||
|
|
||||||
return $entityWorkflowManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildStoredObjectManager(StoredObject $expectedStoredObject, string $expectedContent): StoredObjectManagerInterface
|
|
||||||
{
|
|
||||||
$storedObjectManager = $this->createMock(StoredObjectManagerInterface::class);
|
|
||||||
$storedObjectManager->expects($this->once())
|
|
||||||
->method('write')
|
|
||||||
->with($this->identicalTo($expectedStoredObject), $expectedContent);
|
|
||||||
|
|
||||||
return $storedObjectManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildEntityManager(bool $willFlush): EntityManagerInterface
|
|
||||||
{
|
|
||||||
$em = $this->createMock(EntityManagerInterface::class);
|
|
||||||
$em->expects($willFlush ? $this->once() : $this->never())->method('flush');
|
|
||||||
$em->expects($willFlush ? $this->once() : $this->never())->method('clear');
|
|
||||||
|
|
||||||
return $em;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,63 +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\Signature\Driver\BaseSigner;
|
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\PdfSignedMessage;
|
|
||||||
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\PdfSignedMessageSerializer;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Symfony\Component\Messenger\Envelope;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @coversNothing
|
|
||||||
*/
|
|
||||||
class PdfSignedMessageSerializerTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testDecode(): void
|
|
||||||
{
|
|
||||||
$asString = <<<'JSON'
|
|
||||||
{"signatureId": 0, "content": "dGVzdAo="}
|
|
||||||
JSON;
|
|
||||||
|
|
||||||
$actual = $this->buildSerializer()->decode(['body' => $asString]);
|
|
||||||
|
|
||||||
self::assertInstanceOf(Envelope::class, $actual);
|
|
||||||
$message = $actual->getMessage();
|
|
||||||
self::assertInstanceOf(PdfSignedMessage::class, $message);
|
|
||||||
self::assertEquals("test\n", $message->content);
|
|
||||||
self::assertEquals(0, $message->signatureId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testEncode(): void
|
|
||||||
{
|
|
||||||
$envelope = new Envelope(
|
|
||||||
new PdfSignedMessage(0, "test\n")
|
|
||||||
);
|
|
||||||
|
|
||||||
$actual = $this->buildSerializer()->encode($envelope);
|
|
||||||
|
|
||||||
self::assertIsArray($actual);
|
|
||||||
self::assertArrayHasKey('body', $actual);
|
|
||||||
self::assertArrayHasKey('headers', $actual);
|
|
||||||
self::assertEquals([], $actual['headers']);
|
|
||||||
|
|
||||||
self::assertEquals(<<<'JSON'
|
|
||||||
{"signatureId":0,"content":"dGVzdAo="}
|
|
||||||
JSON, $actual['body']);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildSerializer(): PdfSignedMessageSerializer
|
|
||||||
{
|
|
||||||
return new PdfSignedMessageSerializer();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,137 +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\Signature\Driver\BaseSigner;
|
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Service\Signature\PDFPage;
|
|
||||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
|
|
||||||
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessage;
|
|
||||||
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessageSerializer;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Symfony\Component\Messenger\Envelope;
|
|
||||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
|
||||||
use Symfony\Component\Serializer\Serializer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @coversNothing
|
|
||||||
*/
|
|
||||||
class RequestPdfSignMessageSerializerTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testEncode(): void
|
|
||||||
{
|
|
||||||
$serializer = $this->buildSerializer();
|
|
||||||
|
|
||||||
$envelope = new Envelope(
|
|
||||||
$request = new RequestPdfSignMessage(
|
|
||||||
0,
|
|
||||||
new PDFSignatureZone(0, 10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
|
|
||||||
0,
|
|
||||||
'metadata to add to the signature',
|
|
||||||
'Mme Caroline Diallo',
|
|
||||||
'abc'
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
$actual = $serializer->encode($envelope);
|
|
||||||
$expectedBody = json_encode([
|
|
||||||
'signatureId' => $request->signatureId,
|
|
||||||
'signatureZoneIndex' => $request->signatureZoneIndex,
|
|
||||||
'signatureZone' => ['x' => 10.0],
|
|
||||||
'reason' => $request->reason,
|
|
||||||
'signerText' => $request->signerText,
|
|
||||||
'content' => base64_encode($request->content),
|
|
||||||
]);
|
|
||||||
|
|
||||||
self::assertIsArray($actual);
|
|
||||||
self::assertArrayHasKey('body', $actual);
|
|
||||||
self::assertArrayHasKey('headers', $actual);
|
|
||||||
self::assertEquals($expectedBody, $actual['body']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testDecode(): void
|
|
||||||
{
|
|
||||||
$serializer = $this->buildSerializer();
|
|
||||||
|
|
||||||
$request = new RequestPdfSignMessage(
|
|
||||||
0,
|
|
||||||
new PDFSignatureZone(0, 10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
|
|
||||||
0,
|
|
||||||
'metadata to add to the signature',
|
|
||||||
'Mme Caroline Diallo',
|
|
||||||
'abc'
|
|
||||||
);
|
|
||||||
|
|
||||||
$bodyAsString = json_encode([
|
|
||||||
'signatureId' => $request->signatureId,
|
|
||||||
'signatureZoneIndex' => $request->signatureZoneIndex,
|
|
||||||
'signatureZone' => ['x' => 10.0],
|
|
||||||
'reason' => $request->reason,
|
|
||||||
'signerText' => $request->signerText,
|
|
||||||
'content' => base64_encode($request->content),
|
|
||||||
], JSON_THROW_ON_ERROR);
|
|
||||||
|
|
||||||
$actual = $serializer->decode([
|
|
||||||
'body' => $bodyAsString,
|
|
||||||
'headers' => [
|
|
||||||
'Message' => RequestPdfSignMessage::class,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
self::assertInstanceOf(RequestPdfSignMessage::class, $actual->getMessage());
|
|
||||||
self::assertEquals($request->signatureId, $actual->getMessage()->signatureId);
|
|
||||||
self::assertEquals($request->signatureZoneIndex, $actual->getMessage()->signatureZoneIndex);
|
|
||||||
self::assertEquals($request->reason, $actual->getMessage()->reason);
|
|
||||||
self::assertEquals($request->signerText, $actual->getMessage()->signerText);
|
|
||||||
self::assertEquals($request->content, $actual->getMessage()->content);
|
|
||||||
self::assertNotNull($actual->getMessage()->PDFSignatureZone);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildSerializer(): RequestPdfSignMessageSerializer
|
|
||||||
{
|
|
||||||
$normalizer =
|
|
||||||
new class () implements NormalizerInterface {
|
|
||||||
public function normalize($object, ?string $format = null, array $context = []): array
|
|
||||||
{
|
|
||||||
if (!$object instanceof PDFSignatureZone) {
|
|
||||||
throw new UnexpectedValueException('expected RequestPdfSignMessage');
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'x' => $object->x,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function supportsNormalization($data, ?string $format = null): bool
|
|
||||||
{
|
|
||||||
return $data instanceof PDFSignatureZone;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
$denormalizer = new class () implements DenormalizerInterface {
|
|
||||||
public function denormalize($data, string $type, ?string $format = null, array $context = [])
|
|
||||||
{
|
|
||||||
return new PDFSignatureZone(0, 10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function supportsDenormalization($data, string $type, ?string $format = null)
|
|
||||||
{
|
|
||||||
return PDFSignatureZone::class === $type;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$serializer = new Serializer([$normalizer, $denormalizer]);
|
|
||||||
|
|
||||||
return new RequestPdfSignMessageSerializer($serializer, $serializer);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,79 +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 Tests\Service\Signature;
|
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Service\Signature\PDFPage;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
|
|
||||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneParser;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @coversNothing
|
|
||||||
*/
|
|
||||||
class PDFSignatureZoneParserTest extends TestCase
|
|
||||||
{
|
|
||||||
private static PDFSignatureZoneParser $parser;
|
|
||||||
|
|
||||||
public static function setUpBeforeClass(): void
|
|
||||||
{
|
|
||||||
self::$parser = new PDFSignatureZoneParser();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider provideFiles
|
|
||||||
*
|
|
||||||
* @param list<PDFSignatureZone> $expected
|
|
||||||
*/
|
|
||||||
public function testFindSignatureZones(string $filePath, array $expected): void
|
|
||||||
{
|
|
||||||
$content = file_get_contents($filePath);
|
|
||||||
|
|
||||||
if (false === $content) {
|
|
||||||
throw new \LogicException("Unable to read file {$filePath}");
|
|
||||||
}
|
|
||||||
|
|
||||||
$actual = self::$parser->findSignatureZones($content);
|
|
||||||
|
|
||||||
self::assertEquals(count($expected), count($actual));
|
|
||||||
|
|
||||||
foreach ($actual as $index => $signatureZone) {
|
|
||||||
self::assertObjectEquals($expected[$index], $signatureZone);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function provideFiles(): iterable
|
|
||||||
{
|
|
||||||
yield [
|
|
||||||
__DIR__.'/data/signature_2_signature_page_1.pdf',
|
|
||||||
[
|
|
||||||
new PDFSignatureZone(
|
|
||||||
0,
|
|
||||||
127.7,
|
|
||||||
95.289,
|
|
||||||
90.0,
|
|
||||||
180.0,
|
|
||||||
$page = new PDFPage(0, 595.30393, 841.8897)
|
|
||||||
),
|
|
||||||
new PDFSignatureZone(
|
|
||||||
1,
|
|
||||||
269.5,
|
|
||||||
95.289,
|
|
||||||
90.0,
|
|
||||||
180.0,
|
|
||||||
$page,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
@@ -202,8 +202,7 @@ final class StoredObjectManagerTest extends TestCase
|
|||||||
$response = new SignedUrl(
|
$response = new SignedUrl(
|
||||||
'PUT',
|
'PUT',
|
||||||
'https://example.com/'.$storedObject->getFilename(),
|
'https://example.com/'.$storedObject->getFilename(),
|
||||||
new \DateTimeImmutable('1 hours'),
|
new \DateTimeImmutable('1 hours')
|
||||||
$storedObject->getFilename()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$tempUrlGenerator = $this->createMock(TempUrlGeneratorInterface::class);
|
$tempUrlGenerator = $this->createMock(TempUrlGeneratorInterface::class);
|
||||||
|
@@ -43,7 +43,7 @@ class AsyncFileExistsValidatorTest extends ConstraintValidatorTestCase
|
|||||||
|
|
||||||
$generator = $this->prophesize(TempUrlGeneratorInterface::class);
|
$generator = $this->prophesize(TempUrlGeneratorInterface::class);
|
||||||
$generator->generate(Argument::in(['GET', 'HEAD']), Argument::type('string'), Argument::any())
|
$generator->generate(Argument::in(['GET', 'HEAD']), Argument::type('string'), Argument::any())
|
||||||
->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours'), $args[1]));
|
->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours')));
|
||||||
|
|
||||||
return new AsyncFileExistsValidator($generator->reveal(), $client);
|
return new AsyncFileExistsValidator($generator->reveal(), $client);
|
||||||
}
|
}
|
||||||
|
@@ -12,25 +12,27 @@ declare(strict_types=1);
|
|||||||
namespace Chill\DocStoreBundle\Workflow;
|
namespace Chill\DocStoreBundle\Workflow;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
||||||
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
|
||||||
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
|
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface;
|
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
|
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\EntityRepository;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
/**
|
class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandlerInterface
|
||||||
* @implements EntityWorkflowWithStoredObjectHandlerInterface<AccompanyingCourseDocument>
|
|
||||||
*/
|
|
||||||
readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowWithStoredObjectHandlerInterface
|
|
||||||
{
|
{
|
||||||
|
private readonly EntityRepository $repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: injecter le repository directement.
|
||||||
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private TranslatorInterface $translator,
|
EntityManagerInterface $em,
|
||||||
private EntityWorkflowRepository $workflowRepository,
|
private readonly TranslatorInterface $translator
|
||||||
private AccompanyingCourseDocumentRepository $repository
|
) {
|
||||||
) {}
|
$this->repository = $em->getRepository(AccompanyingCourseDocument::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function getDeletionRoles(): array
|
public function getDeletionRoles(): array
|
||||||
{
|
{
|
||||||
@@ -71,6 +73,8 @@ readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkfl
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param AccompanyingCourseDocument $object
|
||||||
|
*
|
||||||
* @return array[]
|
* @return array[]
|
||||||
*/
|
*/
|
||||||
public function getRelatedObjects(object $object): array
|
public function getRelatedObjects(object $object): array
|
||||||
@@ -118,22 +122,8 @@ readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkfl
|
|||||||
return AccompanyingCourseDocument::class === $entityWorkflow->getRelatedEntityClass();
|
return AccompanyingCourseDocument::class === $entityWorkflow->getRelatedEntityClass();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAssociatedStoredObject(EntityWorkflow $entityWorkflow): ?StoredObject
|
|
||||||
{
|
|
||||||
return $this->getRelatedEntity($entityWorkflow)?->getObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool
|
public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findByRelatedEntity(object $object): array
|
|
||||||
{
|
|
||||||
if (!$object instanceof AccompanyingCourseDocument) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->workflowRepository->findByRelatedEntity(AccompanyingCourseDocument::class, $object->getId());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -5,5 +5,4 @@ module.exports = function(encore)
|
|||||||
});
|
});
|
||||||
encore.addEntry('mod_async_upload', __dirname + '/Resources/public/module/async_upload/index.ts');
|
encore.addEntry('mod_async_upload', __dirname + '/Resources/public/module/async_upload/index.ts');
|
||||||
encore.addEntry('mod_document_action_buttons_group', __dirname + '/Resources/public/module/document_action_buttons_group/index');
|
encore.addEntry('mod_document_action_buttons_group', __dirname + '/Resources/public/module/document_action_buttons_group/index');
|
||||||
encore.addEntry('vue_document_signature', __dirname + '/Resources/public/vuejs/DocumentSignature/index.ts');
|
|
||||||
};
|
};
|
||||||
|
@@ -1,13 +0,0 @@
|
|||||||
services:
|
|
||||||
_defaults:
|
|
||||||
autowire: true
|
|
||||||
autoconfigure: true
|
|
||||||
Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter:
|
|
||||||
arguments:
|
|
||||||
$storedObjectVoters: !tagged_iterator stored_object_voter
|
|
||||||
tags:
|
|
||||||
- { name: security.voter }
|
|
||||||
|
|
||||||
Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter:
|
|
||||||
tags:
|
|
||||||
- { name: security.voter }
|
|
@@ -31,7 +31,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||||||
/**
|
/**
|
||||||
* Class Event.
|
* Class Event.
|
||||||
*/
|
*/
|
||||||
#[ORM\Entity]
|
#[ORM\Entity(repositoryClass: \Chill\EventBundle\Repository\EventRepository::class)]
|
||||||
#[ORM\HasLifecycleCallbacks]
|
#[ORM\HasLifecycleCallbacks]
|
||||||
#[ORM\Table(name: 'chill_event_event')]
|
#[ORM\Table(name: 'chill_event_event')]
|
||||||
class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInterface, TrackUpdateInterface
|
class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInterface, TrackUpdateInterface
|
||||||
@@ -62,9 +62,9 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
|
|||||||
private ?string $name = null;
|
private ?string $name = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, Participation>
|
* @var Collection<Participation>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(mappedBy: 'event', targetEntity: Participation::class)]
|
#[ORM\OneToMany(targetEntity: Participation::class, mappedBy: 'event')]
|
||||||
private Collection $participations;
|
private Collection $participations;
|
||||||
|
|
||||||
#[Assert\NotNull]
|
#[Assert\NotNull]
|
||||||
@@ -79,7 +79,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
|
|||||||
private ?Location $location = null;
|
private ?Location $location = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, StoredObject>
|
* @var Collection<StoredObject>
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: StoredObject::class, cascade: ['persist', 'refresh'])]
|
#[ORM\ManyToMany(targetEntity: StoredObject::class, cascade: ['persist', 'refresh'])]
|
||||||
#[ORM\JoinTable('chill_event_event_documents')]
|
#[ORM\JoinTable('chill_event_event_documents')]
|
||||||
@@ -192,7 +192,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
|
|||||||
{
|
{
|
||||||
$iterator = iterator_to_array($this->participations->getIterator());
|
$iterator = iterator_to_array($this->participations->getIterator());
|
||||||
|
|
||||||
uasort($iterator, static fn ($first, $second) => strnatcasecmp($first->getPerson()->getFirstName(), $second->getPerson()->getFirstName()));
|
uasort($iterator, static fn ($first, $second) => strnatcasecmp((string) $first->getPerson()->getFirstName(), (string) $second->getPerson()->getFirstName()));
|
||||||
|
|
||||||
return new \ArrayIterator($iterator);
|
return new \ArrayIterator($iterator);
|
||||||
}
|
}
|
||||||
|
@@ -38,13 +38,13 @@ class EventType
|
|||||||
private $name;
|
private $name;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, Role>
|
* @var Collection<Role>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(mappedBy: 'type', targetEntity: Role::class)]
|
#[ORM\OneToMany(targetEntity: Role::class, mappedBy: 'type')]
|
||||||
private Collection $roles;
|
private Collection $roles;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, Status>
|
* @var Collection<Status>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(targetEntity: Status::class, mappedBy: 'type')]
|
#[ORM\OneToMany(targetEntity: Status::class, mappedBy: 'type')]
|
||||||
private Collection $statuses;
|
private Collection $statuses;
|
||||||
|
@@ -12,7 +12,7 @@ declare(strict_types=1);
|
|||||||
namespace Chill\EventBundle\Form\ChoiceLoader;
|
namespace Chill\EventBundle\Form\ChoiceLoader;
|
||||||
|
|
||||||
use Chill\EventBundle\Entity\Event;
|
use Chill\EventBundle\Entity\Event;
|
||||||
use Chill\EventBundle\Repository\EventRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
|
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
|
||||||
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
|
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
|
||||||
|
|
||||||
@@ -26,6 +26,9 @@ class EventChoiceLoader implements ChoiceLoaderInterface
|
|||||||
*/
|
*/
|
||||||
protected $centers = [];
|
protected $centers = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var EntityRepository
|
||||||
|
*/
|
||||||
protected $eventRepository;
|
protected $eventRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,7 +40,7 @@ class EventChoiceLoader implements ChoiceLoaderInterface
|
|||||||
* EventChoiceLoader constructor.
|
* EventChoiceLoader constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
EventRepository $eventRepository,
|
EntityRepository $eventRepository,
|
||||||
?array $centers = null
|
?array $centers = null
|
||||||
) {
|
) {
|
||||||
$this->eventRepository = $eventRepository;
|
$this->eventRepository = $eventRepository;
|
||||||
|
@@ -11,65 +11,17 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\EventBundle\Repository;
|
namespace Chill\EventBundle\Repository;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
|
||||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
|
||||||
use Chill\EventBundle\Entity\Event;
|
use Chill\EventBundle\Entity\Event;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
use Doctrine\ORM\QueryBuilder;
|
|
||||||
use Doctrine\Persistence\ObjectRepository;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class EventRepository.
|
* Class EventRepository.
|
||||||
*/
|
*/
|
||||||
class EventRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface
|
class EventRepository extends ServiceEntityRepository
|
||||||
{
|
{
|
||||||
private readonly EntityRepository $repository;
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
|
||||||
public function __construct(EntityManagerInterface $entityManager)
|
|
||||||
{
|
{
|
||||||
$this->repository = $entityManager->getRepository(Event::class);
|
parent::__construct($registry, Event::class);
|
||||||
}
|
|
||||||
|
|
||||||
public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder
|
|
||||||
{
|
|
||||||
return $this->repository->createQueryBuilder($alias, $indexBy);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object
|
|
||||||
{
|
|
||||||
$qb = $this->createQueryBuilder('e');
|
|
||||||
$query = $qb
|
|
||||||
->join('e.documents', 'ed')
|
|
||||||
->where('ed.id = :storedObjectId')
|
|
||||||
->setParameter('storedObjectId', $storedObject->getId())
|
|
||||||
->getQuery();
|
|
||||||
|
|
||||||
return $query->getOneOrNullResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function find($id)
|
|
||||||
{
|
|
||||||
return $this->repository->find($id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function findAll(): array
|
|
||||||
{
|
|
||||||
return $this->repository->findAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
|
||||||
{
|
|
||||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function findOneBy(array $criteria)
|
|
||||||
{
|
|
||||||
return $this->repository->findOneBy($criteria);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getClassName(): string
|
|
||||||
{
|
|
||||||
return Event::class;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,55 +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\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\EventBundle\Entity\Event;
|
|
||||||
use Chill\EventBundle\Repository\EventRepository;
|
|
||||||
use Chill\EventBundle\Security\EventVoter;
|
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
|
|
||||||
class EventStoredObjectVoter extends AbstractStoredObjectVoter
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly EventRepository $repository,
|
|
||||||
Security $security,
|
|
||||||
WorkflowStoredObjectPermissionHelper $workflowDocumentService
|
|
||||||
) {
|
|
||||||
parent::__construct($security, $workflowDocumentService);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getRepository(): AssociatedEntityToStoredObjectInterface
|
|
||||||
{
|
|
||||||
return $this->repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getClass(): string
|
|
||||||
{
|
|
||||||
return Event::class;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
|
|
||||||
{
|
|
||||||
return match ($attribute) {
|
|
||||||
StoredObjectRoleEnum::EDIT => EventVoter::UPDATE,
|
|
||||||
StoredObjectRoleEnum::SEE => EventVoter::SEE_DETAILS,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function canBeAssociatedWithWorkflow(): bool
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -123,7 +123,6 @@ Role: Rôles
|
|||||||
Role creation: Nouveau rôle
|
Role creation: Nouveau rôle
|
||||||
Role edit: Modifier un rôle
|
Role edit: Modifier un rôle
|
||||||
|
|
||||||
'': ''
|
|
||||||
xlsx: xlsx
|
xlsx: xlsx
|
||||||
ods: ods
|
ods: ods
|
||||||
csv: csv
|
csv: csv
|
||||||
|
@@ -0,0 +1,180 @@
|
|||||||
|
<?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\Command;
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\HttpKernel\KernelInterface;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
use Symfony\Component\Console\Helper\Table;
|
||||||
|
|
||||||
|
|
||||||
|
class DetectTranslationDuplicatesCommand extends Command
|
||||||
|
{
|
||||||
|
protected static $defaultName = 'chill:detect-duplicate-translations';
|
||||||
|
|
||||||
|
public function __construct(private readonly TranslatorInterface $translator, private readonly KernelInterface $kernel)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setDescription('Detects duplicate translations in YAML files.')
|
||||||
|
->addOption('locale', null, InputOption::VALUE_REQUIRED, 'Locale to check for duplicate translations', 'en')
|
||||||
|
->addOption('exclude-namespaces', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'Namespaces to exclude from duplicate detection', [])
|
||||||
|
->addArgument('verify-hash', InputArgument::OPTIONAL, 'The expected hash to verify translation integrity');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$locale = $input->getOption('locale');
|
||||||
|
$excludedNamespaces = $input->getOption('exclude-namespaces');
|
||||||
|
$expectedHash = $input->getArgument('verify-hash');
|
||||||
|
|
||||||
|
// Loop through all bundles and get the translation directories
|
||||||
|
foreach ($this->kernel->getBundles() as $bundle) {
|
||||||
|
$bundlePath = $bundle->getPath();
|
||||||
|
$translationDir = $this->getTranslationDirectory($bundle->getName(), $bundlePath);
|
||||||
|
|
||||||
|
if ($translationDir && is_dir($translationDir)) {
|
||||||
|
foreach (glob($translationDir . '/*.yaml') as $file) {
|
||||||
|
$this->translator->addResource('yaml', $file, $locale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$catalogue = $this->translator->getCatalogue($locale);
|
||||||
|
|
||||||
|
$allTranslations = [];
|
||||||
|
|
||||||
|
// Iterate through each domain in the catalogue
|
||||||
|
foreach ($catalogue->all() as $domain => $translations) {
|
||||||
|
foreach ($translations as $key => $value) {
|
||||||
|
if ($this->isExcludedNamespace("$domain.$key", $excludedNamespaces)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (is_array($value)) {
|
||||||
|
$this->flattenTranslation($value, "$domain.$key", $allTranslations);
|
||||||
|
} else {
|
||||||
|
if (!isset($allTranslations[$value])) {
|
||||||
|
$allTranslations[$value] = [];
|
||||||
|
}
|
||||||
|
$allTranslations[$value][] = "$domain.$key";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect values that appear in more than one key
|
||||||
|
$duplicates = array_filter($allTranslations, function ($keys) {
|
||||||
|
return count($keys) > 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
$duplicatesHash = $this->generateDuplicatesHash($duplicates);
|
||||||
|
|
||||||
|
if ($expectedHash) {
|
||||||
|
if ($duplicatesHash === $expectedHash) {
|
||||||
|
$output->writeln('<info>Translations are consistent with the expected hash.</info>');
|
||||||
|
|
||||||
|
$output->writeln("<info>Current duplicate hash: $duplicatesHash</info>");
|
||||||
|
return Command::SUCCESS;
|
||||||
|
} else {
|
||||||
|
$output->writeln('<error>Translation hash mismatch! Potential duplicate added.</error>');
|
||||||
|
$this->renderDuplicatesTable($output, $duplicates, $locale);
|
||||||
|
|
||||||
|
$output->writeln("<info>Current duplicate hash: $duplicatesHash</info>");
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->renderDuplicatesTable($output, $duplicates, $locale);
|
||||||
|
|
||||||
|
$output->writeln("<info>Current duplicate hash: $duplicatesHash</info>");
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function flattenTranslation(array $translations, string $prefix, array &$allTranslations): void
|
||||||
|
{
|
||||||
|
foreach ($translations as $key => $value) {
|
||||||
|
$fullKey = "$prefix.$key";
|
||||||
|
if (is_array($value)) {
|
||||||
|
$this->flattenTranslation($value, $fullKey, $allTranslations);
|
||||||
|
} else {
|
||||||
|
if (!isset($allTranslations[$value])) {
|
||||||
|
$allTranslations[$value] = [];
|
||||||
|
}
|
||||||
|
$allTranslations[$value][] = $fullKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getTranslationDirectory(string $bundleName, string $bundlePath): ?string
|
||||||
|
{
|
||||||
|
$translationDir = $bundlePath . '/translations';
|
||||||
|
|
||||||
|
if ($bundleName === 'ChillAsideActivityBundle') {
|
||||||
|
$translationDir = $bundlePath . '/src/translations';
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_dir($translationDir) ? $translationDir : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function wrapText(string $text, int $width): string
|
||||||
|
{
|
||||||
|
return wordwrap($text, $width, "\n", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isExcludedNamespace(string $key, array $excludedNamespaces): bool
|
||||||
|
{
|
||||||
|
foreach ($excludedNamespaces as $namespace) {
|
||||||
|
if (str_starts_with($key, $namespace)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateDuplicatesHash(array $duplicates): string
|
||||||
|
{
|
||||||
|
ksort($duplicates);
|
||||||
|
foreach ($duplicates as $translation => $keys) {
|
||||||
|
sort($keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash('md5', serialize($duplicates));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function renderDuplicatesTable(OutputInterface $output, array $duplicates, string $locale): void
|
||||||
|
{
|
||||||
|
if (empty($duplicates)) {
|
||||||
|
$output->writeln("<info>No duplicate translations found for locale '$locale'.</info>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$output->writeln("<comment>Duplicate translations found for locale '$locale':</comment>");
|
||||||
|
$table = new Table($output);
|
||||||
|
$table->setHeaders(['Translation', 'Used in Keys']);
|
||||||
|
|
||||||
|
foreach ($duplicates as $translation => $keys) {
|
||||||
|
$wrappedTranslation = $this->wrapText($translation, 40);
|
||||||
|
$wrappedKeys = $this->wrapText(implode(', ', $keys), 80);
|
||||||
|
$table->addRow([$wrappedTranslation, $wrappedKeys]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table->render();
|
||||||
|
}
|
||||||
|
}
|
@@ -96,7 +96,7 @@ class SearchController extends AbstractController
|
|||||||
return $this->render('@ChillMain/Search/choose_list.html.twig');
|
return $this->render('@ChillMain/Search/choose_list.html.twig');
|
||||||
}
|
}
|
||||||
|
|
||||||
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/search.{_format}', name: 'chill_main_search', requirements: ['_format' => 'html|json', '_locale' => '[a-z]{1,3}'], defaults: ['_format' => 'html'])]
|
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/search.{_format}', name: 'chill_main_search', requirements: ['_format' => 'html|json'], defaults: ['_format' => 'html'])]
|
||||||
public function searchAction(Request $request, mixed $_format)
|
public function searchAction(Request $request, mixed $_format)
|
||||||
{
|
{
|
||||||
$pattern = trim((string) $request->query->get('q', ''));
|
$pattern = trim((string) $request->query->get('q', ''));
|
||||||
|
@@ -11,30 +11,25 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\MainBundle\Controller;
|
namespace Chill\MainBundle\Controller;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
|
||||||
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneParser;
|
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowComment;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
use Chill\MainBundle\Form\EntityWorkflowCommentType;
|
||||||
use Chill\MainBundle\Form\WorkflowSignatureMetadataType;
|
|
||||||
use Chill\MainBundle\Form\WorkflowStepType;
|
use Chill\MainBundle\Form\WorkflowStepType;
|
||||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
|
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
|
||||||
use Chill\MainBundle\Security\ChillSecurity;
|
use Chill\MainBundle\Security\ChillSecurity;
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\Clock\ClockInterface;
|
|
||||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||||
use Symfony\Component\Workflow\Registry;
|
use Symfony\Component\Workflow\Registry;
|
||||||
@@ -43,20 +38,7 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
|||||||
|
|
||||||
class WorkflowController extends AbstractController
|
class WorkflowController extends AbstractController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly EntityWorkflowRepository $entityWorkflowRepository, private readonly ValidatorInterface $validator, private readonly PaginatorFactory $paginatorFactory, private readonly Registry $registry, private readonly EntityManagerInterface $entityManager, private readonly TranslatorInterface $translator, private readonly ChillSecurity $security, private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry) {}
|
||||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
|
||||||
private readonly EntityWorkflowRepository $entityWorkflowRepository,
|
|
||||||
private readonly ValidatorInterface $validator,
|
|
||||||
private readonly StoredObjectManagerInterface $storedObjectManagerInterface,
|
|
||||||
private readonly PaginatorFactory $paginatorFactory,
|
|
||||||
private readonly Registry $registry,
|
|
||||||
private readonly EntityManagerInterface $entityManager,
|
|
||||||
private readonly TranslatorInterface $translator,
|
|
||||||
private readonly ChillSecurity $security,
|
|
||||||
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
|
|
||||||
private readonly ClockInterface $clock,
|
|
||||||
private readonly PDFSignatureZoneParser $PDFSignatureZoneParser,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
#[Route(path: '/{_locale}/main/workflow/create', name: 'chill_main_workflow_create')]
|
#[Route(path: '/{_locale}/main/workflow/create', name: 'chill_main_workflow_create')]
|
||||||
public function create(Request $request): Response
|
public function create(Request $request): Response
|
||||||
@@ -294,11 +276,10 @@ class WorkflowController extends AbstractController
|
|||||||
$handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
|
$handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
|
||||||
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||||
$errors = [];
|
$errors = [];
|
||||||
$signatures = $entityWorkflow->getCurrentStep()->getSignatures();
|
|
||||||
|
|
||||||
if (\count($workflow->getEnabledTransitions($entityWorkflow)) > 0) {
|
if (\count($workflow->getEnabledTransitions($entityWorkflow)) > 0) {
|
||||||
// possible transition
|
// possible transition
|
||||||
$stepDTO = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$usersInvolved = $entityWorkflow->getUsersInvolved();
|
$usersInvolved = $entityWorkflow->getUsersInvolved();
|
||||||
$currentUserFound = array_search($this->security->getUser(), $usersInvolved, true);
|
$currentUserFound = array_search($this->security->getUser(), $usersInvolved, true);
|
||||||
|
|
||||||
@@ -308,8 +289,9 @@ class WorkflowController extends AbstractController
|
|||||||
|
|
||||||
$transitionForm = $this->createForm(
|
$transitionForm = $this->createForm(
|
||||||
WorkflowStepType::class,
|
WorkflowStepType::class,
|
||||||
$stepDTO,
|
$entityWorkflow->getCurrentStep(),
|
||||||
[
|
[
|
||||||
|
'transition' => true,
|
||||||
'entity_workflow' => $entityWorkflow,
|
'entity_workflow' => $entityWorkflow,
|
||||||
'suggested_users' => $usersInvolved,
|
'suggested_users' => $usersInvolved,
|
||||||
]
|
]
|
||||||
@@ -328,14 +310,12 @@ class WorkflowController extends AbstractController
|
|||||||
throw $this->createAccessDeniedException(sprintf("not allowed to apply transition {$transition}: %s", implode(', ', $msgs)));
|
throw $this->createAccessDeniedException(sprintf("not allowed to apply transition {$transition}: %s", implode(', ', $msgs)));
|
||||||
}
|
}
|
||||||
|
|
||||||
$byUser = $this->security->getUser();
|
// TODO symfony 5: add those "future" on context ($workflow->apply($entityWorkflow, $transition, $context)
|
||||||
|
$entityWorkflow->futureCcUsers = $transitionForm['future_cc_users']->getData() ?? [];
|
||||||
|
$entityWorkflow->futureDestUsers = $transitionForm['future_dest_users']->getData() ?? [];
|
||||||
|
$entityWorkflow->futureDestEmails = $transitionForm['future_dest_emails']->getData() ?? [];
|
||||||
|
|
||||||
$workflow->apply($entityWorkflow, $transition, [
|
$workflow->apply($entityWorkflow, $transition);
|
||||||
'context' => $stepDTO,
|
|
||||||
'byUser' => $byUser,
|
|
||||||
'transition' => $transition,
|
|
||||||
'transitionAt' => $this->clock->now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
@@ -347,6 +327,22 @@ class WorkflowController extends AbstractController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
$commentForm = $this->createForm(EntityWorkflowCommentType::class, $newComment = new EntityWorkflowComment());
|
||||||
|
$commentForm->handleRequest($request);
|
||||||
|
|
||||||
|
if ($commentForm->isSubmitted() && $commentForm->isValid()) {
|
||||||
|
$this->entityManager->persist($newComment);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', $this->translator->trans('workflow.Comment added'));
|
||||||
|
|
||||||
|
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]);
|
||||||
|
} elseif ($commentForm->isSubmitted() && !$commentForm->isValid()) {
|
||||||
|
$this->addFlash('error', $this->translator->trans('This form contains errors'));
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
return $this->render(
|
return $this->render(
|
||||||
'@ChillMain/Workflow/index.html.twig',
|
'@ChillMain/Workflow/index.html.twig',
|
||||||
[
|
[
|
||||||
@@ -356,7 +352,7 @@ class WorkflowController extends AbstractController
|
|||||||
'transition_form' => isset($transitionForm) ? $transitionForm->createView() : null,
|
'transition_form' => isset($transitionForm) ? $transitionForm->createView() : null,
|
||||||
'entity_workflow' => $entityWorkflow,
|
'entity_workflow' => $entityWorkflow,
|
||||||
'transition_form_errors' => $errors,
|
'transition_form_errors' => $errors,
|
||||||
'signatures' => $signatures,
|
// 'comment_form' => $commentForm->createView(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -375,78 +371,4 @@ class WorkflowController extends AbstractController
|
|||||||
|
|
||||||
return $lines;
|
return $lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route(path: '/{_locale}/main/workflow/signature/{signature_id}/metadata', name: 'chill_main_workflow_signature_metadata')]
|
|
||||||
public function addSignatureMetadata(int $signature_id, Request $request): Response
|
|
||||||
{
|
|
||||||
$signature = $this->entityManager->getRepository(EntityWorkflowStepSignature::class)->find($signature_id);
|
|
||||||
|
|
||||||
if ($signature->getSigner() instanceof User) {
|
|
||||||
return $this->redirectToRoute('chill_main_workflow_signature', ['signature_id' => $signature_id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$metadataForm = $this->createForm(WorkflowSignatureMetadataType::class);
|
|
||||||
$metadataForm->add('submit', SubmitType::class, ['label' => $this->translator->trans('Save')]);
|
|
||||||
|
|
||||||
$metadataForm->handleRequest($request);
|
|
||||||
|
|
||||||
if ($metadataForm->isSubmitted() && $metadataForm->isValid()) {
|
|
||||||
$data = $metadataForm->getData();
|
|
||||||
|
|
||||||
$signature->setSignatureMetadata(
|
|
||||||
[
|
|
||||||
'base_signer' => [
|
|
||||||
'document_type' => $data['documentType'],
|
|
||||||
'document_number' => $data['documentNumber'],
|
|
||||||
'expiration_date' => $data['expirationDate'],
|
|
||||||
],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->entityManager->persist($signature);
|
|
||||||
$this->entityManager->flush();
|
|
||||||
|
|
||||||
return $this->redirectToRoute('chill_main_workflow_signature', ['signature_id' => $signature_id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->render(
|
|
||||||
'@ChillMain/Workflow/_signature_metadata.html.twig',
|
|
||||||
[
|
|
||||||
'metadata_form' => $metadataForm->createView(),
|
|
||||||
'person' => $signature->getSigner(),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route(path: '/{_locale}/main/workflow/signature/{signature_id}/sign', name: 'chill_main_workflow_signature', methods: 'GET')]
|
|
||||||
public function addSignature(int $signature_id, Request $request): Response
|
|
||||||
{
|
|
||||||
$signature = $this->entityManager->getRepository(EntityWorkflowStepSignature::class)->find($signature_id);
|
|
||||||
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
|
|
||||||
|
|
||||||
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
|
|
||||||
if (null === $storedObject) {
|
|
||||||
throw new NotFoundHttpException('No stored object found');
|
|
||||||
}
|
|
||||||
|
|
||||||
$zones = [];
|
|
||||||
$content = $this->storedObjectManagerInterface->read($storedObject);
|
|
||||||
if (null != $content) {
|
|
||||||
$zones = $this->PDFSignatureZoneParser->findSignatureZones($content);
|
|
||||||
}
|
|
||||||
|
|
||||||
$signatureClient = [];
|
|
||||||
$signatureClient['id'] = $signature->getId();
|
|
||||||
$signatureClient['storedObject'] = [
|
|
||||||
'filename' => $storedObject->getFilename(),
|
|
||||||
'iv' => $storedObject->getIv(),
|
|
||||||
'keyInfos' => $storedObject->getKeyInfos(),
|
|
||||||
];
|
|
||||||
$signatureClient['zones'] = $zones;
|
|
||||||
|
|
||||||
return $this->render(
|
|
||||||
'@ChillMain/Workflow/_signature_sign.html.twig',
|
|
||||||
['signature' => $signatureClient]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -85,29 +85,6 @@ class Configuration implements ConfigurationInterface
|
|||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
->end() // end of notifications
|
->end() // end of notifications
|
||||||
->arrayNode('workflow_signature')
|
|
||||||
->children()
|
|
||||||
->arrayNode('base_signer')
|
|
||||||
->children()
|
|
||||||
->arrayNode('document_kinds')
|
|
||||||
->arrayPrototype()
|
|
||||||
->children()
|
|
||||||
->scalarNode('key')->cannotBeEmpty()->end()
|
|
||||||
->arrayNode('labels')
|
|
||||||
->arrayPrototype()
|
|
||||||
->children()
|
|
||||||
->scalarNode('lang')->cannotBeEmpty()->end()
|
|
||||||
->scalarNode('label')->cannotBeEmpty()->end()
|
|
||||||
->end()
|
|
||||||
->end()
|
|
||||||
->end()
|
|
||||||
->end()
|
|
||||||
->end()
|
|
||||||
->end()
|
|
||||||
->end()
|
|
||||||
->end()
|
|
||||||
->end()
|
|
||||||
->end() // end of workflow signature document types
|
|
||||||
->arrayNode('phone_helper')
|
->arrayNode('phone_helper')
|
||||||
->canBeUnset()
|
->canBeUnset()
|
||||||
->children()
|
->children()
|
||||||
|
@@ -92,7 +92,7 @@ class Address implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
* This list is computed by a materialized view. It won't be populated until a refresh is done
|
* This list is computed by a materialized view. It won't be populated until a refresh is done
|
||||||
* on the materialized view.
|
* on the materialized view.
|
||||||
*
|
*
|
||||||
* @var Collection<int, GeographicalUnit>
|
* @var Collection<GeographicalUnit>
|
||||||
*
|
*
|
||||||
* @readonly
|
* @readonly
|
||||||
*/
|
*/
|
||||||
|
@@ -21,9 +21,9 @@ use Symfony\Component\Serializer\Annotation as Serializer;
|
|||||||
class Center implements HasCenterInterface, \Stringable
|
class Center implements HasCenterInterface, \Stringable
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, GroupCenter>
|
* @var Collection<GroupCenter>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(mappedBy: 'center', targetEntity: GroupCenter::class)]
|
#[ORM\OneToMany(targetEntity: GroupCenter::class, mappedBy: 'center')]
|
||||||
private Collection $groupCenters;
|
private Collection $groupCenters;
|
||||||
|
|
||||||
#[Serializer\Groups(['docgen:read'])]
|
#[Serializer\Groups(['docgen:read'])]
|
||||||
@@ -40,7 +40,7 @@ class Center implements HasCenterInterface, \Stringable
|
|||||||
private bool $isActive = true;
|
private bool $isActive = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, Regroupment>
|
* @var Collection<Regroupment>
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: Regroupment::class, mappedBy: 'centers')]
|
#[ORM\ManyToMany(targetEntity: Regroupment::class, mappedBy: 'centers')]
|
||||||
private Collection $regroupments;
|
private Collection $regroupments;
|
||||||
|
@@ -36,9 +36,9 @@ class GeographicalUnitLayer
|
|||||||
private string $refId = '';
|
private string $refId = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, GeographicalUnit>
|
* @var Collection<GeographicalUnit>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(mappedBy: 'layer', targetEntity: GeographicalUnit::class)]
|
#[ORM\OneToMany(targetEntity: GeographicalUnit::class, mappedBy: 'layer')]
|
||||||
private Collection $units;
|
private Collection $units;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
|
@@ -34,7 +34,7 @@ class GroupCenter
|
|||||||
private ?PermissionsGroup $permissionsGroup = null;
|
private ?PermissionsGroup $permissionsGroup = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, User>
|
* @var Collection<User::class>
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: User::class, mappedBy: 'groupCenters')]
|
#[ORM\ManyToMany(targetEntity: User::class, mappedBy: 'groupCenters')]
|
||||||
private Collection $users;
|
private Collection $users;
|
||||||
|
@@ -30,7 +30,7 @@ class Notification implements TrackUpdateInterface
|
|||||||
private array $addedAddresses = [];
|
private array $addedAddresses = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, User>
|
* @var Collection<User>
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: User::class)]
|
#[ORM\ManyToMany(targetEntity: User::class)]
|
||||||
#[ORM\JoinTable(name: 'chill_main_notification_addresses_user')]
|
#[ORM\JoinTable(name: 'chill_main_notification_addresses_user')]
|
||||||
@@ -54,9 +54,9 @@ class Notification implements TrackUpdateInterface
|
|||||||
private ?ArrayCollection $addressesOnLoad = null;
|
private ?ArrayCollection $addressesOnLoad = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, NotificationComment>
|
* @var Collection<NotificationComment>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(mappedBy: 'notification', targetEntity: NotificationComment::class, orphanRemoval: true)]
|
#[ORM\OneToMany(targetEntity: NotificationComment::class, mappedBy: 'notification', orphanRemoval: true)]
|
||||||
#[ORM\OrderBy(['createdAt' => \Doctrine\Common\Collections\Criteria::ASC])]
|
#[ORM\OrderBy(['createdAt' => \Doctrine\Common\Collections\Criteria::ASC])]
|
||||||
private Collection $comments;
|
private Collection $comments;
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ class Notification implements TrackUpdateInterface
|
|||||||
private string $title = '';
|
private string $title = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, User>
|
* @var Collection<User>
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: User::class)]
|
#[ORM\ManyToMany(targetEntity: User::class)]
|
||||||
#[ORM\JoinTable(name: 'chill_main_notification_addresses_unread')]
|
#[ORM\JoinTable(name: 'chill_main_notification_addresses_unread')]
|
||||||
|
@@ -28,9 +28,9 @@ class PermissionsGroup
|
|||||||
private array $flags = [];
|
private array $flags = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, GroupCenter>
|
* @var Collection<GroupCenter>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(mappedBy: 'permissionsGroup', targetEntity: GroupCenter::class)]
|
#[ORM\OneToMany(targetEntity: GroupCenter::class, mappedBy: 'permissionsGroup')]
|
||||||
private Collection $groupCenters;
|
private Collection $groupCenters;
|
||||||
|
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
@@ -42,7 +42,7 @@ class PermissionsGroup
|
|||||||
private string $name = '';
|
private string $name = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, RoleScope>
|
* @var Collection<RoleScope>
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: RoleScope::class, inversedBy: 'permissionsGroups', cascade: ['persist'])]
|
#[ORM\ManyToMany(targetEntity: RoleScope::class, inversedBy: 'permissionsGroups', cascade: ['persist'])]
|
||||||
#[ORM\Cache(usage: 'NONSTRICT_READ_WRITE')]
|
#[ORM\Cache(usage: 'NONSTRICT_READ_WRITE')]
|
||||||
|
@@ -20,7 +20,7 @@ use Doctrine\ORM\Mapping as ORM;
|
|||||||
class Regroupment
|
class Regroupment
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, Center>
|
* @var Collection<Center>
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: Center::class, inversedBy: 'regroupments')]
|
#[ORM\ManyToMany(targetEntity: Center::class, inversedBy: 'regroupments')]
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
|
@@ -26,7 +26,7 @@ class RoleScope
|
|||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, PermissionsGroup>
|
* @var Collection<PermissionsGroup>
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: PermissionsGroup::class, mappedBy: 'roleScopes')]
|
#[ORM\ManyToMany(targetEntity: PermissionsGroup::class, mappedBy: 'roleScopes')]
|
||||||
private Collection $permissionsGroups;
|
private Collection $permissionsGroups;
|
||||||
|
@@ -42,9 +42,9 @@ class Scope
|
|||||||
private array $name = [];
|
private array $name = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, RoleScope>
|
* @var Collection<RoleScope>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(mappedBy: 'scope', targetEntity: RoleScope::class)]
|
#[ORM\OneToMany(targetEntity: RoleScope::class, mappedBy: 'scope')]
|
||||||
#[ORM\Cache(usage: 'NONSTRICT_READ_WRITE')]
|
#[ORM\Cache(usage: 'NONSTRICT_READ_WRITE')]
|
||||||
private Collection $roleScopes;
|
private Collection $roleScopes;
|
||||||
|
|
||||||
|
@@ -64,7 +64,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
private bool $enabled = true;
|
private bool $enabled = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, \Chill\MainBundle\Entity\GroupCenter>
|
* @var Collection<GroupCenter>
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: GroupCenter::class, inversedBy: 'users')]
|
#[ORM\ManyToMany(targetEntity: GroupCenter::class, inversedBy: 'users')]
|
||||||
#[ORM\Cache(usage: 'NONSTRICT_READ_WRITE')]
|
#[ORM\Cache(usage: 'NONSTRICT_READ_WRITE')]
|
||||||
@@ -83,9 +83,9 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
private ?Location $mainLocation = null;
|
private ?Location $mainLocation = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var \Doctrine\Common\Collections\Collection<int, \Chill\MainBundle\Entity\User\UserScopeHistory>&Selectable
|
* @var Collection&Selectable<int, UserScopeHistory>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(mappedBy: 'user', targetEntity: UserScopeHistory::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
#[ORM\OneToMany(targetEntity: UserScopeHistory::class, mappedBy: 'user', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||||
private Collection&Selectable $scopeHistories;
|
private Collection&Selectable $scopeHistories;
|
||||||
|
|
||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)]
|
||||||
@@ -98,9 +98,9 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
private ?string $salt = null;
|
private ?string $salt = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var \Doctrine\Common\Collections\Collection<int, \Chill\MainBundle\Entity\User\UserJobHistory>&Selectable
|
* @var Collection&Selectable<int, UserJobHistory>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(mappedBy: 'user', targetEntity: UserJobHistory::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
#[ORM\OneToMany(targetEntity: UserJobHistory::class, mappedBy: 'user', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||||
private Collection&Selectable $jobHistories;
|
private Collection&Selectable $jobHistories;
|
||||||
|
|
||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 80)]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 80)]
|
||||||
|
@@ -17,9 +17,9 @@ use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
|||||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Workflow\Validator\EntityWorkflowCreation;
|
use Chill\MainBundle\Workflow\Validator\EntityWorkflowCreation;
|
||||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\Common\Collections\Order;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
@@ -35,7 +35,36 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
use TrackUpdateTrait;
|
use TrackUpdateTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, \Chill\MainBundle\Entity\Workflow\EntityWorkflowComment>
|
* a list of future cc users for the next steps.
|
||||||
|
*
|
||||||
|
* @var array|User[]
|
||||||
|
*/
|
||||||
|
public array $futureCcUsers = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a list of future dest emails for the next steps.
|
||||||
|
*
|
||||||
|
* This is in used in order to let controller inform who will be the future emails which will validate
|
||||||
|
* the next step. This is necessary to perform some computation about the next emails, before they are
|
||||||
|
* associated to the entity EntityWorkflowStep.
|
||||||
|
*
|
||||||
|
* @var array|string[]
|
||||||
|
*/
|
||||||
|
public array $futureDestEmails = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a list of future dest users for the next steps.
|
||||||
|
*
|
||||||
|
* This is in used in order to let controller inform who will be the future users which will validate
|
||||||
|
* the next step. This is necessary to perform some computation about the next users, before they are
|
||||||
|
* associated to the entity EntityWorkflowStep.
|
||||||
|
*
|
||||||
|
* @var array|User[]
|
||||||
|
*/
|
||||||
|
public array $futureDestUsers = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<EntityWorkflowComment>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(targetEntity: EntityWorkflowComment::class, mappedBy: 'entityWorkflow', orphanRemoval: true)]
|
#[ORM\OneToMany(targetEntity: EntityWorkflowComment::class, mappedBy: 'entityWorkflow', orphanRemoval: true)]
|
||||||
private Collection $comments;
|
private Collection $comments;
|
||||||
@@ -52,10 +81,10 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
private int $relatedEntityId;
|
private int $relatedEntityId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, EntityWorkflowStep>
|
* @var Collection<EntityWorkflowStep>
|
||||||
*/
|
*/
|
||||||
#[Assert\Valid(traverse: true)]
|
#[Assert\Valid(traverse: true)]
|
||||||
#[ORM\OneToMany(mappedBy: 'entityWorkflow', targetEntity: EntityWorkflowStep::class, cascade: ['persist'], orphanRemoval: true)]
|
#[ORM\OneToMany(targetEntity: EntityWorkflowStep::class, mappedBy: 'entityWorkflow', orphanRemoval: true, cascade: ['persist'])]
|
||||||
#[ORM\OrderBy(['transitionAt' => \Doctrine\Common\Collections\Criteria::ASC, 'id' => 'ASC'])]
|
#[ORM\OrderBy(['transitionAt' => \Doctrine\Common\Collections\Criteria::ASC, 'id' => 'ASC'])]
|
||||||
private Collection $steps;
|
private Collection $steps;
|
||||||
|
|
||||||
@@ -65,14 +94,14 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
private ?array $stepsChainedCache = null;
|
private ?array $stepsChainedCache = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, User>
|
* @var Collection<User>
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: User::class)]
|
#[ORM\ManyToMany(targetEntity: User::class)]
|
||||||
#[ORM\JoinTable(name: 'chill_main_workflow_entity_subscriber_to_final')]
|
#[ORM\JoinTable(name: 'chill_main_workflow_entity_subscriber_to_final')]
|
||||||
private Collection $subscriberToFinal;
|
private Collection $subscriberToFinal;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, User>
|
* @var Collection<User>
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: User::class)]
|
#[ORM\ManyToMany(targetEntity: User::class)]
|
||||||
#[ORM\JoinTable(name: 'chill_main_workflow_entity_subscriber_to_step')]
|
#[ORM\JoinTable(name: 'chill_main_workflow_entity_subscriber_to_step')]
|
||||||
@@ -247,16 +276,12 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
return $this->steps;
|
return $this->steps;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws \Exception
|
|
||||||
*/
|
|
||||||
public function getStepsChained(): array
|
public function getStepsChained(): array
|
||||||
{
|
{
|
||||||
if (\is_array($this->stepsChainedCache)) {
|
if (\is_array($this->stepsChainedCache)) {
|
||||||
return $this->stepsChainedCache;
|
return $this->stepsChainedCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var \ArrayIterator $iterator */
|
|
||||||
$iterator = $this->steps->getIterator();
|
$iterator = $this->steps->getIterator();
|
||||||
$current = null;
|
$current = null;
|
||||||
$steps = [];
|
$steps = [];
|
||||||
@@ -417,43 +442,11 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
*
|
*
|
||||||
* @return $this
|
* @return $this
|
||||||
*/
|
*/
|
||||||
public function setStep(
|
public function setStep(string $step): self
|
||||||
string $step,
|
{
|
||||||
WorkflowTransitionContextDTO $transitionContextDTO,
|
|
||||||
string $transition,
|
|
||||||
\DateTimeImmutable $transitionAt,
|
|
||||||
?User $byUser = null
|
|
||||||
): self {
|
|
||||||
$previousStep = $this->getCurrentStep();
|
|
||||||
|
|
||||||
$previousStep
|
|
||||||
->setTransitionAfter($transition)
|
|
||||||
->setTransitionAt($transitionAt)
|
|
||||||
->setTransitionBy($byUser);
|
|
||||||
|
|
||||||
$newStep = new EntityWorkflowStep();
|
$newStep = new EntityWorkflowStep();
|
||||||
$newStep->setCurrentStep($step);
|
$newStep->setCurrentStep($step);
|
||||||
|
|
||||||
foreach ($transitionContextDTO->futureCcUsers as $user) {
|
|
||||||
$newStep->addCcUser($user);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($transitionContextDTO->futureDestUsers as $user) {
|
|
||||||
$newStep->addDestUser($user);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($transitionContextDTO->futureDestEmails as $email) {
|
|
||||||
$newStep->addDestEmail($email);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null !== $transitionContextDTO->futureUserSignature) {
|
|
||||||
new EntityWorkflowStepSignature($newStep, $transitionContextDTO->futureUserSignature);
|
|
||||||
} else {
|
|
||||||
foreach ($transitionContextDTO->futurePersonSignatures as $personSignature) {
|
|
||||||
new EntityWorkflowStepSignature($newStep, $personSignature);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// copy the freeze
|
// copy the freeze
|
||||||
if ($this->isFreeze()) {
|
if ($this->isFreeze()) {
|
||||||
$newStep->setFreezeAfter(true);
|
$newStep->setFreezeAfter(true);
|
||||||
|
@@ -1,20 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Entity\Workflow;
|
|
||||||
|
|
||||||
enum EntityWorkflowSignatureStateEnum: string
|
|
||||||
{
|
|
||||||
case PENDING = 'pending';
|
|
||||||
case SIGNED = 'signed';
|
|
||||||
case REJECTED = 'rejected';
|
|
||||||
case CANCELED = 'canceled';
|
|
||||||
}
|
|
@@ -26,7 +26,7 @@ class EntityWorkflowStep
|
|||||||
private string $accessKey;
|
private string $accessKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, User>
|
* @var Collection<User>
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: User::class)]
|
#[ORM\ManyToMany(targetEntity: User::class)]
|
||||||
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_cc_user')]
|
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_cc_user')]
|
||||||
@@ -42,25 +42,19 @@ class EntityWorkflowStep
|
|||||||
private array $destEmail = [];
|
private array $destEmail = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, User>
|
* @var Collection<User>
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: User::class)]
|
#[ORM\ManyToMany(targetEntity: User::class)]
|
||||||
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user')]
|
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user')]
|
||||||
private Collection $destUser;
|
private Collection $destUser;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, User>
|
* @var Collection<User>
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: User::class)]
|
#[ORM\ManyToMany(targetEntity: User::class)]
|
||||||
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user_by_accesskey')]
|
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user_by_accesskey')]
|
||||||
private Collection $destUserByAccessKey;
|
private Collection $destUserByAccessKey;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var Collection <int, EntityWorkflowStepSignature>
|
|
||||||
*/
|
|
||||||
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepSignature::class, cascade: ['persist'], orphanRemoval: true)]
|
|
||||||
private Collection $signatures;
|
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: EntityWorkflow::class, inversedBy: 'steps')]
|
#[ORM\ManyToOne(targetEntity: EntityWorkflow::class, inversedBy: 'steps')]
|
||||||
private ?EntityWorkflow $entityWorkflow = null;
|
private ?EntityWorkflow $entityWorkflow = null;
|
||||||
|
|
||||||
@@ -103,7 +97,6 @@ class EntityWorkflowStep
|
|||||||
$this->ccUser = new ArrayCollection();
|
$this->ccUser = new ArrayCollection();
|
||||||
$this->destUser = new ArrayCollection();
|
$this->destUser = new ArrayCollection();
|
||||||
$this->destUserByAccessKey = new ArrayCollection();
|
$this->destUserByAccessKey = new ArrayCollection();
|
||||||
$this->signatures = new ArrayCollection();
|
|
||||||
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(32));
|
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(32));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,27 +136,6 @@ class EntityWorkflowStep
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal use @see{EntityWorkflowStepSignature}'s constructor instead
|
|
||||||
*/
|
|
||||||
public function addSignature(EntityWorkflowStepSignature $signature): self
|
|
||||||
{
|
|
||||||
if (!$this->signatures->contains($signature)) {
|
|
||||||
$this->signatures[] = $signature;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function removeSignature(EntityWorkflowStepSignature $signature): self
|
|
||||||
{
|
|
||||||
if ($this->signatures->contains($signature)) {
|
|
||||||
$this->signatures->removeElement($signature);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getAccessKey(): string
|
public function getAccessKey(): string
|
||||||
{
|
{
|
||||||
return $this->accessKey;
|
return $this->accessKey;
|
||||||
@@ -226,14 +198,6 @@ class EntityWorkflowStep
|
|||||||
return $this->entityWorkflow;
|
return $this->entityWorkflow;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Collection<int, EntityWorkflowStepSignature>
|
|
||||||
*/
|
|
||||||
public function getSignatures(): Collection
|
|
||||||
{
|
|
||||||
return $this->signatures;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
|
@@ -1,138 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Entity\Workflow;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
|
||||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
|
||||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
|
||||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
|
||||||
use Chill\MainBundle\Entity\User;
|
|
||||||
use Chill\PersonBundle\Entity\Person;
|
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
|
||||||
|
|
||||||
#[ORM\Entity]
|
|
||||||
#[ORM\Table(name: 'chill_main_workflow_entity_step_signature')]
|
|
||||||
class EntityWorkflowStepSignature implements TrackCreationInterface, TrackUpdateInterface
|
|
||||||
{
|
|
||||||
use TrackCreationTrait;
|
|
||||||
use TrackUpdateTrait;
|
|
||||||
|
|
||||||
#[ORM\Id]
|
|
||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, unique: true)]
|
|
||||||
#[ORM\GeneratedValue(strategy: 'AUTO')]
|
|
||||||
private ?int $id = null;
|
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
|
||||||
#[ORM\JoinColumn(nullable: true)]
|
|
||||||
private ?User $userSigner = null;
|
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: Person::class)]
|
|
||||||
#[ORM\JoinColumn(nullable: true)]
|
|
||||||
private ?Person $personSigner = null;
|
|
||||||
|
|
||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 50, nullable: false, enumType: EntityWorkflowSignatureStateEnum::class)]
|
|
||||||
private EntityWorkflowSignatureStateEnum $state = EntityWorkflowSignatureStateEnum::PENDING;
|
|
||||||
|
|
||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIMETZ_IMMUTABLE, nullable: true, options: ['default' => null])]
|
|
||||||
private ?\DateTimeImmutable $stateDate = null;
|
|
||||||
|
|
||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
|
|
||||||
private array $signatureMetadata = [];
|
|
||||||
|
|
||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true, options: ['default' => null])]
|
|
||||||
private ?int $zoneSignatureIndex = null;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
#[ORM\ManyToOne(targetEntity: EntityWorkflowStep::class, inversedBy: 'signatures')]
|
|
||||||
private EntityWorkflowStep $step,
|
|
||||||
User|Person $signer,
|
|
||||||
) {
|
|
||||||
$this->step->addSignature($this);
|
|
||||||
$this->setSigner($signer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function setSigner(User|Person $signer): void
|
|
||||||
{
|
|
||||||
if ($signer instanceof User) {
|
|
||||||
$this->userSigner = $signer;
|
|
||||||
} else {
|
|
||||||
$this->personSigner = $signer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getId(): ?int
|
|
||||||
{
|
|
||||||
return $this->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getStep(): EntityWorkflowStep
|
|
||||||
{
|
|
||||||
return $this->step;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getSigner(): User|Person
|
|
||||||
{
|
|
||||||
if (null !== $this->userSigner) {
|
|
||||||
return $this->userSigner;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->personSigner;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getSignatureMetadata(): array
|
|
||||||
{
|
|
||||||
return $this->signatureMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setSignatureMetadata(array $signatureMetadata): EntityWorkflowStepSignature
|
|
||||||
{
|
|
||||||
$this->signatureMetadata = $signatureMetadata;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getState(): EntityWorkflowSignatureStateEnum
|
|
||||||
{
|
|
||||||
return $this->state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setState(EntityWorkflowSignatureStateEnum $state): EntityWorkflowStepSignature
|
|
||||||
{
|
|
||||||
$this->state = $state;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getStateDate(): ?\DateTimeImmutable
|
|
||||||
{
|
|
||||||
return $this->stateDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setStateDate(?\DateTimeImmutable $stateDate): EntityWorkflowStepSignature
|
|
||||||
{
|
|
||||||
$this->stateDate = $stateDate;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getZoneSignatureIndex(): ?int
|
|
||||||
{
|
|
||||||
return $this->zoneSignatureIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setZoneSignatureIndex(?int $zoneSignatureIndex): EntityWorkflowStepSignature
|
|
||||||
{
|
|
||||||
$this->zoneSignatureIndex = $zoneSignatureIndex;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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\MainBundle\Form;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Form\Type\ChillDateType;
|
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
|
||||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
|
||||||
use Symfony\Component\Form\AbstractType;
|
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
|
||||||
|
|
||||||
class WorkflowSignatureMetadataType extends AbstractType
|
|
||||||
{
|
|
||||||
public function __construct(private readonly ParameterBagInterface $parameterBag, private readonly TranslatableStringHelperInterface $translatableStringHelper) {}
|
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
|
||||||
{
|
|
||||||
$documentTypeChoices = $this->parameterBag->get('chill_main')['workflow_signature']['base_signer']['document_kinds'];
|
|
||||||
|
|
||||||
$choices = [];
|
|
||||||
|
|
||||||
foreach ($documentTypeChoices as $documentType) {
|
|
||||||
$labels = [];
|
|
||||||
|
|
||||||
foreach ($documentType['labels'] as $label) {
|
|
||||||
$labels[$label['lang']] = $label['label'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$localizedLabel = $this->translatableStringHelper->localize($labels);
|
|
||||||
if (null !== $localizedLabel) {
|
|
||||||
$choices[$localizedLabel] = $documentType['key'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$builder
|
|
||||||
->add('documentType', ChoiceType::class, [
|
|
||||||
'label' => 'workflow.signature_zone.metadata.docType',
|
|
||||||
'expanded' => false,
|
|
||||||
'required' => true,
|
|
||||||
'choices' => $choices,
|
|
||||||
])
|
|
||||||
->add('documentNumber', TextType::class, [
|
|
||||||
'required' => true,
|
|
||||||
'label' => 'workflow.signature_zone.metadata.docNumber',
|
|
||||||
])
|
|
||||||
->add('expirationDate', ChillDateType::class, [
|
|
||||||
'required' => true,
|
|
||||||
'input' => 'datetime_immutable',
|
|
||||||
'label' => 'workflow.signature_zone.metadata.docExpiration',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user