Implement signature cancellation feature

Added functionality to cancel signatures in workflow, including controller, view, and tests. Updated translations and adjusted templates to support and display cancellation actions.
This commit is contained in:
Julien Fastré 2024-09-25 10:58:53 +02:00
parent 5a467ae38d
commit 83121c2a83
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
9 changed files with 223 additions and 15 deletions

View File

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
use Chill\MainBundle\Routing\ChillUrlGeneratorInterface;
use Chill\MainBundle\Security\Authorization\EntityWorkflowStepSignatureVoter;
use Chill\MainBundle\Workflow\SignatureStepStateChanger;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Twig\Environment;
final readonly class WorkflowSignatureCancelController
{
public function __construct(
private EntityManagerInterface $entityManager,
private Security $security,
private FormFactoryInterface $formFactory,
private Environment $twig,
private SignatureStepStateChanger $signatureStepStateChanger,
private ChillUrlGeneratorInterface $chillUrlGenerator,
) {}
#[Route('/{_locale}/main/workflow/signature/{id}/cancel', name: 'chill_main_workflow_signature_cancel')]
public function cancelSignature(EntityWorkflowStepSignature $signature, Request $request): Response
{
if (!$this->security->isGranted(EntityWorkflowStepSignatureVoter::CANCEL, $signature)) {
throw new AccessDeniedHttpException('not allowed to cancel this signature');
}
$form = $this->formFactory->create();
$form->add('confirm', SubmitType::class, ['label' => 'Confirm']);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->signatureStepStateChanger->markSignatureAsCanceled($signature);
$this->entityManager->flush();
return new RedirectResponse(
$this->chillUrlGenerator->returnPathOr('chill_main_workflow_show', ['id' => $signature->getStep()->getEntityWorkflow()->getId()])
);
}
return
new Response(
$this->twig->render(
'@ChillMain/WorkflowSignature/cancel.html.twig',
['form' => $form->createView(), 'signature' => $signature]
)
);
}
}

View File

