mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-08-22 23:53:50 +00:00
Merge signature-app-master into branch
This commit is contained in:
@@ -632,7 +632,7 @@ class ExportController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
private function rebuildRawData(string $key): array
|
||||
private function rebuildRawData(?string $key): array
|
||||
{
|
||||
if (null === $key) {
|
||||
throw $this->createNotFoundException('key does not exists');
|
||||
|
@@ -61,8 +61,6 @@ final class PermissionsGroupController extends AbstractController
|
||||
$form = $this->createAddRoleScopeForm($permissionsGroup);
|
||||
$form->handleRequest($request);
|
||||
|
||||
dump($form->isSubmitted());
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$roleScope = $this->getPersistentRoleScopeBy(
|
||||
$form['composed_role_scope']->getData()->getRole(),
|
||||
|
@@ -22,6 +22,7 @@ use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
|
||||
use Chill\MainBundle\Security\ChillSecurity;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
@@ -279,7 +280,7 @@ class WorkflowController extends AbstractController
|
||||
|
||||
if (\count($workflow->getEnabledTransitions($entityWorkflow)) > 0) {
|
||||
// possible transition
|
||||
|
||||
$stepDTO = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$usersInvolved = $entityWorkflow->getUsersInvolved();
|
||||
$currentUserFound = array_search($this->security->getUser(), $usersInvolved, true);
|
||||
|
||||
@@ -289,9 +290,8 @@ class WorkflowController extends AbstractController
|
||||
|
||||
$transitionForm = $this->createForm(
|
||||
WorkflowStepType::class,
|
||||
$entityWorkflow->getCurrentStep(),
|
||||
$stepDTO,
|
||||
[
|
||||
'transition' => true,
|
||||
'entity_workflow' => $entityWorkflow,
|
||||
'suggested_users' => $usersInvolved,
|
||||
]
|
||||
@@ -310,12 +310,7 @@ class WorkflowController extends AbstractController
|
||||
throw $this->createAccessDeniedException(sprintf("not allowed to apply transition {$transition}: %s", implode(', ', $msgs)));
|
||||
}
|
||||
|
||||
// TODO symfony 5: add those "future" on context ($workflow->apply($entityWorkflow, $transition, $context)
|
||||
$entityWorkflow->futureCcUsers = $transitionForm['future_cc_users']->getData() ?? [];
|
||||
$entityWorkflow->futureDestUsers = $transitionForm['future_dest_users']->getData() ?? [];
|
||||
$entityWorkflow->futureDestEmails = $transitionForm['future_dest_emails']->getData() ?? [];
|
||||
|
||||
$workflow->apply($entityWorkflow, $transition);
|
||||
$workflow->apply($entityWorkflow, $transition, ['context' => $stepDTO]);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
|
@@ -216,13 +216,13 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
||||
return $this->mainLocation;
|
||||
}
|
||||
|
||||
public function getMainScope(?\DateTimeImmutable $at = null): ?Scope
|
||||
public function getMainScope(?\DateTimeImmutable $atDate = null): ?Scope
|
||||
{
|
||||
$at ??= new \DateTimeImmutable('now');
|
||||
$atDate ??= new \DateTimeImmutable('now');
|
||||
|
||||
foreach ($this->scopeHistories as $scopeHistory) {
|
||||
if ($at >= $scopeHistory->getStartDate() && (
|
||||
null === $scopeHistory->getEndDate() || $at < $scopeHistory->getEndDate()
|
||||
if ($atDate >= $scopeHistory->getStartDate() && (
|
||||
null === $scopeHistory->getEndDate() || $atDate < $scopeHistory->getEndDate()
|
||||
)) {
|
||||
return $scopeHistory->getScope();
|
||||
}
|
||||
@@ -265,13 +265,13 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
||||
return $this->salt;
|
||||
}
|
||||
|
||||
public function getUserJob(?\DateTimeImmutable $at = null): ?UserJob
|
||||
public function getUserJob(?\DateTimeImmutable $atDate = null): ?UserJob
|
||||
{
|
||||
$at ??= new \DateTimeImmutable('now');
|
||||
$atDate ??= new \DateTimeImmutable('now');
|
||||
|
||||
foreach ($this->jobHistories as $jobHistory) {
|
||||
if ($at >= $jobHistory->getStartDate() && (
|
||||
null === $jobHistory->getEndDate() || $at < $jobHistory->getEndDate()
|
||||
if ($atDate >= $jobHistory->getStartDate() && (
|
||||
null === $jobHistory->getEndDate() || $atDate < $jobHistory->getEndDate()
|
||||
)) {
|
||||
return $jobHistory->getJob();
|
||||
}
|
||||
@@ -285,6 +285,11 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
||||
return $this->jobHistories;
|
||||
}
|
||||
|
||||
public function getUserScopeHistories(): Collection
|
||||
{
|
||||
return $this->scopeHistories;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayCollection|UserJobHistory[]
|
||||
*/
|
||||
|
@@ -17,9 +17,9 @@ use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Workflow\Validator\EntityWorkflowCreation;
|
||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
@@ -34,35 +34,6 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
||||
|
||||
use TrackUpdateTrait;
|
||||
|
||||
/**
|
||||
* a list of future cc users for the next steps.
|
||||
*
|
||||
* @var array|User[]
|
||||
*/
|
||||
public array $futureCcUsers = [];
|
||||
|
||||
/**
|
||||
* a list of future dest emails for the next steps.
|
||||
*
|
||||
* This is in used in order to let controller inform who will be the future emails which will validate
|
||||
* the next step. This is necessary to perform some computation about the next emails, before they are
|
||||
* associated to the entity EntityWorkflowStep.
|
||||
*
|
||||
* @var array|string[]
|
||||
*/
|
||||
public array $futureDestEmails = [];
|
||||
|
||||
/**
|
||||
* a list of future dest users for the next steps.
|
||||
*
|
||||
* This is in used in order to let controller inform who will be the future users which will validate
|
||||
* the next step. This is necessary to perform some computation about the next users, before they are
|
||||
* associated to the entity EntityWorkflowStep.
|
||||
*
|
||||
* @var array|User[]
|
||||
*/
|
||||
public array $futureDestUsers = [];
|
||||
|
||||
/**
|
||||
* @var Collection<EntityWorkflowComment>
|
||||
*/
|
||||
@@ -442,11 +413,23 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setStep(string $step): self
|
||||
public function setStep(string $step, WorkflowTransitionContextDTO $transitionContextDTO): self
|
||||
{
|
||||
$newStep = new EntityWorkflowStep();
|
||||
$newStep->setCurrentStep($step);
|
||||
|
||||
foreach ($transitionContextDTO->futureCcUsers as $user) {
|
||||
$newStep->addCcUser($user);
|
||||
}
|
||||
|
||||
foreach ($transitionContextDTO->futureDestUsers as $user) {
|
||||
$newStep->addDestUser($user);
|
||||
}
|
||||
|
||||
foreach ($transitionContextDTO->futureDestEmails as $email) {
|
||||
$newStep->addDestEmail($email);
|
||||
}
|
||||
|
||||
// copy the freeze
|
||||
if ($this->isFreeze()) {
|
||||
$newStep->setFreezeAfter(true);
|
||||
|
@@ -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\Entity\Workflow;
|
||||
|
||||
enum EntityWorkflowSignatureStateEnum: string
|
||||
{
|
||||
case PENDING = 'pending';
|
||||
case SIGNED = 'signed';
|
||||
case REJECTED = 'rejected';
|
||||
case CANCELED = 'canceled';
|
||||
}
|
@@ -42,19 +42,25 @@ class EntityWorkflowStep
|
||||
private array $destEmail = [];
|
||||
|
||||
/**
|
||||
* @var Collection<User>
|
||||
* @var Collection<int, User>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: User::class)]
|
||||
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user')]
|
||||
private Collection $destUser;
|
||||
|
||||
/**
|
||||
* @var Collection<User>
|
||||
* @var Collection<int, User>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: User::class)]
|
||||
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user_by_accesskey')]
|
||||
private Collection $destUserByAccessKey;
|
||||
|
||||
/**
|
||||
* @var Collection <int, EntityWorkflowStepSignature>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepSignature::class, cascade: ['persist'], orphanRemoval: true)]
|
||||
private Collection $signatures;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: EntityWorkflow::class, inversedBy: 'steps')]
|
||||
private ?EntityWorkflow $entityWorkflow = null;
|
||||
|
||||
@@ -97,6 +103,7 @@ class EntityWorkflowStep
|
||||
$this->ccUser = new ArrayCollection();
|
||||
$this->destUser = new ArrayCollection();
|
||||
$this->destUserByAccessKey = new ArrayCollection();
|
||||
$this->signatures = new ArrayCollection();
|
||||
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(32));
|
||||
}
|
||||
|
||||
@@ -136,6 +143,29 @@ class EntityWorkflowStep
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal use @see{EntityWorkflowStepSignature}'s constructor instead
|
||||
*/
|
||||
public function addSignature(EntityWorkflowStepSignature $signature): self
|
||||
{
|
||||
if (!$this->signatures->contains($signature)) {
|
||||
$this->signatures[] = $signature;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeSignature(EntityWorkflowStepSignature $signature): self
|
||||
{
|
||||
if ($this->signatures->contains($signature)) {
|
||||
$this->signatures->removeElement($signature);
|
||||
}
|
||||
|
||||
$signature->detachEntityWorkflowStep();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAccessKey(): string
|
||||
{
|
||||
return $this->accessKey;
|
||||
@@ -198,6 +228,14 @@ class EntityWorkflowStep
|
||||
return $this->entityWorkflow;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, EntityWorkflowStepSignature>
|
||||
*/
|
||||
public function getSignatures(): Collection
|
||||
{
|
||||
return $this->signatures;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
|
@@ -0,0 +1,156 @@
|
||||
<?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\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'chill_main_workflow_entity_step_signature')]
|
||||
class EntityWorkflowStepSignature implements TrackCreationInterface, TrackUpdateInterface
|
||||
{
|
||||
use TrackCreationTrait;
|
||||
use TrackUpdateTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, unique: true)]
|
||||
#[ORM\GeneratedValue(strategy: 'AUTO')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
private ?User $userSigner = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Person::class)]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
private ?Person $personSigner = null;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 50, nullable: false, enumType: EntityWorkflowSignatureStateEnum::class)]
|
||||
private EntityWorkflowSignatureStateEnum $state = EntityWorkflowSignatureStateEnum::PENDING;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIMETZ_IMMUTABLE, nullable: true, options: ['default' => null])]
|
||||
private ?\DateTimeImmutable $stateDate = null;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
|
||||
private array $signatureMetadata = [];
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true, options: ['default' => null])]
|
||||
private ?int $zoneSignatureIndex = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: EntityWorkflowStep::class, inversedBy: 'signatures')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?EntityWorkflowStep $step = null;
|
||||
|
||||
public function __construct(
|
||||
EntityWorkflowStep $step,
|
||||
User|Person $signer,
|
||||
) {
|
||||
$this->step = $step;
|
||||
$step->addSignature($this);
|
||||
$this->setSigner($signer);
|
||||
}
|
||||
|
||||
private function setSigner(User|Person $signer): void
|
||||
{
|
||||
if ($signer instanceof User) {
|
||||
$this->userSigner = $signer;
|
||||
} else {
|
||||
$this->personSigner = $signer;
|
||||
}
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getStep(): EntityWorkflowStep
|
||||
{
|
||||
return $this->step;
|
||||
}
|
||||
|
||||
public function getSigner(): User|Person
|
||||
{
|
||||
if (null !== $this->userSigner) {
|
||||
return $this->userSigner;
|
||||
}
|
||||
|
||||
return $this->personSigner;
|
||||
}
|
||||
|
||||
public function getSignatureMetadata(): array
|
||||
{
|
||||
return $this->signatureMetadata;
|
||||
}
|
||||
|
||||
public function setSignatureMetadata(array $signatureMetadata): EntityWorkflowStepSignature
|
||||
{
|
||||
$this->signatureMetadata = $signatureMetadata;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getState(): EntityWorkflowSignatureStateEnum
|
||||
{
|
||||
return $this->state;
|
||||
}
|
||||
|
||||
public function setState(EntityWorkflowSignatureStateEnum $state): EntityWorkflowStepSignature
|
||||
{
|
||||
$this->state = $state;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStateDate(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->stateDate;
|
||||
}
|
||||
|
||||
public function setStateDate(?\DateTimeImmutable $stateDate): EntityWorkflowStepSignature
|
||||
{
|
||||
$this->stateDate = $stateDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getZoneSignatureIndex(): ?int
|
||||
{
|
||||
return $this->zoneSignatureIndex;
|
||||
}
|
||||
|
||||
public function setZoneSignatureIndex(?int $zoneSignatureIndex): EntityWorkflowStepSignature
|
||||
{
|
||||
$this->zoneSignatureIndex = $zoneSignatureIndex;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach from the @see{EntityWorkflowStep}.
|
||||
*
|
||||
* @internal used internally to remove the current signature
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function detachEntityWorkflowStep(): self
|
||||
{
|
||||
$this->step = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
@@ -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\Export;
|
||||
|
||||
/**
|
||||
* Transform data from filter.
|
||||
*
|
||||
* This interface defines a method for transforming filter's form data before it is processed.
|
||||
*
|
||||
* You can implement this interface on @see{FilterInterface} or @see{AggregatorInterface}, to allow to transform existing data in saved exports
|
||||
* and replace it with some default values, or new default values.
|
||||
*/
|
||||
interface DataTransformerInterface
|
||||
{
|
||||
public function transformData(?array $before): array;
|
||||
}
|
@@ -32,6 +32,9 @@ interface FilterInterface extends ModifierInterface
|
||||
|
||||
/**
|
||||
* Get the default data, that can be use as "data" for the form.
|
||||
*
|
||||
* In case of adding new parameters to a filter, you can implement a @see{DataTransformerFilterInterface} to
|
||||
* transforme the filters's data saved in an export to the desired state.
|
||||
*/
|
||||
public function getFormDefaultData(): array;
|
||||
|
||||
|
@@ -11,7 +11,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Form\Type\Export;
|
||||
|
||||
use Chill\MainBundle\Export\DataTransformerInterface;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\CallbackTransformer;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
@@ -19,9 +21,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class AggregatorType extends AbstractType
|
||||
{
|
||||
public function __construct() {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$exportManager = $options['export_manager'];
|
||||
$aggregator = $exportManager->getAggregator($options['aggregator_alias']);
|
||||
@@ -32,17 +32,24 @@ class AggregatorType extends AbstractType
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
$filterFormBuilder = $builder->create('form', FormType::class, [
|
||||
$aggregatorFormBuilder = $builder->create('form', FormType::class, [
|
||||
'compound' => true,
|
||||
'required' => false,
|
||||
'error_bubbling' => false,
|
||||
]);
|
||||
$aggregator->buildForm($filterFormBuilder);
|
||||
$aggregator->buildForm($aggregatorFormBuilder);
|
||||
|
||||
$builder->add($filterFormBuilder);
|
||||
if ($aggregator instanceof DataTransformerInterface) {
|
||||
$aggregatorFormBuilder->addViewTransformer(new CallbackTransformer(
|
||||
fn (?array $data) => $data,
|
||||
fn (?array $data) => $aggregator->transformData($data),
|
||||
));
|
||||
}
|
||||
|
||||
$builder->add($aggregatorFormBuilder);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setRequired('aggregator_alias')
|
||||
->setRequired('export_manager')
|
||||
|
@@ -11,8 +11,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Form\Type\Export;
|
||||
|
||||
use Chill\MainBundle\Export\DataTransformerInterface;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\CallbackTransformer;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
@@ -41,6 +43,13 @@ class FilterType extends AbstractType
|
||||
]);
|
||||
$filter->buildForm($filterFormBuilder);
|
||||
|
||||
if ($filter instanceof DataTransformerInterface) {
|
||||
$filterFormBuilder->addViewTransformer(new CallbackTransformer(
|
||||
fn (?array $data) => $data,
|
||||
fn (?array $data) => $filter->transformData($data),
|
||||
));
|
||||
}
|
||||
|
||||
$builder->add($filterFormBuilder);
|
||||
}
|
||||
|
||||
|
@@ -12,14 +12,12 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
||||
use Chill\MainBundle\Form\Type\ChillCollectionType;
|
||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
@@ -34,169 +32,151 @@ use Symfony\Component\Workflow\Transition;
|
||||
|
||||
class WorkflowStepType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly Registry $registry, private readonly TranslatableStringHelperInterface $translatableStringHelper) {}
|
||||
public function __construct(
|
||||
private readonly Registry $registry,
|
||||
private readonly TranslatableStringHelperInterface $translatableStringHelper
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
/** @var EntityWorkflow $entityWorkflow */
|
||||
$entityWorkflow = $options['entity_workflow'];
|
||||
$handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
|
||||
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||
$place = $workflow->getMarking($entityWorkflow);
|
||||
$placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata(array_keys($place->getPlaces())[0]);
|
||||
|
||||
if (true === $options['transition']) {
|
||||
if (null === $options['entity_workflow']) {
|
||||
throw new \LogicException('if transition is true, entity_workflow should be defined');
|
||||
}
|
||||
if (null === $options['entity_workflow']) {
|
||||
throw new \LogicException('if transition is true, entity_workflow should be defined');
|
||||
}
|
||||
|
||||
$transitions = $this->registry
|
||||
->get($options['entity_workflow'], $entityWorkflow->getWorkflowName())
|
||||
->getEnabledTransitions($entityWorkflow);
|
||||
$transitions = $this->registry
|
||||
->get($options['entity_workflow'], $entityWorkflow->getWorkflowName())
|
||||
->getEnabledTransitions($entityWorkflow);
|
||||
|
||||
$choices = array_combine(
|
||||
array_map(
|
||||
static fn (Transition $transition) => $transition->getName(),
|
||||
$transitions
|
||||
),
|
||||
$choices = array_combine(
|
||||
array_map(
|
||||
static fn (Transition $transition) => $transition->getName(),
|
||||
$transitions
|
||||
);
|
||||
),
|
||||
$transitions
|
||||
);
|
||||
|
||||
if (\array_key_exists('validationFilterInputLabels', $placeMetadata)) {
|
||||
$inputLabels = $placeMetadata['validationFilterInputLabels'];
|
||||
if (\array_key_exists('validationFilterInputLabels', $placeMetadata)) {
|
||||
$inputLabels = $placeMetadata['validationFilterInputLabels'];
|
||||
|
||||
$builder->add('transitionFilter', ChoiceType::class, [
|
||||
'multiple' => false,
|
||||
'label' => 'workflow.My decision',
|
||||
'choices' => [
|
||||
'forward' => 'forward',
|
||||
'backward' => 'backward',
|
||||
'neutral' => 'neutral',
|
||||
],
|
||||
'choice_label' => fn (string $key) => $this->translatableStringHelper->localize($inputLabels[$key]),
|
||||
'choice_attr' => static fn (string $key) => [
|
||||
$key => $key,
|
||||
],
|
||||
'mapped' => false,
|
||||
'expanded' => true,
|
||||
'data' => 'forward',
|
||||
]);
|
||||
}
|
||||
|
||||
$builder
|
||||
->add('transition', ChoiceType::class, [
|
||||
'label' => 'workflow.Next step',
|
||||
'mapped' => false,
|
||||
'multiple' => false,
|
||||
'expanded' => true,
|
||||
'choices' => $choices,
|
||||
'constraints' => [new NotNull()],
|
||||
'choice_label' => function (Transition $transition) use ($workflow) {
|
||||
$meta = $workflow->getMetadataStore()->getTransitionMetadata($transition);
|
||||
|
||||
if (\array_key_exists('label', $meta)) {
|
||||
return $this->translatableStringHelper->localize($meta['label']);
|
||||
}
|
||||
|
||||
return $transition->getName();
|
||||
},
|
||||
'choice_attr' => static function (Transition $transition) use ($workflow) {
|
||||
$toFinal = true;
|
||||
$isForward = 'neutral';
|
||||
|
||||
$metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition);
|
||||
|
||||
if (\array_key_exists('isForward', $metadata)) {
|
||||
if ($metadata['isForward']) {
|
||||
$isForward = 'forward';
|
||||
} else {
|
||||
$isForward = 'backward';
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($transition->getTos() as $to) {
|
||||
$meta = $workflow->getMetadataStore()->getPlaceMetadata($to);
|
||||
|
||||
if (
|
||||
!\array_key_exists('isFinal', $meta) || false === $meta['isFinal']
|
||||
) {
|
||||
$toFinal = false;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'data-is-transition' => 'data-is-transition',
|
||||
'data-to-final' => $toFinal ? '1' : '0',
|
||||
'data-is-forward' => $isForward,
|
||||
];
|
||||
},
|
||||
])
|
||||
->add('future_dest_users', PickUserDynamicType::class, [
|
||||
'label' => 'workflow.dest for next steps',
|
||||
'multiple' => true,
|
||||
'mapped' => false,
|
||||
'suggested' => $options['suggested_users'],
|
||||
])
|
||||
->add('future_cc_users', PickUserDynamicType::class, [
|
||||
'label' => 'workflow.cc for next steps',
|
||||
'multiple' => true,
|
||||
'mapped' => false,
|
||||
'required' => false,
|
||||
'suggested' => $options['suggested_users'],
|
||||
])
|
||||
->add('future_dest_emails', ChillCollectionType::class, [
|
||||
'label' => 'workflow.dest by email',
|
||||
'help' => 'workflow.dest by email help',
|
||||
'mapped' => false,
|
||||
'allow_add' => true,
|
||||
'entry_type' => EmailType::class,
|
||||
'button_add_label' => 'workflow.Add an email',
|
||||
'button_remove_label' => 'workflow.Remove an email',
|
||||
'empty_collection_explain' => 'workflow.Any email',
|
||||
'entry_options' => [
|
||||
'constraints' => [
|
||||
new NotNull(), new NotBlank(), new Email(),
|
||||
],
|
||||
'label' => 'Email',
|
||||
],
|
||||
]);
|
||||
$builder->add('transitionFilter', ChoiceType::class, [
|
||||
'multiple' => false,
|
||||
'label' => 'workflow.My decision',
|
||||
'choices' => [
|
||||
'forward' => 'forward',
|
||||
'backward' => 'backward',
|
||||
'neutral' => 'neutral',
|
||||
],
|
||||
'choice_label' => fn (string $key) => $this->translatableStringHelper->localize($inputLabels[$key]),
|
||||
'choice_attr' => static fn (string $key) => [
|
||||
$key => $key,
|
||||
],
|
||||
'mapped' => false,
|
||||
'expanded' => true,
|
||||
'data' => 'forward',
|
||||
]);
|
||||
}
|
||||
|
||||
if (
|
||||
$handler->supportsFreeze($entityWorkflow)
|
||||
&& !$entityWorkflow->isFreeze()
|
||||
) {
|
||||
$builder
|
||||
->add('freezeAfter', CheckboxType::class, [
|
||||
'required' => false,
|
||||
'label' => 'workflow.Freeze',
|
||||
'help' => 'workflow.The associated element will be freezed',
|
||||
]);
|
||||
}
|
||||
$builder
|
||||
->add('transition', ChoiceType::class, [
|
||||
'label' => 'workflow.Next step',
|
||||
'mapped' => false,
|
||||
'multiple' => false,
|
||||
'expanded' => true,
|
||||
'choices' => $choices,
|
||||
'constraints' => [new NotNull()],
|
||||
'choice_label' => function (Transition $transition) use ($workflow) {
|
||||
$meta = $workflow->getMetadataStore()->getTransitionMetadata($transition);
|
||||
|
||||
if (\array_key_exists('label', $meta)) {
|
||||
return $this->translatableStringHelper->localize($meta['label']);
|
||||
}
|
||||
|
||||
return $transition->getName();
|
||||
},
|
||||
'choice_attr' => static function (Transition $transition) use ($workflow) {
|
||||
$toFinal = true;
|
||||
$isForward = 'neutral';
|
||||
|
||||
$metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition);
|
||||
|
||||
if (\array_key_exists('isForward', $metadata)) {
|
||||
if ($metadata['isForward']) {
|
||||
$isForward = 'forward';
|
||||
} else {
|
||||
$isForward = 'backward';
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($transition->getTos() as $to) {
|
||||
$meta = $workflow->getMetadataStore()->getPlaceMetadata($to);
|
||||
|
||||
if (
|
||||
!\array_key_exists('isFinal', $meta) || false === $meta['isFinal']
|
||||
) {
|
||||
$toFinal = false;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'data-is-transition' => 'data-is-transition',
|
||||
'data-to-final' => $toFinal ? '1' : '0',
|
||||
'data-is-forward' => $isForward,
|
||||
];
|
||||
},
|
||||
])
|
||||
->add('futureDestUsers', PickUserDynamicType::class, [
|
||||
'label' => 'workflow.dest for next steps',
|
||||
'multiple' => true,
|
||||
'suggested' => $options['suggested_users'],
|
||||
])
|
||||
->add('futureCcUsers', PickUserDynamicType::class, [
|
||||
'label' => 'workflow.cc for next steps',
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
'suggested' => $options['suggested_users'],
|
||||
])
|
||||
->add('futureDestEmails', ChillCollectionType::class, [
|
||||
'label' => 'workflow.dest by email',
|
||||
'help' => 'workflow.dest by email help',
|
||||
'allow_add' => true,
|
||||
'entry_type' => EmailType::class,
|
||||
'button_add_label' => 'workflow.Add an email',
|
||||
'button_remove_label' => 'workflow.Remove an email',
|
||||
'empty_collection_explain' => 'workflow.Any email',
|
||||
'entry_options' => [
|
||||
'constraints' => [
|
||||
new NotNull(), new NotBlank(), new Email(),
|
||||
],
|
||||
'label' => 'Email',
|
||||
],
|
||||
]);
|
||||
|
||||
$builder
|
||||
->add('comment', ChillTextareaType::class, [
|
||||
'required' => false,
|
||||
'label' => 'Comment',
|
||||
'empty_data' => '',
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver
|
||||
->setDefined('class')
|
||||
->setRequired('transition')
|
||||
->setAllowedTypes('transition', 'bool')
|
||||
->setDefault('data_class', WorkflowTransitionContextDTO::class)
|
||||
->setRequired('entity_workflow')
|
||||
->setAllowedTypes('entity_workflow', EntityWorkflow::class)
|
||||
->setDefault('suggested_users', [])
|
||||
->setDefault('constraints', [
|
||||
new Callback(
|
||||
function ($step, ExecutionContextInterface $context, $payload) {
|
||||
/** @var EntityWorkflowStep $step */
|
||||
$form = $context->getObject();
|
||||
$workflow = $this->registry->get($step->getEntityWorkflow(), $step->getEntityWorkflow()->getWorkflowName());
|
||||
$transition = $form['transition']->getData();
|
||||
function (WorkflowTransitionContextDTO $step, ExecutionContextInterface $context, $payload) {
|
||||
$workflow = $this->registry->get($step->entityWorkflow, $step->entityWorkflow->getWorkflowName());
|
||||
$transition = $step->transition;
|
||||
$toFinal = true;
|
||||
|
||||
if (null === $transition) {
|
||||
@@ -212,8 +192,8 @@ class WorkflowStepType extends AbstractType
|
||||
$toFinal = false;
|
||||
}
|
||||
}
|
||||
$destUsers = $form['future_dest_users']->getData();
|
||||
$destEmails = $form['future_dest_emails']->getData();
|
||||
$destUsers = $step->futureDestUsers;
|
||||
$destEmails = $step->futureDestEmails;
|
||||
|
||||
if (!$toFinal && [] === $destUsers && [] === $destEmails) {
|
||||
$context
|
||||
@@ -224,20 +204,6 @@ class WorkflowStepType extends AbstractType
|
||||
}
|
||||
}
|
||||
),
|
||||
new Callback(
|
||||
function ($step, ExecutionContextInterface $context, $payload) {
|
||||
$form = $context->getObject();
|
||||
|
||||
foreach ($form->get('future_dest_users')->getData() as $u) {
|
||||
if (in_array($u, $form->get('future_cc_users')->getData(), true)) {
|
||||
$context
|
||||
->buildViolation('workflow.The user in cc cannot be a dest user in the same workflow step')
|
||||
->atPath('ccUsers')
|
||||
->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\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
/**
|
||||
* @template-implements ObjectRepository<EntityWorkflowStepSignature>
|
||||
*/
|
||||
class EntityWorkflowStepSignatureRepository implements ObjectRepository
|
||||
{
|
||||
private \Doctrine\ORM\EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->repository = $entityManager->getRepository(EntityWorkflowStepSignature::class);
|
||||
}
|
||||
|
||||
public function find($id): ?EntityWorkflowStepSignature
|
||||
{
|
||||
return $this->repository->find($id);
|
||||
}
|
||||
|
||||
public function findAll(): array
|
||||
{
|
||||
return $this->repository->findAll();
|
||||
}
|
||||
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
return $this->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
||||
public function findOneBy(array $criteria): ?EntityWorkflowStepSignature
|
||||
{
|
||||
return $this->findOneBy($criteria);
|
||||
}
|
||||
|
||||
public function getClassName(): string
|
||||
{
|
||||
return EntityWorkflowStepSignature::class;
|
||||
}
|
||||
}
|
@@ -43,7 +43,14 @@ export const download_report = (url, container) => {
|
||||
content = URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
extension = mime.getExtension(type);
|
||||
const extensions = new Map();
|
||||
extensions.set('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'xlsx');
|
||||
extensions.set('application/vnd.oasis.opendocument.spreadsheet', 'ods');
|
||||
extensions.set('application/vnd.ms-excel', 'xlsx');
|
||||
extensions.set('text/csv', 'csv');
|
||||
extensions.set('text/csv; charset=utf-8', 'csv');
|
||||
|
||||
extension = extensions.get(type);
|
||||
|
||||
link.appendChild(document.createTextNode(download_text));
|
||||
link.classList.add("btn", "btn-action");
|
||||
@@ -55,7 +62,7 @@ export const download_report = (url, container) => {
|
||||
container.innerHTML = "";
|
||||
container.appendChild(link);
|
||||
}).catch(function(error) {
|
||||
console.log(error);
|
||||
console.error(error);
|
||||
var problem_text =
|
||||
document.createTextNode("Problem during download");
|
||||
|
||||
|
@@ -12,5 +12,5 @@ window.addEventListener("DOMContentLoaded", function(e) {
|
||||
container = document.querySelector("#download_container")
|
||||
;
|
||||
|
||||
download_report(export_generate_url + "?" + query.toString(), container);
|
||||
download_report(export_generate_url + query.toString(), container);
|
||||
});
|
||||
|
@@ -139,7 +139,7 @@ const postprocess = (html: string): string => {
|
||||
}
|
||||
|
||||
const convertMarkdownToHtml = (markdown: string): string => {
|
||||
marked.use({'hooks': {postprocess, preprocess}});
|
||||
marked.use({'hooks': {postprocess, preprocess}, 'async': false});
|
||||
const rawHtml = marked(markdown) as string;
|
||||
return rawHtml;
|
||||
};
|
||||
|
@@ -40,10 +40,10 @@
|
||||
{{ 'by_user'|trans ~ ' ' }}
|
||||
{% endif %}
|
||||
<span class="user">
|
||||
{{ user|chill_entity_render_box(options['user']) }}
|
||||
{{ user|chill_entity_render_box({'at_date': comment.date}) }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</blockquote>
|
||||
{{ closing_box|raw }}
|
||||
{{ closing_box|raw }}
|
||||
|
@@ -1,10 +1,10 @@
|
||||
<span class="chill-entity entity-user">
|
||||
{{- user.label }}
|
||||
{%- if opts['user_job'] and user.userJob(opts['at']) is not null %}
|
||||
<span class="user-job">({{ user.userJob(opts['at']).label|localize_translatable_string }})</span>
|
||||
{%- if opts['user_job'] and user.userJob(opts['at_date']) is not null %}
|
||||
<span class="user-job">({{ user.userJob(opts['at_date']).label|localize_translatable_string }})</span>
|
||||
{%- endif -%}
|
||||
{%- if opts['main_scope'] and user.mainScope(opts['at']) is not null %}
|
||||
<span class="main-scope">({{ user.mainScope(opts['at']).name|localize_translatable_string }})</span>
|
||||
{%- if opts['main_scope'] and user.mainScope(opts['at_date']) is not null %}
|
||||
<span class="main-scope">({{ user.mainScope(opts['at_date']).name|localize_translatable_string }})</span>
|
||||
{%- endif -%}
|
||||
{%- if opts['absence'] and user.isAbsent %}
|
||||
<span class="badge bg-danger rounded-pill" title="{{ 'absence.Absent'|trans|escape('html_attr') }}">{{ 'absence.A'|trans }}</span>
|
||||
|
@@ -0,0 +1,20 @@
|
||||
{% if menus|length > 0 %}
|
||||
<li class="dropdown">
|
||||
<a class="dropdown-toggle btn btn-sm btn-outline-primary"
|
||||
href="#"
|
||||
role="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<i class="fa fa-flash"></i>
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
{% for menu in menus %}
|
||||
<a class="dropdown-item"
|
||||
href="{{ menu.uri }}"
|
||||
><i class="fa fa-{{- menu.extras.icon }} fa-fw"></i>
|
||||
{{ menu.label|trans }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
@@ -21,7 +21,7 @@
|
||||
</span>
|
||||
{% if not c.notification.isSystem %}
|
||||
<span class="badge-user">
|
||||
{{ c.notification.sender|chill_entity_render_string }}
|
||||
{{ c.notification.sender|chill_entity_render_string({'at_date': c.notification.date}) }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge-user system">{{ 'notification.is_system'|trans }}</span>
|
||||
@@ -53,7 +53,7 @@
|
||||
{% endif %}
|
||||
{% for a in c.notification.addressees %}
|
||||
<span class="badge-user">
|
||||
{{ a|chill_entity_render_string }}
|
||||
{{ a|chill_entity_render_string({'at_date': c.notification.date}) }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
{% for a in c.notification.addressesEmails %}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{% extends "@ChillMain/layout.html.twig" %}
|
||||
|
||||
{% block title 'notification.show notification from %sender%'|trans(
|
||||
{ '%sender%': notification.sender|chill_entity_render_string }
|
||||
{ '%sender%': notification.sender|chill_entity_render_string({'at_date': notification.date}) }
|
||||
) ~ ' ' ~ notification.title %}
|
||||
|
||||
{% block js %}
|
||||
|
@@ -31,14 +31,14 @@
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{{ 'By'|trans }}
|
||||
{{ step.previous.transitionBy|chill_entity_render_box }},
|
||||
{{ step.previous.transitionBy|chill_entity_render_box({'at_date': step.previous.transitionAt }) }},
|
||||
{{ step.previous.transitionAt|format_datetime('short', 'short') }}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row">
|
||||
<div class="col-sm-4">{{ 'workflow.Created by'|trans }}</div>
|
||||
<div class="col-sm-8">{{ step.entityWorkflow.createdBy|chill_entity_render_box }}</div>
|
||||
<div class="col-sm-8">{{ step.entityWorkflow.createdBy|chill_entity_render_box({'at_date': step.entityWorkflow.createdAt}) }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-4">{{ 'Le'|trans }}</div>
|
||||
@@ -58,17 +58,15 @@
|
||||
{{ form_row(transition_form.transition) }}
|
||||
</div>
|
||||
|
||||
{% if transition_form.freezeAfter is defined %}
|
||||
{{ form_row(transition_form.freezeAfter) }}
|
||||
{% endif %}
|
||||
|
||||
<div id="futureDests">
|
||||
{{ form_row(transition_form.future_dest_users) }}
|
||||
{{ form_row(transition_form.futureDestUsers) }}
|
||||
{{ form_errors(transition_form.futureDestUsers) }}
|
||||
|
||||
{{ form_row(transition_form.future_cc_users) }}
|
||||
{{ form_row(transition_form.futureCcUsers) }}
|
||||
{{ form_errors(transition_form.futureCcUsers) }}
|
||||
|
||||
{{ form_row(transition_form.future_dest_emails) }}
|
||||
{{ form_errors(transition_form.future_dest_users) }}
|
||||
{{ form_row(transition_form.futureDestEmails) }}
|
||||
{{ form_errors(transition_form.futureDestEmails) }}
|
||||
</div>
|
||||
|
||||
<p>{{ form_label(transition_form.comment) }}</p>
|
||||
@@ -110,8 +108,8 @@
|
||||
{% if entity_workflow.currentStep.destUserByAccessKey|length > 0 %}
|
||||
<p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }} :</b></p>
|
||||
<ul>
|
||||
{% for u in entity_workflow.currentStep.destUserByAccessKey %}
|
||||
<li>{{ u|chill_entity_render_box }}</li>
|
||||
{% for u in entity_workflow.currentStepChained.destUserByAccessKey %}
|
||||
<li>{{ u|chill_entity_render_box({'at_date': entity_workflow.currentStepChained.previous.transitionAt }) }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
@@ -42,7 +42,7 @@
|
||||
<div class="item-col" style="width: inherit;">
|
||||
{% if step.transitionBy is not null %}
|
||||
<div>
|
||||
{{ step.transitionBy|chill_entity_render_box }}
|
||||
{{ step.transitionBy|chill_entity_render_box({'at_date': step.transitionAt}) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
@@ -76,7 +76,7 @@
|
||||
<p><b>{{ 'workflow.Users allowed to apply transition'|trans }} : </b></p>
|
||||
<ul>
|
||||
{% for u in step.destUser %}
|
||||
<li>{{ u|chill_entity_render_box }}</li>
|
||||
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
@@ -85,7 +85,7 @@
|
||||
<p><b>{{ 'workflow.Users put in Cc'|trans }} : </b></p>
|
||||
<ul>
|
||||
{% for u in step.ccUser %}
|
||||
<li>{{ u|chill_entity_render_box }}</li>
|
||||
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
@@ -103,7 +103,7 @@
|
||||
<p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }} :</b></p>
|
||||
<ul>
|
||||
{% for u in step.destUserByAccessKey %}
|
||||
<li>{{ u|chill_entity_render_box }}</li>
|
||||
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
@@ -3,7 +3,7 @@
|
||||
{% if step.previous is not null %}
|
||||
<li>
|
||||
<span class="item-key">{{ 'By'|trans ~ ' : ' }}</span>
|
||||
<b>{{ step.previous.transitionBy|chill_entity_render_box }}</b>
|
||||
<b>{{ step.previous.transitionBy|chill_entity_render_box({'at_date': step.previous.transitionAt }) }}</b>
|
||||
</li>
|
||||
<li>
|
||||
<span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span>
|
||||
@@ -12,19 +12,19 @@
|
||||
<li>
|
||||
<span class="item-key">{{ 'workflow.For'|trans ~ ' : ' }}</span>
|
||||
<b>
|
||||
{% for d in step.destUser %}{{ d|chill_entity_render_string }}{% if not loop.last %}, {% endif %}{% endfor %}
|
||||
{% for d in step.destUser %}{{ d|chill_entity_render_string({'at_date': step.previous.transitionAt}) }}{% if not loop.last %}, {% endif %}{% endfor %}
|
||||
</b>
|
||||
</li>
|
||||
<li>
|
||||
<span class="item-key">{{ 'workflow.Cc'|trans ~ ' : ' }}</span>
|
||||
<b>
|
||||
{% for u in step.ccUser %}{{ u|chill_entity_render_string }}{% if not loop.last %}, {% endif %}{% endfor %}
|
||||
{% for u in step.ccUser %}{{ u|chill_entity_render_string({'at_date': step.previous.transitionAt }) }}{% if not loop.last %}, {% endif %}{% endfor %}
|
||||
</b>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<span class="item-key">{{ 'workflow.Created by'|trans ~ ' : ' }}</span>
|
||||
<b>{{ step.entityWorkflow.createdBy|chill_entity_render_box }}</b>
|
||||
<b>{{ step.entityWorkflow.createdBy|chill_entity_render_box({'at_date': step.entityWorkflow.createdAt }) }}</b>
|
||||
</li>
|
||||
<li>
|
||||
<span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span>
|
||||
|
@@ -11,8 +11,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Routing;
|
||||
|
||||
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Twig\Environment;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
@@ -20,10 +18,8 @@ use Twig\TwigFunction;
|
||||
/**
|
||||
* Add the filter 'chill_menu'.
|
||||
*/
|
||||
class MenuTwig extends AbstractExtension implements ContainerAwareInterface
|
||||
class MenuTwig extends AbstractExtension
|
||||
{
|
||||
private ?ContainerInterface $container = null;
|
||||
|
||||
/**
|
||||
* the default parameters for chillMenu.
|
||||
*
|
||||
@@ -84,9 +80,4 @@ class MenuTwig extends AbstractExtension implements ContainerAwareInterface
|
||||
{
|
||||
return 'chill_menu';
|
||||
}
|
||||
|
||||
public function setContainer(?ContainerInterface $container = null)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
}
|
||||
|
@@ -52,7 +52,7 @@ class EntityWorkflowStepNormalizer implements NormalizerAwareInterface, Normaliz
|
||||
$data['transitionPreviousBy'] = $this->normalizer->normalize(
|
||||
$previous->getTransitionBy(),
|
||||
$format,
|
||||
$context
|
||||
[...$context, UserNormalizer::AT_DATE => $previous->getTransitionAt()]
|
||||
);
|
||||
$data['transitionPreviousAt'] = $this->normalizer->normalize(
|
||||
$previous->getTransitionAt(),
|
||||
|
@@ -45,7 +45,7 @@ class NotificationNormalizer implements NormalizerAwareInterface, NormalizerInte
|
||||
'message' => $object->getMessage(),
|
||||
'relatedEntityClass' => $object->getRelatedEntityClass(),
|
||||
'relatedEntityId' => $object->getRelatedEntityId(),
|
||||
'sender' => $this->normalizer->normalize($object->getSender(), $format, $context),
|
||||
'sender' => $this->normalizer->normalize($object->getSender(), $format, [...$context, UserNormalizer::AT_DATE => $object->getDate()]),
|
||||
'title' => $object->getTitle(),
|
||||
'entity' => null !== $entity ? $this->normalizer->normalize($entity, $format, $context) : null,
|
||||
];
|
||||
|
@@ -19,6 +19,7 @@ use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserJob;
|
||||
use Chill\MainBundle\Templating\Entity\UserRender;
|
||||
use libphonenumber\PhoneNumber;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||
@@ -27,6 +28,8 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
|
||||
{
|
||||
use NormalizerAwareTrait;
|
||||
|
||||
final public const AT_DATE = 'chill:user:at_date';
|
||||
|
||||
final public const NULL_USER = [
|
||||
'type' => 'user',
|
||||
'id' => '',
|
||||
@@ -38,10 +41,16 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
|
||||
'isAbsent' => false,
|
||||
];
|
||||
|
||||
public function __construct(private readonly UserRender $userRender) {}
|
||||
public function __construct(private readonly UserRender $userRender, private readonly ClockInterface $clock) {}
|
||||
|
||||
/**
|
||||
* @param mixed|null $format
|
||||
*
|
||||
* @throws \Symfony\Component\Serializer\Exception\ExceptionInterface
|
||||
*/
|
||||
public function normalize($object, $format = null, array $context = [])
|
||||
{
|
||||
/** @var array{"chill:user:at_date"?: \DateTimeImmutable|\DateTime} $context */
|
||||
/** @var User $object */
|
||||
$userJobContext = array_merge(
|
||||
$context,
|
||||
@@ -72,18 +81,23 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
|
||||
return [...self::NULL_USER, 'phonenumber' => $this->normalizer->normalize(null, $format, $phonenumberContext), 'civility' => $this->normalizer->normalize(null, $format, $civilityContext), 'user_job' => $this->normalizer->normalize(null, $format, $userJobContext), 'main_center' => $this->normalizer->normalize(null, $format, $centerContext), 'main_scope' => $this->normalizer->normalize(null, $format, $scopeContext), 'current_location' => $this->normalizer->normalize(null, $format, $locationContext), 'main_location' => $this->normalizer->normalize(null, $format, $locationContext)];
|
||||
}
|
||||
|
||||
$at = $context[self::AT_DATE] ?? $this->clock->now();
|
||||
if ($at instanceof \DateTime) {
|
||||
$at = \DateTimeImmutable::createFromMutable($at);
|
||||
}
|
||||
|
||||
$data = [
|
||||
'type' => 'user',
|
||||
'id' => $object->getId(),
|
||||
'username' => $object->getUsername(),
|
||||
'text' => $this->userRender->renderString($object, []),
|
||||
'text' => $this->userRender->renderString($object, ['at_date' => $at]),
|
||||
'text_without_absent' => $this->userRender->renderString($object, ['absence' => false]),
|
||||
'label' => $object->getLabel(),
|
||||
'email' => (string) $object->getEmail(),
|
||||
'phonenumber' => $this->normalizer->normalize($object->getPhonenumber(), $format, $phonenumberContext),
|
||||
'user_job' => $this->normalizer->normalize($object->getUserJob(), $format, $userJobContext),
|
||||
'user_job' => $this->normalizer->normalize($object->getUserJob($at), $format, $userJobContext),
|
||||
'main_center' => $this->normalizer->normalize($object->getMainCenter(), $format, $centerContext),
|
||||
'main_scope' => $this->normalizer->normalize($object->getMainScope(), $format, $scopeContext),
|
||||
'main_scope' => $this->normalizer->normalize($object->getMainScope($at), $format, $scopeContext),
|
||||
'isAbsent' => $object->isAbsent(),
|
||||
];
|
||||
|
||||
|
@@ -12,8 +12,14 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Templating\Entity;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use DateTime;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Error\RuntimeError;
|
||||
use Twig\Error\SyntaxError;
|
||||
|
||||
/**
|
||||
* @implements ChillEntityRenderInterface<User>
|
||||
@@ -24,15 +30,31 @@ class UserRender implements ChillEntityRenderInterface
|
||||
'main_scope' => true,
|
||||
'user_job' => true,
|
||||
'absence' => true,
|
||||
'at' => null,
|
||||
'at_date' => null, // instanceof DateTimeInterface
|
||||
];
|
||||
|
||||
public function __construct(private readonly TranslatableStringHelper $translatableStringHelper, private readonly \Twig\Environment $engine, private readonly TranslatorInterface $translator) {}
|
||||
public function __construct(
|
||||
private readonly TranslatableStringHelperInterface $translatableStringHelper,
|
||||
private readonly \Twig\Environment $engine,
|
||||
private readonly TranslatorInterface $translator,
|
||||
private readonly ClockInterface $clock,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws LoaderError
|
||||
* @throws RuntimeError
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
public function renderBox($entity, array $options): string
|
||||
{
|
||||
$opts = \array_merge(self::DEFAULT_OPTIONS, $options);
|
||||
|
||||
if (null === $opts['at_date']) {
|
||||
$opts['at_date'] = $this->clock->now();
|
||||
} elseif ($opts['at_date'] instanceof \DateTime) {
|
||||
$opts['at_date'] = \DateTimeImmutable::createFromMutable($opts['at_date']);
|
||||
}
|
||||
|
||||
return $this->engine->render('@ChillMain/Entity/user.html.twig', [
|
||||
'user' => $entity,
|
||||
'opts' => $opts,
|
||||
@@ -43,16 +65,24 @@ class UserRender implements ChillEntityRenderInterface
|
||||
{
|
||||
$opts = \array_merge(self::DEFAULT_OPTIONS, $options);
|
||||
|
||||
$str = $entity->getLabel();
|
||||
// $immutableAtDate = $opts['at_date'] instanceOf DateTime ? DateTimeImmutable::createFromMutable($opts['at_date']) : $opts['at_date'];
|
||||
|
||||
if (null !== $entity->getUserJob($opts['at']) && $opts['user_job']) {
|
||||
$str .= ' ('.$this->translatableStringHelper
|
||||
->localize($entity->getUserJob($opts['at'])->getLabel()).')';
|
||||
if (null === $opts['at_date']) {
|
||||
$opts['at_date'] = $this->clock->now();
|
||||
} elseif ($opts['at_date'] instanceof \DateTime) {
|
||||
$opts['at_date'] = \DateTimeImmutable::createFromMutable($opts['at_date']);
|
||||
}
|
||||
|
||||
if (null !== $entity->getMainScope($opts['at']) && $opts['main_scope']) {
|
||||
$str = $entity->getLabel();
|
||||
|
||||
if (null !== $entity->getUserJob($opts['at_date']) && $opts['user_job']) {
|
||||
$str .= ' ('.$this->translatableStringHelper
|
||||
->localize($entity->getMainScope($opts['at'])->getName()).')';
|
||||
->localize($entity->getUserJob($opts['at_date'])->getLabel()).')';
|
||||
}
|
||||
|
||||
if (null !== $entity->getMainScope($opts['at_date']) && $opts['main_scope']) {
|
||||
$str .= ' ('.$this->translatableStringHelper
|
||||
->localize($entity->getMainScope($opts['at_date'])->getName()).')';
|
||||
}
|
||||
|
||||
if ($entity->isAbsent() && $opts['absence']) {
|
||||
|
@@ -338,15 +338,11 @@ abstract class AbstractAggregatorTest extends KernelTestCase
|
||||
.'is a string or an be converted to a string', $key)
|
||||
);
|
||||
|
||||
$this->assertTrue(
|
||||
// conditions
|
||||
\is_string((string) \call_user_func($closure, '_header'))
|
||||
&& !empty(\call_user_func($closure, '_header'))
|
||||
&& '_header' !== \call_user_func($closure, '_header'),
|
||||
// message
|
||||
sprintf('Test that the callable return by `getLabels` for key %s '
|
||||
.'can provide an header', $key)
|
||||
);
|
||||
$head = \call_user_func($closure, '_header');
|
||||
|
||||
self::assertIsString($head);
|
||||
self::assertNotEquals('', $head);
|
||||
self::assertNotEquals('_header', $head);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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\Tests\Entity\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class EntityWorkflowStepSignatureTest extends KernelTestCase
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
|
||||
}
|
||||
|
||||
public function testConstruct()
|
||||
{
|
||||
$workflow = new EntityWorkflow();
|
||||
$workflow->setWorkflowName('vendee_internal')
|
||||
->setRelatedEntityId(0)
|
||||
->setRelatedEntityClass(AccompanyingPeriodWorkEvaluationDocument::class);
|
||||
|
||||
$step = $workflow->getCurrentStep();
|
||||
|
||||
$person = $this->entityManager->createQuery('SELECT p FROM '.Person::class.' p')
|
||||
->setMaxResults(1)
|
||||
->getSingleResult();
|
||||
|
||||
$signature = new EntityWorkflowStepSignature($step, $person);
|
||||
|
||||
self::assertCount(1, $step->getSignatures());
|
||||
self::assertSame($signature, $step->getSignatures()->first());
|
||||
|
||||
$this->entityManager->getConnection()->beginTransaction();
|
||||
$this->entityManager->persist($workflow);
|
||||
$this->entityManager->persist($step);
|
||||
$this->entityManager->persist($signature);
|
||||
|
||||
$this->entityManager->flush();
|
||||
$this->entityManager->getConnection()->commit();
|
||||
|
||||
$this->entityManager->clear();
|
||||
|
||||
$signatureBis = $this->entityManager->find(EntityWorkflowStepSignature::class, $signature->getId());
|
||||
|
||||
self::assertEquals($signature->getId(), $signatureBis->getId());
|
||||
self::assertEquals($step->getId(), $signatureBis->getStep()->getId());
|
||||
}
|
||||
}
|
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Tests\Entity\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
@@ -25,7 +26,7 @@ final class EntityWorkflowTest extends TestCase
|
||||
{
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
|
||||
$entityWorkflow->setStep('final');
|
||||
$entityWorkflow->setStep('final', new WorkflowTransitionContextDTO($entityWorkflow));
|
||||
$entityWorkflow->getCurrentStep()->setIsFinal(true);
|
||||
|
||||
$this->assertTrue($entityWorkflow->isFinal());
|
||||
@@ -37,16 +38,16 @@ final class EntityWorkflowTest extends TestCase
|
||||
|
||||
$this->assertFalse($entityWorkflow->isFinal());
|
||||
|
||||
$entityWorkflow->setStep('two');
|
||||
$entityWorkflow->setStep('two', new WorkflowTransitionContextDTO($entityWorkflow));
|
||||
|
||||
$this->assertFalse($entityWorkflow->isFinal());
|
||||
|
||||
$entityWorkflow->setStep('previous_final');
|
||||
$entityWorkflow->setStep('previous_final', new WorkflowTransitionContextDTO($entityWorkflow));
|
||||
|
||||
$this->assertFalse($entityWorkflow->isFinal());
|
||||
|
||||
$entityWorkflow->getCurrentStep()->setIsFinal(true);
|
||||
$entityWorkflow->setStep('final');
|
||||
$entityWorkflow->setStep('final', new WorkflowTransitionContextDTO($entityWorkflow));
|
||||
|
||||
$this->assertTrue($entityWorkflow->isFinal());
|
||||
}
|
||||
@@ -57,20 +58,20 @@ final class EntityWorkflowTest extends TestCase
|
||||
|
||||
$this->assertFalse($entityWorkflow->isFreeze());
|
||||
|
||||
$entityWorkflow->setStep('step_one');
|
||||
$entityWorkflow->setStep('step_one', new WorkflowTransitionContextDTO($entityWorkflow));
|
||||
|
||||
$this->assertFalse($entityWorkflow->isFreeze());
|
||||
|
||||
$entityWorkflow->setStep('step_three');
|
||||
$entityWorkflow->setStep('step_three', new WorkflowTransitionContextDTO($entityWorkflow));
|
||||
|
||||
$this->assertFalse($entityWorkflow->isFreeze());
|
||||
|
||||
$entityWorkflow->setStep('freezed');
|
||||
$entityWorkflow->setStep('freezed', new WorkflowTransitionContextDTO($entityWorkflow));
|
||||
$entityWorkflow->getCurrentStep()->setFreezeAfter(true);
|
||||
|
||||
$this->assertTrue($entityWorkflow->isFreeze());
|
||||
|
||||
$entityWorkflow->setStep('after_freeze');
|
||||
$entityWorkflow->setStep('after_freeze', new WorkflowTransitionContextDTO($entityWorkflow));
|
||||
|
||||
$this->assertTrue($entityWorkflow->isFreeze());
|
||||
|
||||
|
@@ -25,6 +25,7 @@ use libphonenumber\PhoneNumberUtil;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
use Symfony\Component\Serializer\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
@@ -122,7 +123,9 @@ final class UserNormalizerTest extends TestCase
|
||||
$userRender = $this->prophesize(UserRender::class);
|
||||
$userRender->renderString(Argument::type(User::class), Argument::type('array'))->willReturn($user ? $user->getLabel() : '');
|
||||
|
||||
$normalizer = new UserNormalizer($userRender->reveal());
|
||||
$clock = new MockClock(new \DateTimeImmutable('now'));
|
||||
|
||||
$normalizer = new UserNormalizer($userRender->reveal(), $clock);
|
||||
$normalizer->setNormalizer(new class () implements NormalizerInterface {
|
||||
public function normalize($object, ?string $format = null, array $context = [])
|
||||
{
|
||||
|
@@ -0,0 +1,109 @@
|
||||
<?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 Templating\Entity;
|
||||
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserJob;
|
||||
use Chill\MainBundle\Templating\Entity\UserRender;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class UserRenderTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testRenderUserWithJobAndScopeAtCertainDate(): void
|
||||
{
|
||||
// Create a user with a certain user job
|
||||
|
||||
$user = new User();
|
||||
$userJobA = new UserJob();
|
||||
$scopeA = new Scope();
|
||||
|
||||
$userJobA->setLabel(['fr' => 'assistant social'])
|
||||
->setActive(true);
|
||||
$scopeA->setName(['fr' => 'service A']);
|
||||
$user->setLabel('BOB ISLA');
|
||||
|
||||
$userJobB = new UserJob();
|
||||
$scopeB = new Scope();
|
||||
|
||||
$userJobB->setLabel(['fr' => 'directrice'])
|
||||
->setActive(true);
|
||||
$scopeB->setName(['fr' => 'service B']);
|
||||
|
||||
$userJobHistoryA = (new User\UserJobHistory())
|
||||
->setUser($user)
|
||||
->setJob($userJobA)
|
||||
->setStartDate(new \DateTimeImmutable('2023-11-01 12:00:00'))
|
||||
->setEndDate(new \DateTimeImmutable('2023-11-30 00:00:00'));
|
||||
|
||||
$userScopeHistoryA = (new User\UserScopeHistory())
|
||||
->setUser($user)
|
||||
->setScope($scopeA)
|
||||
->setStartDate(new \DateTimeImmutable('2023-11-01 12:00:00'))
|
||||
->setEndDate(new \DateTimeImmutable('2023-11-30 00:00:00'));
|
||||
|
||||
$userJobHistoryB = (new User\UserJobHistory())
|
||||
->setUser($user)
|
||||
->setJob($userJobB)
|
||||
->setStartDate(new \DateTimeImmutable('2023-12-01 12:00:00'));
|
||||
|
||||
$userScopeHistoryB = (new User\UserScopeHistory())
|
||||
->setUser($user)
|
||||
->setScope($scopeB)
|
||||
->setStartDate(new \DateTimeImmutable('2023-12-01 12:00:00'));
|
||||
|
||||
$user->getUserJobHistories()->add($userJobHistoryA);
|
||||
$user->getUserScopeHistories()->add($userScopeHistoryA);
|
||||
|
||||
$user->getUserJobHistories()->add($userJobHistoryB);
|
||||
$user->getUserScopeHistories()->add($userScopeHistoryB);
|
||||
|
||||
// Create renderer
|
||||
$translatableStringHelperMock = $this->prophesize(TranslatableStringHelperInterface::class);
|
||||
$translatableStringHelperMock->localize(Argument::type('array'))->will(fn ($args) => $args[0]['fr']);
|
||||
|
||||
$engineMock = $this->createMock(Environment::class);
|
||||
$translatorMock = $this->createMock(TranslatorInterface::class);
|
||||
$clock = new MockClock(new \DateTimeImmutable('2023-12-15 12:00:00'));
|
||||
|
||||
$renderer = new UserRender($translatableStringHelperMock->reveal(), $engineMock, $translatorMock, $clock);
|
||||
|
||||
$optionsNoDate['at_date'] = null;
|
||||
$options['at_date'] = new \DateTime('2023-11-25 12:00:00');
|
||||
$optionsTwo['at_date'] = new \DateTime('2024-01-30 12:00:00');
|
||||
|
||||
// Check that the user render for the first activity corresponds with the first user job
|
||||
$expectedStringA = 'BOB ISLA (assistant social) (service A)';
|
||||
$this->assertEquals($expectedStringA, $renderer->renderString($user, $options));
|
||||
|
||||
// Check that the user render for the second activity corresponds with the second user job
|
||||
$expectedStringB = 'BOB ISLA (directrice) (service B)';
|
||||
$this->assertEquals($expectedStringB, $renderer->renderString($user, $optionsTwo));
|
||||
|
||||
// Check that the user renders the job and scope that is active now, when no date is given
|
||||
$expectedStringC = 'BOB ISLA (directrice) (service B)';
|
||||
$this->assertEquals($expectedStringC, $renderer->renderString($user, $optionsNoDate));
|
||||
}
|
||||
}
|
@@ -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\MainBundle\Tests\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
|
||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Workflow\Marking;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class EntityWorkflowMarkingStoreTest extends TestCase
|
||||
{
|
||||
public function testGetMarking(): void
|
||||
{
|
||||
$markingStore = $this->buildMarkingStore();
|
||||
$workflow = new EntityWorkflow();
|
||||
|
||||
$marking = $markingStore->getMarking($workflow);
|
||||
|
||||
self::assertEquals(['initial' => 1], $marking->getPlaces());
|
||||
}
|
||||
|
||||
public function testSetMarking(): void
|
||||
{
|
||||
$markingStore = $this->buildMarkingStore();
|
||||
$workflow = new EntityWorkflow();
|
||||
|
||||
$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]);
|
||||
|
||||
$currentStep = $workflow->getCurrentStep();
|
||||
self::assertEquals('foo', $currentStep->getCurrentStep());
|
||||
self::assertContains($email, $currentStep->getDestEmail());
|
||||
self::assertContains($user1, $currentStep->getCcUser());
|
||||
self::assertContains($user2, $currentStep->getDestUser());
|
||||
}
|
||||
|
||||
private function buildMarkingStore(): EntityWorkflowMarkingStore
|
||||
{
|
||||
return new EntityWorkflowMarkingStore();
|
||||
}
|
||||
}
|
@@ -0,0 +1,49 @@
|
||||
<?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\EntityWorkflow;
|
||||
use Symfony\Component\Workflow\Marking;
|
||||
use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;
|
||||
|
||||
final readonly class EntityWorkflowMarkingStore implements MarkingStoreInterface
|
||||
{
|
||||
public function getMarking(object $subject): Marking
|
||||
{
|
||||
if (!$subject instanceof EntityWorkflow) {
|
||||
throw new \UnexpectedValueException('Expected instance of EntityWorkflow');
|
||||
}
|
||||
$step = $subject->getCurrentStep();
|
||||
|
||||
return new Marking([$step->getCurrentStep() => 1]);
|
||||
}
|
||||
|
||||
public function setMarking(object $subject, Marking $marking, array $context = []): void
|
||||
{
|
||||
if (!$subject instanceof EntityWorkflow) {
|
||||
throw new \UnexpectedValueException('Expected instance of EntityWorkflow');
|
||||
}
|
||||
|
||||
$places = $marking->getPlaces();
|
||||
if (1 < count($places)) {
|
||||
throw new \LogicException('Expected maximum one place');
|
||||
}
|
||||
$next = array_keys($places)[0];
|
||||
|
||||
$transitionDTO = $context['context'] ?? null;
|
||||
if (!$transitionDTO instanceof WorkflowTransitionContextDTO) {
|
||||
throw new \UnexpectedValueException(sprintf('Expected instance of %s', WorkflowTransitionContextDTO::class));
|
||||
}
|
||||
|
||||
$subject->setStep($next, $transitionDTO);
|
||||
}
|
||||
}
|
@@ -21,31 +21,13 @@ use Symfony\Component\Workflow\Event\Event;
|
||||
use Symfony\Component\Workflow\Event\GuardEvent;
|
||||
use Symfony\Component\Workflow\TransitionBlocker;
|
||||
|
||||
class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterface
|
||||
final readonly class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(private readonly LoggerInterface $chillLogger, private readonly Security $security, private readonly UserRender $userRender) {}
|
||||
|
||||
public function addDests(Event $event): void
|
||||
{
|
||||
if (!$event->getSubject() instanceof EntityWorkflow) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var EntityWorkflow $entityWorkflow */
|
||||
$entityWorkflow = $event->getSubject();
|
||||
|
||||
foreach ($entityWorkflow->futureCcUsers as $user) {
|
||||
$entityWorkflow->getCurrentStep()->addCcUser($user);
|
||||
}
|
||||
|
||||
foreach ($entityWorkflow->futureDestUsers as $user) {
|
||||
$entityWorkflow->getCurrentStep()->addDestUser($user);
|
||||
}
|
||||
|
||||
foreach ($entityWorkflow->futureDestEmails as $email) {
|
||||
$entityWorkflow->getCurrentStep()->addDestEmail($email);
|
||||
}
|
||||
}
|
||||
public function __construct(
|
||||
private LoggerInterface $chillLogger,
|
||||
private Security $security,
|
||||
private UserRender $userRender
|
||||
) {}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
@@ -53,7 +35,6 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac
|
||||
'workflow.transition' => 'onTransition',
|
||||
'workflow.completed' => [
|
||||
['markAsFinal', 2048],
|
||||
['addDests', 2048],
|
||||
],
|
||||
'workflow.guard' => [
|
||||
['guardEntityWorkflow', 0],
|
||||
@@ -99,6 +80,10 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac
|
||||
|
||||
public function markAsFinal(Event $event): void
|
||||
{
|
||||
// NOTE: it is not possible to move this method to the marking store, because
|
||||
// there is dependency between the Workflow definition and the MarkingStoreInterface (the workflow
|
||||
// constructor need a MarkingStoreInterface)
|
||||
|
||||
if (!$event->getSubject() instanceof EntityWorkflow) {
|
||||
return;
|
||||
}
|
||||
|
@@ -23,7 +23,13 @@ use Symfony\Component\Workflow\Registry;
|
||||
|
||||
class NotificationOnTransition implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly \Twig\Environment $engine, private readonly MetadataExtractor $metadataExtractor, private readonly Security $security, private readonly Registry $registry) {}
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly \Twig\Environment $engine,
|
||||
private readonly MetadataExtractor $metadataExtractor,
|
||||
private readonly Security $security,
|
||||
private readonly Registry $registry
|
||||
) {}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
@@ -85,7 +91,10 @@ class NotificationOnTransition implements EventSubscriberInterface
|
||||
'dest' => $subscriber,
|
||||
'place' => $place,
|
||||
'workflow' => $workflow,
|
||||
'is_dest' => \in_array($subscriber->getId(), array_map(static fn (User $u) => $u->getId(), $entityWorkflow->futureDestUsers), true),
|
||||
'is_dest' => \in_array($subscriber->getId(), array_map(
|
||||
static fn (User $u) => $u->getId(),
|
||||
$entityWorkflow->getCurrentStep()->getDestUser()->toArray()
|
||||
), true),
|
||||
];
|
||||
|
||||
$notification = new Notification();
|
||||
|
@@ -20,7 +20,13 @@ use Symfony\Component\Workflow\Registry;
|
||||
|
||||
class SendAccessKeyEventSubscriber
|
||||
{
|
||||
public function __construct(private readonly \Twig\Environment $engine, private readonly MetadataExtractor $metadataExtractor, private readonly Registry $registry, private readonly EntityWorkflowManager $entityWorkflowManager, private readonly MailerInterface $mailer) {}
|
||||
public function __construct(
|
||||
private readonly \Twig\Environment $engine,
|
||||
private readonly MetadataExtractor $metadataExtractor,
|
||||
private readonly Registry $registry,
|
||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||
private readonly MailerInterface $mailer
|
||||
) {}
|
||||
|
||||
public function postPersist(EntityWorkflowStep $step): void
|
||||
{
|
||||
@@ -32,7 +38,7 @@ class SendAccessKeyEventSubscriber
|
||||
);
|
||||
$handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
|
||||
|
||||
foreach ($entityWorkflow->futureDestEmails as $emailAddress) {
|
||||
foreach ($step->getDestEmail() as $emailAddress) {
|
||||
$context = [
|
||||
'entity_workflow' => $entityWorkflow,
|
||||
'dest' => $emailAddress,
|
||||
|
@@ -0,0 +1,74 @@
|
||||
<?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\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Symfony\Component\Workflow\Transition;
|
||||
|
||||
/**
|
||||
* Context for a transition on an workflow entity.
|
||||
*/
|
||||
class WorkflowTransitionContextDTO
|
||||
{
|
||||
/**
|
||||
* a list of future dest users for the next steps.
|
||||
*
|
||||
* This is in used in order to let controller inform who will be the future users which will validate
|
||||
* the next step. This is necessary to perform some computation about the next users, before they are
|
||||
* associated to the entity EntityWorkflowStep.
|
||||
*
|
||||
* @var array|User[]
|
||||
*/
|
||||
public array $futureDestUsers = [];
|
||||
|
||||
/**
|
||||
* a list of future cc users for the next steps.
|
||||
*
|
||||
* @var array|User[]
|
||||
*/
|
||||
public array $futureCcUsers = [];
|
||||
|
||||
/**
|
||||
* a list of future dest emails for the next steps.
|
||||
*
|
||||
* This is in used in order to let controller inform who will be the future emails which will validate
|
||||
* the next step. This is necessary to perform some computation about the next emails, before they are
|
||||
* associated to the entity EntityWorkflowStep.
|
||||
*
|
||||
* @var array|string[]
|
||||
*/
|
||||
public array $futureDestEmails = [];
|
||||
|
||||
public ?Transition $transition = null;
|
||||
|
||||
public string $comment = '';
|
||||
|
||||
public function __construct(
|
||||
public EntityWorkflow $entityWorkflow
|
||||
) {}
|
||||
|
||||
#[Assert\Callback()]
|
||||
public function validateCCUserIsNotInDest(ExecutionContextInterface $context, $payload): void
|
||||
{
|
||||
foreach ($this->futureDestUsers as $u) {
|
||||
if (in_array($u, $this->futureCcUsers, true)) {
|
||||
$context
|
||||
->buildViolation('workflow.The user in cc cannot be a dest user in the same workflow step')
|
||||
->atPath('ccUsers')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -20,9 +20,5 @@ services:
|
||||
|
||||
chill.main.twig.chill_menu:
|
||||
class: Chill\MainBundle\Routing\MenuTwig
|
||||
arguments:
|
||||
- "@chill.main.menu_composer"
|
||||
calls:
|
||||
- [setContainer, ["@service_container"]]
|
||||
tags:
|
||||
- { name: twig.extension }
|
||||
|
@@ -0,0 +1,51 @@
|
||||
<?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 Version20240628095159 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add signatures to workflow';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE SEQUENCE chill_main_workflow_entity_step_signature_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||
$this->addSql('CREATE TABLE chill_main_workflow_entity_step_signature (id INT NOT NULL, step_id INT NOT NULL, '.
|
||||
'state VARCHAR(50) NOT NULL, stateDate TIMESTAMP(0) WITH TIME ZONE DEFAULT NULL, signatureMetadata JSON DEFAULT \'[]\' NOT NULL,'.
|
||||
' zoneSignatureIndex INT DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL,'.
|
||||
' userSigner_id INT DEFAULT NULL, personSigner_id INT DEFAULT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE INDEX IDX_C47D4BA3D934E3A4 ON chill_main_workflow_entity_step_signature (userSigner_id)');
|
||||
$this->addSql('CREATE INDEX IDX_C47D4BA3ADFFA293 ON chill_main_workflow_entity_step_signature (personSigner_id)');
|
||||
$this->addSql('CREATE INDEX IDX_C47D4BA373B21E9C ON chill_main_workflow_entity_step_signature (step_id)');
|
||||
$this->addSql('CREATE INDEX IDX_C47D4BA33174800F ON chill_main_workflow_entity_step_signature (createdBy_id)');
|
||||
$this->addSql('CREATE INDEX IDX_C47D4BA365FF1AEC ON chill_main_workflow_entity_step_signature (updatedBy_id)');
|
||||
$this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_step_signature.stateDate IS \'(DC2Type:datetimetz_immutable)\'');
|
||||
$this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_step_signature.createdAt IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_step_signature.updatedAt IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA3D934E3A4 FOREIGN KEY (userSigner_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA3ADFFA293 FOREIGN KEY (personSigner_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA373B21E9C FOREIGN KEY (step_id) REFERENCES chill_main_workflow_entity_step (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA33174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA365FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP SEQUENCE chill_main_workflow_entity_step_signature_id_seq CASCADE');
|
||||
$this->addSql('DROP TABLE chill_main_workflow_entity_step_signature');
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user