Merge remote-tracking branch 'origin/signature-app-master' into 286-storedobject-voter

This commit is contained in:
2024-07-16 13:34:36 +02:00
17 changed files with 326 additions and 41 deletions

View File

@@ -25,6 +25,7 @@ use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request;
@@ -39,7 +40,18 @@ use Symfony\Contracts\Translation\TranslatorInterface;
class WorkflowController extends AbstractController
{
public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly EntityWorkflowRepository $entityWorkflowRepository, private readonly ValidatorInterface $validator, private readonly PaginatorFactory $paginatorFactory, private readonly Registry $registry, private readonly EntityManagerInterface $entityManager, private readonly TranslatorInterface $translator, private readonly ChillSecurity $security, private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry) {}
public function __construct(
private readonly EntityWorkflowManager $entityWorkflowManager,
private readonly EntityWorkflowRepository $entityWorkflowRepository,
private readonly ValidatorInterface $validator,
private readonly PaginatorFactory $paginatorFactory,
private readonly Registry $registry,
private readonly EntityManagerInterface $entityManager,
private readonly TranslatorInterface $translator,
private readonly ChillSecurity $security,
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
private readonly ClockInterface $clock,
) {}
#[Route(path: '/{_locale}/main/workflow/create', name: 'chill_main_workflow_create')]
public function create(Request $request): Response
@@ -310,7 +322,14 @@ class WorkflowController extends AbstractController
throw $this->createAccessDeniedException(sprintf("not allowed to apply transition {$transition}: %s", implode(', ', $msgs)));
}
$workflow->apply($entityWorkflow, $transition, ['context' => $stepDTO]);
$byUser = $this->security->getUser();
$workflow->apply($entityWorkflow, $transition, [
'context' => $stepDTO,
'byUser' => $byUser,
'transition' => $transition,
'transitionAt' => $this->clock->now(),
]);
$this->entityManager->flush();

View File

