mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge branch 'signature-app/OP730-create-entities-sending' into 'signature-app-master'
Implement feature to send document to an external See merge request Chill-Projet/chill-bundles!745
This commit is contained in:
commit
418794e586
@ -278,16 +278,10 @@ class StoredObject implements Document, TrackCreationInterface
|
|||||||
{
|
{
|
||||||
$versions = $this->getVersions()->toArray();
|
$versions = $this->getVersions()->toArray();
|
||||||
|
|
||||||
switch ($order) {
|
match ($order) {
|
||||||
case 'ASC':
|
'ASC' => usort($versions, static fn (StoredObjectVersion $a, StoredObjectVersion $b) => $a->getVersion() <=> $b->getVersion()),
|
||||||
usort($versions, static fn (StoredObjectVersion $a, StoredObjectVersion $b) => $a->getVersion() <=> $b->getVersion());
|
'DESC' => usort($versions, static fn (StoredObjectVersion $a, StoredObjectVersion $b) => $b->getVersion() <=> $a->getVersion()),
|
||||||
break;
|
};
|
||||||
case 'DESC':
|
|
||||||
usort($versions, static fn (StoredObjectVersion $a, StoredObjectVersion $b) => $b->getVersion() <=> $a->getVersion());
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new \UnexpectedValueException('Unknown order');
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ArrayCollection($versions);
|
return new ArrayCollection($versions);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
import {_createI18n} from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
|
||||||
|
import {createApp} from "vue";
|
||||||
|
import DocumentActionButtonsGroup from "../../vuejs/DocumentActionButtonsGroup.vue";
|
||||||
|
import {StoredObject, StoredObjectStatusChange} from "../../types";
|
||||||
|
import {defineComponent} from "vue";
|
||||||
|
import DownloadButton from "../../vuejs/StoredObjectButton/DownloadButton.vue";
|
||||||
|
import ToastPlugin from "vue-toast-notification";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const i18n = _createI18n({});
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', function (e) {
|
||||||
|
document.querySelectorAll<HTMLDivElement>('div[data-download-button-single]').forEach((el) => {
|
||||||
|
const storedObject = JSON.parse(el.dataset.storedObject as string) as StoredObject;
|
||||||
|
const title = el.dataset.title as string;
|
||||||
|
const app = createApp({
|
||||||
|
components: {DownloadButton},
|
||||||
|
data() {
|
||||||
|
return {storedObject, title, classes: {btn: true, "btn-outline-primary": true}};
|
||||||
|
},
|
||||||
|
template: '<download-button :stored-object="storedObject" :at-version="storedObject.currentVersion" :classes="classes" :filename="title" :direct-download="true"></download-button>',
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(i18n).use(ToastPlugin).mount(el);
|
||||||
|
});
|
||||||
|
});
|
@ -2,6 +2,7 @@ import {
|
|||||||
DateTime,
|
DateTime,
|
||||||
User,
|
User,
|
||||||
} from "../../../ChillMainBundle/Resources/public/types";
|
} from "../../../ChillMainBundle/Resources/public/types";
|
||||||
|
import {SignedUrlGet} from "./vuejs/StoredObjectButton/helpers";
|
||||||
|
|
||||||
export type StoredObjectStatus = "empty" | "ready" | "failure" | "pending";
|
export type StoredObjectStatus = "empty" | "ready" | "failure" | "pending";
|
||||||
|
|
||||||
@ -30,6 +31,7 @@ export interface StoredObject {
|
|||||||
href: string;
|
href: string;
|
||||||
expiration: number;
|
expiration: number;
|
||||||
};
|
};
|
||||||
|
downloadLink?: SignedUrlGet;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<a v-if="!state.is_ready" :class="props.classes" @click="download_and_open($event)" title="Télécharger">
|
<a v-if="!state.is_ready" :class="props.classes" @click="download_and_open()" title="Télécharger">
|
||||||
<i class="fa fa-download"></i>
|
<i class="fa fa-download"></i>
|
||||||
<template v-if="displayActionStringInButton">Télécharger</template>
|
<template v-if="displayActionStringInButton">Télécharger</template>
|
||||||
</a>
|
</a>
|
||||||
@ -20,7 +20,15 @@ interface DownloadButtonConfig {
|
|||||||
atVersion: StoredObjectVersion,
|
atVersion: StoredObjectVersion,
|
||||||
classes: { [k: string]: boolean },
|
classes: { [k: string]: boolean },
|
||||||
filename?: string,
|
filename?: string,
|
||||||
displayActionStringInButton: boolean,
|
/**
|
||||||
|
* if true, display the action string into the button. If false, displays only
|
||||||
|
* the icon
|
||||||
|
*/
|
||||||
|
displayActionStringInButton?: boolean,
|
||||||
|
/**
|
||||||
|
* if true, will download directly the file on load
|
||||||
|
*/
|
||||||
|
directDownload?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DownloadButtonState {
|
interface DownloadButtonState {
|
||||||
@ -29,13 +37,17 @@ interface DownloadButtonState {
|
|||||||
href_url: string,
|
href_url: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<DownloadButtonConfig>(), {displayActionStringInButton: true});
|
const props = withDefaults(defineProps<DownloadButtonConfig>(), {displayActionStringInButton: true, directDownload: false});
|
||||||
const state: DownloadButtonState = reactive({is_ready: false, is_running: false, href_url: "#"});
|
const state: DownloadButtonState = reactive({is_ready: false, is_running: false, href_url: "#"});
|
||||||
|
|
||||||
const open_button = ref<HTMLAnchorElement | null>(null);
|
const open_button = ref<HTMLAnchorElement | null>(null);
|
||||||
|
|
||||||
function buildDocumentName(): string {
|
function buildDocumentName(): string {
|
||||||
const document_name = props.filename ?? props.storedObject.title ?? 'document';
|
let document_name = props.filename ?? props.storedObject.title;
|
||||||
|
|
||||||
|
if ('' === document_name) {
|
||||||
|
document_name = 'document';
|
||||||
|
}
|
||||||
|
|
||||||
const ext = mime.getExtension(props.atVersion.type);
|
const ext = mime.getExtension(props.atVersion.type);
|
||||||
|
|
||||||
@ -46,9 +58,7 @@ function buildDocumentName(): string {
|
|||||||
return document_name;
|
return document_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function download_and_open(event: Event): Promise<void> {
|
async function download_and_open(): Promise<void> {
|
||||||
const button = event.target as HTMLAnchorElement;
|
|
||||||
|
|
||||||
if (state.is_running) {
|
if (state.is_running) {
|
||||||
console.log('state is running, aborting');
|
console.log('state is running, aborting');
|
||||||
return;
|
return;
|
||||||
@ -75,11 +85,13 @@ async function download_and_open(event: Event): Promise<void> {
|
|||||||
state.is_running = false;
|
state.is_running = false;
|
||||||
state.is_ready = true;
|
state.is_ready = true;
|
||||||
|
|
||||||
await nextTick();
|
if (!props.directDownload) {
|
||||||
open_button.value?.click();
|
await nextTick();
|
||||||
console.log('open button should have been clicked');
|
open_button.value?.click();
|
||||||
|
|
||||||
const timer = setTimeout(reset_state, 45000);
|
console.log('open button should have been clicked');
|
||||||
|
setTimeout(reset_state, 45000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset_state(): void {
|
function reset_state(): void {
|
||||||
@ -87,10 +99,19 @@ function reset_state(): void {
|
|||||||
state.is_ready = false;
|
state.is_ready = false;
|
||||||
state.is_running = false;
|
state.is_running = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.directDownload) {
|
||||||
|
download_and_open();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
i.fa::before {
|
i.fa::before {
|
||||||
color: var(--bs-dropdown-link-hover-color);
|
color: var(--bs-dropdown-link-hover-color);
|
||||||
}
|
}
|
||||||
|
i.fa {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -50,7 +50,6 @@ const onRestored = ({newVersion}: {newVersion: StoredObjectVersionWithPointInTim
|
|||||||
:version="v"
|
:version="v"
|
||||||
:can-edit="canEdit"
|
:can-edit="canEdit"
|
||||||
:is-current="higher_version === v.version"
|
:is-current="higher_version === v.version"
|
||||||
:is-restored="v.version === state.restored"
|
|
||||||
:stored-object="storedObject"
|
:stored-object="storedObject"
|
||||||
@restore-version="onRestored"
|
@restore-version="onRestored"
|
||||||
></history-button-list-item>
|
></history-button-list-item>
|
||||||
|
@ -12,7 +12,6 @@ interface HistoryButtonListItemConfig {
|
|||||||
storedObject: StoredObject;
|
storedObject: StoredObject;
|
||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
isCurrent: boolean;
|
isCurrent: boolean;
|
||||||
isRestored: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -31,7 +30,9 @@ const isKeptBeforeConversion = computed<boolean>(() => props.version["point-in-t
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const isRestored = computed<boolean>(() => null !== props.version["from-restored"]);
|
const isRestored = computed<boolean>(() => props.version.version > 0 && null !== props.version["from-restored"]);
|
||||||
|
|
||||||
|
const isDuplicated = computed<boolean>(() => props.version.version === 0 && null !== props.version["from-restored"]);
|
||||||
|
|
||||||
const classes = computed<{row: true, 'row-hover': true, 'blinking-1': boolean, 'blinking-2': boolean}>(() => ({row: true, 'row-hover': true, 'blinking-1': props.isRestored && 0 === props.version.version % 2, 'blinking-2': props.isRestored && 1 === props.version.version % 2}));
|
const classes = computed<{row: true, 'row-hover': true, 'blinking-1': boolean, 'blinking-2': boolean}>(() => ({row: true, 'row-hover': true, 'blinking-1': props.isRestored && 0 === props.version.version % 2, 'blinking-2': props.isRestored && 1 === props.version.version % 2}));
|
||||||
|
|
||||||
@ -39,13 +40,14 @@ const classes = computed<{row: true, 'row-hover': true, 'blinking-1': boolean, '
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="classes">
|
<div :class="classes">
|
||||||
<div class="col-12 tags" v-if="isCurrent || isKeptBeforeConversion || isRestored">
|
<div class="col-12 tags" v-if="isCurrent || isKeptBeforeConversion || isRestored || isDuplicated">
|
||||||
<span class="badge bg-success" v-if="isCurrent">Version actuelle</span>
|
<span class="badge bg-success" v-if="isCurrent">Version actuelle</span>
|
||||||
<span class="badge bg-info" v-if="isKeptBeforeConversion">Conservée avant conversion dans un autre format</span>
|
<span class="badge bg-info" v-if="isKeptBeforeConversion">Conservée avant conversion dans un autre format</span>
|
||||||
<span class="badge bg-info" v-if="isRestored">Restaurée depuis la version {{ version["from-restored"]?.version }}</span>
|
<span class="badge bg-info" v-if="isRestored">Restaurée depuis la version {{ version["from-restored"]?.version + 1 }}</span>
|
||||||
|
<span class="badge bg-info" v-if="isDuplicated">Dupliqué depuis un autre document</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<file-icon :type="version.type"></file-icon> <span><strong>#{{ version.version + 1 }}</strong></span> <strong v-if="version.version == 0">Créé par</strong><strong v-else>modifié par</strong> <span class="badge-user"><UserRenderBoxBadge :user="version.createdBy"></UserRenderBoxBadge></span> <strong>à</strong> {{ $d(ISOToDatetime(version.createdAt.datetime8601), 'long') }}
|
<file-icon :type="version.type"></file-icon> <span><strong>#{{ version.version + 1 }}</strong></span> <template v-if="version.createdBy !== null && version.createdAt !== null"><strong v-if="version.version == 0">Créé par</strong><strong v-else>modifié par</strong> <span class="badge-user"><UserRenderBoxBadge :user="version.createdBy"></UserRenderBoxBadge></span> <strong>à</strong> {{ $d(ISOToDatetime(version.createdAt.datetime8601), 'long') }}</template><template v-if="version.createdBy === null && version.createdAt !== null"><strong v-if="version.version == 0">Créé le</strong><strong v-else>modifié le</strong> {{ $d(ISOToDatetime(version.createdAt.datetime8601), 'long') }}</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<ul class="record_actions small slim on-version-actions">
|
<ul class="record_actions small slim on-version-actions">
|
||||||
|
@ -22,7 +22,6 @@ const props = defineProps<HistoryButtonListConfig>();
|
|||||||
const state = reactive<HistoryButtonModalState>({opened: false});
|
const state = reactive<HistoryButtonModalState>({opened: false});
|
||||||
|
|
||||||
const open = () => {
|
const open = () => {
|
||||||
console.log('open');
|
|
||||||
state.opened = true;
|
state.opened = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,7 +161,14 @@ async function download_and_decrypt_doc(storedObject: StoredObject, atVersion: n
|
|||||||
throw new Error("no version associated to stored object");
|
throw new Error("no version associated to stored object");
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloadInfo= await download_info_link(storedObject, atVersionToDownload);
|
// sometimes, the downloadInfo may be embedded into the storedObject
|
||||||
|
console.log('storedObject', storedObject);
|
||||||
|
let downloadInfo;
|
||||||
|
if (typeof storedObject._links !== 'undefined' && typeof storedObject._links.downloadLink !== 'undefined') {
|
||||||
|
downloadInfo = storedObject._links.downloadLink;
|
||||||
|
} else {
|
||||||
|
downloadInfo = await download_info_link(storedObject, atVersionToDownload);
|
||||||
|
}
|
||||||
|
|
||||||
const rawResponse = await window.fetch(downloadInfo.url);
|
const rawResponse = await window.fetch(downloadInfo.url);
|
||||||
|
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
<div data-download-button-single="data-download-button-single" data-stored-object="{{ document_json|json_encode|escape('html_attr') }}" data-title="{{ title|escape('html_attr') }}"></div>
|
@ -0,0 +1,43 @@
|
|||||||
|
{% extends '@ChillMain/Workflow/workflow_view_send_public_layout.html.twig' %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{{ parent() }}
|
||||||
|
{{ encore_entry_link_tags('mod_document_download_button') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
{{ parent() }}
|
||||||
|
{{ encore_entry_script_tags('mod_document_download_button') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block title %}{{ 'workflow.public_link.title'|trans }} - {{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block public_content %}
|
||||||
|
<h1>{{ 'workflow.public_link.shared_doc'|trans }}</h1>
|
||||||
|
|
||||||
|
{% set previous = send.entityWorkflowStepChained.previous %}
|
||||||
|
{% if previous is not null %}
|
||||||
|
{% if previous.transitionBy is not null %}
|
||||||
|
<p>{{ 'workflow.public_link.doc_shared_by_at_explanation'|trans({'byUser': previous.transitionBy|chill_entity_render_string( { 'at_date': previous.transitionAt } ), 'at': previous.transitionAt }) }}</p>
|
||||||
|
{% else %}
|
||||||
|
<p>{{ 'workflow.public_link.doc_shared_automatically_at_explanation'|trans({'at': previous.transitionAt}) }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12 col-sm-6 col-md-4">
|
||||||
|
<div class="card"">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title">{{ title }}</h2>
|
||||||
|
<h3>{{ 'workflow.public_link.main_document'|trans }}</h3>
|
||||||
|
|
||||||
|
<ul class="record_actions slim small">
|
||||||
|
<li>
|
||||||
|
{{ storedObject|chill_document_download_only_button(storedObject.title(), false) }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -11,6 +11,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\DocStoreBundle\Serializer\Normalizer;
|
namespace Chill\DocStoreBundle\Serializer\Normalizer;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||||
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\Guard\JWTDavTokenProviderInterface;
|
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
|
||||||
@ -30,10 +31,17 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa
|
|||||||
{
|
{
|
||||||
use NormalizerAwareTrait;
|
use NormalizerAwareTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* when added to the groups, a download link is included in the normalization,
|
||||||
|
* and no webdav links are generated.
|
||||||
|
*/
|
||||||
|
public const DOWNLOAD_LINK_ONLY = 'read:download-link-only';
|
||||||
|
|
||||||
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,
|
private readonly Security $security,
|
||||||
|
private readonly TempUrlGeneratorInterface $tempUrlGenerator,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function normalize($object, ?string $format = null, array $context = [])
|
public function normalize($object, ?string $format = null, array $context = [])
|
||||||
@ -55,6 +63,24 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa
|
|||||||
// deprecated property
|
// deprecated property
|
||||||
$datas['creationDate'] = $datas['createdAt'];
|
$datas['creationDate'] = $datas['createdAt'];
|
||||||
|
|
||||||
|
if (array_key_exists(AbstractNormalizer::GROUPS, $context)) {
|
||||||
|
$groupsNormalized = is_array($context[AbstractNormalizer::GROUPS]) ? $context[AbstractNormalizer::GROUPS] : [$context[AbstractNormalizer::GROUPS]];
|
||||||
|
} else {
|
||||||
|
$groupsNormalized = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array(self::DOWNLOAD_LINK_ONLY, $groupsNormalized, true)) {
|
||||||
|
$datas['_permissions'] = [
|
||||||
|
'canSee' => true,
|
||||||
|
'canEdit' => false,
|
||||||
|
];
|
||||||
|
$datas['_links'] = [
|
||||||
|
'downloadLink' => $this->normalizer->normalize($this->tempUrlGenerator->generate('GET', $object->getCurrentVersion()->getFilename(), 180), $format, [AbstractNormalizer::GROUPS => ['read']]),
|
||||||
|
];
|
||||||
|
|
||||||
|
return $datas;
|
||||||
|
}
|
||||||
|
|
||||||
$canSee = $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $object);
|
$canSee = $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $object);
|
||||||
$canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $object);
|
$canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $object);
|
||||||
|
|
||||||
|
@ -14,28 +14,41 @@ namespace Chill\DocStoreBundle\Service;
|
|||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
use Symfony\Component\Workflow\Registry;
|
||||||
|
|
||||||
class WorkflowStoredObjectPermissionHelper
|
class WorkflowStoredObjectPermissionHelper
|
||||||
{
|
{
|
||||||
public function __construct(private readonly Security $security, private readonly EntityWorkflowManager $entityWorkflowManager) {}
|
public function __construct(
|
||||||
|
private readonly Security $security,
|
||||||
|
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||||
|
private readonly Registry $registry,
|
||||||
|
) {}
|
||||||
|
|
||||||
public function notBlockedByWorkflow(object $entity): bool
|
public function notBlockedByWorkflow(object $entity): bool
|
||||||
{
|
{
|
||||||
$workflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
|
$entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
|
||||||
$currentUser = $this->security->getUser();
|
$currentUser = $this->security->getUser();
|
||||||
|
|
||||||
foreach ($workflows as $workflow) {
|
foreach ($entityWorkflows as $entityWorkflow) {
|
||||||
if ($workflow->isFinal()) {
|
if ($entityWorkflow->isFinal()) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$workflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) {
|
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||||
return false;
|
$marking = $workflow->getMarkingStore()->getMarking($entityWorkflow);
|
||||||
|
foreach ($marking->getPlaces() as $place => $active) {
|
||||||
|
$metadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
|
||||||
|
if ($metadata['isFinalPositive'] ?? true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!$entityWorkflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// as soon as there is one signatured applyied, we are not able to
|
// as soon as there is one signatured applyied, we are not able to
|
||||||
// edit the document any more
|
// edit the document any more
|
||||||
foreach ($workflow->getSteps() as $step) {
|
foreach ($entityWorkflow->getSteps() as $step) {
|
||||||
foreach ($step->getSignatures() as $signature) {
|
foreach ($step->getSignatures() as $signature) {
|
||||||
if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) {
|
if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -28,6 +28,10 @@ class WopiEditTwigExtension extends AbstractExtension
|
|||||||
'needs_environment' => true,
|
'needs_environment' => true,
|
||||||
'is_safe' => ['html'],
|
'is_safe' => ['html'],
|
||||||
]),
|
]),
|
||||||
|
new TwigFilter('chill_document_download_only_button', [WopiEditTwigExtensionRuntime::class, 'renderDownloadButton'], [
|
||||||
|
'needs_environment' => true,
|
||||||
|
'is_safe' => ['html'],
|
||||||
|
]),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ use ChampsLibres\WopiLib\Contract\Service\Discovery\DiscoveryInterface;
|
|||||||
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\Guard\JWTDavTokenProviderInterface;
|
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
|
||||||
|
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectNormalizer;
|
||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||||
@ -177,6 +178,17 @@ final readonly class WopiEditTwigExtensionRuntime implements RuntimeExtensionInt
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function renderDownloadButton(Environment $environment, StoredObject $storedObject, string $title = ''): string
|
||||||
|
{
|
||||||
|
return $environment->render(
|
||||||
|
'@ChillDocStore/Button/button_download.html.twig',
|
||||||
|
[
|
||||||
|
'document_json' => $this->normalizer->normalize($storedObject, 'json', [AbstractNormalizer::GROUPS => ['read', StoredObjectNormalizer::DOWNLOAD_LINK_ONLY]]),
|
||||||
|
'title' => $title,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function renderEditButton(Environment $environment, StoredObject $document, ?array $options = null): string
|
public function renderEditButton(Environment $environment, StoredObject $document, ?array $options = null): string
|
||||||
{
|
{
|
||||||
return $environment->render(self::TEMPLATE, [
|
return $environment->render(self::TEMPLATE, [
|
||||||
|
@ -11,6 +11,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\DocStoreBundle\Tests\Form;
|
namespace Chill\DocStoreBundle\Tests\Form;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Chill\DocStoreBundle\Form\DataMapper\StoredObjectDataMapper;
|
use Chill\DocStoreBundle\Form\DataMapper\StoredObjectDataMapper;
|
||||||
use Chill\DocStoreBundle\Form\DataTransformer\StoredObjectDataTransformer;
|
use Chill\DocStoreBundle\Form\DataTransformer\StoredObjectDataTransformer;
|
||||||
@ -132,7 +133,8 @@ class StoredObjectTypeTest extends TypeTestCase
|
|||||||
new StoredObjectNormalizer(
|
new StoredObjectNormalizer(
|
||||||
$jwtTokenProvider->reveal(),
|
$jwtTokenProvider->reveal(),
|
||||||
$urlGenerator->reveal(),
|
$urlGenerator->reveal(),
|
||||||
$security->reveal()
|
$security->reveal(),
|
||||||
|
$this->createMock(TempUrlGeneratorInterface::class)
|
||||||
),
|
),
|
||||||
new StoredObjectDenormalizer($storedObjectRepository->reveal()),
|
new StoredObjectDenormalizer($storedObjectRepository->reveal()),
|
||||||
new StoredObjectVersionNormalizer(),
|
new StoredObjectVersionNormalizer(),
|
||||||
|
@ -11,6 +11,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\DocStoreBundle\Tests\Serializer\Normalizer;
|
namespace Chill\DocStoreBundle\Tests\Serializer\Normalizer;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||||
|
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||||
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\Guard\JWTDavTokenProviderInterface;
|
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
|
||||||
@ -70,7 +72,9 @@ class StoredObjectNormalizerTest extends TestCase
|
|||||||
return ['sub' => 'sub'];
|
return ['sub' => 'sub'];
|
||||||
});
|
});
|
||||||
|
|
||||||
$normalizer = new StoredObjectNormalizer($jwtProvider, $urlGenerator, $security);
|
$tempUrlGenerator = $this->createMock(TempUrlGeneratorInterface::class);
|
||||||
|
|
||||||
|
$normalizer = new StoredObjectNormalizer($jwtProvider, $urlGenerator, $security, $tempUrlGenerator);
|
||||||
$normalizer->setNormalizer($globalNormalizer);
|
$normalizer->setNormalizer($globalNormalizer);
|
||||||
|
|
||||||
$actual = $normalizer->normalize($storedObject, 'json');
|
$actual = $normalizer->normalize($storedObject, 'json');
|
||||||
@ -95,4 +99,48 @@ class StoredObjectNormalizerTest extends TestCase
|
|||||||
self::assertArrayHasKey('dav_link', $actual['_links']);
|
self::assertArrayHasKey('dav_link', $actual['_links']);
|
||||||
self::assertEqualsCanonicalizing(['href' => $davLink, 'expiration' => $d->getTimestamp()], $actual['_links']['dav_link']);
|
self::assertEqualsCanonicalizing(['href' => $davLink, 'expiration' => $d->getTimestamp()], $actual['_links']['dav_link']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testWithDownloadLinkOnly(): void
|
||||||
|
{
|
||||||
|
$storedObject = new StoredObject();
|
||||||
|
$storedObject->registerVersion();
|
||||||
|
$storedObject->setTitle('test');
|
||||||
|
$reflection = new \ReflectionClass(StoredObject::class);
|
||||||
|
$idProperty = $reflection->getProperty('id');
|
||||||
|
$idProperty->setValue($storedObject, 1);
|
||||||
|
|
||||||
|
$jwtProvider = $this->createMock(JWTDavTokenProviderInterface::class);
|
||||||
|
$jwtProvider->expects($this->never())->method('createToken')->withAnyParameters();
|
||||||
|
|
||||||
|
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||||
|
$urlGenerator->expects($this->never())->method('generate');
|
||||||
|
|
||||||
|
$security = $this->createMock(Security::class);
|
||||||
|
$security->expects($this->never())->method('isGranted');
|
||||||
|
|
||||||
|
$globalNormalizer = $this->createMock(NormalizerInterface::class);
|
||||||
|
$globalNormalizer->expects($this->exactly(4))->method('normalize')
|
||||||
|
->withAnyParameters()
|
||||||
|
->willReturnCallback(function (?object $object, string $format, array $context) {
|
||||||
|
if (null === $object) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['sub' => 'sub'];
|
||||||
|
});
|
||||||
|
|
||||||
|
$tempUrlGenerator = $this->createMock(TempUrlGeneratorInterface::class);
|
||||||
|
$tempUrlGenerator->expects($this->once())->method('generate')->with('GET', $storedObject->getCurrentVersion()->getFilename(), $this->isType('int'))
|
||||||
|
->willReturn(new SignedUrl('GET', 'https://some-link/test', new \DateTimeImmutable('300 seconds'), $storedObject->getCurrentVersion()->getFilename()));
|
||||||
|
|
||||||
|
$normalizer = new StoredObjectNormalizer($jwtProvider, $urlGenerator, $security, $tempUrlGenerator);
|
||||||
|
$normalizer->setNormalizer($globalNormalizer);
|
||||||
|
|
||||||
|
$actual = $normalizer->normalize($storedObject, 'json', ['groups' => ['read', 'read:download-link-only']]);
|
||||||
|
|
||||||
|
self::assertIsArray($actual);
|
||||||
|
self::assertArrayHasKey('_links', $actual);
|
||||||
|
self::assertArrayHasKey('downloadLink', $actual['_links']);
|
||||||
|
self::assertEquals(['sub' => 'sub'], $actual['_links']['downloadLink']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,12 +17,19 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
|||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
|
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
|
||||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
use Prophecy\PhpUnit\ProphecyTrait;
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
use Symfony\Component\Workflow\DefinitionBuilder;
|
||||||
|
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
|
||||||
|
use Symfony\Component\Workflow\Registry;
|
||||||
|
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
|
||||||
|
use Symfony\Component\Workflow\Workflow;
|
||||||
|
use Symfony\Component\Workflow\WorkflowInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
@ -38,6 +45,8 @@ class WorkflowStoredObjectPermissionHelperTest extends TestCase
|
|||||||
*/
|
*/
|
||||||
public function testNotBlockByWorkflow(EntityWorkflow $entityWorkflow, User $user, bool $expected, string $message): void
|
public function testNotBlockByWorkflow(EntityWorkflow $entityWorkflow, User $user, bool $expected, string $message): void
|
||||||
{
|
{
|
||||||
|
// all entities must have this workflow name, so we are ok to set it here
|
||||||
|
$entityWorkflow->setWorkflowName('dummy');
|
||||||
$object = new \stdClass();
|
$object = new \stdClass();
|
||||||
$helper = $this->buildHelper($object, $entityWorkflow, $user);
|
$helper = $this->buildHelper($object, $entityWorkflow, $user);
|
||||||
|
|
||||||
@ -52,7 +61,7 @@ class WorkflowStoredObjectPermissionHelperTest extends TestCase
|
|||||||
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
||||||
$entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn([$entityWorkflow]);
|
$entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn([$entityWorkflow]);
|
||||||
|
|
||||||
return new WorkflowStoredObjectPermissionHelper($security->reveal(), $entityWorkflowManager->reveal());
|
return new WorkflowStoredObjectPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $this->buildRegistry());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function provideDataNotBlockByWorkflow(): iterable
|
public static function provideDataNotBlockByWorkflow(): iterable
|
||||||
@ -73,10 +82,18 @@ class WorkflowStoredObjectPermissionHelperTest extends TestCase
|
|||||||
$entityWorkflow = new EntityWorkflow();
|
$entityWorkflow = new EntityWorkflow();
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
$dto->futureDestUsers[] = $user = new User();
|
$dto->futureDestUsers[] = $user = new User();
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user);
|
$entityWorkflow->setStep('final_positive', $dto, 'to_final_positive', new \DateTimeImmutable(), $user);
|
||||||
$entityWorkflow->getCurrentStep()->setIsFinal(true);
|
$entityWorkflow->getCurrentStep()->setIsFinal(true);
|
||||||
|
|
||||||
yield [$entityWorkflow, $user, false, 'blocked because the step is final'];
|
yield [$entityWorkflow, $user, false, 'blocked because the step is final, and final positive'];
|
||||||
|
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futureDestUsers[] = $user = new User();
|
||||||
|
$entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable(), $user);
|
||||||
|
$entityWorkflow->getCurrentStep()->setIsFinal(true);
|
||||||
|
|
||||||
|
yield [$entityWorkflow, $user, true, 'allowed because the step is final, and final negative'];
|
||||||
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
$entityWorkflow = new EntityWorkflow();
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
@ -97,5 +114,48 @@ class WorkflowStoredObjectPermissionHelperTest extends TestCase
|
|||||||
|
|
||||||
yield [$entityWorkflow, $user, false, 'blocked, a signature is present and signed'];
|
yield [$entityWorkflow, $user, false, 'blocked, a signature is present and signed'];
|
||||||
|
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futureDestUsers[] = $user = new User();
|
||||||
|
$entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable(), $user);
|
||||||
|
$step = $entityWorkflow->getCurrentStep();
|
||||||
|
$signature = new EntityWorkflowStepSignature($step, new Person());
|
||||||
|
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED);
|
||||||
|
|
||||||
|
yield [$entityWorkflow, $user, false, 'blocked, a signature is present and signed, although the workflow is final negative'];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function buildRegistry(): Registry
|
||||||
|
{
|
||||||
|
$builder = new DefinitionBuilder();
|
||||||
|
$builder
|
||||||
|
->setInitialPlaces(['initial'])
|
||||||
|
->addPlaces(['initial', 'test', 'final_positive', 'final_negative'])
|
||||||
|
->setMetadataStore(
|
||||||
|
new InMemoryMetadataStore(
|
||||||
|
placesMetadata: [
|
||||||
|
'final_positive' => [
|
||||||
|
'isFinal' => true,
|
||||||
|
'isFinalPositive' => true,
|
||||||
|
],
|
||||||
|
'final_negative' => [
|
||||||
|
'isFinal' => true,
|
||||||
|
'isFinalPositive' => false,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$workflow = new Workflow($builder->build(), new EntityWorkflowMarkingStore(), name: 'dummy');
|
||||||
|
$registry = new Registry();
|
||||||
|
$registry->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface {
|
||||||
|
public function supports(WorkflowInterface $workflow, object $subject): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return $registry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,20 +16,24 @@ use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
|
|||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
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\Entity\Workflow\EntityWorkflowSend;
|
||||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||||
|
use Chill\MainBundle\Workflow\EntityWorkflowWithPublicViewInterface;
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface;
|
use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface;
|
||||||
|
use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO;
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
|
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @implements EntityWorkflowWithStoredObjectHandlerInterface<AccompanyingCourseDocument>
|
* @implements EntityWorkflowWithStoredObjectHandlerInterface<AccompanyingCourseDocument>
|
||||||
*/
|
*/
|
||||||
readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowWithStoredObjectHandlerInterface
|
final readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowWithStoredObjectHandlerInterface, EntityWorkflowWithPublicViewInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private TranslatorInterface $translator,
|
private TranslatorInterface $translator,
|
||||||
private EntityWorkflowRepository $workflowRepository,
|
private EntityWorkflowRepository $workflowRepository,
|
||||||
private AccompanyingCourseDocumentRepository $repository,
|
private AccompanyingCourseDocumentRepository $repository,
|
||||||
|
private WorkflowWithPublicViewDocumentHelper $publicViewDocumentHelper,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function getDeletionRoles(): array
|
public function getDeletionRoles(): array
|
||||||
@ -136,4 +140,9 @@ readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkfl
|
|||||||
|
|
||||||
return $this->workflowRepository->findByRelatedEntity(AccompanyingCourseDocument::class, $object->getId());
|
return $this->workflowRepository->findByRelatedEntity(AccompanyingCourseDocument::class, $object->getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function renderPublicView(EntityWorkflowSend $entityWorkflowSend, EntityWorkflowViewMetadataDTO $metadata): string
|
||||||
|
{
|
||||||
|
return $this->publicViewDocumentHelper->render($entityWorkflowSend, $metadata, $this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\DocStoreBundle\Workflow;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
|
||||||
|
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
|
||||||
|
use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface;
|
||||||
|
use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
class WorkflowWithPublicViewDocumentHelper
|
||||||
|
{
|
||||||
|
public function __construct(private readonly Environment $twig) {}
|
||||||
|
|
||||||
|
public function render(EntityWorkflowSend $send, EntityWorkflowViewMetadataDTO $metadata, EntityWorkflowHandlerInterface&EntityWorkflowWithStoredObjectHandlerInterface $handler): string
|
||||||
|
{
|
||||||
|
$entityWorkflow = $send->getEntityWorkflowStep()->getEntityWorkflow();
|
||||||
|
$storedObject = $handler->getAssociatedStoredObject($entityWorkflow);
|
||||||
|
|
||||||
|
if (null === $storedObject) {
|
||||||
|
return 'document removed';
|
||||||
|
}
|
||||||
|
|
||||||
|
$title = $handler->getEntityTitle($entityWorkflow);
|
||||||
|
|
||||||
|
return $this->twig->render(
|
||||||
|
'@ChillDocStore/Workflow/public_view_with_document_render.html.twig',
|
||||||
|
[
|
||||||
|
'title' => $title,
|
||||||
|
'storedObject' => $storedObject,
|
||||||
|
'send' => $send,
|
||||||
|
'metadata' => $metadata,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -5,5 +5,6 @@ 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');
|
encore.addEntry('mod_document_download_button', __dirname + '/Resources/public/module/button_download/index');
|
||||||
|
encore.addEntry('vue_document_signature', __dirname + '/Resources/public/vuejs/DocumentSignature/index');
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,11 @@
|
|||||||
acc_course_document:
|
acc_course_document:
|
||||||
duplicated_at: >-
|
duplicated_at: >-
|
||||||
Dupliqué le {at, date, long} à {at, time, short}
|
Dupliqué le {at, date, long} à {at, time, short}
|
||||||
|
|
||||||
|
workflow:
|
||||||
|
public_link:
|
||||||
|
doc_shared_by_at_explanation: >-
|
||||||
|
Le document a été partagé avec vous par {byUser}, le {at, date, long} à {at, time, short}.
|
||||||
|
doc_shared_automatically_at_explanation: >-
|
||||||
|
Le document a été partagé avec vous le {at, date, long} à {at, time, short}
|
||||||
|
|
||||||
|
@ -80,6 +80,10 @@ online_edit_document: Éditer en ligne
|
|||||||
|
|
||||||
workflow:
|
workflow:
|
||||||
Document deleted: Document supprimé
|
Document deleted: Document supprimé
|
||||||
|
public_link:
|
||||||
|
shared_doc: Document partagé
|
||||||
|
title: Document partagé
|
||||||
|
main_document: Document principal
|
||||||
|
|
||||||
# ROLES
|
# ROLES
|
||||||
accompanyingCourseDocument: Documents dans les parcours d'accompagnement
|
accompanyingCourseDocument: Documents dans les parcours d'accompagnement
|
||||||
|
@ -71,7 +71,7 @@ final readonly class WorkflowAddSignatureController
|
|||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
$this->twig->render(
|
$this->twig->render(
|
||||||
'@ChillMain/Workflow/_signature_sign.html.twig',
|
'@ChillMain/Workflow/signature_sign.html.twig',
|
||||||
['signature' => $signatureClient]
|
['signature' => $signatureClient]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -430,7 +430,7 @@ class WorkflowController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
return $this->render(
|
return $this->render(
|
||||||
'@ChillMain/Workflow/_signature_metadata.html.twig',
|
'@ChillMain/Workflow/signature_metadata.html.twig',
|
||||||
[
|
[
|
||||||
'metadata_form' => $metadataForm->createView(),
|
'metadata_form' => $metadataForm->createView(),
|
||||||
'person' => $signature->getSigner(),
|
'person' => $signature->getSigner(),
|
||||||
|
@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Controller;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSendView;
|
||||||
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
|
use Chill\MainBundle\Workflow\Exception\HandlerWithPublicViewNotFoundException;
|
||||||
|
use Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage;
|
||||||
|
use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\Clock\ClockInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
final readonly class WorkflowViewSendPublicController
|
||||||
|
{
|
||||||
|
public const LOG_PREFIX = '[workflow-view-send-public-controller] ';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private EntityManagerInterface $entityManager,
|
||||||
|
private LoggerInterface $chillLogger,
|
||||||
|
private EntityWorkflowManager $entityWorkflowManager,
|
||||||
|
private ClockInterface $clock,
|
||||||
|
private Environment $environment,
|
||||||
|
private MessageBusInterface $messageBus,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/public/main/workflow/send/{uuid}/view/{verificationKey}', name: 'chill_main_workflow_send_view_public', methods: ['GET'])]
|
||||||
|
public function __invoke(EntityWorkflowSend $workflowSend, string $verificationKey, Request $request): Response
|
||||||
|
{
|
||||||
|
if (50 < $workflowSend->getNumberOfErrorTrials()) {
|
||||||
|
throw new AccessDeniedHttpException('number of trials exceeded, no more access allowed');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($verificationKey !== $workflowSend->getPrivateToken()) {
|
||||||
|
$this->chillLogger->info(self::LOG_PREFIX.'Invalid trial for this send', ['client_ip' => $request->getClientIp()]);
|
||||||
|
$workflowSend->increaseErrorTrials();
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
throw new AccessDeniedHttpException('invalid verification key');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->clock->now() > $workflowSend->getExpireAt()) {
|
||||||
|
return new Response(
|
||||||
|
$this->environment->render('@ChillMain/Workflow/workflow_view_send_public_expired.html.twig'),
|
||||||
|
409
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (100 < $workflowSend->getViews()->count()) {
|
||||||
|
$this->chillLogger->info(self::LOG_PREFIX.'100 view reached, not allowed to see it again');
|
||||||
|
throw new AccessDeniedHttpException('100 views reached, not allowed to see it again');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$metadata = new EntityWorkflowViewMetadataDTO(
|
||||||
|
$workflowSend->getViews()->count(),
|
||||||
|
100 - $workflowSend->getViews()->count(),
|
||||||
|
);
|
||||||
|
$response = new Response(
|
||||||
|
$this->entityWorkflowManager->renderPublicView($workflowSend, $metadata),
|
||||||
|
);
|
||||||
|
|
||||||
|
$view = new EntityWorkflowSendView($workflowSend, $this->clock->now(), $request->getClientIp());
|
||||||
|
$this->entityManager->persist($view);
|
||||||
|
$this->messageBus->dispatch(new PostPublicViewMessage($view->getId()));
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
} catch (HandlerWithPublicViewNotFoundException $e) {
|
||||||
|
throw new \RuntimeException('Could not render the public view', previous: $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,7 @@ use Doctrine\Common\Collections\ReadableCollection;
|
|||||||
use Doctrine\Common\Collections\Selectable;
|
use Doctrine\Common\Collections\Selectable;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
|
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
#[ORM\Entity]
|
#[ORM\Entity]
|
||||||
#[ORM\Table(name: 'chill_main_user_group')]
|
#[ORM\Table(name: 'chill_main_user_group')]
|
||||||
@ -66,6 +67,10 @@ class UserGroup
|
|||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
|
||||||
private string $excludeKey = '';
|
private string $excludeKey = '';
|
||||||
|
|
||||||
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
|
||||||
|
#[Assert\Email]
|
||||||
|
private string $email = '';
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->adminUsers = new ArrayCollection();
|
$this->adminUsers = new ArrayCollection();
|
||||||
@ -187,6 +192,23 @@ class UserGroup
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getEmail(): string
|
||||||
|
{
|
||||||
|
return $this->email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEmail(string $email): self
|
||||||
|
{
|
||||||
|
$this->email = $email;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasEmail(): bool
|
||||||
|
{
|
||||||
|
return '' !== $this->email;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the current object is an instance of the UserGroup class.
|
* Checks if the current object is an instance of the UserGroup class.
|
||||||
*
|
*
|
||||||
|
@ -462,6 +462,13 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach ($transitionContextDTO->futureDestineeThirdParties as $thirdParty) {
|
||||||
|
new EntityWorkflowSend($newStep, $thirdParty, $transitionAt->add(new \DateInterval('P30D')));
|
||||||
|
}
|
||||||
|
foreach ($transitionContextDTO->futureDestineeEmails as $email) {
|
||||||
|
new EntityWorkflowSend($newStep, $email, $transitionAt->add(new \DateInterval('P30D')));
|
||||||
|
}
|
||||||
|
|
||||||
// copy the freeze
|
// copy the freeze
|
||||||
if ($this->isFreeze()) {
|
if ($this->isFreeze()) {
|
||||||
$newStep->setFreezeAfter(true);
|
$newStep->setFreezeAfter(true);
|
||||||
|
@ -0,0 +1,227 @@
|
|||||||
|
<?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\ThirdPartyBundle\Entity\ThirdParty;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Ramsey\Uuid\Uuid;
|
||||||
|
use Ramsey\Uuid\UuidInterface;
|
||||||
|
use Random\Randomizer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An entity which stores then sending of a workflow's content to
|
||||||
|
* some external entity.
|
||||||
|
*/
|
||||||
|
#[ORM\Entity]
|
||||||
|
#[ORM\Table(name: 'chill_main_workflow_entity_send')]
|
||||||
|
class EntityWorkflowSend implements TrackCreationInterface
|
||||||
|
{
|
||||||
|
use TrackCreationTrait;
|
||||||
|
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column(type: Types::INTEGER)]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: ThirdParty::class)]
|
||||||
|
#[ORM\JoinColumn(nullable: true)]
|
||||||
|
private ?ThirdParty $destineeThirdParty = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::TEXT, nullable: false, options: ['default' => ''])]
|
||||||
|
private string $destineeEmail = '';
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'uuid', unique: true, nullable: false)]
|
||||||
|
private UuidInterface $uuid;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::STRING, length: 255, nullable: false)]
|
||||||
|
private string $privateToken;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::INTEGER, nullable: false, options: ['default' => 0])]
|
||||||
|
private int $numberOfErrorTrials = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, EntityWorkflowSendView>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(targetEntity: EntityWorkflowSendView::class, mappedBy: 'send')]
|
||||||
|
private Collection $views;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
#[ORM\ManyToOne(targetEntity: EntityWorkflowStep::class, inversedBy: 'sends')]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
private EntityWorkflowStep $entityWorkflowStep,
|
||||||
|
string|ThirdParty $destinee,
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: false)]
|
||||||
|
private \DateTimeImmutable $expireAt,
|
||||||
|
) {
|
||||||
|
$this->uuid = Uuid::uuid4();
|
||||||
|
$random = new Randomizer();
|
||||||
|
$this->privateToken = bin2hex($random->getBytes(48));
|
||||||
|
|
||||||
|
$this->entityWorkflowStep->addSend($this);
|
||||||
|
|
||||||
|
if ($destinee instanceof ThirdParty) {
|
||||||
|
$this->destineeThirdParty = $destinee;
|
||||||
|
} else {
|
||||||
|
$this->destineeEmail = $destinee;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->views = new ArrayCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal use the @see{EntityWorkflowSendView}'s constructor instead
|
||||||
|
*/
|
||||||
|
public function addView(EntityWorkflowSendView $view): self
|
||||||
|
{
|
||||||
|
if (!$this->views->contains($view)) {
|
||||||
|
$this->views->add($view);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDestineeEmail(): string
|
||||||
|
{
|
||||||
|
return $this->destineeEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDestineeThirdParty(): ?ThirdParty
|
||||||
|
{
|
||||||
|
return $this->destineeThirdParty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNumberOfErrorTrials(): int
|
||||||
|
{
|
||||||
|
return $this->numberOfErrorTrials;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPrivateToken(): string
|
||||||
|
{
|
||||||
|
return $this->privateToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntityWorkflowStep(): EntityWorkflowStep
|
||||||
|
{
|
||||||
|
return $this->entityWorkflowStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntityWorkflowStepChained(): ?EntityWorkflowStep
|
||||||
|
{
|
||||||
|
foreach ($this->getEntityWorkflowStep()->getEntityWorkflow()->getStepsChained() as $step) {
|
||||||
|
if ($this->getEntityWorkflowStep() === $step) {
|
||||||
|
return $step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUuid(): UuidInterface
|
||||||
|
{
|
||||||
|
return $this->uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExpireAt(): \DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->expireAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getViews(): Collection
|
||||||
|
{
|
||||||
|
return $this->views;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function increaseErrorTrials(): void
|
||||||
|
{
|
||||||
|
$this->numberOfErrorTrials = $this->numberOfErrorTrials + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDestinee(): string|ThirdParty
|
||||||
|
{
|
||||||
|
if (null !== $this->getDestineeThirdParty()) {
|
||||||
|
return $this->getDestineeThirdParty();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getDestineeEmail();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the kind of destinee based on whether the destinee is a thirdParty or an emailAddress.
|
||||||
|
*
|
||||||
|
* @return 'thirdParty'|'email' 'thirdParty' if the destinee is a third party, 'email' otherwise
|
||||||
|
*/
|
||||||
|
public function getDestineeKind(): string
|
||||||
|
{
|
||||||
|
if (null !== $this->getDestineeThirdParty()) {
|
||||||
|
return 'thirdParty';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'email';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isViewed(): bool
|
||||||
|
{
|
||||||
|
return $this->views->count() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isExpired(?\DateTimeImmutable $now = null): bool
|
||||||
|
{
|
||||||
|
return ($now ?? new \DateTimeImmutable('now')) >= $this->expireAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the most recent view.
|
||||||
|
*
|
||||||
|
* @return EntityWorkflowSendView|null returns the last view or null if there are no views
|
||||||
|
*/
|
||||||
|
public function getLastView(): ?EntityWorkflowSendView
|
||||||
|
{
|
||||||
|
$last = null;
|
||||||
|
foreach ($this->views as $view) {
|
||||||
|
if (null === $last) {
|
||||||
|
$last = $view;
|
||||||
|
} else {
|
||||||
|
if ($view->getViewAt() > $last->getViewAt()) {
|
||||||
|
$last = $view;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $last;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves an array of views grouped by their remote IP address.
|
||||||
|
*
|
||||||
|
* @return array<string, list<EntityWorkflowSendView>> an associative array where the keys are IP addresses and the values are arrays of views associated with those IPs
|
||||||
|
*/
|
||||||
|
public function getViewsByIp(): array
|
||||||
|
{
|
||||||
|
$views = [];
|
||||||
|
|
||||||
|
foreach ($this->getViews() as $view) {
|
||||||
|
$views[$view->getRemoteIp()][] = $view;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $views;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
<?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 Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the viewing action from an external destinee.
|
||||||
|
*/
|
||||||
|
#[ORM\Entity(readOnly: true)]
|
||||||
|
#[ORM\Table(name: 'chill_main_workflow_entity_send_views')]
|
||||||
|
class EntityWorkflowSendView
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column(type: Types::INTEGER)]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
#[ORM\ManyToOne(targetEntity: EntityWorkflowSend::class, inversedBy: 'views')]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
private EntityWorkflowSend $send,
|
||||||
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
||||||
|
private \DateTimeInterface $viewAt,
|
||||||
|
#[ORM\Column(type: Types::TEXT)]
|
||||||
|
private string $remoteIp = '',
|
||||||
|
) {
|
||||||
|
$this->send->addView($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRemoteIp(): string
|
||||||
|
{
|
||||||
|
return $this->remoteIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSend(): EntityWorkflowSend
|
||||||
|
{
|
||||||
|
return $this->send;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getViewAt(): \DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->viewAt;
|
||||||
|
}
|
||||||
|
}
|
@ -112,6 +112,12 @@ class EntityWorkflowStep
|
|||||||
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepHold::class)]
|
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepHold::class)]
|
||||||
private Collection $holdsOnStep;
|
private Collection $holdsOnStep;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, EntityWorkflowSend>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'entityWorkflowStep', targetEntity: EntityWorkflowSend::class, cascade: ['persist'], orphanRemoval: true)]
|
||||||
|
private Collection $sends;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->ccUser = new ArrayCollection();
|
$this->ccUser = new ArrayCollection();
|
||||||
@ -120,6 +126,7 @@ class EntityWorkflowStep
|
|||||||
$this->destUserByAccessKey = new ArrayCollection();
|
$this->destUserByAccessKey = new ArrayCollection();
|
||||||
$this->signatures = new ArrayCollection();
|
$this->signatures = new ArrayCollection();
|
||||||
$this->holdsOnStep = new ArrayCollection();
|
$this->holdsOnStep = new ArrayCollection();
|
||||||
|
$this->sends = new ArrayCollection();
|
||||||
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(32));
|
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(32));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,6 +197,18 @@ class EntityWorkflowStep
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal use @see{EntityWorkflowSend}'s constructor instead
|
||||||
|
*/
|
||||||
|
public function addSend(EntityWorkflowSend $send): self
|
||||||
|
{
|
||||||
|
if (!$this->sends->contains($send)) {
|
||||||
|
$this->sends[] = $send;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function removeSignature(EntityWorkflowStepSignature $signature): self
|
public function removeSignature(EntityWorkflowStepSignature $signature): self
|
||||||
{
|
{
|
||||||
if ($this->signatures->contains($signature)) {
|
if ($this->signatures->contains($signature)) {
|
||||||
@ -284,6 +303,14 @@ class EntityWorkflowStep
|
|||||||
return $this->signatures;
|
return $this->signatures;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, EntityWorkflowSend>
|
||||||
|
*/
|
||||||
|
public function getSends(): Collection
|
||||||
|
{
|
||||||
|
return $this->sends;
|
||||||
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
|
@ -36,6 +36,7 @@ class ChillCollectionType extends AbstractType
|
|||||||
$view->vars['identifier'] = $options['identifier'];
|
$view->vars['identifier'] = $options['identifier'];
|
||||||
$view->vars['empty_collection_explain'] = $options['empty_collection_explain'];
|
$view->vars['empty_collection_explain'] = $options['empty_collection_explain'];
|
||||||
$view->vars['js_caller'] = $options['js_caller'];
|
$view->vars['js_caller'] = $options['js_caller'];
|
||||||
|
$view->vars['uniqid'] = uniqid();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver)
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
@ -28,10 +28,14 @@ class EntityToJsonTransformer implements DataTransformerInterface
|
|||||||
|
|
||||||
public function reverseTransform($value)
|
public function reverseTransform($value)
|
||||||
{
|
{
|
||||||
if ('' === $value) {
|
if (false === $this->multiple && '' === $value) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->multiple && [] === $value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
$denormalized = json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR);
|
$denormalized = json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
if ($this->multiple) {
|
if ($this->multiple) {
|
||||||
|
@ -18,6 +18,7 @@ use Symfony\Component\Form\FormBuilderInterface;
|
|||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\Form\FormView;
|
use Symfony\Component\Form\FormView;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
use Symfony\Component\Serializer\SerializerInterface;
|
use Symfony\Component\Serializer\SerializerInterface;
|
||||||
@ -29,12 +30,18 @@ use Symfony\Component\Serializer\SerializerInterface;
|
|||||||
*
|
*
|
||||||
* - `multiple`: pick one or more users
|
* - `multiple`: pick one or more users
|
||||||
* - `suggested`: a list of suggested users
|
* - `suggested`: a list of suggested users
|
||||||
|
* - `suggest_myself`: append the current user to the list of suggested
|
||||||
* - `as_id`: only the id will be set in the returned data
|
* - `as_id`: only the id will be set in the returned data
|
||||||
* - `submit_on_adding_new_entity`: the browser will immediately submit the form when new users are checked
|
* - `submit_on_adding_new_entity`: the browser will immediately submit the form when new users are checked
|
||||||
*/
|
*/
|
||||||
class PickUserDynamicType extends AbstractType
|
class PickUserDynamicType extends AbstractType
|
||||||
{
|
{
|
||||||
public function __construct(private readonly DenormalizerInterface $denormalizer, private readonly SerializerInterface $serializer, private readonly NormalizerInterface $normalizer) {}
|
public function __construct(
|
||||||
|
private readonly DenormalizerInterface $denormalizer,
|
||||||
|
private readonly SerializerInterface $serializer,
|
||||||
|
private readonly NormalizerInterface $normalizer,
|
||||||
|
private readonly Security $security,
|
||||||
|
) {}
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
{
|
{
|
||||||
@ -53,6 +60,12 @@ class PickUserDynamicType extends AbstractType
|
|||||||
foreach ($options['suggested'] as $user) {
|
foreach ($options['suggested'] as $user) {
|
||||||
$view->vars['suggested'][] = $this->normalizer->normalize($user, 'json', ['groups' => 'read']);
|
$view->vars['suggested'][] = $this->normalizer->normalize($user, 'json', ['groups' => 'read']);
|
||||||
}
|
}
|
||||||
|
$user = $this->security->getUser();
|
||||||
|
if ($user instanceof User) {
|
||||||
|
if (true === $options['suggest_myself'] && !in_array($user, $options['suggested'], true)) {
|
||||||
|
$view->vars['suggested'][] = $this->normalizer->normalize($user, 'json', ['groups' => 'read']);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver)
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
@ -61,6 +74,8 @@ class PickUserDynamicType extends AbstractType
|
|||||||
->setDefault('multiple', false)
|
->setDefault('multiple', false)
|
||||||
->setAllowedTypes('multiple', ['bool'])
|
->setAllowedTypes('multiple', ['bool'])
|
||||||
->setDefault('compound', false)
|
->setDefault('compound', false)
|
||||||
|
->setDefault('suggest_myself', false)
|
||||||
|
->setAllowedTypes('suggest_myself', ['bool'])
|
||||||
->setDefault('suggested', [])
|
->setDefault('suggested', [])
|
||||||
// if set to true, only the id will be set inside the content. The denormalization will not work.
|
// if set to true, only the id will be set inside the content. The denormalization will not work.
|
||||||
->setDefault('as_id', false)
|
->setDefault('as_id', false)
|
||||||
|
@ -11,12 +11,14 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\MainBundle\Form\Type;
|
namespace Chill\MainBundle\Form\Type;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Form\Type\DataTransformer\EntityToJsonTransformer;
|
use Chill\MainBundle\Form\Type\DataTransformer\EntityToJsonTransformer;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\Form\FormView;
|
use Symfony\Component\Form\FormView;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
use Symfony\Component\Serializer\SerializerInterface;
|
use Symfony\Component\Serializer\SerializerInterface;
|
||||||
@ -26,7 +28,12 @@ use Symfony\Component\Serializer\SerializerInterface;
|
|||||||
*/
|
*/
|
||||||
final class PickUserGroupOrUserDynamicType extends AbstractType
|
final class PickUserGroupOrUserDynamicType extends AbstractType
|
||||||
{
|
{
|
||||||
public function __construct(private readonly DenormalizerInterface $denormalizer, private readonly SerializerInterface $serializer, private readonly NormalizerInterface $normalizer) {}
|
public function __construct(
|
||||||
|
private readonly DenormalizerInterface $denormalizer,
|
||||||
|
private readonly SerializerInterface $serializer,
|
||||||
|
private readonly NormalizerInterface $normalizer,
|
||||||
|
private readonly Security $security,
|
||||||
|
) {}
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
{
|
{
|
||||||
@ -45,6 +52,12 @@ final class PickUserGroupOrUserDynamicType extends AbstractType
|
|||||||
foreach ($options['suggested'] as $userGroup) {
|
foreach ($options['suggested'] as $userGroup) {
|
||||||
$view->vars['suggested'][] = $this->normalizer->normalize($userGroup, 'json', ['groups' => 'read']);
|
$view->vars['suggested'][] = $this->normalizer->normalize($userGroup, 'json', ['groups' => 'read']);
|
||||||
}
|
}
|
||||||
|
$user = $this->security->getUser();
|
||||||
|
if ($user instanceof User) {
|
||||||
|
if (true === $options['suggest_myself'] && !in_array($user, $options['suggested'], true)) {
|
||||||
|
$view->vars['suggested'][] = $this->normalizer->normalize($user, 'json', ['groups' => 'read']);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver)
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
@ -54,6 +67,8 @@ final class PickUserGroupOrUserDynamicType extends AbstractType
|
|||||||
->setAllowedTypes('multiple', ['bool'])
|
->setAllowedTypes('multiple', ['bool'])
|
||||||
->setDefault('compound', false)
|
->setDefault('compound', false)
|
||||||
->setDefault('suggested', [])
|
->setDefault('suggested', [])
|
||||||
|
->setDefault('suggest_myself', false)
|
||||||
|
->setAllowedTypes('suggest_myself', ['bool'])
|
||||||
// if set to true, only the id will be set inside the content. The denormalization will not work.
|
// if set to true, only the id will be set inside the content. The denormalization will not work.
|
||||||
->setDefault('as_id', false)
|
->setDefault('as_id', false)
|
||||||
->setAllowedTypes('as_id', ['bool'])
|
->setAllowedTypes('as_id', ['bool'])
|
||||||
|
@ -15,6 +15,7 @@ use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
|||||||
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
|
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ColorType;
|
use Symfony\Component\Form\Extension\Core\Type\ColorType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
|
||||||
@ -34,6 +35,11 @@ class UserGroupType extends AbstractType
|
|||||||
->add('foregroundColor', ColorType::class, [
|
->add('foregroundColor', ColorType::class, [
|
||||||
'label' => 'user_group.ForegroundColor',
|
'label' => 'user_group.ForegroundColor',
|
||||||
])
|
])
|
||||||
|
->add('email', EmailType::class, [
|
||||||
|
'label' => 'user_group.Email',
|
||||||
|
'help' => 'user_group.EmailHelp',
|
||||||
|
'empty_data' => '',
|
||||||
|
])
|
||||||
->add('excludeKey', TextType::class, [
|
->add('excludeKey', TextType::class, [
|
||||||
'label' => 'user_group.ExcludeKey',
|
'label' => 'user_group.ExcludeKey',
|
||||||
'help' => 'user_group.ExcludeKeyHelp',
|
'help' => 'user_group.ExcludeKeyHelp',
|
||||||
|
@ -12,19 +12,20 @@ declare(strict_types=1);
|
|||||||
namespace Chill\MainBundle\Form;
|
namespace Chill\MainBundle\Form;
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Form\Type\ChillCollectionType;
|
||||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||||
use Chill\MainBundle\Form\Type\PickUserGroupOrUserDynamicType;
|
use Chill\MainBundle\Form\Type\PickUserGroupOrUserDynamicType;
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||||
use Chill\PersonBundle\Form\Type\PickPersonDynamicType;
|
use Chill\PersonBundle\Form\Type\PickPersonDynamicType;
|
||||||
|
use Chill\ThirdPartyBundle\Form\Type\PickThirdpartyDynamicType;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
use Symfony\Component\Validator\Constraints\Callback;
|
|
||||||
use Symfony\Component\Validator\Constraints\NotNull;
|
use Symfony\Component\Validator\Constraints\NotNull;
|
||||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
|
||||||
use Symfony\Component\Workflow\Registry;
|
use Symfony\Component\Workflow\Registry;
|
||||||
use Symfony\Component\Workflow\Transition;
|
use Symfony\Component\Workflow\Transition;
|
||||||
|
|
||||||
@ -83,7 +84,6 @@ class WorkflowStepType extends AbstractType
|
|||||||
$builder
|
$builder
|
||||||
->add('transition', ChoiceType::class, [
|
->add('transition', ChoiceType::class, [
|
||||||
'label' => 'workflow.Next step',
|
'label' => 'workflow.Next step',
|
||||||
'mapped' => false,
|
|
||||||
'multiple' => false,
|
'multiple' => false,
|
||||||
'expanded' => true,
|
'expanded' => true,
|
||||||
'choices' => $choices,
|
'choices' => $choices,
|
||||||
@ -101,6 +101,7 @@ class WorkflowStepType extends AbstractType
|
|||||||
$toFinal = true;
|
$toFinal = true;
|
||||||
$isForward = 'neutral';
|
$isForward = 'neutral';
|
||||||
$isSignature = [];
|
$isSignature = [];
|
||||||
|
$isSentExternal = false;
|
||||||
|
|
||||||
$metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition);
|
$metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition);
|
||||||
|
|
||||||
@ -124,6 +125,8 @@ class WorkflowStepType extends AbstractType
|
|||||||
if (\array_key_exists('isSignature', $meta)) {
|
if (\array_key_exists('isSignature', $meta)) {
|
||||||
$isSignature = $meta['isSignature'];
|
$isSignature = $meta['isSignature'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$isSentExternal = $isSentExternal ? true : $meta['isSentExternal'] ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -131,6 +134,7 @@ class WorkflowStepType extends AbstractType
|
|||||||
'data-to-final' => $toFinal ? '1' : '0',
|
'data-to-final' => $toFinal ? '1' : '0',
|
||||||
'data-is-forward' => $isForward,
|
'data-is-forward' => $isForward,
|
||||||
'data-is-signature' => json_encode($isSignature),
|
'data-is-signature' => json_encode($isSignature),
|
||||||
|
'data-is-sent-external' => $isSentExternal ? '1' : '0',
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
@ -152,12 +156,14 @@ class WorkflowStepType extends AbstractType
|
|||||||
->add('futureUserSignature', PickUserDynamicType::class, [
|
->add('futureUserSignature', PickUserDynamicType::class, [
|
||||||
'label' => 'workflow.signature_zone.user signature',
|
'label' => 'workflow.signature_zone.user signature',
|
||||||
'multiple' => false,
|
'multiple' => false,
|
||||||
|
'suggest_myself' => true,
|
||||||
])
|
])
|
||||||
->add('futureDestUsers', PickUserGroupOrUserDynamicType::class, [
|
->add('futureDestUsers', PickUserGroupOrUserDynamicType::class, [
|
||||||
'label' => 'workflow.dest for next steps',
|
'label' => 'workflow.dest for next steps',
|
||||||
'multiple' => true,
|
'multiple' => true,
|
||||||
'empty_data' => '[]',
|
'empty_data' => '[]',
|
||||||
'suggested' => $options['suggested_users'],
|
'suggested' => $options['suggested_users'],
|
||||||
|
'suggest_myself' => true,
|
||||||
])
|
])
|
||||||
->add('futureCcUsers', PickUserDynamicType::class, [
|
->add('futureCcUsers', PickUserDynamicType::class, [
|
||||||
'label' => 'workflow.cc for next steps',
|
'label' => 'workflow.cc for next steps',
|
||||||
@ -166,6 +172,26 @@ class WorkflowStepType extends AbstractType
|
|||||||
'suggested' => $options['suggested_users'],
|
'suggested' => $options['suggested_users'],
|
||||||
'empty_data' => '[]',
|
'empty_data' => '[]',
|
||||||
'attr' => ['class' => 'future-cc-users'],
|
'attr' => ['class' => 'future-cc-users'],
|
||||||
|
'suggest_myself' => true,
|
||||||
|
])
|
||||||
|
->add('futureDestineeEmails', ChillCollectionType::class, [
|
||||||
|
'entry_type' => EmailType::class,
|
||||||
|
'entry_options' => [
|
||||||
|
'empty_data' => '',
|
||||||
|
],
|
||||||
|
'allow_add' => true,
|
||||||
|
'allow_delete' => true,
|
||||||
|
'delete_empty' => static fn (?string $email) => '' === $email || null === $email,
|
||||||
|
'button_add_label' => 'workflow.transition_destinee_add_emails',
|
||||||
|
'button_remove_label' => 'workflow.transition_destinee_remove_emails',
|
||||||
|
'help' => 'workflow.transition_destinee_emails_help',
|
||||||
|
'label' => 'workflow.transition_destinee_emails_label',
|
||||||
|
])
|
||||||
|
->add('futureDestineeThirdParties', PickThirdpartyDynamicType::class, [
|
||||||
|
'label' => 'workflow.transition_destinee_third_party',
|
||||||
|
'help' => 'workflow.transition_destinee_third_party_help',
|
||||||
|
'multiple' => true,
|
||||||
|
'empty_data' => [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$builder
|
$builder
|
||||||
@ -182,38 +208,6 @@ class WorkflowStepType extends AbstractType
|
|||||||
->setDefault('data_class', WorkflowTransitionContextDTO::class)
|
->setDefault('data_class', WorkflowTransitionContextDTO::class)
|
||||||
->setRequired('entity_workflow')
|
->setRequired('entity_workflow')
|
||||||
->setAllowedTypes('entity_workflow', EntityWorkflow::class)
|
->setAllowedTypes('entity_workflow', EntityWorkflow::class)
|
||||||
->setDefault('suggested_users', [])
|
->setDefault('suggested_users', []);
|
||||||
->setDefault('constraints', [
|
|
||||||
new Callback(
|
|
||||||
function (WorkflowTransitionContextDTO $step, ExecutionContextInterface $context, $payload) {
|
|
||||||
$workflow = $this->registry->get($step->entityWorkflow, $step->entityWorkflow->getWorkflowName());
|
|
||||||
$transition = $step->transition;
|
|
||||||
$toFinal = true;
|
|
||||||
|
|
||||||
if (null === $transition) {
|
|
||||||
$context
|
|
||||||
->buildViolation('workflow.You must select a next step, pick another decision if no next steps are available');
|
|
||||||
} else {
|
|
||||||
foreach ($transition->getTos() as $to) {
|
|
||||||
$meta = $workflow->getMetadataStore()->getPlaceMetadata($to);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!\array_key_exists('isFinal', $meta) || false === $meta['isFinal']
|
|
||||||
) {
|
|
||||||
$toFinal = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$destUsers = $step->futureDestUsers;
|
|
||||||
|
|
||||||
if (!$toFinal && [] === $destUsers) {
|
|
||||||
$context
|
|
||||||
->buildViolation('workflow.You must add at least one dest user or email')
|
|
||||||
->atPath('future_dest_users')
|
|
||||||
->addViolation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Repository;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSendView;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template-implements ObjectRepository<EntityWorkflowSendView>
|
||||||
|
*/
|
||||||
|
class EntityWorkflowSendViewRepository implements ObjectRepository
|
||||||
|
{
|
||||||
|
private readonly ObjectRepository $repository;
|
||||||
|
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
$this->repository = $registry->getRepository($this->getClassName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function find($id): ?EntityWorkflowSendView
|
||||||
|
{
|
||||||
|
return $this->repository->find($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findAll()
|
||||||
|
{
|
||||||
|
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): ?EntityWorkflowSendView
|
||||||
|
{
|
||||||
|
return $this->repository->findOneBy($criteria);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getClassName()
|
||||||
|
{
|
||||||
|
return EntityWorkflowSendView::class;
|
||||||
|
}
|
||||||
|
}
|
@ -205,13 +205,13 @@ class EntityWorkflowRepository implements ObjectRepository
|
|||||||
*
|
*
|
||||||
* @param \DateTimeImmutable $olderThanDate the date to compare against
|
* @param \DateTimeImmutable $olderThanDate the date to compare against
|
||||||
*
|
*
|
||||||
* @return list<int> the list of workflow IDs that meet the criteria
|
* @return iterable<EntityWorkflow>
|
||||||
*/
|
*/
|
||||||
public function findWorkflowsWithoutFinalStepAndOlderThan(\DateTimeImmutable $olderThanDate): array
|
public function findWorkflowsWithoutFinalStepAndOlderThan(\DateTimeImmutable $olderThanDate): iterable
|
||||||
{
|
{
|
||||||
$qb = $this->repository->createQueryBuilder('sw');
|
$qb = $this->repository->createQueryBuilder('sw');
|
||||||
|
|
||||||
$qb->select('sw.id')
|
$qb->select('sw')
|
||||||
// only the workflow which are not finalized
|
// only the workflow which are not finalized
|
||||||
->where(sprintf('NOT EXISTS (SELECT 1 FROM %s ews WHERE ews.isFinal = TRUE AND ews.entityWorkflow = sw.id)', EntityWorkflowStep::class))
|
->where(sprintf('NOT EXISTS (SELECT 1 FROM %s ews WHERE ews.isFinal = TRUE AND ews.entityWorkflow = sw.id)', EntityWorkflowStep::class))
|
||||||
->andWhere(
|
->andWhere(
|
||||||
@ -227,7 +227,7 @@ class EntityWorkflowRepository implements ObjectRepository
|
|||||||
->setParameter('initial', 'initial')
|
->setParameter('initial', 'initial')
|
||||||
;
|
;
|
||||||
|
|
||||||
return $qb->getQuery()->getResult();
|
return $qb->getQuery()->toIterable();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getClassName(): string
|
public function getClassName(): string
|
||||||
|
@ -30,6 +30,12 @@
|
|||||||
*/
|
*/
|
||||||
import './collection.scss';
|
import './collection.scss';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface GlobalEventHandlersEventMap {
|
||||||
|
'show-hide-show': CustomEvent<{id: number, froms: HTMLElement[], container: HTMLElement}>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class CollectionEventPayload {
|
export class CollectionEventPayload {
|
||||||
collection: HTMLUListElement;
|
collection: HTMLUListElement;
|
||||||
entry: HTMLLIElement;
|
entry: HTMLLIElement;
|
||||||
@ -107,20 +113,34 @@ export const buildRemoveButton = (collection: HTMLUListElement, entry: HTMLLIEle
|
|||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
const collectionsInit = new Set<string>;
|
||||||
|
const buttonsInit = new Set<string>();
|
||||||
|
|
||||||
|
const initialize = function (target: Document|Element): void {
|
||||||
let
|
let
|
||||||
addButtons: NodeListOf<HTMLButtonElement> = document.querySelectorAll("button[data-collection-add-target]"),
|
addButtons: NodeListOf<HTMLButtonElement> = document.querySelectorAll("button[data-collection-add-target]"),
|
||||||
collections: NodeListOf<HTMLUListElement> = document.querySelectorAll("ul[data-collection-regular]");
|
collections: NodeListOf<HTMLUListElement> = document.querySelectorAll("ul[data-collection-regular]");
|
||||||
|
|
||||||
for (let i = 0; i < addButtons.length; i++) {
|
for (let i = 0; i < addButtons.length; i++) {
|
||||||
let addButton = addButtons[i];
|
const addButton = addButtons[i];
|
||||||
|
const uniqid = addButton.dataset.uniqid as string;
|
||||||
|
if (buttonsInit.has(uniqid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
buttonsInit.add(uniqid);
|
||||||
addButton.addEventListener('click', (e: Event) => {
|
addButton.addEventListener('click', (e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleAdd(e.target);
|
handleAdd(e.target);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (let i = 0; i < collections.length; i++) {
|
for (let i = 0; i < collections.length; i++) {
|
||||||
let entries: NodeListOf<HTMLLIElement> = collections[i].querySelectorAll(':scope > li');
|
const collection = collections[i];
|
||||||
|
const uniqid = collection.dataset.uniqid as string;
|
||||||
|
if (collectionsInit.has(uniqid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
collectionsInit.add(uniqid);
|
||||||
|
let entries: NodeListOf<HTMLLIElement> = collection.querySelectorAll(':scope > li');
|
||||||
for (let j = 0; j < entries.length; j++) {
|
for (let j = 0; j < entries.length; j++) {
|
||||||
if (entries[j].dataset.collectionEmptyExplain === "1") {
|
if (entries[j].dataset.collectionEmptyExplain === "1") {
|
||||||
continue;
|
continue;
|
||||||
@ -128,4 +148,13 @@ window.addEventListener('load', () => {
|
|||||||
initializeRemove(collections[i], entries[j]);
|
initializeRemove(collections[i], entries[j]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
initialize(document);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener('show-hide-show', (event: CustomEvent<{id: number; container: HTMLElement; froms: HTMLElement[]}>) => {
|
||||||
|
const container = event.detail.container as HTMLElement;
|
||||||
|
initialize(container);
|
||||||
|
})
|
||||||
|
@ -1,27 +1,20 @@
|
|||||||
import {ShowHide} from 'ChillMainAssets/lib/show_hide/show_hide.js';
|
import {ShowHide} from 'ChillMainAssets/lib/show_hide/show_hide.js';
|
||||||
|
|
||||||
window.addEventListener('DOMContentLoaded', function() {
|
window.addEventListener('DOMContentLoaded', function() {
|
||||||
let
|
const
|
||||||
divTransitions = document.querySelector('#transitions'),
|
divTransitions = document.querySelector('#transitions'),
|
||||||
futureDestUsersContainer = document.querySelector('#futureDests')
|
futureDestUsersContainer = document.querySelector('#futureDests'),
|
||||||
personSignatureField = document.querySelector('#person-signature-field');
|
personSignatureField = document.querySelector('#person-signature-field'),
|
||||||
userSignatureField = document.querySelector('#user-signature-field');
|
userSignatureField = document.querySelector('#user-signature-field'),
|
||||||
signatureTypeChoices = document.querySelector('#signature-type-choice');
|
signatureTypeChoices = document.querySelector('#signature-type-choice'),
|
||||||
personChoice = document.querySelector('#workflow_step_isPersonOrUserSignature_0');
|
personChoice = document.querySelector('#workflow_step_isPersonOrUserSignature_0'),
|
||||||
userChoice = document.querySelector('#workflow_step_isPersonOrUserSignature_1');
|
userChoice = document.querySelector('#workflow_step_isPersonOrUserSignature_1'),
|
||||||
signatureZone = document.querySelector('#signature-zone');
|
signatureZone = document.querySelector('#signature-zone'),
|
||||||
;
|
transitionFilterContainer = document.querySelector('#transitionFilter'),
|
||||||
|
transitionsContainer = document.querySelector('#transitions'),
|
||||||
let
|
sendExternalContainer = document.querySelector('#sendExternalContainer')
|
||||||
transitionFilterContainer = document.querySelector('#transitionFilter'),
|
|
||||||
transitionsContainer = document.querySelector('#transitions')
|
|
||||||
;
|
;
|
||||||
|
|
||||||
// ShowHide instance for signatureTypeChoices. This should always be present in the DOM and we toggle visibility.
|
|
||||||
// The field is not mapped and so not submitted with the form. Without it's presence upon DOM loading other show hides do not function well.
|
|
||||||
signatureTypeChoices.style.display = 'none';
|
|
||||||
|
|
||||||
|
|
||||||
// ShowHide instance for future dest users
|
// ShowHide instance for future dest users
|
||||||
new ShowHide({
|
new ShowHide({
|
||||||
debug: false,
|
debug: false,
|
||||||
@ -32,15 +25,17 @@ window.addEventListener('DOMContentLoaded', function() {
|
|||||||
for (let transition of froms) {
|
for (let transition of froms) {
|
||||||
for (let input of transition.querySelectorAll('input')) {
|
for (let input of transition.querySelectorAll('input')) {
|
||||||
if (input.checked) {
|
if (input.checked) {
|
||||||
|
if ('1' === input.dataset.isSentExternal) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const inputData = JSON.parse(input.getAttribute('data-is-signature'))
|
const inputData = JSON.parse(input.getAttribute('data-is-signature'))
|
||||||
if (inputData.includes('person') || inputData.includes('user')) {
|
if (inputData.includes('person') || inputData.includes('user')) {
|
||||||
signatureTypeChoices.style.display = '';
|
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
personChoice.checked = false
|
personChoice.checked = false
|
||||||
userChoice.checked = false
|
userChoice.checked = false
|
||||||
|
|
||||||
signatureTypeChoices.style.display = 'none';
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -50,6 +45,26 @@ window.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ShowHide instance for send external
|
||||||
|
new ShowHide({
|
||||||
|
debug: false,
|
||||||
|
load_event: null,
|
||||||
|
froms: [divTransitions],
|
||||||
|
container: [sendExternalContainer],
|
||||||
|
test: function(froms, event) {
|
||||||
|
for (let transition of froms) {
|
||||||
|
for (let input of transition.querySelectorAll('input')) {
|
||||||
|
if (input.checked) {
|
||||||
|
if ('1' === input.dataset.isSentExternal) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// ShowHide signature zone
|
// ShowHide signature zone
|
||||||
new ShowHide({
|
new ShowHide({
|
||||||
debug: false,
|
debug: false,
|
||||||
@ -62,7 +77,6 @@ window.addEventListener('DOMContentLoaded', function() {
|
|||||||
if (input.checked) {
|
if (input.checked) {
|
||||||
const inputData = JSON.parse(input.getAttribute('data-is-signature'))
|
const inputData = JSON.parse(input.getAttribute('data-is-signature'))
|
||||||
if (inputData.includes('person') || inputData.includes('user')) {
|
if (inputData.includes('person') || inputData.includes('user')) {
|
||||||
signatureTypeChoices.style.display = '';
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -163,6 +163,7 @@
|
|||||||
<div class="chill-collection">
|
<div class="chill-collection">
|
||||||
<ul class="list-entry"
|
<ul class="list-entry"
|
||||||
{{ form.vars.js_caller }}="{{ form.vars.js_caller }}"
|
{{ form.vars.js_caller }}="{{ form.vars.js_caller }}"
|
||||||
|
data-uniqid="{{ form.vars.uniqid|escape('html_attr') }}"
|
||||||
data-collection-name="{{ form.vars.name|escape('html_attr') }}"
|
data-collection-name="{{ form.vars.name|escape('html_attr') }}"
|
||||||
data-collection-identifier="{{ form.vars.identifier|escape('html_attr') }}"
|
data-collection-identifier="{{ form.vars.identifier|escape('html_attr') }}"
|
||||||
data-collection-button-remove-label="{{ form.vars.button_remove_label|trans|e }}"
|
data-collection-button-remove-label="{{ form.vars.button_remove_label|trans|e }}"
|
||||||
@ -184,6 +185,8 @@
|
|||||||
|
|
||||||
{% if form.vars.allow_add == 1 %}
|
{% if form.vars.allow_add == 1 %}
|
||||||
<button class="add-entry btn btn-misc"
|
<button class="add-entry btn btn-misc"
|
||||||
|
type="button"
|
||||||
|
data-uniqid="{{ form.vars.uniqid|escape('html_attr') }}"
|
||||||
data-collection-add-target="{{ form.vars.name|escape('html_attr') }}"
|
data-collection-add-target="{{ form.vars.name|escape('html_attr') }}"
|
||||||
data-form-prototype="{{ ('<div>' ~ form_widget(form.vars.prototype) ~ '</div>')|escape('html_attr') }}" >
|
data-form-prototype="{{ ('<div>' ~ form_widget(form.vars.prototype) ~ '</div>')|escape('html_attr') }}" >
|
||||||
<i class="fa fa-plus fa-fw"></i>
|
<i class="fa fa-plus fa-fw"></i>
|
||||||
|
@ -84,6 +84,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="sendExternalContainer">
|
||||||
|
{{ form_row(transition_form.futureDestineeThirdParties) }}
|
||||||
|
{{ form_errors(transition_form.futureDestineeThirdParties) }}
|
||||||
|
{{ form_row(transition_form.futureDestineeEmails) }}
|
||||||
|
{{ form_errors(transition_form.futureDestineeEmails) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<p>{{ form_label(transition_form.comment) }}</p>
|
<p>{{ form_label(transition_form.comment) }}</p>
|
||||||
|
|
||||||
{{ form_widget(transition_form.comment) }}
|
{{ form_widget(transition_form.comment) }}
|
||||||
|
@ -134,6 +134,13 @@
|
|||||||
{{ include('@ChillMain/Workflow/_signature_list.html.twig', {'signatures': step.signatures, is_small: true }) }}
|
{{ include('@ChillMain/Workflow/_signature_list.html.twig', {'signatures': step.signatures, is_small: true }) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if step.sends|length > 0 %}
|
||||||
|
<div>
|
||||||
|
<p><b>{{ 'workflow.sent_through_secured_link'|trans }}</b></p>
|
||||||
|
|
||||||
|
{{ include('@ChillMain/Workflow/_send_views_list.html.twig', {'sends': step.sends}) }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
<div class="container">
|
||||||
|
{% for send in sends %}
|
||||||
|
<div class="row row-hover align-items-center">
|
||||||
|
<div class="col-sm-12 col-md-5">
|
||||||
|
{% if send.destineeKind == 'thirdParty' %}
|
||||||
|
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||||
|
action: 'show', displayBadge: true,
|
||||||
|
targetEntity: { name: 'thirdParty', id: send.destineeThirdParty.id },
|
||||||
|
buttonText: send.destineeThirdParty|chill_entity_render_string,
|
||||||
|
} %}
|
||||||
|
{% else %}
|
||||||
|
<a href="mailto:{{ send.destineeEmail }}">{{ send.destineeEmail }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if not send.expired %}
|
||||||
|
<p><small>{{ 'workflow.send_external_message.document_available_until'|trans({'expiration': send.expireAt}) }}</small></p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-12 col-md-7 text-end">
|
||||||
|
<p><small>{{ 'workflow.external_views.number_of_views'|trans({'numberOfViews': send.views|length}) }}</small></p>
|
||||||
|
{% if send.views|length > 0 %}
|
||||||
|
<p><small>{{ 'workflow.external_views.last_view_at'|trans({'at': send.lastView.viewAt }) }}</small></p>
|
||||||
|
<p>
|
||||||
|
<small>
|
||||||
|
<b>{{ 'workflow.public_views_by_ip'|trans }} :</b>
|
||||||
|
{%- for ip, vs in send.viewsByIp -%}
|
||||||
|
<span title="{% for v in vs %}{{ v.viewAt|format_datetime('short', 'short')|escape('html_attr') }}{% if not loop.last %}{{ ', '|escape('html_attr') }}{% endif %}{% endfor %}">{{ ip }} ({{ vs|length }}x)</span>
|
||||||
|
{%- endfor -%}
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
@ -1,12 +1,11 @@
|
|||||||
{% extends '@ChillMain/layout.html.twig' %}
|
{% extends '@ChillMain/layout.html.twig' %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title -%}
|
||||||
{{ 'Workflow'|trans }}
|
{{ 'Workflow'|trans }}
|
||||||
{% endblock %}
|
{%- endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
{{ parent() }}
|
{{ parent() }}
|
||||||
{{ encore_entry_script_tags('mod_async_upload') }}
|
|
||||||
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
||||||
{{ encore_entry_script_tags('mod_entity_workflow_subscribe') }}
|
{{ encore_entry_script_tags('mod_entity_workflow_subscribe') }}
|
||||||
{{ encore_entry_script_tags('page_workflow_show') }}
|
{{ encore_entry_script_tags('page_workflow_show') }}
|
||||||
@ -62,6 +61,11 @@
|
|||||||
<section class="step my-4">{% include '@ChillMain/Workflow/_follow.html.twig' %}</section>
|
<section class="step my-4">{% include '@ChillMain/Workflow/_follow.html.twig' %}</section>
|
||||||
{% if signatures|length > 0 %}
|
{% if signatures|length > 0 %}
|
||||||
<section class="step my-4">{% include '@ChillMain/Workflow/_signature.html.twig' %}</section>
|
<section class="step my-4">{% include '@ChillMain/Workflow/_signature.html.twig' %}</section>
|
||||||
|
{% elseif entity_workflow.currentStep.sends|length > 0 %}
|
||||||
|
<section class="step my-4">
|
||||||
|
<h2>{{ 'workflow.external_views.title'|trans({'numberOfSends': entity_workflow.currentStep.sends|length }) }}</h2>
|
||||||
|
{% include '@ChillMain/Workflow/_send_views_list.html.twig' with {'sends': entity_workflow.currentStep.sends} %}
|
||||||
|
</section>
|
||||||
{% else %}
|
{% else %}
|
||||||
<section class="step my-4">{% include '@ChillMain/Workflow/_decision.html.twig' %}</section>
|
<section class="step my-4">{% include '@ChillMain/Workflow/_decision.html.twig' %}</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
{% extends '@ChillMain/layout.html.twig' %}
|
{% extends '@ChillMain/layout.html.twig' %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title 'workflow.signature_zone.metadata.sign_by'|trans({ '%name%' : person|chill_entity_render_string}) %}
|
||||||
{{ 'Signature'|trans }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="col-10 workflow">
|
<div class="col-10 workflow">
|
||||||
<h1 class="mb-5">{{ 'workflow.signature_zone.metadata.sign_by'|trans({ '%name%' : person.firstname ~ ' ' ~ person.lastname}) }}</h1>
|
<h1 class="mb-5">{{ block('title') }}</h1>
|
||||||
|
|
||||||
{% if metadata_form is not null %}
|
{% if metadata_form is not null %}
|
||||||
{{ form_start(metadata_form) }}
|
{{ form_start(metadata_form) }}
|
@ -0,0 +1,9 @@
|
|||||||
|
Chers membres du groupe {{ user_group.label|localize_translatable_string }},
|
||||||
|
|
||||||
|
Un suivi "{{ workflow.text }}" a atteint une nouvelle étape: {{ place.text }}
|
||||||
|
|
||||||
|
Vous pouvez visualiser le workflow sur cette page:
|
||||||
|
|
||||||
|
{{ absolute_url(path('chill_main_workflow_show', {'id': entity_workflow.id, '_locale': 'fr'})) }}
|
||||||
|
|
||||||
|
Cordialement,
|
@ -0,0 +1,88 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
{%- set previous = send.entityWorkflowStepChained.previous -%}
|
||||||
|
{%- if previous.transitionBy is not null -%}
|
||||||
|
{%- set sender = previous.transitionBy|chill_entity_render_string -%}
|
||||||
|
{%- else -%}
|
||||||
|
{%- set sender = 'workflow.send_external_message.sender_system_user'|trans -%}
|
||||||
|
{%- endif -%}
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{%- set data = {
|
||||||
|
"@context": "http://schema.org",
|
||||||
|
"@type": "EmailMessage",
|
||||||
|
"potentialAction": {
|
||||||
|
"@type": "ViewAction",
|
||||||
|
"url": absolute_url(path('chill_main_workflow_send_view_public', {'uuid': send.uuid, 'verificationKey': send.privateToken})),
|
||||||
|
"name": 'workflow.send_external_message.see_docs_action_name'|trans
|
||||||
|
},
|
||||||
|
"description": 'workflow.send_external_message.see_doc_action_description'|trans({'sender': sender}),
|
||||||
|
} -%}
|
||||||
|
{{- data|json_encode|raw -}}
|
||||||
|
</script>
|
||||||
|
<div
|
||||||
|
style='background-color:#F5F5F5;color:#262626;font-family:"Helvetica Neue", "Arial Nova", "Nimbus Sans", Arial, sans-serif;font-size:16px;font-weight:400;letter-spacing:0.15008px;line-height:1.5;margin:0;padding:32px 0;min-height:100%;width:100%'
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
align="center"
|
||||||
|
width="100%"
|
||||||
|
style="margin:0 auto;max-width:600px;background-color:#FFFFFF"
|
||||||
|
role="presentation"
|
||||||
|
cellspacing="0"
|
||||||
|
cellpadding="0"
|
||||||
|
border="0"
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
<tr style="width:100%">
|
||||||
|
<td>
|
||||||
|
<div style="font-weight:normal;padding:16px 24px 16px 24px">
|
||||||
|
{{ 'workflow.send_external_message.greeting'|trans }},
|
||||||
|
</div>
|
||||||
|
<div style="font-weight:normal;padding:16px 24px 16px 24px">
|
||||||
|
{{ 'workflow.send_external_message.explanation'|trans({'sender': sender}) }}
|
||||||
|
</div>
|
||||||
|
<div style="font-weight:normal;padding:16px 24px 16px 24px">
|
||||||
|
{{ 'workflow.send_external_message.confidentiality'|trans }}
|
||||||
|
</div>
|
||||||
|
<div style="text-align:center;padding:16px 24px 16px 24px">
|
||||||
|
<a
|
||||||
|
href="{{ absolute_url(path('chill_main_workflow_send_view_public', {'uuid': send.uuid, 'verificationKey': send.privateToken})) }}"
|
||||||
|
style="color:#FFFFFF;font-size:16px;font-weight:bold;background-color:#334d5c;border-radius:4px;display:inline-block;padding:12px 20px;text-decoration:none"
|
||||||
|
target="_blank"
|
||||||
|
><span
|
||||||
|
><!--[if mso
|
||||||
|
]><i
|
||||||
|
style="letter-spacing: 20px;mso-font-width:-100%;mso-text-raise:30"
|
||||||
|
hidden
|
||||||
|
> </i
|
||||||
|
><!
|
||||||
|
[endif]--></span
|
||||||
|
><span>
|
||||||
|
{{ 'workflow.send_external_message.button_content'|trans({'sender': sender}) }}
|
||||||
|
</span><span
|
||||||
|
><!--[if mso
|
||||||
|
]><i
|
||||||
|
style="letter-spacing: 20px;mso-font-width:-100%"
|
||||||
|
hidden
|
||||||
|
> </i
|
||||||
|
><!
|
||||||
|
[endif]--></span
|
||||||
|
></a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div style="font-weight:normal;padding:16px 24px 16px 24px">
|
||||||
|
{{ 'workflow.send_external_message.document_available_until'|trans({ 'expiration': send.expireAt}, null, lang) }}
|
||||||
|
</div>
|
||||||
|
<div style="font-weight:normal;padding:16px 24px 16px 24px">
|
||||||
|
{{ 'workflow.send_external_message.or_see_link'|trans }} :
|
||||||
|
</div>
|
||||||
|
<div style="font-size:16px;padding:16px 24px 16px 24px">
|
||||||
|
<code>{{ absolute_url(path('chill_main_workflow_send_view_public', {'uuid': send.uuid, 'verificationKey': send.privateToken})) }}</code>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>{{ 'workflow.public_link.expired_link_title'|trans }}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{ 'workflow.public_link.expired_link_title'|trans }}</h1>
|
||||||
|
|
||||||
|
<p>{{ 'workflow.public_link.expired_link_explanation'|trans }}</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,58 @@
|
|||||||
|
<!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" >
|
||||||
|
<title>{% block title %}{% endblock %}</title>
|
||||||
|
{% block head_custom %}{% endblock %}
|
||||||
|
<link rel="shortcut icon" href="{{ asset('build/images/favicon.ico') }}" type="image/x-icon">
|
||||||
|
|
||||||
|
{{ encore_entry_link_tags('mod_bootstrap') }}
|
||||||
|
{{ encore_entry_link_tags('mod_forkawesome') }}
|
||||||
|
{{ encore_entry_link_tags('chill') }}
|
||||||
|
|
||||||
|
{% block css %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<nav class="navbar navbar-dark bg-primary navbar-expand-md">
|
||||||
|
<div class="container-xxl">
|
||||||
|
|
||||||
|
<div class="col-4">
|
||||||
|
<a class="navbar-brand" href="{{ path('chill_main_homepage') }}">
|
||||||
|
{{ include('@ChillMain/Layout/_header-logo.html.twig') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-8"></div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="content" id="content">
|
||||||
|
<div class="container-xxl">
|
||||||
|
<div class="row justify-content-center my-5">
|
||||||
|
<div>
|
||||||
|
{% block public_content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 1rem;">
|
||||||
|
<p>
|
||||||
|
{{ 'workflow.public_link.shared_explanation_until_remaining'|trans({'expireAt': send.expireAt, 'viewsCount': metadata.viewsCount, 'viewsRemaining': metadata.viewsRemaining}) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ include('@ChillMain/Layout/_footer.html.twig', {'public_page': true}) }}
|
||||||
|
|
||||||
|
{{ encore_entry_script_tags('mod_bootstrap') }}
|
||||||
|
{{ encore_entry_script_tags('mod_forkawesome') }}
|
||||||
|
{{ encore_entry_script_tags('chill') }}
|
||||||
|
|
||||||
|
{% block js %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -12,9 +12,11 @@ declare(strict_types=1);
|
|||||||
namespace Chill\MainBundle\Service\Mailer;
|
namespace Chill\MainBundle\Service\Mailer;
|
||||||
|
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||||
use Symfony\Component\Mailer\Envelope;
|
use Symfony\Component\Mailer\Envelope;
|
||||||
use Symfony\Component\Mailer\MailerInterface;
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
use Symfony\Component\Mime\Address;
|
use Symfony\Component\Mime\Address;
|
||||||
|
use Symfony\Component\Mime\BodyRendererInterface;
|
||||||
use Symfony\Component\Mime\Email;
|
use Symfony\Component\Mime\Email;
|
||||||
use Symfony\Component\Mime\RawMessage;
|
use Symfony\Component\Mime\RawMessage;
|
||||||
|
|
||||||
@ -22,7 +24,7 @@ class ChillMailer implements MailerInterface
|
|||||||
{
|
{
|
||||||
private string $prefix = '[Chill] ';
|
private string $prefix = '[Chill] ';
|
||||||
|
|
||||||
public function __construct(private readonly MailerInterface $initial, private readonly LoggerInterface $chillLogger) {}
|
public function __construct(private readonly MailerInterface $initial, private readonly LoggerInterface $chillLogger, private readonly BodyRendererInterface $bodyRenderer) {}
|
||||||
|
|
||||||
public function send(RawMessage $message, ?Envelope $envelope = null): void
|
public function send(RawMessage $message, ?Envelope $envelope = null): void
|
||||||
{
|
{
|
||||||
@ -30,6 +32,10 @@ class ChillMailer implements MailerInterface
|
|||||||
$message->subject($this->prefix.$message->getSubject());
|
$message->subject($this->prefix.$message->getSubject());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($message instanceof TemplatedEmail) {
|
||||||
|
$this->bodyRenderer->render($message);
|
||||||
|
}
|
||||||
|
|
||||||
$this->chillLogger->info('chill email sent', [
|
$this->chillLogger->info('chill email sent', [
|
||||||
'to' => array_map(static fn (Address $address) => $address->getAddress(), $message->getTo()),
|
'to' => array_map(static fn (Address $address) => $address->getAddress(), $message->getTo()),
|
||||||
'subject' => $message->getSubject(),
|
'subject' => $message->getSubject(),
|
||||||
|
@ -14,6 +14,7 @@ namespace Chill\MainBundle\Service\Workflow;
|
|||||||
use Chill\MainBundle\Cron\CronJobInterface;
|
use Chill\MainBundle\Cron\CronJobInterface;
|
||||||
use Chill\MainBundle\Entity\CronJobExecution;
|
use Chill\MainBundle\Entity\CronJobExecution;
|
||||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\Clock\ClockInterface;
|
use Symfony\Component\Clock\ClockInterface;
|
||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
@ -31,6 +32,7 @@ class CancelStaleWorkflowCronJob implements CronJobInterface
|
|||||||
private readonly ClockInterface $clock,
|
private readonly ClockInterface $clock,
|
||||||
private readonly MessageBusInterface $messageBus,
|
private readonly MessageBusInterface $messageBus,
|
||||||
private readonly LoggerInterface $logger,
|
private readonly LoggerInterface $logger,
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function canRun(?CronJobExecution $cronJobExecution): bool
|
public function canRun(?CronJobExecution $cronJobExecution): bool
|
||||||
@ -48,19 +50,23 @@ class CancelStaleWorkflowCronJob implements CronJobInterface
|
|||||||
$this->logger->info('Cronjob started: Canceling stale workflows.');
|
$this->logger->info('Cronjob started: Canceling stale workflows.');
|
||||||
|
|
||||||
$olderThanDate = $this->clock->now()->sub(new \DateInterval(self::KEEP_INTERVAL));
|
$olderThanDate = $this->clock->now()->sub(new \DateInterval(self::KEEP_INTERVAL));
|
||||||
$staleWorkflowIds = $this->workflowRepository->findWorkflowsWithoutFinalStepAndOlderThan($olderThanDate);
|
$staleEntityWorkflows = $this->workflowRepository->findWorkflowsWithoutFinalStepAndOlderThan($olderThanDate);
|
||||||
$lastCanceled = $lastExecutionData[self::LAST_CANCELED_WORKFLOW] ?? 0;
|
$lastCanceled = $lastExecutionData[self::LAST_CANCELED_WORKFLOW] ?? 0;
|
||||||
$processedCount = 0;
|
$processedCount = 0;
|
||||||
|
|
||||||
foreach ($staleWorkflowIds as $wId) {
|
foreach ($staleEntityWorkflows as $staleEntityWorkflow) {
|
||||||
try {
|
try {
|
||||||
$this->messageBus->dispatch(new CancelStaleWorkflowMessage($wId));
|
$this->messageBus->dispatch(new CancelStaleWorkflowMessage($staleEntityWorkflow->getId()));
|
||||||
$lastCanceled = max($wId, $lastCanceled);
|
$lastCanceled = max($staleEntityWorkflow->getId(), $lastCanceled);
|
||||||
++$processedCount;
|
++$processedCount;
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->logger->error("Failed to dispatch CancelStaleWorkflow for ID {$wId}", ['exception' => $e]);
|
$this->logger->error('Failed to dispatch CancelStaleWorkflow', ['exception' => $e, 'entityWorkflowId' => $staleEntityWorkflow->getId()]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (0 === $processedCount % 10) {
|
||||||
|
$this->entityManager->clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->logger->info("Cronjob completed: {$processedCount} workflows processed.");
|
$this->logger->info("Cronjob completed: {$processedCount} workflows processed.");
|
||||||
|
@ -18,7 +18,9 @@ use Psr\Log\LoggerInterface;
|
|||||||
use Symfony\Component\Clock\ClockInterface;
|
use Symfony\Component\Clock\ClockInterface;
|
||||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||||
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
|
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
|
||||||
|
use Symfony\Component\Workflow\Metadata\MetadataStoreInterface;
|
||||||
use Symfony\Component\Workflow\Registry;
|
use Symfony\Component\Workflow\Registry;
|
||||||
|
use Symfony\Component\Workflow\Transition;
|
||||||
|
|
||||||
#[AsMessageHandler]
|
#[AsMessageHandler]
|
||||||
final readonly class CancelStaleWorkflowHandler
|
final readonly class CancelStaleWorkflowHandler
|
||||||
@ -57,10 +59,7 @@ final readonly class CancelStaleWorkflowHandler
|
|||||||
$wasInInitialPosition = 'initial' === $workflow->getStep();
|
$wasInInitialPosition = 'initial' === $workflow->getStep();
|
||||||
|
|
||||||
foreach ($transitions as $transition) {
|
foreach ($transitions as $transition) {
|
||||||
$isFinal = $metadataStore->getMetadata('isFinal', $transition);
|
if ($this->willTransitionLeadToFinalNegative($transition, $metadataStore)) {
|
||||||
$isFinalPositive = $metadataStore->getMetadata('isFinalPositive', $transition);
|
|
||||||
|
|
||||||
if ($isFinal && !$isFinalPositive) {
|
|
||||||
$dto = new WorkflowTransitionContextDTO($workflow);
|
$dto = new WorkflowTransitionContextDTO($workflow);
|
||||||
$workflowComponent->apply($workflow, $transition->getName(), [
|
$workflowComponent->apply($workflow, $transition->getName(), [
|
||||||
'context' => $dto,
|
'context' => $dto,
|
||||||
@ -85,4 +84,16 @@ final readonly class CancelStaleWorkflowHandler
|
|||||||
|
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function willTransitionLeadToFinalNegative(Transition $transition, MetadataStoreInterface $metadataStore): bool
|
||||||
|
{
|
||||||
|
foreach ($transition->getTos() as $place) {
|
||||||
|
$metadata = $metadataStore->getPlaceMetadata($place);
|
||||||
|
if (($metadata['isFinal'] ?? true) && false === ($metadata['isFinalPositive'] ?? true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ class WorkflowAddSignatureControllerTest extends TestCase
|
|||||||
->willReturn([]);
|
->willReturn([]);
|
||||||
|
|
||||||
$twig = $this->createMock(Environment::class);
|
$twig = $this->createMock(Environment::class);
|
||||||
$twig->method('render')->with('@ChillMain/Workflow/_signature_sign.html.twig', $this->isType('array'))
|
$twig->method('render')->with('@ChillMain/Workflow/signature_sign.html.twig', $this->isType('array'))
|
||||||
->willReturn('ok');
|
->willReturn('ok');
|
||||||
|
|
||||||
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||||
|
@ -0,0 +1,257 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Tests\Controller;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Controller\WorkflowViewSendPublicController;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSendView;
|
||||||
|
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
|
||||||
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
|
use Chill\MainBundle\Workflow\EntityWorkflowWithPublicViewInterface;
|
||||||
|
use Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage;
|
||||||
|
use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO;
|
||||||
|
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
use Symfony\Component\Clock\MockClock;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
use Symfony\Component\Messenger\Envelope;
|
||||||
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
|
use Symfony\Component\Workflow\Registry;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class WorkflowViewSendPublicControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
public function testTooMuchTrials(): void
|
||||||
|
{
|
||||||
|
$environment = $this->prophesize(Environment::class);
|
||||||
|
$entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
$entityManager->flush()->shouldNotBeCalled();
|
||||||
|
$messageBus = $this->prophesize(MessageBusInterface::class);
|
||||||
|
$messageBus->dispatch(Argument::type(PostPublicViewMessage::class))->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$controller = new WorkflowViewSendPublicController($entityManager->reveal(), new NullLogger(), new EntityWorkflowManager([], new Registry()), new MockClock(), $environment->reveal(), $messageBus->reveal());
|
||||||
|
|
||||||
|
self::expectException(AccessDeniedHttpException::class);
|
||||||
|
|
||||||
|
$send = $this->buildEntityWorkflowSend();
|
||||||
|
|
||||||
|
for ($i = 0; $i < 51; ++$i) {
|
||||||
|
$send->increaseErrorTrials();
|
||||||
|
}
|
||||||
|
|
||||||
|
$controller($send, $send->getPrivateToken(), new Request());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInvalidVerificationKey(): void
|
||||||
|
{
|
||||||
|
$environment = $this->prophesize(Environment::class);
|
||||||
|
$entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
$entityManager->flush()->shouldBeCalled();
|
||||||
|
$messageBus = $this->prophesize(MessageBusInterface::class);
|
||||||
|
$messageBus->dispatch(Argument::type(PostPublicViewMessage::class))->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$controller = new WorkflowViewSendPublicController($entityManager->reveal(), new NullLogger(), new EntityWorkflowManager([], new Registry()), new MockClock(), $environment->reveal(), $messageBus->reveal());
|
||||||
|
|
||||||
|
self::expectException(AccessDeniedHttpException::class);
|
||||||
|
|
||||||
|
$send = $this->buildEntityWorkflowSend();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$controller($send, 'some-token', new Request());
|
||||||
|
} catch (AccessDeniedHttpException $e) {
|
||||||
|
self::assertEquals(1, $send->getNumberOfErrorTrials());
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testExpiredLink(): void
|
||||||
|
{
|
||||||
|
$environment = $this->prophesize(Environment::class);
|
||||||
|
$environment->render('@ChillMain/Workflow/workflow_view_send_public_expired.html.twig')->willReturn('test');
|
||||||
|
|
||||||
|
$entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
$entityManager->flush()->shouldNotBeCalled();
|
||||||
|
$messageBus = $this->prophesize(MessageBusInterface::class);
|
||||||
|
$messageBus->dispatch(Argument::type(PostPublicViewMessage::class))->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$controller = new WorkflowViewSendPublicController($entityManager->reveal(), new NullLogger(), new EntityWorkflowManager([], new Registry()), new MockClock('next year'), $environment->reveal(), $messageBus->reveal());
|
||||||
|
|
||||||
|
$send = $this->buildEntityWorkflowSend();
|
||||||
|
|
||||||
|
$response = $controller($send, $send->getPrivateToken(), new Request());
|
||||||
|
|
||||||
|
self::assertEquals('test', $response->getContent());
|
||||||
|
self::assertEquals(409, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNoHandlerFound(): void
|
||||||
|
{
|
||||||
|
$environment = $this->prophesize(Environment::class);
|
||||||
|
$entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
$entityManager->flush()->shouldNotBeCalled();
|
||||||
|
$messageBus = $this->prophesize(MessageBusInterface::class);
|
||||||
|
$messageBus->dispatch(Argument::type(PostPublicViewMessage::class))->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$controller = new WorkflowViewSendPublicController(
|
||||||
|
$entityManager->reveal(),
|
||||||
|
new NullLogger(),
|
||||||
|
new EntityWorkflowManager([], new Registry()),
|
||||||
|
new MockClock(),
|
||||||
|
$environment->reveal(),
|
||||||
|
$messageBus->reveal(),
|
||||||
|
);
|
||||||
|
|
||||||
|
self::expectException(\RuntimeException::class);
|
||||||
|
|
||||||
|
$send = $this->buildEntityWorkflowSend();
|
||||||
|
$controller($send, $send->getPrivateToken(), new Request());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHappyScenario(): void
|
||||||
|
{
|
||||||
|
$send = $this->buildEntityWorkflowSend();
|
||||||
|
$environment = $this->prophesize(Environment::class);
|
||||||
|
$entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
$entityManager->persist(Argument::that(function (EntityWorkflowSendView $view) use ($send) {
|
||||||
|
$reflection = new \ReflectionClass($view);
|
||||||
|
$idProperty = $reflection->getProperty('id');
|
||||||
|
$idProperty->setAccessible(true);
|
||||||
|
$idProperty->setValue($view, 5);
|
||||||
|
|
||||||
|
return $send === $view->getSend();
|
||||||
|
}))->shouldBeCalled();
|
||||||
|
$entityManager->flush()->shouldBeCalled();
|
||||||
|
$messageBus = $this->prophesize(MessageBusInterface::class);
|
||||||
|
$messageBus->dispatch(Argument::type(PostPublicViewMessage::class))->shouldBeCalled()
|
||||||
|
->will(fn ($args) => new Envelope($args[0]));
|
||||||
|
|
||||||
|
|
||||||
|
$controller = new WorkflowViewSendPublicController(
|
||||||
|
$entityManager->reveal(),
|
||||||
|
new NullLogger(),
|
||||||
|
new EntityWorkflowManager([
|
||||||
|
$this->buildFakeHandler(),
|
||||||
|
], new Registry()),
|
||||||
|
new MockClock(),
|
||||||
|
$environment->reveal(),
|
||||||
|
$messageBus->reveal(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = $controller($send, $send->getPrivateToken(), $this->buildRequest());
|
||||||
|
|
||||||
|
self::assertEquals(200, $response->getStatusCode());
|
||||||
|
self::assertEquals('content', $response->getContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildFakeHandler(): EntityWorkflowHandlerInterface&EntityWorkflowWithPublicViewInterface
|
||||||
|
{
|
||||||
|
return new class () implements EntityWorkflowWithPublicViewInterface, EntityWorkflowHandlerInterface {
|
||||||
|
public function getDeletionRoles(): array
|
||||||
|
{
|
||||||
|
throw new \BadMethodCallException('not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array
|
||||||
|
{
|
||||||
|
throw new \BadMethodCallException('not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntityTitle(EntityWorkflow $entityWorkflow, array $options = []): string
|
||||||
|
{
|
||||||
|
throw new \BadMethodCallException('not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRelatedEntity(EntityWorkflow $entityWorkflow): ?object
|
||||||
|
{
|
||||||
|
throw new \BadMethodCallException('not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRelatedObjects(object $object): array
|
||||||
|
{
|
||||||
|
throw new \BadMethodCallException('not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRoleShow(EntityWorkflow $entityWorkflow): ?string
|
||||||
|
{
|
||||||
|
throw new \BadMethodCallException('not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSuggestedUsers(EntityWorkflow $entityWorkflow): array
|
||||||
|
{
|
||||||
|
throw new \BadMethodCallException('not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTemplate(EntityWorkflow $entityWorkflow, array $options = []): string
|
||||||
|
{
|
||||||
|
throw new \BadMethodCallException('not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTemplateData(EntityWorkflow $entityWorkflow, array $options = []): array
|
||||||
|
{
|
||||||
|
throw new \BadMethodCallException('not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isObjectSupported(object $object): bool
|
||||||
|
{
|
||||||
|
throw new \BadMethodCallException('not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByRelatedEntity(object $object): array
|
||||||
|
{
|
||||||
|
throw new \BadMethodCallException('not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renderPublicView(EntityWorkflowSend $entityWorkflowSend, EntityWorkflowViewMetadataDTO $metadata): string
|
||||||
|
{
|
||||||
|
return 'content';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildRequest(): Request
|
||||||
|
{
|
||||||
|
return Request::create('/test', server: ['REMOTE_ADDR' => '10.0.0.10']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildEntityWorkflowSend(): EntityWorkflowSend
|
||||||
|
{
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
|
||||||
|
$step = $entityWorkflow->getCurrentStep();
|
||||||
|
|
||||||
|
return new EntityWorkflowSend($step, new ThirdParty(), new \DateTimeImmutable('next month'));
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,7 @@ declare(strict_types=1);
|
|||||||
namespace ChillMainBundle\Tests\Repository;
|
namespace ChillMainBundle\Tests\Repository;
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
@ -40,9 +41,13 @@ class EntityWorkflowRepositoryTest extends KernelTestCase
|
|||||||
{
|
{
|
||||||
$repository = new EntityWorkflowRepository($this->em);
|
$repository = new EntityWorkflowRepository($this->em);
|
||||||
|
|
||||||
$actual = $repository->findWorkflowsWithoutFinalStepAndOlderThan(new \DateTimeImmutable('10 years ago'));
|
$actual = $repository->findWorkflowsWithoutFinalStepAndOlderThan((new \DateTimeImmutable('now'))->add(new \DateInterval('P10Y')));
|
||||||
|
|
||||||
self::assertIsArray($actual, 'check that the query is successful');
|
self::assertIsIterable($actual, 'check that the query is successful');
|
||||||
|
|
||||||
|
foreach ($actual as $entityWorkflow) {
|
||||||
|
self::assertInstanceOf(EntityWorkflow::class, $entityWorkflow);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCountQueryByDest(): void
|
public function testCountQueryByDest(): void
|
||||||
|
@ -9,12 +9,14 @@ declare(strict_types=1);
|
|||||||
* the LICENSE file that was distributed with this source code.
|
* the LICENSE file that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace Services\Workflow;
|
namespace ChillMainBundle\Tests\Services\Workflow;
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\CronJobExecution;
|
use Chill\MainBundle\Entity\CronJobExecution;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||||
use Chill\MainBundle\Service\Workflow\CancelStaleWorkflowCronJob;
|
use Chill\MainBundle\Service\Workflow\CancelStaleWorkflowCronJob;
|
||||||
use Chill\MainBundle\Service\Workflow\CancelStaleWorkflowMessage;
|
use Chill\MainBundle\Service\Workflow\CancelStaleWorkflowMessage;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use PHPUnit\Framework\MockObject\Exception;
|
use PHPUnit\Framework\MockObject\Exception;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
@ -39,8 +41,9 @@ class CancelStaleWorkflowCronJobTest extends TestCase
|
|||||||
{
|
{
|
||||||
$clock = new MockClock(new \DateTimeImmutable('2024-01-01 00:00:00', new \DateTimeZone('+00:00')));
|
$clock = new MockClock(new \DateTimeImmutable('2024-01-01 00:00:00', new \DateTimeZone('+00:00')));
|
||||||
$logger = $this->createMock(LoggerInterface::class);
|
$logger = $this->createMock(LoggerInterface::class);
|
||||||
|
$entityManager = $this->createMock(EntityManagerInterface::class);
|
||||||
|
|
||||||
$cronJob = new CancelStaleWorkflowCronJob($this->createMock(EntityWorkflowRepository::class), $clock, $this->buildMessageBus(), $logger);
|
$cronJob = new CancelStaleWorkflowCronJob($this->createMock(EntityWorkflowRepository::class), $clock, $this->buildMessageBus(), $logger, $entityManager);
|
||||||
|
|
||||||
self::assertEquals($expected, $cronJob->canRun($cronJobExecution));
|
self::assertEquals($expected, $cronJob->canRun($cronJobExecution));
|
||||||
}
|
}
|
||||||
@ -54,11 +57,16 @@ class CancelStaleWorkflowCronJobTest extends TestCase
|
|||||||
{
|
{
|
||||||
$clock = new MockClock((new \DateTimeImmutable('now', new \DateTimeZone('+00:00')))->add(new \DateInterval('P120D')));
|
$clock = new MockClock((new \DateTimeImmutable('now', new \DateTimeZone('+00:00')))->add(new \DateInterval('P120D')));
|
||||||
$workflowRepository = $this->createMock(EntityWorkflowRepository::class);
|
$workflowRepository = $this->createMock(EntityWorkflowRepository::class);
|
||||||
|
$entityManager = $this->createMock(EntityManagerInterface::class);
|
||||||
|
|
||||||
$workflowRepository->method('findWorkflowsWithoutFinalStepAndOlderThan')->willReturn([1, 3, 2]);
|
$workflowRepository->method('findWorkflowsWithoutFinalStepAndOlderThan')->willReturn([
|
||||||
|
$this->buildEntityWorkflow(1),
|
||||||
|
$this->buildEntityWorkflow(3),
|
||||||
|
$this->buildEntityWorkflow(2),
|
||||||
|
]);
|
||||||
$messageBus = $this->buildMessageBus(true);
|
$messageBus = $this->buildMessageBus(true);
|
||||||
|
|
||||||
$cronJob = new CancelStaleWorkflowCronJob($workflowRepository, $clock, $messageBus, new NullLogger());
|
$cronJob = new CancelStaleWorkflowCronJob($workflowRepository, $clock, $messageBus, new NullLogger(), $entityManager);
|
||||||
|
|
||||||
$results = $cronJob->run([]);
|
$results = $cronJob->run([]);
|
||||||
|
|
||||||
@ -67,6 +75,16 @@ class CancelStaleWorkflowCronJobTest extends TestCase
|
|||||||
self::assertEquals(3, $results['last-canceled-workflow-id']);
|
self::assertEquals(3, $results['last-canceled-workflow-id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildEntityWorkflow(int $id): EntityWorkflow
|
||||||
|
{
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$reflectionClass = new \ReflectionClass($entityWorkflow);
|
||||||
|
$idProperty = $reflectionClass->getProperty('id');
|
||||||
|
$idProperty->setValue($entityWorkflow, $id);
|
||||||
|
|
||||||
|
return $entityWorkflow;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws \Exception
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
|
@ -131,18 +131,22 @@ class CancelStaleWorkflowHandlerTest extends TestCase
|
|||||||
->setInitialPlaces('initial')
|
->setInitialPlaces('initial')
|
||||||
->addPlaces(['initial', 'step1', 'canceled', 'final'])
|
->addPlaces(['initial', 'step1', 'canceled', 'final'])
|
||||||
->addTransition(new Transition('to_step1', 'initial', 'step1'))
|
->addTransition(new Transition('to_step1', 'initial', 'step1'))
|
||||||
->addTransition($cancelInit = new Transition('cancel', 'initial', 'canceled'))
|
->addTransition(new Transition('cancel', 'initial', 'canceled'))
|
||||||
->addTransition($finalizeInit = new Transition('finalize', 'initial', 'final'))
|
->addTransition(new Transition('finalize', 'initial', 'final'))
|
||||||
->addTransition($cancelStep1 = new Transition('cancel', 'step1', 'canceled'))
|
->addTransition(new Transition('cancel', 'step1', 'canceled'))
|
||||||
->addTransition($finalizeStep1 = new Transition('finalize', 'step1', 'final'));
|
->addTransition(new Transition('finalize', 'step1', 'final'));
|
||||||
|
|
||||||
$transitionStorage = new \SplObjectStorage();
|
$definitionBuilder->setMetadataStore(new InMemoryMetadataStore(placesMetadata: [
|
||||||
$transitionStorage->attach($finalizeInit, ['isFinal' => true, 'isFinalPositive' => true]);
|
'canceled' => [
|
||||||
$transitionStorage->attach($cancelInit, ['isFinal' => true, 'isFinalPositive' => false]);
|
'isFinal' => true,
|
||||||
$transitionStorage->attach($finalizeStep1, ['isFinal' => true, 'isFinalPositive' => true]);
|
'isFinalPositive' => false,
|
||||||
$transitionStorage->attach($cancelStep1, ['isFinal' => true, 'isFinalPositive' => false]);
|
],
|
||||||
|
'final' => [
|
||||||
|
'isFinal' => true,
|
||||||
|
'isFinalPositive', true,
|
||||||
|
],
|
||||||
|
]));
|
||||||
|
|
||||||
$definitionBuilder->setMetadataStore(new InMemoryMetadataStore([], [], $transitionStorage));
|
|
||||||
$workflow = new Workflow($definitionBuilder->build(), new EntityWorkflowMarkingStore(), null, 'dummy_workflow');
|
$workflow = new Workflow($definitionBuilder->build(), new EntityWorkflowMarkingStore(), null, 'dummy_workflow');
|
||||||
$supports =
|
$supports =
|
||||||
new class () implements WorkflowSupportStrategyInterface {
|
new class () implements WorkflowSupportStrategyInterface {
|
||||||
|
@ -0,0 +1,140 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Tests\Workflow\EventSubscriber;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
|
||||||
|
use Chill\MainBundle\Workflow\EventSubscriber\EntityWorkflowPrepareEmailOnSendExternalEventSubscriber;
|
||||||
|
use Chill\MainBundle\Workflow\Messenger\PostSendExternalMessage;
|
||||||
|
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||||
|
use Symfony\Component\Messenger\Envelope;
|
||||||
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
|
use Symfony\Component\Workflow\DefinitionBuilder;
|
||||||
|
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
|
||||||
|
use Symfony\Component\Workflow\Registry;
|
||||||
|
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
|
||||||
|
use Symfony\Component\Workflow\Transition;
|
||||||
|
use Symfony\Component\Workflow\Workflow;
|
||||||
|
use Symfony\Component\Workflow\WorkflowInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class EntityWorkflowPrepareEmailOnSendExternalEventSubscriberTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
private Transition $transitionSendExternal;
|
||||||
|
private Transition $transitionRegular;
|
||||||
|
|
||||||
|
public function testToSendExternalGenerateMessage(): void
|
||||||
|
{
|
||||||
|
$messageBus = $this->prophesize(MessageBusInterface::class);
|
||||||
|
$messageBus->dispatch(Argument::type(PostSendExternalMessage::class))
|
||||||
|
->will(fn ($args) => new Envelope($args[0]))
|
||||||
|
->shouldBeCalled();
|
||||||
|
|
||||||
|
$registry = $this->buildRegistry($messageBus->reveal());
|
||||||
|
|
||||||
|
$entityWorkflow = $this->buildEntityWorkflow();
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
|
||||||
|
$workflow = $registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||||
|
$workflow->apply(
|
||||||
|
$entityWorkflow,
|
||||||
|
$this->transitionSendExternal->getName(),
|
||||||
|
['context' => $dto, 'byUser' => new User(), 'transition' => $this->transitionSendExternal->getName(),
|
||||||
|
'transitionAt' => new \DateTimeImmutable()]
|
||||||
|
);
|
||||||
|
|
||||||
|
// at this step, prophecy should check that the dispatch method has been called
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testToRegularDoNotGenerateMessage(): void
|
||||||
|
{
|
||||||
|
$messageBus = $this->prophesize(MessageBusInterface::class);
|
||||||
|
$messageBus->dispatch(Argument::type(PostSendExternalMessage::class))
|
||||||
|
->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$registry = $this->buildRegistry($messageBus->reveal());
|
||||||
|
|
||||||
|
$entityWorkflow = $this->buildEntityWorkflow();
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
|
||||||
|
$workflow = $registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||||
|
$workflow->apply(
|
||||||
|
$entityWorkflow,
|
||||||
|
$this->transitionRegular->getName(),
|
||||||
|
['context' => $dto, 'byUser' => new User(), 'transition' => $this->transitionRegular->getName(),
|
||||||
|
'transitionAt' => new \DateTimeImmutable()]
|
||||||
|
);
|
||||||
|
|
||||||
|
// at this step, prophecy should check that the dispatch method has been called
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildEntityWorkflow(): EntityWorkflow
|
||||||
|
{
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$entityWorkflow->setWorkflowName('dummy');
|
||||||
|
|
||||||
|
// set an id
|
||||||
|
$reflectionClass = new \ReflectionClass($entityWorkflow);
|
||||||
|
$idProperty = $reflectionClass->getProperty('id');
|
||||||
|
$idProperty->setValue($entityWorkflow, 1);
|
||||||
|
|
||||||
|
return $entityWorkflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildRegistry(MessageBusInterface $messageBus): Registry
|
||||||
|
{
|
||||||
|
$builder = new DefinitionBuilder(
|
||||||
|
['initial', 'sendExternal', 'regular'],
|
||||||
|
[
|
||||||
|
$this->transitionSendExternal = new Transition('toSendExternal', 'initial', 'sendExternal'),
|
||||||
|
$this->transitionRegular = new Transition('toRegular', 'initial', 'regular'),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$builder
|
||||||
|
->setInitialPlaces('initial')
|
||||||
|
->setMetadataStore(new InMemoryMetadataStore(
|
||||||
|
placesMetadata: [
|
||||||
|
'sendExternal' => ['isSentExternal' => true],
|
||||||
|
]
|
||||||
|
));
|
||||||
|
|
||||||
|
$entityMarkingStore = new EntityWorkflowMarkingStore();
|
||||||
|
$registry = new Registry();
|
||||||
|
|
||||||
|
$eventSubscriber = new EntityWorkflowPrepareEmailOnSendExternalEventSubscriber($registry, $messageBus);
|
||||||
|
$eventSubscriber->setLocale('fr');
|
||||||
|
$eventDispatcher = new EventDispatcher();
|
||||||
|
$eventDispatcher->addSubscriber($eventSubscriber);
|
||||||
|
|
||||||
|
$workflow = new Workflow($builder->build(), $entityMarkingStore, $eventDispatcher, 'dummy');
|
||||||
|
$registry->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface {
|
||||||
|
public function supports(WorkflowInterface $workflow, object $subject): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return $registry;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,141 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Tests\Workflow\EventSubscriber;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Entity\UserGroup;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
|
||||||
|
use Chill\MainBundle\Workflow\EventSubscriber\NotificationToUserGroupsOnTransition;
|
||||||
|
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
|
||||||
|
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||||
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
|
use Symfony\Component\Mime\BodyRendererInterface;
|
||||||
|
use Symfony\Component\Mime\RawMessage;
|
||||||
|
use Symfony\Component\Workflow\DefinitionBuilder;
|
||||||
|
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
|
||||||
|
use Symfony\Component\Workflow\Registry;
|
||||||
|
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
|
||||||
|
use Symfony\Component\Workflow\Transition;
|
||||||
|
use Symfony\Component\Workflow\Workflow;
|
||||||
|
use Symfony\Component\Workflow\WorkflowInterface;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class NotificationToUserGroupsOnTransitionTest extends KernelTestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
private Environment $twig;
|
||||||
|
private BodyRendererInterface $bodyRenderer;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
$this->twig = self::getContainer()->get('twig');
|
||||||
|
$this->bodyRenderer = self::getContainer()->get(BodyRendererInterface::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOnCompletedSendNotificationToUserGroupWithEmailAddress(): void
|
||||||
|
{
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$reflection = new \ReflectionClass($entityWorkflow);
|
||||||
|
$idProperty = $reflection->getProperty('id');
|
||||||
|
$idProperty->setValue($entityWorkflow, 1);
|
||||||
|
|
||||||
|
$entityWorkflow->setWorkflowName('dummy');
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futureDestUsers = [$ug = new UserGroup()];
|
||||||
|
$ug->setEmail('test@email.com')->setLabel(['fr' => 'test group']);
|
||||||
|
|
||||||
|
$mailer = $this->prophesize(MailerInterface::class);
|
||||||
|
$sendMethod = $mailer->send(Argument::that(function (RawMessage $message): bool {
|
||||||
|
if (!$message instanceof TemplatedEmail) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->bodyRenderer->render($message);
|
||||||
|
|
||||||
|
return 'test@email.com' === $message->getTo()[0]->getAddress();
|
||||||
|
}));
|
||||||
|
$sendMethod->shouldBeCalledOnce();
|
||||||
|
|
||||||
|
$metadataExtractor = $this->prophesize(MetadataExtractor::class);
|
||||||
|
$metadataExtractor->buildArrayPresentationForWorkflow(Argument::type(Workflow::class))->willReturn(['name' => 'dummy', 'text' => 'Dummy Workflow']);
|
||||||
|
$metadataExtractor->buildArrayPresentationForPlace($entityWorkflow)->willReturn(['name' => 'to_one', 'text' => 'Dummy Place']);
|
||||||
|
|
||||||
|
$registry = $this->buildRegistryWithEventSubscriber($mailer->reveal(), $metadataExtractor->reveal());
|
||||||
|
$workflow = $registry->get($entityWorkflow, 'dummy');
|
||||||
|
|
||||||
|
$workflow->apply($entityWorkflow, 'to_one', ['context' => $dto, 'transition' => 'to_one', 'transitionAt' => new \DateTimeImmutable(), 'byUser' => new User()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOnCompletedSendNotificationToUserGroupWithoutAnyEmailAddress(): void
|
||||||
|
{
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$reflection = new \ReflectionClass($entityWorkflow);
|
||||||
|
$idProperty = $reflection->getProperty('id');
|
||||||
|
$idProperty->setValue($entityWorkflow, 1);
|
||||||
|
|
||||||
|
$entityWorkflow->setWorkflowName('dummy');
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futureDestUsers = [$ug = new UserGroup()];
|
||||||
|
|
||||||
|
$mailer = $this->prophesize(MailerInterface::class);
|
||||||
|
$mailer->send(Argument::any())->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$metadataExtractor = $this->prophesize(MetadataExtractor::class);
|
||||||
|
$metadataExtractor->buildArrayPresentationForWorkflow(Argument::type(Workflow::class))->willReturn(['name' => 'dummy', 'text' => 'Dummy Workflow']);
|
||||||
|
$metadataExtractor->buildArrayPresentationForPlace($entityWorkflow)->willReturn(['name' => 'to_one', 'text' => 'Dummy Place']);
|
||||||
|
|
||||||
|
$registry = $this->buildRegistryWithEventSubscriber($mailer->reveal(), $metadataExtractor->reveal());
|
||||||
|
$workflow = $registry->get($entityWorkflow, 'dummy');
|
||||||
|
|
||||||
|
$workflow->apply($entityWorkflow, 'to_one', ['context' => $dto, 'transition' => 'to_one', 'transitionAt' => new \DateTimeImmutable(), 'byUser' => new User()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildRegistryWithEventSubscriber(MailerInterface $mailer, MetadataExtractor $metadataExtractor): Registry
|
||||||
|
{
|
||||||
|
$builder = new DefinitionBuilder();
|
||||||
|
$builder
|
||||||
|
->setInitialPlaces('initial')
|
||||||
|
->addPlaces(['initial', 'to_one'])
|
||||||
|
->addTransition(new Transition('to_one', 'initial', 'to_one'));
|
||||||
|
|
||||||
|
$metadata = new InMemoryMetadataStore(
|
||||||
|
['label' => ['fr' => 'dummy workflow']],
|
||||||
|
);
|
||||||
|
$builder->setMetadataStore($metadata);
|
||||||
|
|
||||||
|
$workflow = new Workflow($builder->build(), new EntityWorkflowMarkingStore(), $eventDispatcher = new EventDispatcher(), 'dummy');
|
||||||
|
$registry = new Registry();
|
||||||
|
$registry->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface {
|
||||||
|
public function supports(WorkflowInterface $workflow, object $subject): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$notificationEventSubscriber = new NotificationToUserGroupsOnTransition($this->twig, $metadataExtractor, $registry, $mailer);
|
||||||
|
$eventDispatcher->addSubscriber($notificationEventSubscriber);
|
||||||
|
|
||||||
|
return $registry;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,159 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Tests\Workflow\Messenger;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSendView;
|
||||||
|
use Chill\MainBundle\Repository\EntityWorkflowSendViewRepository;
|
||||||
|
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
|
||||||
|
use Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage;
|
||||||
|
use Chill\MainBundle\Workflow\Messenger\PostPublicViewMessageHandler;
|
||||||
|
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||||
|
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
use Symfony\Component\Workflow\DefinitionBuilder;
|
||||||
|
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
|
||||||
|
use Symfony\Component\Workflow\Registry;
|
||||||
|
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
|
||||||
|
use Symfony\Component\Workflow\Transition;
|
||||||
|
use Symfony\Component\Workflow\Workflow;
|
||||||
|
use Symfony\Component\Workflow\WorkflowInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class PostPublicViewMessageHandlerTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
private function buildRegistry(): Registry
|
||||||
|
{
|
||||||
|
$builder = new DefinitionBuilder();
|
||||||
|
$builder
|
||||||
|
->setInitialPlaces(['initial'])
|
||||||
|
->addPlaces(['initial', 'waiting_for_views', 'waiting_for_views_transition_unavailable', 'post_view'])
|
||||||
|
->addTransitions([
|
||||||
|
new Transition('post_view', 'waiting_for_views', 'post_view'),
|
||||||
|
])
|
||||||
|
->setMetadataStore(
|
||||||
|
new InMemoryMetadataStore(
|
||||||
|
placesMetadata: [
|
||||||
|
'waiting_for_views' => [
|
||||||
|
'isSentExternal' => true,
|
||||||
|
'onExternalView' => 'post_view',
|
||||||
|
],
|
||||||
|
'waiting_for_views_transition_unavailable' => [
|
||||||
|
'isSentExternal' => true,
|
||||||
|
'onExternalView' => 'post_view',
|
||||||
|
],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$workflow = new Workflow($builder->build(), new EntityWorkflowMarkingStore(), name: 'dummy');
|
||||||
|
$registry = new Registry();
|
||||||
|
$registry
|
||||||
|
->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface {
|
||||||
|
public function supports(WorkflowInterface $workflow, object $subject): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return $registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildEntityManager(bool $mustBeFlushed = false): EntityManagerInterface
|
||||||
|
{
|
||||||
|
$entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
$flush = $entityManager->flush();
|
||||||
|
$entityManager->clear()->shouldBeCalled();
|
||||||
|
|
||||||
|
if ($mustBeFlushed) {
|
||||||
|
$flush->shouldBeCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $entityManager->reveal();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandleTransitionToPostViewSuccessful(): void
|
||||||
|
{
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$entityWorkflow->setWorkflowName('dummy');
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futureDestineeThirdParties = [new ThirdParty()];
|
||||||
|
$entityWorkflow->setStep('waiting_for_views', $dto, 'to_waiting_for_views', new \DateTimeImmutable(), new User());
|
||||||
|
$send = $entityWorkflow->getCurrentStep()->getSends()->first();
|
||||||
|
$view = new EntityWorkflowSendView($send, new \DateTimeImmutable(), '127.0.0.1');
|
||||||
|
|
||||||
|
$repository = $this->prophesize(EntityWorkflowSendViewRepository::class);
|
||||||
|
$repository->find(6)->willReturn($view);
|
||||||
|
|
||||||
|
$handler = new PostPublicViewMessageHandler($repository->reveal(), $this->buildRegistry(), new NullLogger(), $this->buildEntityManager(true));
|
||||||
|
|
||||||
|
$handler(new PostPublicViewMessage(6));
|
||||||
|
|
||||||
|
self::assertEquals('post_view', $entityWorkflow->getStep());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandleTransitionToPostViewAlreadyMoved(): void
|
||||||
|
{
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$entityWorkflow->setWorkflowName('dummy');
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futureDestineeThirdParties = [new ThirdParty()];
|
||||||
|
$entityWorkflow->setStep('waiting_for_views', $dto, 'to_waiting_for_views', new \DateTimeImmutable(), new User());
|
||||||
|
$send = $entityWorkflow->getCurrentStep()->getSends()->first();
|
||||||
|
$view = new EntityWorkflowSendView($send, new \DateTimeImmutable(), '127.0.0.1');
|
||||||
|
// move again
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$entityWorkflow->setStep('post_view', $dto, 'post_view', new \DateTimeImmutable(), new User());
|
||||||
|
$lastStep = $entityWorkflow->getCurrentStep();
|
||||||
|
|
||||||
|
|
||||||
|
$repository = $this->prophesize(EntityWorkflowSendViewRepository::class);
|
||||||
|
$repository->find(6)->willReturn($view);
|
||||||
|
|
||||||
|
$handler = new PostPublicViewMessageHandler($repository->reveal(), $this->buildRegistry(), new NullLogger(), $this->buildEntityManager());
|
||||||
|
|
||||||
|
$handler(new PostPublicViewMessage(6));
|
||||||
|
|
||||||
|
self::assertEquals('post_view', $entityWorkflow->getStep());
|
||||||
|
self::assertSame($lastStep, $entityWorkflow->getCurrentStep());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandleTransitionToPostViewBlocked(): void
|
||||||
|
{
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$entityWorkflow->setWorkflowName('dummy');
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futureDestineeThirdParties = [new ThirdParty()];
|
||||||
|
$entityWorkflow->setStep('waiting_for_views_transition_unavailable', $dto, 'to_waiting_for_views', new \DateTimeImmutable(), new User());
|
||||||
|
$send = $entityWorkflow->getCurrentStep()->getSends()->first();
|
||||||
|
$view = new EntityWorkflowSendView($send, new \DateTimeImmutable(), '127.0.0.1');
|
||||||
|
|
||||||
|
$repository = $this->prophesize(EntityWorkflowSendViewRepository::class);
|
||||||
|
$repository->find(6)->willReturn($view);
|
||||||
|
|
||||||
|
$handler = new PostPublicViewMessageHandler($repository->reveal(), $this->buildRegistry(), new NullLogger(), $this->buildEntityManager());
|
||||||
|
|
||||||
|
$handler(new PostPublicViewMessage(6));
|
||||||
|
|
||||||
|
self::assertEquals('waiting_for_views_transition_unavailable', $entityWorkflow->getStep());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Tests\Workflow\Messenger;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||||
|
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
|
||||||
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
|
use Chill\MainBundle\Workflow\Messenger\PostSendExternalMessage;
|
||||||
|
use Chill\MainBundle\Workflow\Messenger\PostSendExternalMessageHandler;
|
||||||
|
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||||
|
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||||
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
|
use Symfony\Component\Mime\Address;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class PostSendExternalMessageHandlerTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
public function testSendMessageHappyScenario(): void
|
||||||
|
{
|
||||||
|
$entityWorkflow = $this->buildEntityWorkflow();
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futureDestineeEmails = ['external@example.com'];
|
||||||
|
$dto->futureDestineeThirdParties = [(new ThirdParty())->setEmail('3party@example.com')];
|
||||||
|
$entityWorkflow->setStep('send_external', $dto, 'to_send_external', new \DateTimeImmutable(), new User());
|
||||||
|
|
||||||
|
$repository = $this->prophesize(EntityWorkflowRepository::class);
|
||||||
|
$repository->find(1)->willReturn($entityWorkflow);
|
||||||
|
|
||||||
|
$mailer = $this->prophesize(MailerInterface::class);
|
||||||
|
$mailer->send(Argument::that($this->buildCheckAddressCallback('3party@example.com')))->shouldBeCalledOnce();
|
||||||
|
$mailer->send(Argument::that($this->buildCheckAddressCallback('external@example.com')))->shouldBeCalledOnce();
|
||||||
|
|
||||||
|
$workflowHandler = $this->prophesize(EntityWorkflowHandlerInterface::class);
|
||||||
|
$workflowHandler->getEntityTitle($entityWorkflow, Argument::any())->willReturn('title');
|
||||||
|
$workflowManager = $this->prophesize(EntityWorkflowManager::class);
|
||||||
|
$workflowManager->getHandler($entityWorkflow)->willReturn($workflowHandler->reveal());
|
||||||
|
|
||||||
|
$handler = new PostSendExternalMessageHandler($repository->reveal(), $mailer->reveal(), $workflowManager->reveal());
|
||||||
|
|
||||||
|
$handler(new PostSendExternalMessage(1, 'fr'));
|
||||||
|
|
||||||
|
// prophecy should do the check at the end of this test
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildCheckAddressCallback(string $emailToCheck): callable
|
||||||
|
{
|
||||||
|
return fn (TemplatedEmail $email): bool => in_array($emailToCheck, array_map(fn (Address $addr) => $addr->getAddress(), $email->getTo()), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildEntityWorkflow(): EntityWorkflow
|
||||||
|
{
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$reflection = new \ReflectionClass($entityWorkflow);
|
||||||
|
$idProperty = $reflection->getProperty('id');
|
||||||
|
$idProperty->setValue($entityWorkflow, 1);
|
||||||
|
|
||||||
|
return $entityWorkflow;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,209 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Tests\Workflow\Validator;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Workflow\Validator\TransitionHasDestUserIfRequired;
|
||||||
|
use Chill\MainBundle\Workflow\Validator\TransitionHasDestUserIfRequiredValidator;
|
||||||
|
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||||
|
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
||||||
|
use Symfony\Component\Workflow\DefinitionBuilder;
|
||||||
|
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
|
||||||
|
use Symfony\Component\Workflow\Registry;
|
||||||
|
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
|
||||||
|
use Symfony\Component\Workflow\Transition;
|
||||||
|
use Symfony\Component\Workflow\Workflow;
|
||||||
|
use Symfony\Component\Workflow\WorkflowInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class TransitionHasDestUserIfRequiredValidatorTest extends ConstraintValidatorTestCase
|
||||||
|
{
|
||||||
|
private Transition $transitionToSent;
|
||||||
|
private Transition $transitionRegular;
|
||||||
|
private Transition $transitionSignature;
|
||||||
|
private Transition $transitionFinal;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->transitionToSent = new Transition('send', 'initial', 'sent');
|
||||||
|
$this->transitionSignature = new Transition('signature', 'initial', 'signature');
|
||||||
|
$this->transitionRegular = new Transition('regular', 'initial', 'regular');
|
||||||
|
$this->transitionFinal = new Transition('final', 'initial', 'final');
|
||||||
|
|
||||||
|
parent::setUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTransitionToRegularWithDestUsersRaiseNoViolation(): void
|
||||||
|
{
|
||||||
|
$dto = $this->buildDto();
|
||||||
|
$dto->transition = $this->transitionRegular;
|
||||||
|
$dto->futureDestUsers = [new User()];
|
||||||
|
|
||||||
|
$constraint = new TransitionHasDestUserIfRequired();
|
||||||
|
|
||||||
|
$this->validator->validate($dto, $constraint);
|
||||||
|
|
||||||
|
self::assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTransitionToRegularWithNoUsersRaiseViolation(): void
|
||||||
|
{
|
||||||
|
$dto = $this->buildDto();
|
||||||
|
$dto->transition = $this->transitionRegular;
|
||||||
|
|
||||||
|
$constraint = new TransitionHasDestUserIfRequired();
|
||||||
|
$constraint->messageDestUserRequired = 'validation_message';
|
||||||
|
|
||||||
|
$this->validator->validate($dto, $constraint);
|
||||||
|
|
||||||
|
self::buildViolation($constraint->messageDestUserRequired)
|
||||||
|
->setCode($constraint->codeDestUserRequired)
|
||||||
|
->atPath('property.path.futureDestUsers')
|
||||||
|
->assertRaised();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTransitionToSignatureWithUserRaiseViolation(): void
|
||||||
|
{
|
||||||
|
$dto = $this->buildDto();
|
||||||
|
$dto->transition = $this->transitionSignature;
|
||||||
|
$dto->futureDestUsers = [new User()];
|
||||||
|
|
||||||
|
$constraint = new TransitionHasDestUserIfRequired();
|
||||||
|
|
||||||
|
$this->validator->validate($dto, $constraint);
|
||||||
|
|
||||||
|
self::buildViolation($constraint->messageDestUserNotAuthorized)
|
||||||
|
->setCode($constraint->codeDestUserNotAuthorized)
|
||||||
|
->atPath('property.path.futureDestUsers')
|
||||||
|
->assertRaised();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTransitionToExternalSendWithUserRaiseViolation(): void
|
||||||
|
{
|
||||||
|
$dto = $this->buildDto();
|
||||||
|
$dto->transition = $this->transitionToSent;
|
||||||
|
$dto->futureDestUsers = [new User()];
|
||||||
|
|
||||||
|
$constraint = new TransitionHasDestUserIfRequired();
|
||||||
|
|
||||||
|
$this->validator->validate($dto, $constraint);
|
||||||
|
|
||||||
|
self::buildViolation($constraint->messageDestUserNotAuthorized)
|
||||||
|
->setCode($constraint->codeDestUserNotAuthorized)
|
||||||
|
->atPath('property.path.futureDestUsers')
|
||||||
|
->assertRaised();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTransitionToFinalWithUserRaiseViolation(): void
|
||||||
|
{
|
||||||
|
$dto = $this->buildDto();
|
||||||
|
$dto->transition = $this->transitionFinal;
|
||||||
|
$dto->futureDestUsers = [new User()];
|
||||||
|
|
||||||
|
$constraint = new TransitionHasDestUserIfRequired();
|
||||||
|
|
||||||
|
$this->validator->validate($dto, $constraint);
|
||||||
|
|
||||||
|
self::buildViolation($constraint->messageDestUserNotAuthorized)
|
||||||
|
->setCode($constraint->codeDestUserNotAuthorized)
|
||||||
|
->atPath('property.path.futureDestUsers')
|
||||||
|
->assertRaised();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTransitionToSignatureWithNoUserNoViolation(): void
|
||||||
|
{
|
||||||
|
$dto = $this->buildDto();
|
||||||
|
$dto->transition = $this->transitionSignature;
|
||||||
|
|
||||||
|
$constraint = new TransitionHasDestUserIfRequired();
|
||||||
|
|
||||||
|
$this->validator->validate($dto, $constraint);
|
||||||
|
|
||||||
|
self::assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTransitionToExternalSendWithNoUserNoViolation(): void
|
||||||
|
{
|
||||||
|
$dto = $this->buildDto();
|
||||||
|
$dto->transition = $this->transitionToSent;
|
||||||
|
|
||||||
|
$constraint = new TransitionHasDestUserIfRequired();
|
||||||
|
|
||||||
|
$this->validator->validate($dto, $constraint);
|
||||||
|
|
||||||
|
self::assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTransitionToFinalWithNoUserNoViolation(): void
|
||||||
|
{
|
||||||
|
$dto = $this->buildDto();
|
||||||
|
$dto->transition = $this->transitionFinal;
|
||||||
|
|
||||||
|
$constraint = new TransitionHasDestUserIfRequired();
|
||||||
|
|
||||||
|
$this->validator->validate($dto, $constraint);
|
||||||
|
|
||||||
|
self::assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildDto(): WorkflowTransitionContextDTO
|
||||||
|
{
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$entityWorkflow->setWorkflowName('dummy');
|
||||||
|
|
||||||
|
return new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildRegistry(): Registry
|
||||||
|
{
|
||||||
|
$builder = new DefinitionBuilder(
|
||||||
|
['initial', 'sent', 'signature', 'regular', 'final'],
|
||||||
|
[
|
||||||
|
$this->transitionToSent,
|
||||||
|
$this->transitionSignature,
|
||||||
|
$this->transitionRegular,
|
||||||
|
$this->transitionFinal,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$builder
|
||||||
|
->setInitialPlaces('initial')
|
||||||
|
->setMetadataStore(new InMemoryMetadataStore(
|
||||||
|
placesMetadata: [
|
||||||
|
'sent' => ['isSentExternal' => true],
|
||||||
|
'signature' => ['isSignature' => ['person', 'user']],
|
||||||
|
'final' => ['isFinal' => true],
|
||||||
|
]
|
||||||
|
))
|
||||||
|
;
|
||||||
|
|
||||||
|
$workflow = new Workflow($builder->build(), name: 'dummy');
|
||||||
|
$registry = new Registry();
|
||||||
|
$registry->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface {
|
||||||
|
public function supports(WorkflowInterface $workflow, object $subject): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return $registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createValidator(): TransitionHasDestUserIfRequiredValidator
|
||||||
|
{
|
||||||
|
return new TransitionHasDestUserIfRequiredValidator($this->buildRegistry());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,181 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Tests\Workflow\Validator;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Workflow\Validator\TransitionHasDestineeIfIsSentExternal;
|
||||||
|
use Chill\MainBundle\Workflow\Validator\TransitionHasDestineeIfIsSentExternalValidator;
|
||||||
|
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||||
|
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||||
|
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
||||||
|
use Symfony\Component\Workflow\DefinitionBuilder;
|
||||||
|
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
|
||||||
|
use Symfony\Component\Workflow\Registry;
|
||||||
|
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
|
||||||
|
use Symfony\Component\Workflow\Transition;
|
||||||
|
use Symfony\Component\Workflow\Workflow;
|
||||||
|
use Symfony\Component\Workflow\WorkflowInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class TransitionHasDestineeIfIsSentExternalValidatorTest extends ConstraintValidatorTestCase
|
||||||
|
{
|
||||||
|
private Transition $transitionToSent;
|
||||||
|
private Transition $transitionToNotSent;
|
||||||
|
|
||||||
|
private function buildRegistry(): Registry
|
||||||
|
{
|
||||||
|
$builder = new DefinitionBuilder(
|
||||||
|
['initial', 'sent', 'notSent'],
|
||||||
|
[
|
||||||
|
$this->transitionToSent = new Transition('send', 'initial', 'sent'),
|
||||||
|
$this->transitionToNotSent = new Transition('notSend', 'initial', 'notSent'),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$builder
|
||||||
|
->setInitialPlaces('initial')
|
||||||
|
->setMetadataStore(new InMemoryMetadataStore(
|
||||||
|
placesMetadata: [
|
||||||
|
'sent' => ['isSentExternal' => true],
|
||||||
|
]
|
||||||
|
))
|
||||||
|
;
|
||||||
|
|
||||||
|
$workflow = new Workflow($builder->build(), name: 'dummy');
|
||||||
|
$registry = new Registry();
|
||||||
|
$registry->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface {
|
||||||
|
public function supports(WorkflowInterface $workflow, object $subject): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return $registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testToSentPlaceWithoutDestineeAddViolation(): void
|
||||||
|
{
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$entityWorkflow->setWorkflowName('dummy');
|
||||||
|
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->transition = $this->transitionToSent;
|
||||||
|
|
||||||
|
$constraint = new TransitionHasDestineeIfIsSentExternal();
|
||||||
|
$constraint->messageDestineeRequired = 'validation_message';
|
||||||
|
|
||||||
|
$this->validator->validate($dto, $constraint);
|
||||||
|
|
||||||
|
self::buildViolation('validation_message')
|
||||||
|
->setCode('d78ea142-819d-11ef-a459-b7009a3e4caf')
|
||||||
|
->atPath('property.path.futureDestineeThirdParties')
|
||||||
|
->assertRaised();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testToSentPlaceWithDestineeThirdPartyDoesNotAddViolation(): void
|
||||||
|
{
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$entityWorkflow->setWorkflowName('dummy');
|
||||||
|
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->transition = $this->transitionToSent;
|
||||||
|
$dto->futureDestineeThirdParties = [new ThirdParty()];
|
||||||
|
|
||||||
|
$constraint = new TransitionHasDestineeIfIsSentExternal();
|
||||||
|
$constraint->messageDestineeRequired = 'validation_message';
|
||||||
|
|
||||||
|
$this->validator->validate($dto, $constraint);
|
||||||
|
|
||||||
|
self::assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testToSentPlaceWithDestineeEmailDoesNotAddViolation(): void
|
||||||
|
{
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$entityWorkflow->setWorkflowName('dummy');
|
||||||
|
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->transition = $this->transitionToSent;
|
||||||
|
$dto->futureDestineeEmails = ['test@example.com'];
|
||||||
|
|
||||||
|
$constraint = new TransitionHasDestineeIfIsSentExternal();
|
||||||
|
$constraint->messageDestineeRequired = 'validation_message';
|
||||||
|
|
||||||
|
$this->validator->validate($dto, $constraint);
|
||||||
|
|
||||||
|
self::assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testToNoSentPlaceWithNoDestineesDoesNotAddViolation(): void
|
||||||
|
{
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$entityWorkflow->setWorkflowName('dummy');
|
||||||
|
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->transition = $this->transitionToNotSent;
|
||||||
|
|
||||||
|
$constraint = new TransitionHasDestineeIfIsSentExternal();
|
||||||
|
$constraint->messageDestineeRequired = 'validation_message';
|
||||||
|
|
||||||
|
$this->validator->validate($dto, $constraint);
|
||||||
|
|
||||||
|
self::assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testToNoSentPlaceWithDestineeThirdPartyAddViolation(): void
|
||||||
|
{
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$entityWorkflow->setWorkflowName('dummy');
|
||||||
|
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->transition = $this->transitionToNotSent;
|
||||||
|
$dto->futureDestineeThirdParties = [new ThirdParty()];
|
||||||
|
|
||||||
|
$constraint = new TransitionHasDestineeIfIsSentExternal();
|
||||||
|
$constraint->messageDestineeRequired = 'validation_message';
|
||||||
|
|
||||||
|
$this->validator->validate($dto, $constraint);
|
||||||
|
|
||||||
|
self::buildViolation('validation_message')
|
||||||
|
->atPath('property.path.futureDestineeThirdParties')
|
||||||
|
->setCode('eb8051fc-8227-11ef-8c3b-7f2de85bdc5b')
|
||||||
|
->assertRaised();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testToNoSentPlaceWithDestineeEmailAddViolation(): void
|
||||||
|
{
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$entityWorkflow->setWorkflowName('dummy');
|
||||||
|
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->transition = $this->transitionToNotSent;
|
||||||
|
$dto->futureDestineeEmails = ['test@example.com'];
|
||||||
|
|
||||||
|
$constraint = new TransitionHasDestineeIfIsSentExternal();
|
||||||
|
$constraint->messageDestineeRequired = 'validation_message';
|
||||||
|
|
||||||
|
$this->validator->validate($dto, $constraint);
|
||||||
|
|
||||||
|
self::buildViolation('validation_message')
|
||||||
|
->atPath('property.path.futureDestineeEmails')
|
||||||
|
->setCode('eb8051fc-8227-11ef-8c3b-7f2de85bdc5b')
|
||||||
|
->assertRaised();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createValidator(): TransitionHasDestineeIfIsSentExternalValidator
|
||||||
|
{
|
||||||
|
return new TransitionHasDestineeIfIsSentExternalValidator($this->buildRegistry());
|
||||||
|
}
|
||||||
|
}
|
@ -13,9 +13,17 @@ namespace Chill\MainBundle\Workflow;
|
|||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
|
||||||
use Chill\MainBundle\Workflow\Exception\HandlerNotFoundException;
|
use Chill\MainBundle\Workflow\Exception\HandlerNotFoundException;
|
||||||
|
use Chill\MainBundle\Workflow\Exception\HandlerWithPublicViewNotFoundException;
|
||||||
|
use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO;
|
||||||
use Symfony\Component\Workflow\Registry;
|
use Symfony\Component\Workflow\Registry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manage the handler and performs some operation on handlers.
|
||||||
|
*
|
||||||
|
* Each handler must implement @{EntityWorkflowHandlerInterface::class}.
|
||||||
|
*/
|
||||||
class EntityWorkflowManager
|
class EntityWorkflowManager
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
@ -63,4 +71,26 @@ class EntityWorkflowManager
|
|||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the public view for the given entity workflow send.
|
||||||
|
*
|
||||||
|
* @param EntityWorkflowSend $entityWorkflowSend the entity workflow send object
|
||||||
|
*
|
||||||
|
* @return string the rendered public view
|
||||||
|
*
|
||||||
|
* @throws HandlerWithPublicViewNotFoundException if no handler with public view is found
|
||||||
|
*/
|
||||||
|
public function renderPublicView(EntityWorkflowSend $entityWorkflowSend, EntityWorkflowViewMetadataDTO $metadata): string
|
||||||
|
{
|
||||||
|
$entityWorkflow = $entityWorkflowSend->getEntityWorkflowStep()->getEntityWorkflow();
|
||||||
|
|
||||||
|
foreach ($this->handlers as $handler) {
|
||||||
|
if ($handler instanceof EntityWorkflowWithPublicViewInterface && $handler->supports($entityWorkflow)) {
|
||||||
|
return $handler->renderPublicView($entityWorkflowSend, $metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new HandlerWithPublicViewNotFoundException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
<?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\Workflow;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
|
||||||
|
use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO;
|
||||||
|
|
||||||
|
interface EntityWorkflowWithPublicViewInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Render the public view for EntityWorkflowSend.
|
||||||
|
*
|
||||||
|
* The public view must be a safe html string
|
||||||
|
*/
|
||||||
|
public function renderPublicView(EntityWorkflowSend $entityWorkflowSend, EntityWorkflowViewMetadataDTO $metadata): string;
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
<?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\Workflow\EventSubscriber;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Workflow\Messenger\PostSendExternalMessage;
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
|
use Symfony\Component\Workflow\Event\CompletedEvent;
|
||||||
|
use Symfony\Component\Workflow\Registry;
|
||||||
|
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||||
|
|
||||||
|
class EntityWorkflowPrepareEmailOnSendExternalEventSubscriber implements EventSubscriberInterface, LocaleAwareInterface
|
||||||
|
{
|
||||||
|
private string $locale;
|
||||||
|
|
||||||
|
public function __construct(private readonly Registry $registry, private readonly MessageBusInterface $messageBus) {}
|
||||||
|
|
||||||
|
public static function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'workflow.completed' => 'onWorkflowCompleted',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onWorkflowCompleted(CompletedEvent $event): void
|
||||||
|
{
|
||||||
|
$entityWorkflow = $event->getSubject();
|
||||||
|
|
||||||
|
if (!$entityWorkflow instanceof EntityWorkflow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||||
|
$store = $workflow->getMetadataStore();
|
||||||
|
|
||||||
|
$mustSend = false;
|
||||||
|
foreach ($event->getTransition()->getTos() as $to) {
|
||||||
|
$metadata = $store->getPlaceMetadata($to);
|
||||||
|
if ($metadata['isSentExternal'] ?? false) {
|
||||||
|
$mustSend = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($mustSend) {
|
||||||
|
$this->messageBus->dispatch(new PostSendExternalMessage($entityWorkflow->getId(), $this->getLocale()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLocale(string $locale): void
|
||||||
|
{
|
||||||
|
$this->locale = $locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLocale(): string
|
||||||
|
{
|
||||||
|
return $this->locale;
|
||||||
|
}
|
||||||
|
}
|
@ -42,8 +42,8 @@ class NotificationOnTransition implements EventSubscriberInterface
|
|||||||
/**
|
/**
|
||||||
* Send a notification to:.
|
* Send a notification to:.
|
||||||
*
|
*
|
||||||
* * the dests of the new step;
|
* * the dests of the new step, or the members of a user group if the user group has no email;
|
||||||
* * the users which subscribed to workflow, on each step, or on final
|
* * the users which subscribed to workflow, on each step, or on final;
|
||||||
*
|
*
|
||||||
* **Warning** take care that this method must be executed **after** the dest users are added to
|
* **Warning** take care that this method must be executed **after** the dest users are added to
|
||||||
* the step (@see{EntityWorkflowStep::addDestUser}). Currently, this is done during
|
* the step (@see{EntityWorkflowStep::addDestUser}). Currently, this is done during
|
||||||
@ -74,6 +74,11 @@ class NotificationOnTransition implements EventSubscriberInterface
|
|||||||
// the users within groups
|
// the users within groups
|
||||||
$entityWorkflow->getCurrentStep()->getDestUserGroups()->reduce(
|
$entityWorkflow->getCurrentStep()->getDestUserGroups()->reduce(
|
||||||
function (array $accumulator, UserGroup $userGroup) {
|
function (array $accumulator, UserGroup $userGroup) {
|
||||||
|
if ($userGroup->hasEmail()) {
|
||||||
|
// this prevent users to be notified twice if they will already be notiied by the group
|
||||||
|
return $accumulator;
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($userGroup->getUsers() as $user) {
|
foreach ($userGroup->getUsers() as $user) {
|
||||||
$accumulator[] = $user;
|
$accumulator[] = $user;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,90 @@
|
|||||||
|
<?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\Workflow\EventSubscriber;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Notification;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
|
||||||
|
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
|
use Symfony\Component\Workflow\Event\Event;
|
||||||
|
use Symfony\Component\Workflow\Registry;
|
||||||
|
|
||||||
|
final readonly class NotificationToUserGroupsOnTransition implements EventSubscriberInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private \Twig\Environment $engine,
|
||||||
|
private MetadataExtractor $metadataExtractor,
|
||||||
|
private Registry $registry,
|
||||||
|
private MailerInterface $mailer,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'workflow.completed' => ['onCompletedSendNotification', 2048],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a notification to:.
|
||||||
|
*
|
||||||
|
* * the dests of the new step;
|
||||||
|
* * the users which subscribed to workflow, on each step, or on final
|
||||||
|
*
|
||||||
|
* **Warning** take care that this method must be executed **after** the dest users are added to
|
||||||
|
* the step (@see{EntityWorkflowStep::addDestUser}). Currently, this is done during
|
||||||
|
*
|
||||||
|
* @see{EntityWorkflowTransitionEventSubscriber::addDests}.
|
||||||
|
*/
|
||||||
|
public function onCompletedSendNotification(Event $event): void
|
||||||
|
{
|
||||||
|
if (!$event->getSubject() instanceof EntityWorkflow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var EntityWorkflow $entityWorkflow */
|
||||||
|
$entityWorkflow = $event->getSubject();
|
||||||
|
|
||||||
|
$place = $this->metadataExtractor->buildArrayPresentationForPlace($entityWorkflow);
|
||||||
|
$workflow = $this->metadataExtractor->buildArrayPresentationForWorkflow(
|
||||||
|
$this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName())
|
||||||
|
);
|
||||||
|
|
||||||
|
// send to groups
|
||||||
|
foreach ($entityWorkflow->getCurrentStep()->getDestUserGroups() as $userGroup) {
|
||||||
|
if (!$userGroup->hasEmail()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$context = [
|
||||||
|
'entity_workflow' => $entityWorkflow,
|
||||||
|
'user_group' => $userGroup,
|
||||||
|
'place' => $place,
|
||||||
|
'workflow' => $workflow,
|
||||||
|
'is_dest' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
$email = new TemplatedEmail();
|
||||||
|
$email
|
||||||
|
->htmlTemplate('@ChillMain/Workflow/workflow_notification_on_transition_completed_content_to_user_group.fr.txt.twig')
|
||||||
|
->context($context)
|
||||||
|
->subject(
|
||||||
|
$this->engine->render('@ChillMain/Workflow/workflow_notification_on_transition_completed_title.fr.txt.twig', $context)
|
||||||
|
)
|
||||||
|
->to($userGroup->getEmail());
|
||||||
|
|
||||||
|
$this->mailer->send($email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
<?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\Workflow\Exception;
|
||||||
|
|
||||||
|
class HandlerWithPublicViewNotFoundException extends \RuntimeException {}
|
@ -25,6 +25,9 @@ class MetadataExtractor
|
|||||||
private readonly DuplicateEntityWorkflowFinder $duplicateEntityWorkflowFinder,
|
private readonly DuplicateEntityWorkflowFinder $duplicateEntityWorkflowFinder,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{name: string, text: string}>
|
||||||
|
*/
|
||||||
public function availableWorkflowFor(string $relatedEntityClass, ?int $relatedEntityId = 0): array
|
public function availableWorkflowFor(string $relatedEntityClass, ?int $relatedEntityId = 0): array
|
||||||
{
|
{
|
||||||
$blankEntityWorkflow = new EntityWorkflow();
|
$blankEntityWorkflow = new EntityWorkflow();
|
||||||
@ -56,6 +59,9 @@ class MetadataExtractor
|
|||||||
return $workflowsList;
|
return $workflowsList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{name: string, text: string}
|
||||||
|
*/
|
||||||
public function buildArrayPresentationForPlace(EntityWorkflow $entityWorkflow, ?EntityWorkflowStep $step = null): array
|
public function buildArrayPresentationForPlace(EntityWorkflow $entityWorkflow, ?EntityWorkflowStep $step = null): array
|
||||||
{
|
{
|
||||||
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||||
@ -69,6 +75,9 @@ class MetadataExtractor
|
|||||||
return ['name' => $step->getCurrentStep(), 'text' => $text];
|
return ['name' => $step->getCurrentStep(), 'text' => $text];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{name?: string, text?: string, isForward?: bool}
|
||||||
|
*/
|
||||||
public function buildArrayPresentationForTransition(EntityWorkflow $entityWorkflow, string $transitionName): array
|
public function buildArrayPresentationForTransition(EntityWorkflow $entityWorkflow, string $transitionName): array
|
||||||
{
|
{
|
||||||
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||||
@ -90,6 +99,9 @@ class MetadataExtractor
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{name: string, text: string}
|
||||||
|
*/
|
||||||
public function buildArrayPresentationForWorkflow(WorkflowInterface $workflow): array
|
public function buildArrayPresentationForWorkflow(WorkflowInterface $workflow): array
|
||||||
{
|
{
|
||||||
$metadata = $workflow->getMetadataStore()->getWorkflowMetadata();
|
$metadata = $workflow->getMetadataStore()->getWorkflowMetadata();
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
<?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\Workflow\Messenger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message sent after a EntityWorkflowSendView was created, which means that
|
||||||
|
* an external user has seen a link for a public view.
|
||||||
|
*/
|
||||||
|
class PostPublicViewMessage
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public int $entityWorkflowSendViewId,
|
||||||
|
) {}
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
<?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\Workflow\Messenger;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Repository\EntityWorkflowSendViewRepository;
|
||||||
|
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||||
|
use Symfony\Component\Workflow\Registry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the behaviour after a EntityWorkflowSentView was created.
|
||||||
|
*
|
||||||
|
* This handler apply a transition if the workflow's configuration defines one.
|
||||||
|
*/
|
||||||
|
final readonly class PostPublicViewMessageHandler implements MessageHandlerInterface
|
||||||
|
{
|
||||||
|
private const LOG_PREFIX = '[PostPublicViewMessageHandler] ';
|
||||||
|
|
||||||
|
private const TRANSITION_ON_VIEW = 'onExternalView';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private EntityWorkflowSendViewRepository $sendViewRepository,
|
||||||
|
private Registry $registry,
|
||||||
|
private LoggerInterface $logger,
|
||||||
|
private EntityManagerInterface $entityManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function __invoke(PostPublicViewMessage $message): void
|
||||||
|
{
|
||||||
|
$view = $this->sendViewRepository->find($message->entityWorkflowSendViewId);
|
||||||
|
|
||||||
|
if (null === $view) {
|
||||||
|
throw new \RuntimeException("EntityworkflowSendViewId {$message->entityWorkflowSendViewId} not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
$step = $view->getSend()->getEntityWorkflowStep();
|
||||||
|
$entityWorkflow = $step->getEntityWorkflow();
|
||||||
|
|
||||||
|
if ($step !== $entityWorkflow->getCurrentStep()) {
|
||||||
|
$this->logger->info(self::LOG_PREFIX."Do not handle view, as the current's step for the associated EntityWorkflow has already moved", [
|
||||||
|
'id' => $message->entityWorkflowSendViewId,
|
||||||
|
'entityWorkflow' => $entityWorkflow->getId(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->entityManager->clear();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||||
|
$metadata = $workflow->getMetadataStore();
|
||||||
|
|
||||||
|
foreach ($workflow->getMarking($entityWorkflow)->getPlaces() as $place => $key) {
|
||||||
|
$placeMetadata = $metadata->getPlaceMetadata($place);
|
||||||
|
if (array_key_exists(self::TRANSITION_ON_VIEW, $placeMetadata)) {
|
||||||
|
if ($workflow->can($entityWorkflow, $placeMetadata[self::TRANSITION_ON_VIEW])) {
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->transition = $workflow->getEnabledTransition($entityWorkflow, $placeMetadata[self::TRANSITION_ON_VIEW]);
|
||||||
|
|
||||||
|
$this->logger->info(
|
||||||
|
self::LOG_PREFIX.'Apply transition to workflow after a first view',
|
||||||
|
['transition' => $placeMetadata[self::TRANSITION_ON_VIEW], 'entityWorkflowId' => $entityWorkflow->getId(),
|
||||||
|
'viewId' => $view->getId()]
|
||||||
|
);
|
||||||
|
|
||||||
|
$workflow->apply($entityWorkflow, $placeMetadata[self::TRANSITION_ON_VIEW], [
|
||||||
|
'context' => $dto,
|
||||||
|
'transitionAt' => $view->getViewAt(),
|
||||||
|
'transition' => $placeMetadata[self::TRANSITION_ON_VIEW],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
$this->entityManager->clear();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->logger->info(self::LOG_PREFIX.'Not able to apply this transition', ['transition' => $placeMetadata[self::TRANSITION_ON_VIEW],
|
||||||
|
'entityWorkflowId' => $entityWorkflow->getId(), 'viewId' => $view->getId()]);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->info(
|
||||||
|
self::LOG_PREFIX.'No transition applyied for this entityWorkflow after a view',
|
||||||
|
['entityWorkflowId' => $entityWorkflow->getId(), 'viewId' => $view->getId()]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->entityManager->clear();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Workflow\Messenger;
|
||||||
|
|
||||||
|
class PostSendExternalMessage
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly int $entityWorkflowId,
|
||||||
|
public readonly string $lang,
|
||||||
|
) {}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
<?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\Workflow\Messenger;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
|
||||||
|
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||||
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
|
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||||
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
|
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
|
||||||
|
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||||
|
|
||||||
|
final readonly class PostSendExternalMessageHandler implements MessageHandlerInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private EntityWorkflowRepository $entityWorkflowRepository,
|
||||||
|
private MailerInterface $mailer,
|
||||||
|
private EntityWorkflowManager $workflowManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function __invoke(PostSendExternalMessage $message): void
|
||||||
|
{
|
||||||
|
$entityWorkflow = $this->entityWorkflowRepository->find($message->entityWorkflowId);
|
||||||
|
|
||||||
|
if (null === $entityWorkflow) {
|
||||||
|
throw new UnrecoverableMessageHandlingException(sprintf('Entity workflow with id %d not found', $message->entityWorkflowId));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($entityWorkflow->getCurrentStep()->getSends() as $send) {
|
||||||
|
$this->sendEmailToDestinee($send, $message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sendEmailToDestinee(EntityWorkflowSend $send, PostSendExternalMessage $message): void
|
||||||
|
{
|
||||||
|
$entityWorkflow = $send->getEntityWorkflowStep()->getEntityWorkflow();
|
||||||
|
$title = $this->workflowManager->getHandler($entityWorkflow)->getEntityTitle($entityWorkflow);
|
||||||
|
$email = new TemplatedEmail();
|
||||||
|
$email
|
||||||
|
->to($send->getDestineeThirdParty()?->getEmail() ?? $send->getDestineeEmail())
|
||||||
|
->subject($title)
|
||||||
|
->htmlTemplate('@ChillMain/Workflow/workflow_send_external_email_to_destinee.html.twig')
|
||||||
|
->context([
|
||||||
|
'send' => $send,
|
||||||
|
'lang' => $message->lang,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->mailer->send($email);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
<?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\Workflow\Templating;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a DTO to give some metadata about the vizualisation of the
|
||||||
|
* EntityWorkflowView.
|
||||||
|
*
|
||||||
|
* The aim is to give some information from the controller which show the public view and the
|
||||||
|
* handler which will render the view.
|
||||||
|
*/
|
||||||
|
final readonly class EntityWorkflowViewMetadataDTO
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public int $viewsCount,
|
||||||
|
public int $viewsRemaining,
|
||||||
|
) {}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
<?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\Workflow\Templating;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Templating\Entity\ChillEntityRenderInterface;
|
||||||
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
final readonly class WorkflowEntityRender implements ChillEntityRenderInterface
|
||||||
|
{
|
||||||
|
public function __construct(private EntityWorkflowManager $entityWorkflowManager, private Environment $twig) {}
|
||||||
|
|
||||||
|
public function renderBox($entity, array $options): string
|
||||||
|
{
|
||||||
|
/** @var EntityWorkflow $entity */
|
||||||
|
$handler = $this->entityWorkflowManager->getHandler($entity);
|
||||||
|
|
||||||
|
return $this->twig->render(
|
||||||
|
$handler->getTemplate($entity),
|
||||||
|
$handler->getTemplateData($entity)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renderString($entity, array $options): string
|
||||||
|
{
|
||||||
|
/** @var EntityWorkflow $entity */
|
||||||
|
$handler = $this->entityWorkflowManager->getHandler($entity);
|
||||||
|
|
||||||
|
return $handler->getEntityTitle($entity, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supports(object $entity, array $options): bool
|
||||||
|
{
|
||||||
|
return $entity instanceof EntityWorkflow;
|
||||||
|
}
|
||||||
|
}
|
@ -27,7 +27,10 @@ class EntityWorkflowCreation extends \Symfony\Component\Validator\Constraint
|
|||||||
|
|
||||||
public string $messageWorkflowNotAvailable = 'Workflow is not valid';
|
public string $messageWorkflowNotAvailable = 'Workflow is not valid';
|
||||||
|
|
||||||
public function getTargets()
|
/**
|
||||||
|
* @return array<string>
|
||||||
|
*/
|
||||||
|
public function getTargets(): array
|
||||||
{
|
{
|
||||||
return [self::CLASS_CONSTRAINT];
|
return [self::CLASS_CONSTRAINT];
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Workflow\Validator;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that the next stop has a dest user if this is required by the transition.
|
||||||
|
*/
|
||||||
|
#[\Attribute]
|
||||||
|
class TransitionHasDestUserIfRequired extends Constraint
|
||||||
|
{
|
||||||
|
public $messageDestUserRequired = 'workflow.You must add at least one dest user or email';
|
||||||
|
public $codeDestUserRequired = '637c20a6-822c-11ef-a4dd-07b4c0c0efa8';
|
||||||
|
public $messageDestUserNotAuthorized = 'workflow.dest_user_not_authorized';
|
||||||
|
public $codeDestUserNotAuthorized = '8377be2c-822e-11ef-b53a-57ad65828a8e';
|
||||||
|
|
||||||
|
public function getTargets(): string
|
||||||
|
{
|
||||||
|
return self::CLASS_CONSTRAINT;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
<?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\Workflow\Validator;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\ConstraintValidator;
|
||||||
|
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||||
|
use Symfony\Component\Validator\Exception\UnexpectedValueException;
|
||||||
|
use Symfony\Component\Workflow\Registry;
|
||||||
|
|
||||||
|
final class TransitionHasDestUserIfRequiredValidator extends ConstraintValidator
|
||||||
|
{
|
||||||
|
public function __construct(private readonly Registry $registry) {}
|
||||||
|
|
||||||
|
public function validate($value, Constraint $constraint)
|
||||||
|
{
|
||||||
|
if (!$constraint instanceof TransitionHasDestUserIfRequired) {
|
||||||
|
throw new UnexpectedTypeException($constraint, TransitionHasDestUserIfRequired::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$value instanceof WorkflowTransitionContextDTO) {
|
||||||
|
throw new UnexpectedValueException($value, WorkflowTransitionContextDTO::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $value->transition) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$workflow = $this->registry->get($value->entityWorkflow, $value->entityWorkflow->getWorkflowName());
|
||||||
|
$metadataStore = $workflow->getMetadataStore();
|
||||||
|
|
||||||
|
$destUsersRequired = false;
|
||||||
|
|
||||||
|
foreach ($value->transition->getTos() as $to) {
|
||||||
|
$metadata = $metadataStore->getPlaceMetadata($to);
|
||||||
|
|
||||||
|
// if the place are only 'isSentExternal' or 'isSignature' or 'final', then, we skip - a destUser is not required
|
||||||
|
if ($metadata['isSentExternal'] ?? false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($metadata['isSignature'] ?? false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($metadata['isFinal'] ?? false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// if there isn't any 'isSentExternal' or 'isSignature' or final, then we must have a destUser
|
||||||
|
$destUsersRequired = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$destUsersRequired) {
|
||||||
|
if (0 < count($value->futureDestUsers)) {
|
||||||
|
$this->context->buildViolation($constraint->messageDestUserNotAuthorized)
|
||||||
|
->setCode($constraint->codeDestUserNotAuthorized)
|
||||||
|
->atPath('futureDestUsers')
|
||||||
|
->addViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 === count($value->futureDestUsers)) {
|
||||||
|
$this->context->buildViolation($constraint->messageDestUserRequired)
|
||||||
|
->setCode($constraint->codeDestUserRequired)
|
||||||
|
->atPath('futureDestUsers')
|
||||||
|
->addViolation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Workflow\Validator;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that a transition does have at least one external if the 'to' is 'isSentExternal'.
|
||||||
|
*/
|
||||||
|
#[\Attribute]
|
||||||
|
class TransitionHasDestineeIfIsSentExternal extends Constraint
|
||||||
|
{
|
||||||
|
public $messageDestineeRequired = 'workflow.transition_has_destinee_if_sent_external';
|
||||||
|
public $messageDestineeNotNecessary = 'workflow.transition_destinee_not_necessary';
|
||||||
|
public $codeNoNecessaryDestinee = 'd78ea142-819d-11ef-a459-b7009a3e4caf';
|
||||||
|
public $codeDestineeUnauthorized = 'eb8051fc-8227-11ef-8c3b-7f2de85bdc5b';
|
||||||
|
|
||||||
|
public function getTargets(): string
|
||||||
|
{
|
||||||
|
return self::CLASS_CONSTRAINT;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
<?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\Workflow\Validator;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\ConstraintValidator;
|
||||||
|
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||||
|
use Symfony\Component\Workflow\Registry;
|
||||||
|
|
||||||
|
final class TransitionHasDestineeIfIsSentExternalValidator extends ConstraintValidator
|
||||||
|
{
|
||||||
|
public function __construct(private readonly Registry $registry) {}
|
||||||
|
|
||||||
|
public function validate($value, Constraint $constraint)
|
||||||
|
{
|
||||||
|
if (!$constraint instanceof TransitionHasDestineeIfIsSentExternal) {
|
||||||
|
throw new UnexpectedTypeException($constraint, TransitionHasDestineeIfIsSentExternal::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$value instanceof WorkflowTransitionContextDTO) {
|
||||||
|
throw new UnexpectedTypeException($value, WorkflowTransitionContextDTO::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null == $value->transition) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$workflow = $this->registry->get($value->entityWorkflow, $value->entityWorkflow->getWorkflowName());
|
||||||
|
$isSentExternal = false;
|
||||||
|
foreach ($value->transition->getTos() as $to) {
|
||||||
|
$metadata = $workflow->getMetadataStore()->getPlaceMetadata($to);
|
||||||
|
|
||||||
|
$isSentExternal = $isSentExternal ? true : $metadata['isSentExternal'] ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$isSentExternal) {
|
||||||
|
if (0 !== count($value->futureDestineeThirdParties)) {
|
||||||
|
$this->context->buildViolation($constraint->messageDestineeRequired)
|
||||||
|
->atPath('futureDestineeThirdParties')
|
||||||
|
->setCode($constraint->codeDestineeUnauthorized)
|
||||||
|
->addViolation();
|
||||||
|
}
|
||||||
|
if (0 !== count($value->futureDestineeEmails)) {
|
||||||
|
$this->context->buildViolation($constraint->messageDestineeRequired)
|
||||||
|
->atPath('futureDestineeEmails')
|
||||||
|
->setCode($constraint->codeDestineeUnauthorized)
|
||||||
|
->addViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 === count($value->futureDestineeEmails) && 0 === count($value->futureDestineeThirdParties)) {
|
||||||
|
$this->context->buildViolation($constraint->messageDestineeRequired)
|
||||||
|
->atPath('futureDestineeThirdParties')
|
||||||
|
->setCode($constraint->codeNoNecessaryDestinee)
|
||||||
|
->addViolation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,11 @@ namespace Chill\MainBundle\Workflow;
|
|||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Entity\UserGroup;
|
use Chill\MainBundle\Entity\UserGroup;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Workflow\Validator\TransitionHasDestineeIfIsSentExternal;
|
||||||
|
use Chill\MainBundle\Workflow\Validator\TransitionHasDestUserIfRequired;
|
||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||||
|
use Chill\ThirdPartyBundle\Validator\ThirdPartyHasEmail;
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||||
use Symfony\Component\Workflow\Transition;
|
use Symfony\Component\Workflow\Transition;
|
||||||
@ -22,6 +26,8 @@ use Symfony\Component\Workflow\Transition;
|
|||||||
/**
|
/**
|
||||||
* Context for a transition on an workflow entity.
|
* Context for a transition on an workflow entity.
|
||||||
*/
|
*/
|
||||||
|
#[TransitionHasDestineeIfIsSentExternal]
|
||||||
|
#[TransitionHasDestUserIfRequired]
|
||||||
class WorkflowTransitionContextDTO
|
class WorkflowTransitionContextDTO
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
@ -54,6 +60,30 @@ class WorkflowTransitionContextDTO
|
|||||||
*/
|
*/
|
||||||
public ?User $futureUserSignature = null;
|
public ?User $futureUserSignature = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a list of future destinee third parties, when a workflow does send the document
|
||||||
|
* to a remote third party.
|
||||||
|
*
|
||||||
|
* @var array<ThirdParty>
|
||||||
|
*/
|
||||||
|
#[Assert\All(
|
||||||
|
new ThirdPartyHasEmail(),
|
||||||
|
)]
|
||||||
|
public array $futureDestineeThirdParties = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a list of future destinee emails, when a workflow does send the document to a remote
|
||||||
|
* email.
|
||||||
|
*
|
||||||
|
* @var array<string>
|
||||||
|
*/
|
||||||
|
#[Assert\All([
|
||||||
|
new Assert\NotBlank(),
|
||||||
|
new Assert\Email(),
|
||||||
|
])]
|
||||||
|
public array $futureDestineeEmails = [];
|
||||||
|
|
||||||
|
#[Assert\NotNull]
|
||||||
public ?Transition $transition = null;
|
public ?Transition $transition = null;
|
||||||
|
|
||||||
public string $comment = '';
|
public string $comment = '';
|
||||||
|
@ -33,15 +33,15 @@ services:
|
|||||||
# workflow related
|
# workflow related
|
||||||
Chill\MainBundle\Workflow\:
|
Chill\MainBundle\Workflow\:
|
||||||
resource: '../Workflow/'
|
resource: '../Workflow/'
|
||||||
autowire: true
|
|
||||||
autoconfigure: true
|
|
||||||
|
|
||||||
Chill\MainBundle\Workflow\EntityWorkflowManager:
|
Chill\MainBundle\Workflow\EntityWorkflowManager:
|
||||||
autoconfigure: true
|
|
||||||
autowire: true
|
|
||||||
arguments:
|
arguments:
|
||||||
$handlers: !tagged_iterator chill_main.workflow_handler
|
$handlers: !tagged_iterator chill_main.workflow_handler
|
||||||
|
|
||||||
|
# seems to have no alias on symfony 5.4
|
||||||
|
Symfony\Component\Mime\BodyRendererInterface:
|
||||||
|
alias: 'twig.mime_body_renderer'
|
||||||
|
|
||||||
# other stuffes
|
# other stuffes
|
||||||
|
|
||||||
chill.main.helper.translatable_string:
|
chill.main.helper.translatable_string:
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\Migrations\Main;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20241003094904 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Create tables for EntityWorkflowSend and EntityWorkflowSendView';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('CREATE SEQUENCE chill_main_workflow_entity_send_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||||
|
$this->addSql('CREATE SEQUENCE chill_main_workflow_entity_send_views_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||||
|
$this->addSql('CREATE TABLE chill_main_workflow_entity_send (id INT NOT NULL,'
|
||||||
|
.' destineeEmail TEXT DEFAULT \'\' NOT NULL, uuid UUID NOT NULL, privateToken VARCHAR(255) NOT NULL,'
|
||||||
|
.' numberOfErrorTrials INT DEFAULT 0 NOT NULL, expireAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, '
|
||||||
|
.'createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, destineeThirdParty_id INT DEFAULT NULL, '
|
||||||
|
.'entityWorkflowStep_id INT NOT NULL, createdBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX UNIQ_A0C0620FD17F50A6 ON chill_main_workflow_entity_send (uuid)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_A0C0620FDDFA98DE ON chill_main_workflow_entity_send (destineeThirdParty_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_A0C0620F3912FED6 ON chill_main_workflow_entity_send (entityWorkflowStep_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_A0C0620F3174800F ON chill_main_workflow_entity_send (createdBy_id)');
|
||||||
|
$this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_send.uuid IS \'(DC2Type:uuid)\'');
|
||||||
|
$this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_send.expireAt IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_send.createdAt IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('CREATE TABLE chill_main_workflow_entity_send_views (id INT NOT NULL, send_id INT NOT NULL, '
|
||||||
|
.'viewAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, remoteIp TEXT NOT NULL, PRIMARY KEY(id))');
|
||||||
|
$this->addSql('CREATE INDEX IDX_2659558513933E7B ON chill_main_workflow_entity_send_views (send_id)');
|
||||||
|
$this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_send_views.viewAt IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_workflow_entity_send ADD CONSTRAINT FK_A0C0620FDDFA98DE FOREIGN KEY (destineeThirdParty_id) REFERENCES chill_3party.third_party (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_workflow_entity_send ADD CONSTRAINT FK_A0C0620F3912FED6 FOREIGN KEY (entityWorkflowStep_id) REFERENCES chill_main_workflow_entity_step (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_workflow_entity_send ADD CONSTRAINT FK_A0C0620F3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_workflow_entity_send_views ADD CONSTRAINT FK_2659558513933E7B FOREIGN KEY (send_id) REFERENCES chill_main_workflow_entity_send (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP SEQUENCE chill_main_workflow_entity_send_id_seq CASCADE');
|
||||||
|
$this->addSql('DROP SEQUENCE chill_main_workflow_entity_send_views_id_seq CASCADE');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_workflow_entity_send DROP CONSTRAINT FK_A0C0620FDDFA98DE');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_workflow_entity_send DROP CONSTRAINT FK_A0C0620F3912FED6');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_workflow_entity_send DROP CONSTRAINT FK_A0C0620F3174800F');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_workflow_entity_send_views DROP CONSTRAINT FK_2659558513933E7B');
|
||||||
|
$this->addSql('DROP TABLE chill_main_workflow_entity_send');
|
||||||
|
$this->addSql('DROP TABLE chill_main_workflow_entity_send_views');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\Migrations\Main;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20241015142142 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add an email address to user groups';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_main_user_group ADD email TEXT DEFAULT \'\' NOT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_main_user_group DROP COLUMN email');
|
||||||
|
}
|
||||||
|
}
|
@ -67,6 +67,44 @@ workflow:
|
|||||||
one {Signature demandée}
|
one {Signature demandée}
|
||||||
other {Signatures demandées}
|
other {Signatures demandées}
|
||||||
}
|
}
|
||||||
|
pending_signatures: >-
|
||||||
|
{nb_signatures, plural,
|
||||||
|
=0 {Aucune signature demandée}
|
||||||
|
one {Une signature demandée}
|
||||||
|
other {# signatures demandées}
|
||||||
|
}
|
||||||
|
send_external_message:
|
||||||
|
document_available_until: Le lien sera valable jusqu'au {expiration, date, long} à {expiration, time, short}.
|
||||||
|
explanation: '{sender} vous fait parvenir des documents.'
|
||||||
|
button_content: 'Consulter les documents envoyés par {sender}'
|
||||||
|
confidentiality: Nous attirons votre attention sur le fait que ces documents sont confidentiels.
|
||||||
|
see_doc_action_description: 'Voir les documents confidentiels envoyés par {sender}'
|
||||||
|
|
||||||
|
external_views:
|
||||||
|
title: >-
|
||||||
|
{numberOfSends, plural,
|
||||||
|
=0 {En attente de consultation}
|
||||||
|
=1 {En attente de consultation}
|
||||||
|
other {En attente de consultations}
|
||||||
|
}
|
||||||
|
last_view_at: Dernière vue le {at, date, long} à {at, time, short}
|
||||||
|
number_of_views: >-
|
||||||
|
{numberOfViews, plural,
|
||||||
|
=0 {Le partage n'a jamais été visualisé}
|
||||||
|
=1 {Le partage a été visualisé une fois}
|
||||||
|
other {Le partage a été visualisé # fois}
|
||||||
|
}
|
||||||
|
public_link:
|
||||||
|
shared_explanation_until_remaining: >-
|
||||||
|
Ce partage sera actif jusqu'au {expireAt, date, long} à {expireAt, time, short}. {viewsCount, plural,
|
||||||
|
=0 {Ce partage n'a pas encore été visualisé}
|
||||||
|
one {Ce partage a été visualisé une fois}
|
||||||
|
other {Ce partage a été visualisé # fois}
|
||||||
|
}, {viewsRemaining, plural,
|
||||||
|
=0 {il ne reste plus aucune visualisation possible.}
|
||||||
|
one {il reste encore une visualisation possible.}
|
||||||
|
other {il reste encore # visualisations possibles.}
|
||||||
|
}
|
||||||
|
|
||||||
duration:
|
duration:
|
||||||
minute: >-
|
minute: >-
|
||||||
|
@ -69,6 +69,8 @@ user_group:
|
|||||||
me_only: Uniquement moi
|
me_only: Uniquement moi
|
||||||
me: Moi
|
me: Moi
|
||||||
append_users: Ajouter des utilisateurs
|
append_users: Ajouter des utilisateurs
|
||||||
|
Email: Adresse email du groupe
|
||||||
|
EmailHelp: Si rempli, des notifications supplémentaires seront envoyées à l'adresse email du groupe (workflow en attente, etc.)
|
||||||
|
|
||||||
inactive: inactif
|
inactive: inactif
|
||||||
|
|
||||||
@ -558,7 +560,25 @@ workflow:
|
|||||||
Automated transition: Transition automatique
|
Automated transition: Transition automatique
|
||||||
waiting_for_signature: En attente de signature
|
waiting_for_signature: En attente de signature
|
||||||
Permissions: Workflows (suivi de décision)
|
Permissions: Workflows (suivi de décision)
|
||||||
|
transition_destinee_third_party: Destinataire à partir des tiers externes
|
||||||
|
transition_destinee_third_party_help: Chaque destinataire recevra un lien sécurisé par courriel.
|
||||||
|
transition_destinee_emails_label: Envoi par courriel
|
||||||
|
transition_destinee_add_emails: Ajouter une adresse de courriel
|
||||||
|
transition_destinee_remove_emails: Supprimer
|
||||||
|
transition_destinee_emails_help: Le lien sécurisé sera envoyé à chaque adresse indiquée
|
||||||
|
sent_through_secured_link: Envoi par lien sécurisé
|
||||||
|
public_views_by_ip: Visualisation par adresse IP
|
||||||
|
|
||||||
|
public_link:
|
||||||
|
expired_link_title: Lien expiré
|
||||||
|
expired_link_explanation: Le lien a expiré, vous ne pouvez plus visualiser ce document.
|
||||||
|
|
||||||
|
send_external_message:
|
||||||
|
greeting: Bonjour
|
||||||
|
or_see_link: 'Si le clic sur le bouton ne fonctionnait pas, vous pouvez consulter les documents en copiant le lien ci-dessous, et en l''ouvrant dans votre navigateur'
|
||||||
|
use_button: Pour accéder à ces documents, vous pouvez utiliser le bouton ci-dessous
|
||||||
|
see_docs_action_name: Voir les documents confidentiels
|
||||||
|
sender_system_user: le logiciel chill
|
||||||
|
|
||||||
signature_zone:
|
signature_zone:
|
||||||
title: Signatures électroniques
|
title: Signatures électroniques
|
||||||
|
@ -34,6 +34,8 @@ notification:
|
|||||||
workflow:
|
workflow:
|
||||||
You must add at least one dest user or email: Indiquez au moins un destinataire ou une adresse email
|
You must add at least one dest user or email: Indiquez au moins un destinataire ou une adresse email
|
||||||
The user in cc cannot be a dest user in the same workflow step: L'utilisateur en copie ne peut pas être présent dans les utilisateurs qui valideront la prochaine étape
|
The user in cc cannot be a dest user in the same workflow step: L'utilisateur en copie ne peut pas être présent dans les utilisateurs qui valideront la prochaine étape
|
||||||
|
transition_has_destinee_if_sent_external: Indiquez un destinataire de l'envoi externe
|
||||||
|
transition_destinee_not_necessary: Pour cette transition, vous ne pouvez pas indiquer de destinataires externes
|
||||||
|
|
||||||
rolling_date:
|
rolling_date:
|
||||||
When fixed date is selected, you must provide a date: Indiquez la date fixe choisie
|
When fixed date is selected, you must provide a date: Indiquez la date fixe choisie
|
||||||
|
@ -24,6 +24,8 @@ use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
|
|||||||
use Chill\MainBundle\Entity\HasCenterInterface;
|
use Chill\MainBundle\Entity\HasCenterInterface;
|
||||||
use Chill\MainBundle\Entity\Language;
|
use Chill\MainBundle\Entity\Language;
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||||
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
||||||
use Chill\PersonBundle\Entity\Household\Household;
|
use Chill\PersonBundle\Entity\Household\Household;
|
||||||
use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
||||||
@ -39,6 +41,7 @@ use DateTime;
|
|||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\Common\Collections\Criteria;
|
use Doctrine\Common\Collections\Criteria;
|
||||||
|
use Doctrine\Common\Collections\ReadableCollection;
|
||||||
use Doctrine\Common\Collections\Selectable;
|
use Doctrine\Common\Collections\Selectable;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use libphonenumber\PhoneNumber;
|
use libphonenumber\PhoneNumber;
|
||||||
@ -382,6 +385,12 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
|||||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||||
private ?User $updatedBy = null;
|
private ?User $updatedBy = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, EntityWorkflowStepSignature>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(targetEntity: EntityWorkflowStepSignature::class, mappedBy: 'personSigner', orphanRemoval: true)]
|
||||||
|
private Collection $signatures;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Person constructor.
|
* Person constructor.
|
||||||
*/
|
*/
|
||||||
@ -403,6 +412,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
|||||||
$this->budgetCharges = new ArrayCollection();
|
$this->budgetCharges = new ArrayCollection();
|
||||||
$this->resources = new ArrayCollection();
|
$this->resources = new ArrayCollection();
|
||||||
$this->centerHistory = new ArrayCollection();
|
$this->centerHistory = new ArrayCollection();
|
||||||
|
$this->signatures = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __toString(): string
|
public function __toString(): string
|
||||||
@ -474,6 +484,35 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addSignature(EntityWorkflowStepSignature $signature): self
|
||||||
|
{
|
||||||
|
if (!$this->signatures->contains($signature)) {
|
||||||
|
$this->signatures->add($signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeSignature(EntityWorkflowStepSignature $signature): self
|
||||||
|
{
|
||||||
|
$this->signatures->removeElement($signature);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return ReadableCollection<int, EntityWorkflowStepSignature>
|
||||||
|
*/
|
||||||
|
public function getSignatures(): ReadableCollection
|
||||||
|
{
|
||||||
|
return $this->signatures;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSignaturesPending(): ReadableCollection
|
||||||
|
{
|
||||||
|
return $this->signatures->filter(fn (EntityWorkflowStepSignature $signature) => EntityWorkflowSignatureStateEnum::PENDING === $signature->getState());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function used for validation that check if the accompanying periods of
|
* Function used for validation that check if the accompanying periods of
|
||||||
* the person are not collapsing (i.e. have not shared days) or having
|
* the person are not collapsing (i.e. have not shared days) or having
|
||||||
|
@ -224,6 +224,33 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if person.signaturesPending|length > 0 %}
|
||||||
|
<div class="item-row separator">
|
||||||
|
<div class="wrap-list periods-list">
|
||||||
|
<div class="wl-row">
|
||||||
|
<div class="wl-col title">
|
||||||
|
<h3>{{ 'workflow.pending_signatures'|trans({nb_signatures: person.signaturesPending|length}) }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="wl-col list">
|
||||||
|
{% for signature in person.signaturesPending %}
|
||||||
|
{% set entityWorkflow = signature.step.entityWorkflow %}
|
||||||
|
{{ entityWorkflow|chill_entity_render_string }}
|
||||||
|
<ul class="record_actions small slim">
|
||||||
|
<li>
|
||||||
|
<a href="{{ chill_path_add_return_path('chill_main_workflow_signature_metadata', {signature_id: signature.id}) }}" class="btn btn-misc">
|
||||||
|
<i class="fa fa-pencil"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ chill_path_add_return_path('chill_main_workflow_show', {id: entityWorkflow.id}) }}" class="btn btn-show"></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
<div class="list-with-period">
|
<div class="list-with-period">
|
||||||
|
@ -12,10 +12,14 @@ declare(strict_types=1);
|
|||||||
namespace Chill\PersonBundle\Workflow;
|
namespace Chill\PersonBundle\Workflow;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Workflow\WorkflowWithPublicViewDocumentHelper;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
|
||||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
|
use Chill\MainBundle\Workflow\EntityWorkflowWithPublicViewInterface;
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface;
|
use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface;
|
||||||
|
use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO;
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
|
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
|
||||||
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository;
|
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository;
|
||||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationDocumentVoter;
|
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationDocumentVoter;
|
||||||
@ -24,13 +28,14 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
|||||||
/**
|
/**
|
||||||
* @implements EntityWorkflowWithStoredObjectHandlerInterface<AccompanyingPeriodWorkEvaluationDocument>
|
* @implements EntityWorkflowWithStoredObjectHandlerInterface<AccompanyingPeriodWorkEvaluationDocument>
|
||||||
*/
|
*/
|
||||||
class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityWorkflowWithStoredObjectHandlerInterface
|
class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityWorkflowWithStoredObjectHandlerInterface, EntityWorkflowWithPublicViewInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly AccompanyingPeriodWorkEvaluationDocumentRepository $repository,
|
private readonly AccompanyingPeriodWorkEvaluationDocumentRepository $repository,
|
||||||
private readonly EntityWorkflowRepository $workflowRepository,
|
private readonly EntityWorkflowRepository $workflowRepository,
|
||||||
private readonly TranslatableStringHelperInterface $translatableStringHelper,
|
private readonly TranslatableStringHelperInterface $translatableStringHelper,
|
||||||
private readonly TranslatorInterface $translator,
|
private readonly TranslatorInterface $translator,
|
||||||
|
private readonly WorkflowWithPublicViewDocumentHelper $publicViewDocumentHelper,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function getDeletionRoles(): array
|
public function getDeletionRoles(): array
|
||||||
@ -150,4 +155,9 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW
|
|||||||
|
|
||||||
return $this->workflowRepository->findByRelatedEntity(AccompanyingPeriodWorkEvaluationDocument::class, $object->getId());
|
return $this->workflowRepository->findByRelatedEntity(AccompanyingPeriodWorkEvaluationDocument::class, $object->getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function renderPublicView(EntityWorkflowSend $entityWorkflowSend, EntityWorkflowViewMetadataDTO $metadata): string
|
||||||
|
{
|
||||||
|
return $this->publicViewDocumentHelper->render($entityWorkflowSend, $metadata, $this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace ChillThirdParty\Tests\Validator;
|
||||||
|
|
||||||
|
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||||
|
use Chill\ThirdPartyBundle\Templating\Entity\ThirdPartyRender;
|
||||||
|
use Chill\ThirdPartyBundle\Validator\ThirdPartyHasEmail;
|
||||||
|
use Chill\ThirdPartyBundle\Validator\ThirdPartyHasEmailValidator;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class ThirdPartyHasEmailValidatorTest extends \Symfony\Component\Validator\Test\ConstraintValidatorTestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
public function testThirdPartyWithEmail(): void
|
||||||
|
{
|
||||||
|
$thirdParty = new ThirdParty();
|
||||||
|
$thirdParty->setEmail('third@example.com');
|
||||||
|
|
||||||
|
$this->validator->validate($thirdParty, new ThirdPartyHasEmail());
|
||||||
|
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThirdPartyHasNoEmail(): void
|
||||||
|
{
|
||||||
|
$thirdParty = new ThirdParty();
|
||||||
|
|
||||||
|
$constraint = new ThirdPartyHasEmail();
|
||||||
|
$constraint->message = 'message';
|
||||||
|
|
||||||
|
$this->validator->validate($thirdParty, $constraint);
|
||||||
|
|
||||||
|
$this->buildViolation($constraint->message)
|
||||||
|
->setParameter('{{ thirdParty }}', '3party-string')
|
||||||
|
->setCode($constraint->code)
|
||||||
|
->assertRaised();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createValidator(): ThirdPartyHasEmailValidator
|
||||||
|
{
|
||||||
|
$render = $this->prophesize(ThirdPartyRender::class);
|
||||||
|
$render->renderString(Argument::type(ThirdParty::class), [])
|
||||||
|
->willReturn('3party-string');
|
||||||
|
|
||||||
|
return new ThirdPartyHasEmailValidator($render->reveal());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\ThirdPartyBundle\Validator;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a constraint to check that the thirdparty has an email address.
|
||||||
|
*/
|
||||||
|
class ThirdPartyHasEmail extends Constraint
|
||||||
|
{
|
||||||
|
public string $message = 'thirdParty.thirdParty_has_no_email';
|
||||||
|
public string $code = 'f2da71f8-818c-11ef-9f6a-3ba3597ab60b';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string>|string
|
||||||
|
*/
|
||||||
|
public function getTargets(): array|string
|
||||||
|
{
|
||||||
|
return [self::PROPERTY_CONSTRAINT];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
<?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\ThirdPartyBundle\Validator;
|
||||||
|
|
||||||
|
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||||
|
use Chill\ThirdPartyBundle\Templating\Entity\ThirdPartyRender;
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\ConstraintValidator;
|
||||||
|
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||||
|
|
||||||
|
final class ThirdPartyHasEmailValidator extends ConstraintValidator
|
||||||
|
{
|
||||||
|
public function __construct(private readonly ThirdPartyRender $thirdPartyRender) {}
|
||||||
|
|
||||||
|
public function validate($value, Constraint $constraint): void
|
||||||
|
{
|
||||||
|
if (!$constraint instanceof ThirdPartyHasEmail) {
|
||||||
|
throw new UnexpectedTypeException($constraint, ThirdPartyHasEmail::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$value instanceof ThirdParty) {
|
||||||
|
throw new UnexpectedTypeException($value, ThirdParty::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $value->getEmail() || '' === $value->getEmail()) {
|
||||||
|
$this->context->buildViolation($constraint->message)
|
||||||
|
->setParameter('{{ thirdParty }}', $this->thirdPartyRender->renderString($value, []))
|
||||||
|
->setCode($constraint->code)
|
||||||
|
->addViolation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,3 +11,8 @@ services:
|
|||||||
autoconfigure: true
|
autoconfigure: true
|
||||||
resource: '../Export/'
|
resource: '../Export/'
|
||||||
|
|
||||||
|
Chill\ThirdPartyBundle\Validator\:
|
||||||
|
autoconfigure: true
|
||||||
|
autowire: true
|
||||||
|
resource: '../Validator/'
|
||||||
|
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
thirdParty:
|
||||||
|
thirdParty_has_no_email: Le tiers {{ thirdParty }} n'a pas d'adresse email configurée.
|
Loading…
x
Reference in New Issue
Block a user