Merge remote-tracking branch 'origin/master' into issue439_residential_address_otf

This commit is contained in:
2022-03-02 21:53:17 +01:00
147 changed files with 3136 additions and 771 deletions

View File

@@ -11,21 +11,26 @@ declare(strict_types=1);
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowComment;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
use Chill\MainBundle\Form\EntityWorkflowCommentType;
use Chill\MainBundle\Form\WorkflowStepType;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\Validator\StepDestValid;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\TransitionBlocker;
@@ -80,7 +85,9 @@ class WorkflowController extends AbstractController
$entityWorkflow
->setRelatedEntityClass($request->query->get('entityClass'))
->setRelatedEntityId($request->query->getInt('entityId'))
->setWorkflowName($request->query->get('workflow'));
->setWorkflowName($request->query->get('workflow'))
->addSubscriberToStep($this->getUser())
->addSubscriberToFinal($this->getUser());
$errors = $this->validator->validate($entityWorkflow, null, ['creation']);
@@ -104,6 +111,121 @@ class WorkflowController extends AbstractController
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]);
}
/**
* @Route("/{_locale}/main/workflow/{id}/delete", name="chill_main_workflow_delete")
*/
public function delete(EntityWorkflow $entityWorkflow, Request $request): Response
{
$this->denyAccessUnlessGranted(EntityWorkflowVoter::DELETE, $entityWorkflow);
$form = $this->createForm(FormType::class);
$form->add('submit', SubmitType::class, ['label' => 'workflow.Delete workflow']);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->remove($entityWorkflow);
$this->entityManager->flush();
$this->addFlash('success', $this->translator->trans('workflow.Workflow deleted with success'));
return $this->redirectToRoute('chill_main_homepage');
}
return $this->render('@ChillMain/Workflow/delete.html.twig', [
'entityWorkflow' => $entityWorkflow,
'delete_form' => $form->createView(),
'handler' => $this->entityWorkflowManager->getHandler($entityWorkflow),
]);
}
/**
* @Route("/{_locale}/main/workflow-step/{id}/access_key", name="chill_main_workflow_grant_access_by_key")
*/
public function getAccessByAccessKey(EntityWorkflowStep $entityWorkflowStep, Request $request): Response
{
if (null === $accessKey = $request->query->get('accessKey', null)) {
throw new BadRequestException('accessKey is missing');
}
if (!$this->getUser() instanceof User) {
throw new AccessDeniedException('Not a valid user');
}
if ($entityWorkflowStep->getAccessKey() !== $accessKey) {
throw new AccessDeniedException('Access key is invalid');
}
if (!$entityWorkflowStep->isWaitingForTransition()) {
$this->addFlash('error', $this->translator->trans('workflow.Steps is not waiting for transition. Maybe someone apply the transition before you ?'));
} else {
$entityWorkflowStep->addDestUserByAccessKey($this->getUser());
$this->entityManager->flush();
$this->addFlash('success', $this->translator->trans('workflow.You get access to this step'));
}
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflowStep->getEntityWorkflow()->getId()]);
}
/**
* Previous workflows where the user has applyed a transition.
*
* @Route("/{_locale}/main/workflow/list/previous_transitionned", name="chill_main_workflow_list_previous_transitionned")
*/
public function myPreviousWorkflowsTransitionned(Request $request): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
$total = $this->entityWorkflowRepository->countByPreviousTransitionned($this->getUser());
$paginator = $this->paginatorFactory->create($total);
$workflows = $this->entityWorkflowRepository->findByPreviousTransitionned(
$this->getUser(),
['createdAt' => 'DESC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
return $this->render(
'@ChillMain/Workflow/list.html.twig',
[
'help' => 'workflow.Previous workflow transitionned help',
'workflows' => $this->buildHandler($workflows),
'paginator' => $paginator,
'step' => 'previous_transitionned',
]
);
}
/**
* Previous workflows where the user was mentioned, but did not give any reaction.
*
* @Route("/{_locale}/main/workflow/list/previous_without_reaction", name="chill_main_workflow_list_previous_without_reaction")
*/
public function myPreviousWorkflowsWithoutReaction(Request $request): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
$total = $this->entityWorkflowRepository->countByPreviousDestWithoutReaction($this->getUser());
$paginator = $this->paginatorFactory->create($total);
$workflows = $this->entityWorkflowRepository->findByPreviousDestWithoutReaction(
$this->getUser(),
['createdAt' => 'DESC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
return $this->render(
'@ChillMain/Workflow/list.html.twig',
[
'help' => 'workflow.Previous workflow without reaction help',
'workflows' => $this->buildHandler($workflows),
'paginator' => $paginator,
'step' => 'previous_without_reaction',
]
);
}
/**
* @Route("/{_locale}/main/workflow/list/dest", name="chill_main_workflow_list_dest")
*/
@@ -198,6 +320,7 @@ class WorkflowController extends AbstractController
}
$entityWorkflow->futureDestUsers = $transitionForm['future_dest_users']->getData();
$entityWorkflow->futureDestEmails = $transitionForm['future_dest_emails']->getData();
$workflow->apply($entityWorkflow, $transition);
@@ -205,18 +328,13 @@ class WorkflowController extends AbstractController
$entityWorkflow->getCurrentStep()->addDestUser($user);
}
$errors = $this->validator->validate(
$entityWorkflow->getCurrentStep(),
new StepDestValid()
);
if (count($errors) === 0) {
$this->entityManager->flush();
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]);
foreach ($transitionForm['future_dest_emails']->getData() as $email) {
$entityWorkflow->getCurrentStep()->addDestEmail($email);
}
return new Response((string) $errors, Response::HTTP_UNPROCESSABLE_ENTITY);
$this->entityManager->flush();
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]);
}
if ($transitionForm->isSubmitted() && !$transitionForm->isValid()) {
@@ -243,8 +361,8 @@ class WorkflowController extends AbstractController
return $this->render(
'@ChillMain/Workflow/index.html.twig',
[
'handler' => $handler,
'handler_template' => $handler->getTemplate($entityWorkflow),
'handler_template_title' => $handler->getTemplateTitle($entityWorkflow),
'handler_template_data' => $handler->getTemplateData($entityWorkflow),
'transition_form' => isset($transitionForm) ? $transitionForm->createView() : null,
'entity_workflow' => $entityWorkflow,

View File

@@ -39,6 +39,7 @@ use Chill\MainBundle\Form\LocationTypeType;
use Chill\MainBundle\Form\UserJobType;
use Chill\MainBundle\Form\UserType;
use Exception;
use Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType;
use Ramsey\Uuid\Doctrine\UuidType;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -235,6 +236,7 @@ class ChillMainExtension extends Extension implements
'dateinterval' => NativeDateIntervalType::class,
'point' => PointType::class,
'uuid' => UuidType::class,
'phone_number' => PhoneNumberType::class,
],
],
]

View File

@@ -97,6 +97,9 @@ class Configuration implements ConfigurationInterface
->scalarNode('twilio_secret')
->defaultNull()
->end()
->scalarNode('default_carrier_code')
->defaultNull()
->end()
->end()
->end()
->arrayNode('acl')

View File