@@ -413,8 +413,20 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
*
* @return $this
*/
public function setStep(string $step, WorkflowTransitionContextDTO $transitionContextDTO): self
{
public function setStep(
string $step,
WorkflowTransitionContextDTO $transitionContextDTO,
string $transition,
\DateTimeImmutable $transitionAt,
?User $byUser = null
): self {
$previousStep = $this->getCurrentStep();
$previousStep
->setTransitionAfter($transition)
->setTransitionAt($transitionAt)
->setTransitionBy($byUser);
$newStep = new EntityWorkflowStep();
$newStep->setCurrentStep($step);
@@ -430,6 +442,14 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
$newStep->addDestEmail($email);
}
if (null !== $transitionContextDTO->futureUserSignature) {
new EntityWorkflowStepSignature($newStep, $transitionContextDTO->futureUserSignature);
} else {
foreach ($transitionContextDTO->futurePersonSignatures as $personSignature) {
new EntityWorkflowStepSignature($newStep, $personSignature);
}
}
// copy the freeze
if ($this->isFreeze()) {
$newStep->setFreezeAfter(true);

View File

@@ -12,13 +12,14 @@ declare(strict_types=1);
namespace Chill\MainBundle\Security\Authorization;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class WorkflowEntityDeletionVoter extends Voter
{
/**
* @param \Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface[] $handlers
* @param EntityWorkflowHandlerInterface[] $handlers
*/
public function __construct(private $handlers, private readonly EntityWorkflowRepository $entityWorkflowRepository) {}
@@ -30,7 +31,7 @@ class WorkflowEntityDeletionVoter extends Voter
foreach ($this->handlers as $handler) {
if ($handler->isObjectSupported($subject)
&& \in_array($attribute, $handler->getDeletionRoles($subject), true)) {
&& \in_array($attribute, $handler->getDeletionRoles(), true)) {
return true;
}
}

View File

@@ -11,8 +11,11 @@ declare(strict_types=1);
namespace Chill\MainBundle\Tests\Entity\Workflow;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use Chill\PersonBundle\Entity\Person;
use PHPUnit\Framework\TestCase;
/**
@@ -26,7 +29,7 @@ final class EntityWorkflowTest extends TestCase
{
$entityWorkflow = new EntityWorkflow();
$entityWorkflow->setStep('final', new WorkflowTransitionContextDTO($entityWorkflow));
$entityWorkflow->setStep('final', new WorkflowTransitionContextDTO($entityWorkflow), 'finalize', new \DateTimeImmutable());
$entityWorkflow->getCurrentStep()->setIsFinal(true);
$this->assertTrue($entityWorkflow->isFinal());
@@ -38,16 +41,16 @@ final class EntityWorkflowTest extends TestCase
$this->assertFalse($entityWorkflow->isFinal());
$entityWorkflow->setStep('two', new WorkflowTransitionContextDTO($entityWorkflow));
$entityWorkflow->setStep('two', new WorkflowTransitionContextDTO($entityWorkflow), 'two', new \DateTimeImmutable());
$this->assertFalse($entityWorkflow->isFinal());
$entityWorkflow->setStep('previous_final', new WorkflowTransitionContextDTO($entityWorkflow));
$entityWorkflow->setStep('previous_final', new WorkflowTransitionContextDTO($entityWorkflow), 'three', new \DateTimeImmutable());
$this->assertFalse($entityWorkflow->isFinal());
$entityWorkflow->getCurrentStep()->setIsFinal(true);
$entityWorkflow->setStep('final', new WorkflowTransitionContextDTO($entityWorkflow));
$entityWorkflow->setStep('final', new WorkflowTransitionContextDTO($entityWorkflow), 'four', new \DateTimeImmutable());
$this->assertTrue($entityWorkflow->isFinal());
}
@@ -58,23 +61,81 @@ final class EntityWorkflowTest extends TestCase
$this->assertFalse($entityWorkflow->isFreeze());
$entityWorkflow->setStep('step_one', new WorkflowTransitionContextDTO($entityWorkflow));
$entityWorkflow->setStep('step_one', new WorkflowTransitionContextDTO($entityWorkflow), 'to_step_one', new \DateTimeImmutable());
$this->assertFalse($entityWorkflow->isFreeze());
$entityWorkflow->setStep('step_three', new WorkflowTransitionContextDTO($entityWorkflow));
$entityWorkflow->setStep('step_three', new WorkflowTransitionContextDTO($entityWorkflow), 'to_step_three', new \DateTimeImmutable());
$this->assertFalse($entityWorkflow->isFreeze());
$entityWorkflow->setStep('freezed', new WorkflowTransitionContextDTO($entityWorkflow));
$entityWorkflow->setStep('freezed', new WorkflowTransitionContextDTO($entityWorkflow), 'to_freezed', new \DateTimeImmutable());
$entityWorkflow->getCurrentStep()->setFreezeAfter(true);
$this->assertTrue($entityWorkflow->isFreeze());
$entityWorkflow->setStep('after_freeze', new WorkflowTransitionContextDTO($entityWorkflow));
$entityWorkflow->setStep('after_freeze', new WorkflowTransitionContextDTO($entityWorkflow), 'to_after_freeze', new \DateTimeImmutable());
$this->assertTrue($entityWorkflow->isFreeze());
$this->assertTrue($entityWorkflow->getCurrentStep()->isFreezeAfter());
}
public function testPreviousStepMetadataAreFilled()
{
$entityWorkflow = new EntityWorkflow();
$initialStep = $entityWorkflow->getCurrentStep();
$entityWorkflow->setStep('step_one', new WorkflowTransitionContextDTO($entityWorkflow), 'to_step_one', new \DateTimeImmutable('2024-01-01'), $user1 = new User());
$previous = $entityWorkflow->getCurrentStep();
$entityWorkflow->setStep('step_one', new WorkflowTransitionContextDTO($entityWorkflow), 'to_step_two', new \DateTimeImmutable('2024-01-02'), $user2 = new User());
$final = $entityWorkflow->getCurrentStep();
$stepsChained = $entityWorkflow->getStepsChained();
self::assertCount(3, $stepsChained);
self::assertSame($initialStep, $stepsChained[0]);
self::assertSame($previous, $stepsChained[1]);
self::assertSame($final, $stepsChained[2]);
self::assertEquals($user1, $initialStep->getTransitionBy());
self::assertEquals('2024-01-01', $initialStep->getTransitionAt()?->format('Y-m-d'));
self::assertEquals('to_step_one', $initialStep->getTransitionAfter());
self::assertEquals($user2, $previous->getTransitionBy());
self::assertEquals('2024-01-02', $previous->getTransitionAt()?->format('Y-m-d'));
self::assertEquals('to_step_two', $previous->getTransitionAfter());
}
public function testSetStepSignatureForUserIsCreated()
{
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureUserSignature = $user = new User();
$entityWorkflow->setStep('new', $dto, 'to_new', new \DateTimeImmutable());
$actual = $entityWorkflow->getCurrentStep();
self::assertCount(1, $actual->getSignatures());
self::assertSame($user, $actual->getSignatures()->first()->getSigner());
}
public function testSetStepSignatureForPersonIsCreated()
{
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futurePersonSignatures[] = $person1 = new Person();
$dto->futurePersonSignatures[] = $person2 = new Person();
$entityWorkflow->setStep('new', $dto, 'to_new', new \DateTimeImmutable());
$actual = $entityWorkflow->getCurrentStep();
$persons = $actual->getSignatures()->map(fn (EntityWorkflowStepSignature $signature) => $signature->getSigner());
self::assertCount(2, $actual->getSignatures());
self::assertContains($person1, $persons);
self::assertContains($person2, $persons);
}
}

View File

@@ -39,19 +39,29 @@ class EntityWorkflowMarkingStoreTest extends TestCase
{
$markingStore = $this->buildMarkingStore();
$workflow = new EntityWorkflow();
$previousStep = $workflow->getCurrentStep();
$dto = new WorkflowTransitionContextDTO($workflow);
$dto->futureCcUsers[] = $user1 = new User();
$dto->futureDestUsers[] = $user2 = new User();
$dto->futureDestEmails[] = $email = 'test@example.com';
$markingStore->setMarking($workflow, new Marking(['foo' => 1]), ['context' => $dto]);
$markingStore->setMarking($workflow, new Marking(['foo' => 1]), [
'context' => $dto,
'transition' => 'bar_transition',
'byUser' => $user3 = new User(),
'transitionAt' => $at = new \DateTimeImmutable(),
]);
$currentStep = $workflow->getCurrentStep();
self::assertEquals('foo', $currentStep->getCurrentStep());
self::assertContains($email, $currentStep->getDestEmail());
self::assertContains($user1, $currentStep->getCcUser());
self::assertContains($user2, $currentStep->getDestUser());
self::assertSame($user3, $previousStep->getTransitionBy());
self::assertSame($at, $previousStep->getTransitionAt());
self::assertEquals('bar_transition', $previousStep->getTransitionAfter());
}
private function buildMarkingStore(): EntityWorkflowMarkingStore

View File

@@ -14,6 +14,9 @@ namespace Chill\MainBundle\Workflow;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
/**
* @template T of object
*/
interface EntityWorkflowHandlerInterface
{
/**
@@ -25,6 +28,9 @@ interface EntityWorkflowHandlerInterface
public function getEntityTitle(EntityWorkflow $entityWorkflow, array $options = []): string;
/**
* @return T|null
*/
public function getRelatedEntity(EntityWorkflow $entityWorkflow): ?object;
public function getRelatedObjects(object $object): array;

View File

@@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Workflow;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Workflow\Exception\HandlerNotFoundException;
use Symfony\Component\Workflow\Registry;
@@ -38,6 +39,17 @@ class EntityWorkflowManager
return $this->registry->all($entityWorkflow);
}
public function getAssociatedStoredObject(EntityWorkflow $entityWorkflow): ?StoredObject
{
foreach ($this->handlers as $handler) {
if ($handler instanceof EntityWorkflowWithStoredObjectHandlerInterface && $handler->supports($entityWorkflow)) {
return $handler->getAssociatedStoredObject($entityWorkflow);
}
}
return null;
}
/**
* @return list<EntityWorkflow>
*/

View File

@@ -40,10 +40,14 @@ final readonly class EntityWorkflowMarkingStore implements MarkingStoreInterface
$next = array_keys($places)[0];
$transitionDTO = $context['context'] ?? null;
$transition = $context['transition'];
$byUser = $context['byUser'] ?? null;
$at = $context['transitionAt'];
if (!$transitionDTO instanceof WorkflowTransitionContextDTO) {
throw new \UnexpectedValueException(sprintf('Expected instance of %s', WorkflowTransitionContextDTO::class));
}
$subject->setStep($next, $transitionDTO);
$subject->setStep($next, $transitionDTO, $transition, $at, $byUser);
}
}

View File

@@ -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;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
/**
* Add methods to handle workflows associated with @see{StoredObject}.
*
* @template T of object
*
* @template-extends EntityWorkflowHandlerInterface<T>
*/
interface EntityWorkflowWithStoredObjectHandlerInterface extends EntityWorkflowHandlerInterface
{
public function getAssociatedStoredObject(EntityWorkflow $entityWorkflow): ?StoredObject;
}

View File

@@ -108,12 +108,6 @@ final readonly class EntityWorkflowTransitionEventSubscriber implements EventSub
/** @var EntityWorkflow $entityWorkflow */
$entityWorkflow = $event->getSubject();
$step = $entityWorkflow->getCurrentStep();
$step
->setTransitionAfter($event->getTransition()->getName())
->setTransitionAt(new \DateTimeImmutable('now'))
->setTransitionBy($this->security->getUser());
$this->chillLogger->info('[workflow] apply transition on entityWorkflow', [
'relatedEntityClass' => $entityWorkflow->getRelatedEntityClass(),

View File

@@ -13,6 +13,7 @@ namespace Chill\MainBundle\Workflow;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Workflow\Transition;
@@ -51,6 +52,18 @@ class WorkflowTransitionContextDTO
*/
public array $futureDestEmails = [];
/**
* A list of future @see{Person} with will sign the next step.
*
* @var list<Person>
*/
public array $futurePersonSignatures = [];
/**
* An eventual user which is requested to apply a signature.
*/
public ?User $futureUserSignature = null;
public ?Transition $transition = null;
public string $comment = '';