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(); 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. * Checks whether all signatures associated with a given workflow step are not pending.
* *

View File

@ -3,7 +3,7 @@
<div class="container"> <div class="container">
{% for s in signatures %} {% for s in signatures %}
<div class="row row-hover align-items-center"> <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' %} {% if s.signerKind == 'person' %}
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
action: 'show', displayBadge: true, action: 'show', displayBadge: true,
@ -19,21 +19,27 @@
} %} } %}
{% endif %} {% endif %}
</div> </div>
<div class="col-sm-12 col-md-4"> <div class="col-sm-12 col-md-7 text-end">
{% if s.isSigned %} {% 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 %} {% 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"> <ul class="record_actions slim">
<li> {% if is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_CANCEL', s) %}
<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>
{% if s.state is same as('signed') %} <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>
<p class="updatedBy">{{ s.stateDate }}</p> </li>
{% endif %} {% endif %}
</li> {% 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> </ul>
{% else %}
<span class="text-end">{{ 'workflow.waiting_for_signature'|trans }}</span>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </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 SIGN = 'CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_SIGN';
public const CANCEL = 'CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_CANCEL';
protected function supports(string $attribute, $subject) 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) 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()]); $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 private function getPreviousSender(EntityWorkflowStep $entityWorkflowStep): ?User
{ {
$stepsChained = $entityWorkflowStep->getEntityWorkflow()->getStepsChained(); $stepsChained = $entityWorkflowStep->getEntityWorkflow()->getStepsChained();

View File

@ -45,8 +45,10 @@ workflow:
few {# workflows} few {# workflows}
other {# workflows} other {# workflows}
} }
signature_zone: signature:
has_signed_statement: 'A signé le {datetime, date, short} à {datetime, time, short}' 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: duration:

View File

@ -538,6 +538,7 @@ workflow:
signature_zone: signature_zone:
title: Signatures électroniques title: Signatures électroniques
button_sign: Signer button_sign: Signer
button_cancel: Annuler
metadata: metadata:
sign_by: 'Signature pour %name%' sign_by: 'Signature pour %name%'
docType: Type de document docType: Type de document
@ -550,6 +551,10 @@ workflow:
user: Utilisateur user: Utilisateur
already_signed_alert: La signature a déjà été appliquée 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 final: Recevoir une notification à l'étape finale
Subscribe all steps: Recevoir une notification à chaque étape 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());
}
}