@@ -18,9 +18,9 @@ use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\ORM\Mapping as ORM;
use libphonenumber\PhoneNumber;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Table(name="chill_main_location")
@@ -90,20 +90,18 @@ class Location implements TrackCreationInterface, TrackUpdateInterface
private ?string $name = null;
/**
* @ORM\Column(type="string", length=64, nullable=true)
* @ORM\Column(type="phone_number", nullable=true)
* @Serializer\Groups({"read", "write", "docgen:read"})
* @Assert\Regex(pattern="/^([\+{1}])([0-9\s*]{4,20})$/")
* @PhonenumberConstraint(type="any")
*/
private ?string $phonenumber1 = null;
private ?PhoneNumber $phonenumber1 = null;
/**
* @ORM\Column(type="string", length=64, nullable=true)
* @ORM\Column(type="phone_number", nullable=true)
* @Serializer\Groups({"read", "write", "docgen:read"})
* @Assert\Regex(pattern="/^([\+{1}])([0-9\s*]{4,20})$/")
* @PhonenumberConstraint(type="any")
*/
private ?string $phonenumber2 = null;
private ?PhoneNumber $phonenumber2 = null;
/**
* @ORM\Column(type="datetime_immutable", nullable=true)
@@ -162,12 +160,12 @@ class Location implements TrackCreationInterface, TrackUpdateInterface
return $this->name;
}
public function getPhonenumber1(): ?string
public function getPhonenumber1(): ?PhoneNumber
{
return $this->phonenumber1;
}
public function getPhonenumber2(): ?string
public function getPhonenumber2(): ?PhoneNumber
{
return $this->phonenumber2;
}
@@ -238,14 +236,14 @@ class Location implements TrackCreationInterface, TrackUpdateInterface
return $this;
}
public function setPhonenumber1(?string $phonenumber1): self
public function setPhonenumber1(?PhoneNumber $phonenumber1): self
{
$this->phonenumber1 = $phonenumber1;
return $this;
}
public function setPhonenumber2(?string $phonenumber2): self
public function setPhonenumber2(?PhoneNumber $phonenumber2): self
{
$this->phonenumber2 = $phonenumber2;

View File

@@ -41,6 +41,17 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
use TrackUpdateTrait;
/**
* 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.
*

View File

@@ -27,6 +27,11 @@ use function in_array;
*/
class EntityWorkflowStep
{
/**
* @ORM\Column(type="text", nullable=false)
*/
private string $accessKey;
/**
* @ORM\Column(type="text", options={"default": ""})
*/
@@ -48,6 +53,12 @@ class EntityWorkflowStep
*/
private Collection $destUser;
/**
* @ORM\ManyToMany(targetEntity=User::class)
* @ORM\JoinTable(name="chill_main_workflow_entity_step_user_by_accesskey")
*/
private Collection $destUserByAccessKey;
/**
* @ORM\ManyToOne(targetEntity=EntityWorkflow::class, inversedBy="steps")
*/
@@ -86,7 +97,7 @@ class EntityWorkflowStep
private ?string $transitionAfter = null;
/**
* @ORM\Column(type="datetime_immutable")
* @ORM\Column(type="datetime_immutable", nullable=true, options={"default": null})
*/
private ?DateTimeImmutable $transitionAt = null;
@@ -104,6 +115,8 @@ class EntityWorkflowStep
public function __construct()
{
$this->destUser = new ArrayCollection();
$this->destUserByAccessKey = new ArrayCollection();
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(32));
}
public function addDestEmail(string $email): self
@@ -119,11 +132,45 @@ class EntityWorkflowStep
{
if (!$this->destUser->contains($user)) {
$this->destUser[] = $user;
$this->getEntityWorkflow()
->addSubscriberToFinal($user)
->addSubscriberToStep($user);
}
return $this;
}
public function addDestUserByAccessKey(User $user): self
{
if (!$this->destUserByAccessKey->contains($user) && !$this->destUser->contains($user)) {
$this->destUserByAccessKey[] = $user;
$this->getEntityWorkflow()
->addSubscriberToFinal($user)
->addSubscriberToStep($user);
}
return $this;
}
public function getAccessKey(): string
{
return $this->accessKey;
}
/**
* get all the users which are allowed to apply a transition: those added manually, and
* those added automatically bu using an access key.
*/
public function getAllDestUser(): Collection
{
return new ArrayCollection(
[
...$this->getDestUser()->toArray(),
...$this->getDestUserByAccessKey()->toArray(),
]
);
}
public function getComment(): string
{
return $this->comment;
@@ -140,13 +187,21 @@ class EntityWorkflowStep
}
/**
* @return ArrayCollection|Collection
* get dest users added by the creator.
*
* You should **not** rely on this method to get all users which are able to
* apply a transition on this step. Use @see{EntityWorkflowStep::getAllDestUser} instead.
*/
public function getDestUser()
public function getDestUser(): collection
{
return $this->destUser;
}
public function getDestUserByAccessKey(): Collection
{
return $this->destUserByAccessKey;
}
public function getEntityWorkflow(): ?EntityWorkflow
{
return $this->entityWorkflow;
@@ -197,6 +252,19 @@ class EntityWorkflowStep
return $this->freezeAfter;
}
public function isWaitingForTransition(): bool
{
if (null !== $this->transitionAfter) {
return false;
}
if ($this->isFinal()) {
return false;
}
return true;
}
public function removeDestEmail(string $email): self
{
$this->destEmail = array_filter($this->destEmail, static function (string $existing) use ($email) {
@@ -213,6 +281,13 @@ class EntityWorkflowStep
return $this;
}
public function removeDestUserByAccessKey(User $user): self
{
$this->destUserByAccessKey->removeElement($user);
return $this;
}
public function setComment(?string $comment): EntityWorkflowStep
{
$this->comment = (string) $comment;

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form;
use Chill\MainBundle\Entity\LocationType as EntityLocationType;
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
use Chill\MainBundle\Form\Type\PickAddressType;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
@@ -46,8 +47,8 @@ final class LocationFormType extends AbstractType
},
])
->add('name', TextType::class)
->add('phonenumber1', TextType::class, ['required' => false])
->add('phonenumber2', TextType::class, ['required' => false])
->add('phonenumber1', ChillPhoneNumberType::class, ['required' => false])
->add('phonenumber2', ChillPhoneNumberType::class, ['required' => false])
->add('email', TextType::class, ['required' => false])
->add('address', PickAddressType::class, [
'required' => false,

View File

@@ -0,0 +1,61 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Form\Type;
use libphonenumber\PhoneNumberFormat;
use libphonenumber\PhoneNumberUtil;
use Misd\PhoneNumberBundle\Form\Type\PhoneNumberType;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use function array_key_exists;
class ChillPhoneNumberType extends AbstractType
{
private string $defaultCarrierCode;
private PhoneNumberUtil $phoneNumberUtil;
public function __construct(ParameterBagInterface $parameterBag)
{
$this->defaultCarrierCode = $parameterBag->get('chill_main')['phone_helper']['default_carrier_code'];
$this->phoneNumberUtil = PhoneNumberUtil::getInstance();
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefault('default_region', $this->defaultCarrierCode)
->setDefault('format', PhoneNumberFormat::NATIONAL)
->setDefault('type', \libphonenumber\PhoneNumberType::FIXED_LINE_OR_MOBILE)
->setNormalizer('attr', function (Options $options, $value) {
if (array_key_exists('placeholder', $value)) {
return $value;
}
$examplePhoneNumber = $this->phoneNumberUtil->getExampleNumberForType($this->defaultCarrierCode, $options['type']);
return array_merge(
$value,
[
'placeholder' => PhoneNumberUtil::getInstance()->format($examplePhoneNumber, $options['format']),
]
);
});
}
public function getParent()
{
return PhoneNumberType::class;
}
}

View File

@@ -38,7 +38,7 @@ class ObjectToIdTransformer implements DataTransformerInterface
*/
public function reverseTransform($id)
{
if (!$id) {
if (null === $id) {
return null;
}
@@ -46,7 +46,7 @@ class ObjectToIdTransformer implements DataTransformerInterface
->getRepository($this->class)
->find($id);
if (!$object) {
if (null === $object) {
throw new TransformationFailedException();
}
@@ -62,7 +62,7 @@ class ObjectToIdTransformer implements DataTransformerInterface
*/
public function transform($object)
{
if (!$object) {
if (null === $object) {
return '';
}

View File

@@ -13,6 +13,7 @@ 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;
@@ -21,8 +22,14 @@ use LogicException;
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;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotNull;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\Transition;
use function array_key_exists;
@@ -146,6 +153,22 @@ class WorkflowStepType extends AbstractType
'label' => 'workflow.dest for next steps',
'multiple' => true,
'mapped' => false,
])
->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(['checkMX' => true]),
],
'label' => 'Email',
],
]);
}
@@ -175,6 +198,37 @@ class WorkflowStepType extends AbstractType
->setRequired('transition')
->setAllowedTypes('transition', 'bool')
->setRequired('entity_workflow')
->setAllowedTypes('entity_workflow', EntityWorkflow::class);
->setAllowedTypes('entity_workflow', EntityWorkflow::class)
->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();
$toFinal = true;
foreach ($transition->getTos() as $to) {
$meta = $workflow->getMetadataStore()->getPlaceMetadata($to);
if (
!array_key_exists('isFinal', $meta) || false === $meta['isFinal']
) {
$toFinal = false;
}
}
$destUsers = $form['future_dest_users']->getData();
$destEmails = $form['future_dest_emails']->getData();
if (!$toFinal && [] === $destUsers && [] === $destEmails) {
$context
->buildViolation('workflow.You must add at least one dest user or email')
->atPath('future_dest_users')
->addViolation();
}
}
),
]);
}
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Phonenumber;
use libphonenumber\PhoneNumber;
/**
* Helper to some task linked to phonenumber.
*
* Currently, only Twilio is supported (https://www.twilio.com/lookup). A method
* allow to check if the helper is configured for validation. This should be used
* before doing some validation.
*/
interface PhoneNumberHelperInterface
{
public function format(PhoneNumber $phoneNumber): string;
/**
* Get type (mobile, landline, ...) for phone number.
*/
public function getType(string $phonenumber): string;
/**
* Return true if the validation is configured and available.
*/
public function isPhonenumberValidationConfigured(): bool;
/**
* Return true if the phonenumber is a landline or voip phone. Return always true
* if the validation is not configured.
*/
public function isValidPhonenumberAny(string $phonenumber): bool;
/**
* Return true if the phonenumber is a landline or voip phone. Return always true
* if the validation is not configured.
*/
public function isValidPhonenumberLandOrVoip(string $phonenumber): bool;
/**
* REturn true if the phoennumber is a mobile phone. Return always true
* if the validation is not configured.
*/
public function isValidPhonenumberMobile(string $phonenumber): bool;
}