@ -161,6 +161,16 @@ class EntityWorkflowStepSignature implements TrackCreationInterface, TrackUpdate
return EntityWorkflowSignatureStateEnum::PENDING == $this->getState();
}
public function isCanceled(): bool
{
return EntityWorkflowSignatureStateEnum::CANCELED === $this->getState();
}
public function isRejected(): bool
{
return EntityWorkflowSignatureStateEnum::REJECTED === $this->getState();
}
/**
* Checks whether all signatures associated with a given workflow step are not pending.
*

View File

@ -3,7 +3,7 @@
<div class="container">
{% for s in signatures %}
<div class="row row-hover align-items-center">
<div class="col-sm-12 col-md-8">
<div class="col-sm-12 col-md-5">
{% if s.signerKind == 'person' %}
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
action: 'show', displayBadge: true,
@ -19,21 +19,27 @@
} %}
{% endif %}
</div>
<div class="col-sm-12 col-md-4">
<div class="col-sm-12 col-md-7 text-end">
{% if s.isSigned %}
<span class="text-end">{{ 'workflow.signature_zone.has_signed_statement'|trans({ 'datetime' : s.stateDate }) }}</span>
<span class="text-end">{{ 'workflow.signature.signed_statement'|trans({ 'datetime' : s.stateDate }) }}</span>
{% elseif s.isCanceled %}
<span class="text-end">{{ 'workflow.signature.canceled_statement'|trans({ 'datetime' : s.stateDate }) }}</span>
{% elseif s.isRejected%}
<span class="text-end">{{ 'workflow.signature.rejected_statement'|trans({ 'datetime' : s.stateDate }) }}</span>
{% else %}
{% if is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_SIGN', s) %}
{% if (is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_CANCEL', s) or is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_SIGN', s)) %}
<ul class="record_actions slim">
<li>
<a class="btn btn-misc" href="{{ chill_path_add_return_path('chill_main_workflow_signature_metadata', { 'signature_id': s.id}) }}"><i class="fa fa-pencil-square-o"></i> {{ 'workflow.signature_zone.button_sign'|trans }}</a>
{% if s.state is same as('signed') %}
<p class="updatedBy">{{ s.stateDate }}</p>
{% endif %}
</li>
{% if is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_CANCEL', s) %}
<li>
<a class="btn btn-misc" href="{{ chill_path_add_return_path('chill_main_workflow_signature_cancel', { 'id': s.id}) }}">{{ 'workflow.signature_zone.button_cancel'|trans }}</a>
</li>
{% endif %}
{% if is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_SIGN', s) %}
<li>
<a class="btn btn-misc" href="{{ chill_path_add_return_path('chill_main_workflow_signature_metadata', { 'signature_id': s.id}) }}"><i class="fa fa-pencil-square-o"></i> {{ 'workflow.signature_zone.button_sign'|trans }}</a>
</li>
{% endif %}
</ul>
{% else %}
<span class="text-end">{{ 'workflow.waiting_for_signature'|trans }}</span>
{% endif %}
{% endif %}
</div>

View File

@ -0,0 +1,20 @@
{% extends '@ChillMain/layout.html.twig' %}
{% block title %}{{ 'workflow.signature.cancel_signature_of'|trans({ '%signer%': signature.signer|chill_entity_render_string }) }}{% endblock %}
{% block content %}
<h1>{{ block('title') }}</h1>
<p>{{ 'workflow.signature.are_you_sure'|trans({'%signer%': signature.signer|chill_entity_render_string}) }}</p>
{{ form_start(form) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a class="btn btn-cancel" href="{{ chill_return_path_or('chill_main_workflow_show', {'id': signature.step.entityWorkflow.id}) }}">{{ 'Cancel'|trans }}</a>
</li>
<li>
{{ form_widget(form.confirm, {'attr': {'class': 'btn btn-misc'}}) }}
</li>
</ul>
{{ form_end(form) }}
{% endblock %}

View File

@ -20,9 +20,12 @@ final class EntityWorkflowStepSignatureVoter extends Voter
{
public const SIGN = 'CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_SIGN';
public const CANCEL = 'CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_CANCEL';
protected function supports(string $attribute, $subject)
{
return $subject instanceof EntityWorkflowStepSignature && self::SIGN === $attribute;
return $subject instanceof EntityWorkflowStepSignature
&& in_array($attribute, [self::SIGN, self::CANCEL], true);
}
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token)

View File

@ -89,6 +89,13 @@ class SignatureStepStateChanger
$this->logger->info(self::LOG_PREFIX.'Transition automatically applied', ['signatureId' => $signature->getId()]);
}
public function markSignatureAsCanceled(EntityWorkflowStepSignature $signature): void
{
$signature
->setState(EntityWorkflowSignatureStateEnum::CANCELED)
->setStateDate($this->clock->now());
}
private function getPreviousSender(EntityWorkflowStep $entityWorkflowStep): ?User
{
$stepsChained = $entityWorkflowStep->getEntityWorkflow()->getStepsChained();

View File

@ -45,8 +45,10 @@ workflow:
few {# workflows}
other {# workflows}
}
signature_zone:
has_signed_statement: 'A signé le {datetime, date, short} à {datetime, time, short}'
signature:
signed_statement: 'Signature appliquée le {datetime, date, short} à {datetime, time, short}'
rejected_statement: 'Signature rejectée le {datetime, date, short} à {datetime, time, short}'
canceled_statement: 'Signature annulée le {datetime, date, short} à {datetime, time, short}'
duration:

View File

@ -538,6 +538,7 @@ workflow:
signature_zone:
title: Signatures électroniques
button_sign: Signer
button_cancel: Annuler
metadata:
sign_by: 'Signature pour %name%'
docType: Type de document
@ -550,6 +551,10 @@ workflow:
user: Utilisateur
already_signed_alert: La signature a déjà été appliquée
signature:
cancel_signature_of: Annulation de la signature de %signer%
are_you_sure: Êtes-vous sûr de vouloir annuler la signature de %signer%
Subscribe final: Recevoir une notification à l'étape finale
Subscribe all steps: Recevoir une notification à chaque étape

View File

@ -0,0 +1,86 @@
<?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\PersonBundle\Tests\Controller;
use Chill\MainBundle\Controller\WorkflowSignatureCancelController;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Routing\ChillUrlGeneratorInterface;
use Chill\MainBundle\Security\Authorization\EntityWorkflowStepSignatureVoter;
use Chill\MainBundle\Workflow\SignatureStepStateChanger;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use Doctrine\ORM\EntityManager;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Security\Core\Security;
use Twig\Environment;
/**
* @internal
*
* @coversNothing
*/
class WorkflowSignatureCancelControllerStepTest extends WebTestCase
{
private FormFactoryInterface $formFactory;
private SignatureStepStateChanger $signatureStepStateChanger;
private ChillUrlGeneratorInterface $chillUrlGenerator;
private RequestStack $requestStack;
protected function setUp(): void
{
self::bootKernel();
$this->formFactory = self::getContainer()->get('form.factory');
$this->signatureStepStateChanger = self::getContainer()->get(SignatureStepStateChanger::class);
$this->chillUrlGenerator = self::getContainer()->get(ChillUrlGeneratorInterface::class);
$requestContext = self::getContainer()->get(RequestContext::class);
$requestContext->setParameter('_locale', 'fr');
$this->requestStack = self::getContainer()->get(RequestStack::class);
}
public function testCancelSignatureGet(): void
{
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureUserSignature = new User();
$entityWorkflow->setStep('signature', $dto, 'to_signature', new \DateTimeImmutable(), new User());
$signature = $entityWorkflow->getCurrentStep()->getSignatures()->first();
$security = $this->createMock(Security::class);
$security->expects($this->once())->method('isGranted')
->with(EntityWorkflowStepSignatureVoter::CANCEL, $signature)->willReturn(true);
$entityManager = $this->createMock(EntityManager::class);
$twig = $this->createMock(Environment::class);
$twig->expects($this->once())->method('render')->withAnyParameters()
->willReturn('template');
$controller = new WorkflowSignatureCancelController($entityManager, $security, $this->formFactory, $twig, $this->signatureStepStateChanger, $this->chillUrlGenerator);
$request = new Request();
$request->setMethod('GET');
$this->requestStack->push($request);
$response = $controller->cancelSignature($signature, $request);
self::assertEquals(200, $response->getStatusCode());
}
}