mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-08-22 07:33:50 +00:00
Merge remote-tracking branch 'origin/signature-app-master' into 286-storedobject-voter
This commit is contained in:
@@ -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();
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
@@ -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>
|
||||
*/
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
@@ -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(),
|
||||
|
@@ -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 = '';
|
||||
|
Reference in New Issue
Block a user