View File

@@ -15,8 +15,12 @@ use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\ServerException;
use libphonenumber\NumberParseException;
use libphonenumber\PhoneNumber;
use libphonenumber\PhoneNumberUtil;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use function array_key_exists;
use function in_array;
@@ -24,40 +28,32 @@ use function json_decode;
use function preg_replace;
use function strlen;
/**
* Helper to some task linked to phonenumber.
*
* Currently, only Twilio is supported (https://www.twilio.com/lookup). A method
* allow to check if the helper is configured for validation. This should be used
* before doing some validation.
*/
class PhonenumberHelper
final class PhonenumberHelper implements PhoneNumberHelperInterface
{
public const FORMAT_URI = 'https://lookups.twilio.com/v1/PhoneNumbers/%s';
public const LOOKUP_URI = 'https://lookups.twilio.com/v1/PhoneNumbers/%s';
protected CacheItemPoolInterface $cachePool;
private CacheItemPoolInterface $cachePool;
/**
* TRUE if the client is properly configured.
*/
protected bool $isConfigured = false;
private array $config;
protected LoggerInterface $logger;
private bool $isConfigured = false;
/**
* Twilio client.
*/
protected Client $twilioClient;
private LoggerInterface $logger;
private PhonenumberUtil $phoneNumberUtil;
private Client $twilioClient;
public function __construct(
CacheItemPoolInterface $cachePool,
$config,
CacheItemPoolInterface $cacheUserData,
ParameterBagInterface $parameterBag,
LoggerInterface $logger
) {
$this->logger = $logger;
$this->cachePool = $cachePool;
$this->cachePool = $cacheUserData;
$this->config = $config = $parameterBag->get('chill_main.phone_helper');
if (
array_key_exists('twilio_sid', $config)
@@ -72,11 +68,19 @@ class PhonenumberHelper
]);
$this->isConfigured = true;
}
$this->phoneNumberUtil = PhoneNumberUtil::getInstance();
}
public function format($phonenumber)
/**
* @param string $phoneNumber A national phone number starting with +
*
* @throws NumberParseException
*/
public function format(PhoneNumber $phoneNumber): string
{
return $this->performTwilioFormat($phonenumber);
return $this->phoneNumberUtil
->formatOutOfCountryCallingNumber($phoneNumber, $this->config['default_carrier_code']);
}
/**
@@ -137,7 +141,7 @@ class PhonenumberHelper
}
/**
* REturn true if the phoennumber is a mobile phone. Return always true
* REturn true if the phonenumber is a mobile phone. Return always true
* if the validation is not configured.
*
* @param string $phonenumber
@@ -157,68 +161,7 @@ class PhonenumberHelper
return 'mobile' === $validation;
}
protected function performTwilioFormat($phonenumber)
{
if (false === $this->isPhonenumberValidationConfigured()) {
return $phonenumber;
}
// filter only number
$filtered = preg_replace('/[^0-9]/', '', $phonenumber);
$item = $this->cachePool->getItem('pnum_format_nat_' . $filtered);
if ($item->isHit()) {
return $item->get();
}
try {
$response = $this->twilioClient->get(sprintf(self::FORMAT_URI, '+' . $filtered), [
'http_errors' => true,
]);
} catch (ClientException $e) {
$response = $e->getResponse();
$this->logger->error('[phonenumber helper] Could not format number '
. 'due to client error', [
'message' => $response->getBody()->getContents(),
'status_code' => $response->getStatusCode(),
'phonenumber' => $phonenumber,
]);
return $phonenumber;
} catch (ServerException $e) {
$response = $e->getResponse();
$this->logger->error('[phonenumber helper] Could not format number '
. 'due to server error', [
'message' => $response->getBody()->getContents(),
'status_code' => $response->getStatusCode(),
'phonenumber' => $phonenumber,
]);
return null;
} catch (ConnectException $e) {
$this->logger->error('[phonenumber helper] Could not format number '
. 'due to connect error', [
'message' => $e->getMessage(),
'phonenumber' => $phonenumber,
]);
return null;
}
$format = json_decode($response->getBody()->getContents())->national_format;
$item
->set($format)
// expires after 3d
->expiresAfter(3600 * 24 * 3);
$this->cachePool->save($item);
return $format;
}
protected function performTwilioLookup($phonenumber)
private function performTwilioLookup($phonenumber)
{
if (false === $this->isPhonenumberValidationConfigured()) {
return null;
@@ -230,7 +173,7 @@ class PhonenumberHelper
$item = $this->cachePool->getItem('pnum_' . $filtered);
if ($item->isHit()) {
//return $item->get();
return $item->get();
}
try {

View File

@@ -16,10 +16,7 @@ use Twig\TwigFilter;
class Templating extends AbstractExtension
{
/**
* @var PhonenumberHelper
*/
protected $phonenumberHelper;
protected PhonenumberHelper $phonenumberHelper;
public function __construct(PhonenumberHelper $phonenumberHelper)
{

View File

@@ -34,6 +34,20 @@ class EntityWorkflowRepository implements ObjectRepository
return (int) $qb->getQuery()->getSingleScalarResult();
}
public function countByPreviousDestWithoutReaction(User $user): int
{
$qb = $this->buildQueryByPreviousDestWithoutReaction($user)->select('count(ew)');
return (int) $qb->getQuery()->getSingleScalarResult();
}
public function countByPreviousTransitionned(User $user): int
{
$qb = $this->buildQueryByPreviousTransitionned($user)->select('count(ew)');
return (int) $qb->getQuery()->getSingleScalarResult();
}
public function countBySubscriber(User $user): int
{
$qb = $this->buildQueryBySubscriber($user)->select('count(ew)');
@@ -78,6 +92,32 @@ class EntityWorkflowRepository implements ObjectRepository
return $qb->getQuery()->getResult();
}
public function findByPreviousDestWithoutReaction(User $user, ?array $orderBy = null, $limit = null, $offset = null): array
{
$qb = $this->buildQueryByPreviousDestWithoutReaction($user)->select('ew');
foreach ($orderBy as $key => $sort) {
$qb->addOrderBy('ew.' . $key, $sort);
}
$qb->setMaxResults($limit)->setFirstResult($offset);
return $qb->getQuery()->getResult();
}
public function findByPreviousTransitionned(User $user, ?array $orderBy = null, $limit = null, $offset = null): array
{
$qb = $this->buildQueryByPreviousTransitionned($user)->select('ew')->distinct(true);
foreach ($orderBy as $key => $sort) {
$qb->addOrderBy('ew.' . $key, $sort);
}
$qb->setMaxResults($limit)->setFirstResult($offset);
return $qb->getQuery()->getResult();
}
public function findBySubscriber(User $user, ?array $orderBy = null, $limit = null, $offset = null): array
{
$qb = $this->buildQueryBySubscriber($user)->select('ew');
@@ -120,6 +160,41 @@ class EntityWorkflowRepository implements ObjectRepository
return $qb;
}
private function buildQueryByPreviousDestWithoutReaction(User $user): QueryBuilder
{
$qb = $this->repository->createQueryBuilder('ew');
$qb->join('ew.steps', 'step');
$qb->where(
$qb->expr()->andX(
$qb->expr()->isMemberOf(':user', 'step.destUser'),
$qb->expr()->neq('step.transitionBy', ':user'),
)
);
$qb->setParameter('user', $user);
return $qb;
}
private function buildQueryByPreviousTransitionned(User $user): QueryBuilder
{
$qb = $this->repository->createQueryBuilder('ew');
$qb->join('ew.steps', 'step');
$qb->where(
$qb->expr()->andX(
$qb->expr()->eq('step.transitionBy', ':user'),
)
);
$qb->setParameter('user', $user);
return $qb;
}
private function buildQueryBySubscriber(User $user): QueryBuilder
{
$qb = $this->repository->createQueryBuilder('ew');

View File

@@ -278,6 +278,7 @@ table.table-bordered {
}
/// meta-data
div.createdBy,
div.updatedBy,
div.metadata {
span.user, span.date {

View File

@@ -279,6 +279,7 @@ div.wrap-header {
li {
margin-right: 5px;
margin-bottom: 5px;
}
}
}
@@ -316,7 +317,7 @@ div.float-button {
div.action {
height: calc(100% - 0em);
shape-outside: inset(calc(100% - 4em) 0 0);
shape-outside: inset(calc(100% - 2em) 0 0);
display: flex;
align-items: flex-end;
padding: 0 0 0 1em;
@@ -328,6 +329,10 @@ div.float-button {
padding: 0;
}
}
.chill-entity.entity-address .address p {
display: unset;
}
}
&.debug {
padding: 1em;

View File

@@ -1,7 +1,7 @@
ul.record_actions {
display: flex;
flex-direction: row;
flex-wrap: wrap-reverse;
flex-wrap: wrap;
justify-content: flex-end;
padding: 0.5em 0;

View File

@@ -72,9 +72,9 @@ section.chill-entity {
}
}
p {
// display: inline-block;
margin: 0 0 0 1.5em;
text-indent: -1.5em;
display: inline-block;
//margin: 0 0 0 1.5em;
//text-indent: -1.5em;
&.street {
span.streetnumber {

View File

@@ -1,7 +1,7 @@
/**
* Generic api method that can be adapted to any fetch request
*/
const makeFetch = (method, url, body) => {
const makeFetch = (method, url, body) => {
return fetch(url, {
method: method,
headers: {
@@ -11,19 +11,20 @@
})
.then(response => {
if (response.ok) {
console.log('200 error')
return response.json();
}
if (response.status === 422) {
console.log('422 error')
return response.json().then(response => {
throw ValidationException(response)
});
}
if (response.status === 403) {
return response.json().then(() => {
throw AccessException();
});
console.log('403 error')
throw AccessException(response);
}
throw {
@@ -88,14 +89,13 @@ const ValidationException = (response) => {
error.violations = response.violations.map((violation) => `${violation.title}: ${violation.propertyPath}`);
error.titles = response.violations.map((violation) => violation.title);
error.propertyPaths = response.violations.map((violation) => violation.propertyPath);
return error;
}
const AccessException = () => {
const AccessException = (response) => {
const error = {};
error.name = 'AccessException';
error.violations = ['You are no longer permitted to perform this action'];
error.violations = ['You are not allowed to perform this action'];
return error;
}

View File

@@ -50,7 +50,7 @@ var handleAdd = function(button) {
empty_explain = collection.querySelector('li[data-collection-empty-explain]'),
entry = document.createElement('li'),
event = new CustomEvent('collection-add-entry', { detail: { collection: collection, entry: entry } }),
counter = collection.childNodes.length,
counter = collection.childNodes.length + parseInt(Math.random() * 1000000)
content
;
content = prototype.replace(new RegExp('__name__', 'g'), counter);

View File

@@ -1,11 +1,39 @@
.confidential{
.confidential {
display: flex;
position: relative;
}
.toggle-far-twig {
i {
bottom: 0px;
right: -30px;
}
}
.toggle-close-twig {
i {
bottom: 0px;
right: -5px;
}
}
.toggle{
margin-left: 30px;
margin-top: 5px;
cursor: pointer;
position: absolute;
z-index: 5;
right: -30px
}
.toggle-far {
bottom: 0px;
right: 20px !important;
}
.toggle-close {
bottom: 125px;
right: 15px !important;
}
.blur {
-webkit-filter: blur(5px);
-moz-filter: blur(5px);

View File

@@ -3,7 +3,7 @@ import {ShowHide} from 'ChillMainAssets/lib/show_hide/show_hide.js';
window.addEventListener('DOMContentLoaded', function() {
let
divTransitions = document.querySelector('#transitions'),
futureDestUsersContainer = document.querySelector('#futureDestUsers')
futureDestUsersContainer = document.querySelector('#futureDests')
;
if (null !== divTransitions) {
@@ -67,24 +67,4 @@ window.addEventListener('DOMContentLoaded', function() {
});
}
// validate form
let form = document.querySelector('form[name="workflow_step"]');
if (form === null) {
console.error('form to validate not found');
}
form.addEventListener('submit', function (event) {
const datas = new FormData(event.target);
if (datas.has('workflow_step[future_dest_users]')) {
const dests = JSON.parse(datas.get('workflow_step[future_dest_users]'));
if (dests === null || (dests instanceof Array && dests.length === 0)) {
event.preventDefault();
console.log('no users!');
window.alert('Indiquez un utilisateur pour traiter la prochaine étape.');
}
}
});
});

View File

@@ -1,11 +1,11 @@
<template>
<a v-if="isDisplayBadge" @click="openModal">
<span class="chill-entity" :class="badgeType">
{{ buttonText }}<span v-if="isDead"> ()</span>
</span>
</a>
<a v-else class="btn btn-sm" target="_blank"
<a v-if="isDisplayBadge" @click="openModal">
<span class="chill-entity" :class="badgeType">
{{ buttonText }}<span v-if="isDead"> ()</span>
</span>
</a>
<a v-else class="btn btn-sm" target="_blank"
:class="classAction"
:title="$t(titleAction)"
@click="openModal">
@@ -115,7 +115,6 @@ export default {
},
computed: {
hasResourceComment() {
//console.log('hasResourceComment', this.parent);
return (typeof this.parent !== 'undefined' && this.parent !== null)
&& this.action === 'show'
&& this.parent.type === 'accompanying_period_resource'

View File

@@ -1,10 +1,10 @@
<template>
<div :class="classes">
<div class="confidential-content" :class="{ 'blur': isBlurred }">
<div class="confidential">
<div :class="{ 'blur': isBlurred }">
<slot name="confidential-content"></slot>
</div>
<div>
<i class="fa fa-eye toggle" aria-hidden="true" @click="toggleBlur"></i>
<i class="fa fa-eye toggle" :class="positionBtn" aria-hidden="true" @click="toggleBlur"></i>
</div>
</div>
</template>
@@ -12,6 +12,7 @@
<script>
export default {
name: "Confidential",
props: ['positionBtnFar'],
data() {
return {
isBlurred: true,
@@ -19,9 +20,14 @@ export default {
},
methods : {
toggleBlur() {
console.log('toggle blur');
console.log(this.positionBtnFar);
this.isBlurred = !this.isBlurred;
},
},
computed: {
positionBtn() {
return this.positionBtnFar ? 'toggle-far' : 'toggle-close'
}
}
}
</script>
@@ -29,18 +35,5 @@ export default {
<style scoped lang='scss'>
.confidential{
align-items: center;
display: flex;
}
.toggle{
margin-top: 28px;
cursor: pointer;
display: block;
float: right;
margin-right: 20px;
}
.blur {
-webkit-filter: blur(5px);
-moz-filter: blur(5px);
filter: blur(5px);
}
</style>

View File

@@ -5,7 +5,7 @@
<component :is="component" class="address" :class="multiline">
<div v-if="isConfidential">
<confidential>
<confidential :positionBtnFar="true">
<template v-slot:confidential-content>
<div v-if="isMultiline === true">
<p v-for="(l, i) in address.lines" :key="`line-${i}`">
@@ -27,7 +27,6 @@
</p>
</div>
</template>
</confidential>
</div>
@@ -94,6 +93,7 @@ export default {
return this.isMultiline === true ? "multiline" : "";
},
isConfidential() {
console.log(this.address.confidential)
return this.address.confidential;
}
}

View File

@@ -5,7 +5,7 @@
<div>
<div class="item-row col">
<h2>Workflow</h2>
<h2>{{ w.title }}</h2>
<div class="flex-grow-1 ms-3 h3">
<div class="visually-hidden">
{{ w.relatedEntityClass }}

View File

@@ -12,6 +12,8 @@
:relatedEntityClass="this.relatedEntityClass"
:relatedEntityId="this.relatedEntityId"
:workflowsAvailables="workflowsAvailables"
:preventDefaultMoveToGenerate="this.$props.preventDefaultMoveToGenerate"
:goToGenerateWorkflowPayload="this.goToGenerateWorkflowPayload"
@go-to-generate-workflow="goToGenerateWorkflow"
></pick-workflow>
@@ -36,6 +38,7 @@
:relatedEntityId="this.relatedEntityId"
:workflowsAvailables="workflowsAvailables"
:preventDefaultMoveToGenerate="this.$props.preventDefaultMoveToGenerate"
:goToGenerateWorkflowPayload="this.goToGenerateWorkflowPayload"
@go-to-generate-workflow="this.goToGenerateWorkflow"
></pick-workflow>
</template>
@@ -83,6 +86,10 @@ export default {
required: false,
default: false,
},
goToGenerateWorkflowPayload: {
required: false,
default: {}
},
},
data() {
return {
@@ -105,7 +112,7 @@ export default {
this.modal.showModal = true;
},
goToGenerateWorkflow(data) {
console.log('go to generate workflow intercepted');
console.log('go to generate workflow intercepted', data);
this.$emit('goToGenerateWorkflow', data);
}
},

View File

@@ -37,6 +37,10 @@ export default {
required: false,
default: false,
},
goToGenerateWorkflowPayload: {
required: false,
default: {}
},
},
emits: ['goToGenerateWorkflow'],
methods: {
@@ -51,7 +55,7 @@ export default {
window.location.assign(this.makeLink(workflowName));
}
this.$emit('goToGenerateWorkflow', {event, workflowName, link: this.makeLink(workflowName)});
this.$emit('goToGenerateWorkflow', {event, workflowName, link: this.makeLink(workflowName), payload: this.goToGenerateWorkflowPayload});
}
}
}

View File

@@ -59,7 +59,7 @@
must be shown in such list
#}
{%- if render == 'list' -%}
<li class="chill-entity entity-address {% if address.confidential %}confidential{% endif %}">
<li class="chill-entity entity-address {% if address.confidential %} confidential toggle-far-twig {% endif %}">
{% if options['with_picto'] %}
<i class="fa fa-li fa-map-marker"></i>
{% endif %}
@@ -68,7 +68,7 @@
{%- endif -%}
{%- if render == 'inline' -%}
<span class="chill-entity entity-address {% if address.confidential %}confidential{% endif %}">
<span class="chill-entity entity-address {% if address.confidential %} confidential toggle-far-twig {% endif %}">
{% if options['with_picto'] %}
<i class="fa fa-fw fa-map-marker"></i>
{% endif %}
@@ -77,7 +77,7 @@
{%- endif -%}
{%- if render == 'bloc' -%}
<div class="chill-entity entity-address {% if address.confidential %}confidential{% endif %}">
<div class="chill-entity entity-address {% if address.confidential %} confidential toggle-close-twig {% endif %}">
{% if options['has_no_address'] == true and address.isNoAddress == true %}
{% if address.postCode is not empty %}
<div class="address{% if options['multiline'] %} multiline{% endif %}{% if options['with_delimiter'] %} delimiter{% endif %}">

View File

@@ -18,8 +18,10 @@
{% for entity in entities %}
<tr>
<td>{{ entity.name }}</td>
<td>{{ entity.phonenumber1 }}</td>
<td>{{ entity.phonenumber2 }}</td>
<td>
{{ entity.phonenumber1|chill_format_phonenumber }}
</td>
<td>{{ entity.phonenumber2|chill_format_phonenumber }}</td>
<td>{{ entity.email }}</td>
<td>
{% if entity.address is not null %}

View File

@@ -3,6 +3,8 @@
{% if transition_form is not null %}
{{ form_start(transition_form) }}
{{ form_errors(transition_form) }}
{% set step = entity_workflow.currentStepChained %}
{% set labels = workflow_metadata(entity_workflow, 'label', step.currentStep) %}
{% set label = labels is null ? step.currentStep : labels|localize_translatable_string %}
@@ -60,8 +62,10 @@
{{ form_row(transition_form.freezeAfter) }}
{% endif %}
<div id="futureDestUsers">
<div id="futureDests">
{{ form_row(transition_form.future_dest_users) }}
{{ form_row(transition_form.future_dest_emails) }}
</div>
<p>{{ form_label(transition_form.comment) }}</p>
@@ -82,13 +86,23 @@
<p>{{ 'workflow.This workflow is finalized'|trans }}</p>
{% else %}
<p>{{ 'workflow.You are not allowed to apply a transition on this workflow'|trans }}</p>
<p>{{ 'workflow.Only those users are allowed'|trans }}:</p>
{% if entity_workflow.currentStep.destUser|length > 0 %}
<p><b>{{ 'workflow.Only those users are allowed'|trans }}&nbsp;:</b></p>
<ul>
{% for u in entity_workflow.currentStep.destUser -%}
<li>{{ u|chill_entity_render_box }}</li>
{%- endfor %}
</ul>
{% endif %}
<ul>
{% for u in entity_workflow.currentStep.destUser -%}
<li>{{ u|chill_entity_render_box }}</li>
{%- endfor %}
</ul>
{% 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 }}&nbsp;:</b></p>
<ul>
{% for u in entity_workflow.currentStep.destUserByAccessKey %}
<li>{{ u|chill_entity_render_box }}</li>
{% endfor %}
</ul>
{% endif %}
{% endif %}
</div>

View File

@@ -7,7 +7,7 @@
<div data-list-workflows="1"
data-workflows="{{ entity_workflows_json|json_encode|e('html_attr') }}"
data-allow-create="{{ acl }}"
data-related-entity-class="{{ blank_workflow.relatedEntityClass }}"
data-related-entity-id="{{ blank_workflow.relatedEntityId }}"
data-workflows-availables="{{ workflows_availables|json_encode()|e('html_attr') }}"
data-related-entity-class="{{ relatedEntityClass }}"
data-related-entity-id="{{ relatedEntityId }}"
data-workflows-availables="{{ workflows_available|json_encode()|e('html_attr') }}"
></div>

View File

@@ -69,15 +69,26 @@
</blockquote>
</div>
{% endif %}
{% if loop.last and step.destUser|length > 0 %}
{% if loop.last and step.allDestUser|length > 0 %}
<div class="item-row separator">
<div>
<p><b>{{ 'workflow.Users allowed to apply transition'|trans }}&nbsp;: </b></p>
<ul>
{% for u in step.destUser %}
<li>{{ u|chill_entity_render_box }}</li>
{% endfor %}
</ul>
{% if step.destUser|length > 0 %}
<p><b>{{ 'workflow.Users allowed to apply transition'|trans }}&nbsp;: </b></p>
<ul>
{% for u in step.destUser %}
<li>{{ u|chill_entity_render_box }}</li>
{% endfor %}
</ul>
{% endif %}
{% if step.destUserByAccessKey|length > 0 %}
<p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }}&nbsp;:</b></p>
<ul>
{% for u in step.destUserByAccessKey %}
<li>{{ u|chill_entity_render_box }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
{% endif %}

View File

@@ -0,0 +1,29 @@
{% extends '@ChillMain/layout.html.twig' %}
{% block title 'workflow.Delete workflow ?'|trans %}
{% block display_content %}
<h2>
{{ handler.entityTitle(entityWorkflow) }}
</h2>
{% include handler.template(entityWorkflow) with handler.templateData(entityWorkflow)|merge({
'display_action': false
}) %}
{% endblock %}
{% block content %}
<div class="container chill-md-10">
{{ include('@ChillMain/Util/confirmation_template.html.twig',
{
'title' : 'workflow.Delete workflow ?'|trans,
'confirm_question' : 'workflow.Are you sure you want to delete this workflow ?'|trans,
'display_content' : block('display_content'),
'cancel_route' : 'chill_main_workflow_show',
'cancel_parameters' : {'id' : entityWorkflow.id},
'form' : delete_form
} ) }}
</div>
{% endblock %}

View File

@@ -21,10 +21,12 @@
{{ encore_entry_link_tags('mod_wopi_link') }}
{% endblock %}
{% import '@ChillMain/Workflow/macro_breadcrumb.html.twig' as macro %}
{% block content %}
<div class="col-10 workflow">
<h1 class="mb-5">{{ block('title') }}</h1>
{# handler_template:
- src/Bundle/ChillPersonBundle/Resources/views/Workflow/_evaluation.html.twig
- src/Bundle/ChillPersonBundle/Resources/views/Workflow/_accompanying_period_work.html.twig
@@ -32,11 +34,26 @@
#}
<section class="step my-4">
<div class="mb-5">
{% include handler_template_title with handler_template_data|merge({'breadcrumb': true }) %}
<h2>{{ handler.entityTitle(entity_workflow) }}</h2>
{{ macro.breadcrumb({'entity_workflow': entity_workflow}) }}
</div>
{% include handler_template with handler_template_data|merge({'display_action': true }) %}
{% if is_granted('CHILL_MAIN_WORKFLOW_DELETE', entity_workflow) %}
<ul class="record_actions">
<li>
<a class="btn btn-delete"
href="{{ chill_path_add_return_path('chill_main_workflow_delete', {'id': entity_workflow.id}) }}"
>
{{ 'workflow.Delete workflow'|trans }}
</a>
</li>
</ul>
{% endif %}
</section>
<section class="step my-4">{% include '@ChillMain/Workflow/_follow.html.twig' %}</section>
<section class="step my-4">{% include '@ChillMain/Workflow/_decision.html.twig' %}</section>{#
<section class="step my-4">{% include '@ChillMain/Workflow/_comment.html.twig' %}</section> #}

View File

@@ -8,9 +8,10 @@
{% block content %}
<div class="col-10 workflow">
<h1 class="mb-5">{{ block('title') }}</h1>
<ul class="nav nav-pills justify-content-center">
<li class="nav-item">
<a href="{{ path('chill_main_workflow_list_subscribed') }}"
@@ -24,8 +25,24 @@
{{ 'workflow.dest'|trans }}
</a>
</li>
<li class="nav-item">
<a href="{{ path('chill_main_workflow_list_previous_without_reaction') }}"
class="nav-link {% if step == 'previous_without_reaction' %}active{% endif %}">
{{ 'workflow.Previous dest without reaction'|trans }}
</a>
</li>
<li class="nav-item">
<a href="{{ path('chill_main_workflow_list_previous_transitionned') }}"
class="nav-link {% if step == 'previous_transitionned' %}active{% endif %}">
{{ 'workflow.Previous transitionned'|trans }}
</a>
</li>
</ul>
{% if help is defined %}
<p style="margin-top: 2rem;">{{ help|trans }}</p>
{% endif %}
{% if workflows|length == 0 %}
<p class="chill-no-data-statement">{{ 'workflow.No workflow'|trans }}</p>
{% else %}
@@ -36,26 +53,24 @@
<button type="button" class="accordion-button collapsed"
data-bs-toggle="collapse" data-bs-target="#flush-collapse-{{ l.entity_workflow.id }}"
aria-expanded="false" aria-controls="flush-collapse-{{ l.entity_workflow.id }}">
<div class="item-row col">
<h2>
{{ 'workflow_'|trans }}
{{ l.handler.entityTitle(l.entity_workflow) }}
</h2>
{% include l.handler.templateTitle(l.entity_workflow) with l.handler.templateTitleData(l.entity_workflow)|merge({
'description': true,
'add_classes': 'ms-3 h3'
}) %}
</div>
</button>
{{ macro.breadcrumb(l) }}
<div>
{{ macro.breadcrumb(l) }}
</div>
</div>
<div id="flush-collapse-{{ l.entity_workflow.id }}"
class="accordion-collapse collapse"
aria-labelledby="flush-heading-{{ l.entity_workflow.id }}"
data-bs-parent="#workflow-fold">
<div class="item-row flex-column">
{% include l.handler.template(l.entity_workflow) with l.handler.templateData(l.entity_workflow)|merge({
'display_action': false
@@ -78,6 +93,13 @@
</div>
<div class="item-col">
<ul class="record_actions">
{% if is_granted('CHILL_MAIN_WORKFLOW_DELETE', l.entity_workflow) %}
<li>
<a class="btn btn-delete"
href="{{ chill_path_add_return_path('chill_main_workflow_delete', {'id': l.entity_workflow.id}) }}"
></a>
</li>
{% endif %}
<li>
<a href="{{ path('chill_main_workflow_show', {'id': l.entity_workflow.id}) }}"
class="btn btn-show">
@@ -87,7 +109,7 @@
</ul>
</div>
</div>
</div>
</div>
{% endfor %}

View File

@@ -0,0 +1,15 @@
Madame, Monsieur,
Un suivi "{{ workflow.text }}" a atteint une nouvelle étape: {{ workflow.text }}.
Titre du workflow: "{{ entityTitle }}".
Vous êtes invité·e à valider cette étape. Pour obtenir un accès, vous pouvez cliquer sur le lien suivant:
{{ absolute_url(path('chill_main_workflow_grant_access_by_key', {'id': entity_workflow.currentStep.id, 'accessKey': entity_workflow.currentStep.accessKey})) }}
Dès que vous aurez cliqué une fois sur le lien, vous serez autorisé à valider cette étape.
Notez que vous devez disposer d'un compte utilisateur valide dans Chill.
Cordialement,

View File

@@ -0,0 +1 @@
Un suivi {{ workflow.text }} demande votre attention: {{ entityTitle }}

View File

@@ -11,8 +11,10 @@ declare(strict_types=1);
namespace Chill\MainBundle\Search\Utils;
use libphonenumber\PhoneNumberUtil;
use LogicException;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use function count;
use function implode;
use function preg_match;
@@ -24,6 +26,13 @@ class ExtractPhonenumberFromPattern
{
private const PATTERN = '([\\+]{0,1}[0-9\\ ]{5,})';
private string $defaultCarrierCode;
public function __construct(ParameterBagInterface $parameterBag)
{
$this->defaultCarrierCode = $parameterBag->get('chill_main')['phone_helper']['default_carrier_code'];
}
public function extractPhonenumber(string $subject): SearchExtractionResult
{
$matches = [];
@@ -35,11 +44,21 @@ class ExtractPhonenumberFromPattern
foreach (str_split(trim($matches[0])) as $key => $char) {
switch ($char) {
case '+':
if (0 === $key) {
$phonenumber[] = $char;
} else {
throw new LogicException('should not match not alnum character');
}
break;
case '0':
$length++;
if (0 === $key) {
$phonenumber[] = '+32';
$util = PhoneNumberUtil::getInstance();
$phonenumber[] = '+' . $util->getCountryCodeForRegion($this->defaultCarrierCode);
} else {
$phonenumber[] = $char;
}

View File

@@ -23,6 +23,8 @@ class EntityWorkflowVoter extends Voter
{
public const CREATE = 'CHILL_MAIN_WORKFLOW_CREATE';
public const DELETE = 'CHILL_MAIN_WORKFLOW_DELETE';
public const SEE = 'CHILL_MAIN_WORKFLOW_SEE';
private EntityWorkflowManager $manager;
@@ -40,7 +42,11 @@ class EntityWorkflowVoter extends Voter
return $subject instanceof EntityWorkflow && in_array($attribute, self::getRoles(), true);
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
/**
* @param EntityWorkflow $subject
* @param mixed $attribute
*/
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{
switch ($attribute) {
case self::CREATE:
@@ -55,6 +61,9 @@ class EntityWorkflowVoter extends Voter
return $this->security->isGranted($entityAttribute, $handler->getRelatedEntity($subject));
case self::DELETE:
return $subject->getStep() === 'initial';
default:
throw new UnexpectedValueException("attribute {$attribute} not supported");
}
@@ -65,6 +74,7 @@ class EntityWorkflowVoter extends Voter
return [
self::SEE,
self::CREATE,
self::DELETE,
];
}
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Serializer\Normalizer;
use libphonenumber\NumberParseException;
use libphonenumber\PhoneNumber;
use libphonenumber\PhoneNumberUtil;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class PhonenumberNormalizer implements NormalizerInterface, DenormalizerInterface
{
private string $defaultCarrierCode;
private PhoneNumberUtil $phoneNumberUtil;
public function __construct(ParameterBagInterface $parameterBag)
{
$this->defaultCarrierCode = $parameterBag->get('chill_main')['phone_helper']['default_carrier_code'];
$this->phoneNumberUtil = PhoneNumberUtil::getInstance();
}
/**
* @param mixed $data
* @param mixed $type
* @param null|mixed $format
*
* @throws UnexpectedValueException
*/
public function denormalize($data, $type, $format = null, array $context = [])
{
try {
return $this->phoneNumberUtil->parse($data, $this->defaultCarrierCode);
} catch (NumberParseException $e) {
throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
}
}
public function normalize($object, ?string $format = null, array $context = []): string
{
return $this->phoneNumberUtil->formatOutOfCountryCallingNumber($object, $this->defaultCarrierCode);
}
public function supportsDenormalization($data, $type, $format = null)
{
return 'libphonenumber\PhoneNumber' === $type;
}
public function supportsNormalization($data, ?string $format = null)
{
return $data instanceof PhoneNumber;
}
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Tests\Routing\Loader;
use Chill\MainBundle\Phonenumber\PhonenumberHelper;
use libphonenumber\PhoneNumberUtil;
use Psr\Log\NullLogger;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
/**
* @internal
* @coversNothing
*/
final class PhonenumberHelperTest extends KernelTestCase
{
public function formatPhonenumbers()
{
yield [
'BE',
'+3281136917',
'081 13 69 17',
];
yield [
'FR',
'+33 6 23 12 45 54',
'06 23 12 45 54',
];
yield [
'FR',
'+32 81 13 69 17',
'00 32 81 13 69 17',
];
yield [
'BE',
'+33 6 23 12 45 54',
'00 33 6 23 12 45 54',
];
}
/**
* @dataProvider formatPhonenumbers
*/
public function testFormatPhonenumbers(string $defaultCarrierCode, string $phoneNumber, string $expected)
{
$util = PhoneNumberUtil::getInstance();
$subject = new PhonenumberHelper(
new ArrayAdapter(),
new ParameterBag([
'chill_main.phone_helper' => [
'default_carrier_code' => $defaultCarrierCode,
],
]),
new NullLogger()
);
$this->assertEquals($expected, $subject->format($util->parse($phoneNumber)));
}
}

View File

@@ -13,6 +13,7 @@ namespace Search\Utils;
use Chill\MainBundle\Search\Utils\ExtractPhonenumberFromPattern;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
/**
* @internal
@@ -22,17 +23,25 @@ final class ExtractPhonenumberFromPatternTest extends KernelTestCase
{
public function provideData()
{
yield ['Diallo', 0, [], 'Diallo', 'no phonenumber'];
yield ['BE', 'Diallo', 0, [], 'Diallo', 'no phonenumber'];
yield ['Diallo 15/06/2021', 0, [], 'Diallo 15/06/2021', 'no phonenumber and a date'];
yield ['BE', 'Diallo 15/06/2021', 0, [], 'Diallo 15/06/2021', 'no phonenumber and a date'];
yield ['Diallo 0486 123 456', 1, ['+32486123456'], 'Diallo', 'a phonenumber and a name'];
yield ['BE', 'Diallo 0486 123 456', 1, ['+32486123456'], 'Diallo', 'a phonenumber and a name'];
yield ['Diallo 123 456', 1, ['123456'], 'Diallo', 'a number and a name, without leadiing 0'];
yield ['BE', 'Diallo 123 456', 1, ['123456'], 'Diallo', 'a number and a name, without leadiing 0'];
yield ['123 456', 1, ['123456'], '', 'only phonenumber'];
yield ['BE', '123 456', 1, ['123456'], '', 'only phonenumber'];
yield ['0123 456', 1, ['+32123456'], '', 'only phonenumber with a leading 0'];
yield ['BE', '0123 456', 1, ['+32123456'], '', 'only phonenumber with a leading 0'];
yield ['FR', '123 456', 1, ['123456'], '', 'only phonenumber'];
yield ['FR', '0123 456', 1, ['+33123456'], '', 'only phonenumber with a leading 0'];
yield ['FR', 'Diallo 0486 123 456', 1, ['+33486123456'], 'Diallo', 'a phonenumber and a name'];
yield ['FR', 'Diallo +32486 123 456', 1, ['+32486123456'], 'Diallo', 'a phonenumber and a name'];
}
/**
@@ -44,9 +53,11 @@ final class ExtractPhonenumberFromPatternTest extends KernelTestCase
* @param mixed $filteredSubject
* @param mixed $msg
*/
public function testExtract($subject, $expectedCount, $expected, $filteredSubject, $msg)
public function testExtract(string $defaultCarrierCode, $subject, $expectedCount, $expected, $filteredSubject, $msg)
{
$extractor = new ExtractPhonenumberFromPattern();
$extractor = new ExtractPhonenumberFromPattern(new ParameterBag(['chill_main' => [
'phone_helper' => ['default_carrier_code' => $defaultCarrierCode],
]]));
$result = $extractor->extractPhonenumber($subject);
$this->assertCount($expectedCount, $result->getFound());

View File

@@ -11,24 +11,21 @@ declare(strict_types=1);
namespace Chill\MainBundle\Validation\Validator;
use Chill\MainBundle\Phonenumber\PhonenumberHelper;
use Chill\MainBundle\Phonenumber\PhoneNumberHelperInterface;
use LogicException;
use Psr\Log\LoggerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class ValidPhonenumber extends ConstraintValidator
final class ValidPhonenumber extends ConstraintValidator
{
protected $logger;
private LoggerInterface $logger;
/**
* @var PhonenumberHelper
*/
protected $phonenumberHelper;
private PhoneNumberHelperInterface $phonenumberHelper;
public function __construct(
LoggerInterface $logger,
PhonenumberHelper $phonenumberHelper
PhoneNumberHelperInterface $phonenumberHelper
) {
$this->phonenumberHelper = $phonenumberHelper;
$this->logger = $logger;
@@ -46,7 +43,7 @@ class ValidPhonenumber extends ConstraintValidator
return;
}
if (empty($value)) {
if ('' === $value) {
return;
}

View File

@@ -33,10 +33,6 @@ interface EntityWorkflowHandlerInterface
public function getTemplateData(EntityWorkflow $entityWorkflow, array $options = []): array;
public function getTemplateTitle(EntityWorkflow $entityWorkflow, array $options = []): string;
public function getTemplateTitleData(EntityWorkflow $entityWorkflow, array $options = []): array;
public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool;
public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool;

View File

@@ -72,7 +72,7 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac
return;
}
if (!$entityWorkflow->getCurrentStep()->getDestUser()->contains($this->security->getUser())) {
if (!$entityWorkflow->getCurrentStep()->getAllDestUser()->contains($this->security->getUser())) {
if (!$event->getMarking()->has('initial')) {
$event->addTransitionBlocker(new TransitionBlocker(
'workflow.You are not allowed to apply a transition on this workflow. Only those users are allowed: %users%',
@@ -80,7 +80,7 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac
[
'%users%' => implode(
', ',
$entityWorkflow->getCurrentStep()->getDestUser()->map(function (User $u) {
$entityWorkflow->getCurrentStep()->getAllDestUser()->map(function (User $u) {
return $this->userRender->renderString($u, []);
})->toArray()
),

View File

@@ -52,11 +52,11 @@ class NotificationOnTransition implements EventSubscriberInterface
public static function getSubscribedEvents(): array
{
return [
'workflow.completed' => 'onCompleted',
'workflow.completed' => 'onCompletedSendNotification',
];
}
public function onCompleted(Event $event): void
public function onCompletedSendNotification(Event $event): void
{
if (!$event->getSubject() instanceof EntityWorkflow) {
return;

View File

@@ -0,0 +1,71 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Workflow\EventSubscriber;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\Workflow\Registry;
class SendAccessKeyEventSubscriber
{
private EngineInterface $engine;
private EntityWorkflowManager $entityWorkflowManager;
private MailerInterface $mailer;
private MetadataExtractor $metadataExtractor;
private Registry $registry;
public function __construct(EngineInterface $engine, MetadataExtractor $metadataExtractor, Registry $registry, EntityWorkflowManager $entityWorkflowManager, MailerInterface $mailer)
{
$this->engine = $engine;
$this->metadataExtractor = $metadataExtractor;
$this->registry = $registry;
$this->entityWorkflowManager = $entityWorkflowManager;
$this->mailer = $mailer;
}
public function postPersist(EntityWorkflowStep $step): void
{
$entityWorkflow = $step->getEntityWorkflow();
$place = $this->metadataExtractor->buildArrayPresentationForPlace($entityWorkflow);
$workflow = $this->metadataExtractor->buildArrayPresentationForWorkflow(
$this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName())
);
$handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
foreach ($entityWorkflow->futureDestEmails as $emailAddress) {
$context = [
'entity_workflow' => $entityWorkflow,
'dest' => $emailAddress,
'place' => $place,
'workflow' => $workflow,
'entityTitle' => $handler->getEntityTitle($entityWorkflow),
];
$email = new Email();
$email
->addTo($emailAddress)
->subject($this->engine->render('@ChillMain/Workflow/workflow_send_access_key_title.fr.txt.twig', $context))
->text($this->engine->render('@ChillMain/Workflow/workflow_send_access_key.fr.txt.twig', $context));
$this->mailer->send($email);
}
}
}

View File

@@ -61,7 +61,38 @@ class WorkflowTwigExtensionRuntime implements RuntimeExtensionInterface
return null;
}
public function listWorkflows(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $options = []): string
/**
* @param array{relatedEntityClass: string, relatedEntityId: int} $supplementaryRelated
*
* @throws \Symfony\Component\Serializer\Exception\ExceptionInterface
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function listWorkflows(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $options = [], array $supplementaryRelated = []): string
{
[$blankEntityWorkflow, $workflowsAvailable, $entityWorkflows] = $this->getWorkflowsForRelated($relatedEntityClass, $relatedEntityId);
foreach ($supplementaryRelated as $supplementary) {
[$supplementaryBlankEntityWorkflow, $supplementaryWorkflowsAvailable, $supplementaryEntityWorkflows]
= $this->getWorkflowsForRelated($supplementary['relatedEntityClass'], $supplementary['relatedEntityId']);
$entityWorkflows = array_merge($entityWorkflows, $supplementaryEntityWorkflows);
}
return $environment->render('@ChillMain/Workflow/_extension_list_workflow_for.html.twig', [
'entity_workflows_json' => $this->normalizer->normalize($entityWorkflows, 'json', ['groups' => 'read']),
'blank_workflow' => $blankEntityWorkflow,
'workflows_available' => $workflowsAvailable,
'relatedEntityClass' => $relatedEntityClass,
'relatedEntityId' => $relatedEntityId,
]);
}
/**
* @return array where keys are: {0: blankWorkflow, 1: entityWorkflows, 2: workflowList}
*/
private function getWorkflowsForRelated(string $relatedEntityClass, int $relatedEntityId): array
{
$blankEntityWorkflow = new EntityWorkflow();
$blankEntityWorkflow
@@ -70,25 +101,13 @@ class WorkflowTwigExtensionRuntime implements RuntimeExtensionInterface
$workflowsList = $this->metadataExtractor->availableWorkflowFor($relatedEntityClass, $relatedEntityId);
// get the related entity already created
$entityWorkflows = [];
foreach ($entityWorkflowsNaked = $this->repository->findBy(
['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId]
) as $entityWorkflow) {
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$entityWorkflows[] = [
'entity_workflow' => $entityWorkflow,
'workflow' => $this->metadataExtractor->buildArrayPresentationForWorkflow($workflow),
'handler' => $this->entityWorkflowManager->getHandler($entityWorkflow),
return
[
$blankEntityWorkflow,
$workflowsList,
$this->repository->findBy(
['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId]
),
];
}
return $environment->render('@ChillMain/Workflow/_extension_list_workflow_for.html.twig', [
'entity_workflows_json' => $this->normalizer->normalize($entityWorkflowsNaked, 'json', ['groups' => 'read']),
'entity_workflows' => $entityWorkflows,
'blank_workflow' => $blankEntityWorkflow,
'workflows_availables' => $workflowsList,
]);
}
}

View File

@@ -26,7 +26,8 @@
],
"require": {
"league/csv": "^9.6",
"phpoffice/phpspreadsheet": "~1.2"
"phpoffice/phpspreadsheet": "~1.2",
"odolbeau/phone-number-bundle": "^3.6"
},
"require-dev": {
},

View File

@@ -38,6 +38,17 @@ services:
arguments:
$handlers: !tagged_iterator chill_main.workflow_handler
Chill\MainBundle\Workflow\EventSubscriber\SendAccessKeyEventSubscriber:
autoconfigure: true
autowire: true
tags:
-
name: 'doctrine.orm.entity_listener'
event: 'postPersist'
entity: 'Chill\MainBundle\Entity\Workflow\EntityWorkflowStep'
# set the 'lazy' option to TRUE to only instantiate listeners when they are used
lazy: true
# other stuffes
chill.main.helper.translatable_string:
@@ -85,3 +96,4 @@ services:
- "@security.token_storage"
Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface: '@Chill\MainBundle\Security\Resolver\CenterResolverDispatcher'

View File

@@ -3,19 +3,12 @@ services:
autowire: true
autoconfigure: true
Chill\MainBundle\Phonenumber\PhonenumberHelper:
arguments:
$config: '%chill_main.phone_helper%'
$cachePool: '@cache.user_data'
Chill\MainBundle\Phonenumber\PhoneNumberHelperInterface: '@Chill\MainBundle\Phonenumber\PhonenumberHelper'
Chill\MainBundle\Phonenumber\Templating:
arguments:
$phonenumberHelper: '@Chill\MainBundle\Phonenumber\PhonenumberHelper'
tags:
- { name: twig.extension }
Chill\MainBundle\Phonenumber\PhonenumberHelper: ~
Chill\MainBundle\Phonenumber\Templating: ~
Chill\MainBundle\Validation\Validator\ValidPhonenumber:
arguments:
$phonenumberHelper: '@Chill\MainBundle\Phonenumber\PhonenumberHelper'
tags:
- { name: validator.constraint_validator }

View File

@@ -0,0 +1,51 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20220223171457 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_workflow_entity_step DROP accessKey');
$this->addSql('DROP TABLE chill_main_workflow_entity_step_user_by_accesskey');
}
public function getDescription(): string
{
return 'Add access key to EntityWorkflowStep';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_workflow_entity_step ADD accessKey TEXT DEFAULT NULL');
$this->addSql('WITH randoms AS (select
cmwes.id,
string_agg(substr(characters, (random() * length(characters) + 0.5)::integer, 1), \'\') as random_word
from (values(\'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\')) as symbols(characters)
-- length of word
join generate_series(1, 32) on 1 = 1
JOIN chill_main_workflow_entity_step cmwes ON true
GROUP BY cmwes.id)
UPDATE chill_main_workflow_entity_step SET accessKey = randoms.random_word FROM randoms WHERE chill_main_workflow_entity_step.id = randoms.id');
$this->addSql('ALTER TABLE chill_main_workflow_entity_step ALTER accessKey DROP DEFAULT ');
$this->addSql('ALTER TABLE chill_main_workflow_entity_step ALTER accessKey SET NOT NULL');
$this->addSql('CREATE TABLE chill_main_workflow_entity_step_user_by_accesskey (entityworkflowstep_id INT NOT NULL, user_id INT NOT NULL, PRIMARY KEY(entityworkflowstep_id, user_id))');
$this->addSql('CREATE INDEX IDX_8296D8397E6AF9D4 ON chill_main_workflow_entity_step_user_by_accesskey (entityworkflowstep_id)');
$this->addSql('CREATE INDEX IDX_8296D839A76ED395 ON chill_main_workflow_entity_step_user_by_accesskey (user_id)');
$this->addSql('ALTER TABLE chill_main_workflow_entity_step_user_by_accesskey ADD CONSTRAINT FK_8296D8397E6AF9D4 FOREIGN KEY (entityworkflowstep_id) REFERENCES chill_main_workflow_entity_step (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_main_workflow_entity_step_user_by_accesskey ADD CONSTRAINT FK_8296D839A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
}
}

View File

@@ -0,0 +1,79 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
use libphonenumber\PhoneNumberUtil;
use RuntimeException;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
final class Version20220302132728 extends AbstractMigration implements ContainerAwareInterface
{
use ContainerAwareTrait;
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_location ALTER phonenumber1 TYPE VARCHAR(64)');
$this->addSql('ALTER TABLE chill_main_location ALTER phonenumber1 DROP DEFAULT');
$this->addSql('ALTER TABLE chill_main_location ALTER phonenumber2 TYPE VARCHAR(64)');
$this->addSql('ALTER TABLE chill_main_location ALTER phonenumber2 DROP DEFAULT');
$this->addSql('COMMENT ON COLUMN chill_main_location.phonenumber1 IS NULL');
$this->addSql('COMMENT ON COLUMN chill_main_location.phonenumber2 IS NULL');
}
public function getDescription(): string
{
return 'Upgrade phonenumber on location';
}
public function up(Schema $schema): void
{
$carrier_code = $this->container
->getParameter('chill_main')['phone_helper']['default_carrier_code'];
if (null === $carrier_code) {
throw new RuntimeException('no carrier code');
}
$this->addSql('ALTER TABLE chill_main_location ALTER phonenumber1 TYPE VARCHAR(35)');
$this->addSql('ALTER TABLE chill_main_location ALTER phonenumber1 DROP DEFAULT');
$this->addSql('ALTER TABLE chill_main_location ALTER phonenumber1 TYPE VARCHAR(35)');
$this->addSql('ALTER TABLE chill_main_location ALTER phonenumber2 TYPE VARCHAR(35)');
$this->addSql('ALTER TABLE chill_main_location ALTER phonenumber2 DROP DEFAULT');
$this->addSql('ALTER TABLE chill_main_location ALTER phonenumber2 TYPE VARCHAR(35)');
$this->addSql('COMMENT ON COLUMN chill_main_location.phonenumber1 IS \'(DC2Type:phone_number)\'');
$this->addSql('COMMENT ON COLUMN chill_main_location.phonenumber2 IS \'(DC2Type:phone_number)\'');
$this->addSql(
'UPDATE chill_main_location SET ' .
$this->buildMigrationPhonenumberClause($carrier_code, 'phonenumber1') .
', ' .
$this->buildMigrationPhoneNumberClause($carrier_code, 'phonenumber2')
);
}
private function buildMigrationPhoneNumberClause(string $defaultCarriercode, string $field): string
{
$util = PhoneNumberUtil::getInstance();
$countryCode = $util->getCountryCodeForRegion($defaultCarriercode);
return sprintf('%s=CASE
WHEN %s = \'\' THEN NULL
WHEN LEFT(%s, 1) = \'0\'
THEN \'+%s\' || replace(replace(substr(%s, 2), \'(0)\', \'\'), \' \', \'\')
ELSE replace(replace(%s, \'(0)\', \'\'),\' \', \'\')
END', $field, $field, $field, $countryCode, $field, $field);
}
}

View File

@@ -397,6 +397,23 @@ workflow:
Current step: Étape actuelle
Comment on last change: Commentaire à la transition précédente
Users allowed to apply transition: Utilisateurs pouvant valider cette étape
Workflow deleted with success: Le workflow a été supprimé
Delete workflow ?: Supprimer le workflow ?
Are you sure you want to delete this workflow ?: Êtes-vous sûr·e de vouloir supprimer ce workflow ?
Delete workflow: Supprimer le workflow
Steps is not waiting for transition. Maybe someone apply the transition before you ?: L'étape que vous cherchez a déjà été modifiée par un autre utilisateur. Peut-être quelqu'un a-t-il modifié cette étape avant vous ?
You get access to this step: Vous avez acquis les droits pour appliquer une transition sur ce workflow.
Those users are also granted to apply a transition by using an access key: Ces utilisateurs peuvent également valider cette étape, grâce à un lien d'accès
dest by email: Liens d'autorisation par email
dest by email help: Les adresses email mentionnées ici recevront un lien d'accès. Ce lien d'accès permettra à l'utilisateur de valider cette étape.
Add an email: Ajouter une adresse email
Remove an email: Enlever cette adresse email
Any email: Aucune adresse email
Previous dest without reaction: Workflows clotûrés après action d'un autre utilisateur
Previous workflow without reaction help: Liste des workflows où vous avez été cité comme pouvant réagir à une étape, mais où un autre utilisateur a exécuté une action avant vous.
Previous transitionned: Anciens workflows
Previous workflow transitionned help: Workflows où vous avez exécuté une action.
Subscribe final: Recevoir une notification à l'étape finale
Subscribe all steps: Recevoir une notification à chaque étape

View File

@@ -28,3 +28,6 @@ notification:
At least one addressee: Indiquez au moins un destinataire
Title must be defined: Un titre doit être indiqué
Comment content might not be blank: Le commentaire ne peut pas être vide
workflow:
You must add at least one dest user or email: Indiquez au moins un destinataire ou une adresse email