From fb51e44e45a8bf5395e58b49419416dee3308665 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 11 Jan 2023 17:16:37 +0100 Subject: [PATCH 001/106] FEATURE [absence][in_progress] add absence property to user, create form, controller, template, migration and menu entry --- .../Controller/AbsenceController.php | 42 +++++++++++++++++++ src/Bundle/ChillMainBundle/Entity/User.php | 23 +++++++++- .../ChillMainBundle/Form/AbsenceType.php | 36 ++++++++++++++++ .../Resources/views/Menu/absence.html.twig | 10 +++++ .../Routing/MenuBuilder/UserMenuBuilder.php | 9 ++++ src/Bundle/ChillMainBundle/config/routes.yaml | 4 ++ .../migrations/Version20230111160610.php | 36 ++++++++++++++++ 7 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 src/Bundle/ChillMainBundle/Controller/AbsenceController.php create mode 100644 src/Bundle/ChillMainBundle/Form/AbsenceType.php create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Menu/absence.html.twig create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20230111160610.php diff --git a/src/Bundle/ChillMainBundle/Controller/AbsenceController.php b/src/Bundle/ChillMainBundle/Controller/AbsenceController.php new file mode 100644 index 000000000..97ca2979c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/AbsenceController.php @@ -0,0 +1,42 @@ +getUser(); + $form = $this->createForm(AbsenceType::class, $user); + $form->add('submit', SubmitType::class, ['label' => 'Create']); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $em = $this->getDoctrine()->getManager(); + $em->persist($user); + $em->flush(); + + return $this->redirect($this->generateUrl('chill_main_homepage')); + } + + return $this->render('@ChillMain/Menu/absence.html.twig', [ + 'user' => $user, + 'form' => $form->createView(), + ]); + } +} diff --git a/src/Bundle/ChillMainBundle/Entity/User.php b/src/Bundle/ChillMainBundle/Entity/User.php index 80a0b5b2a..10620dc0f 100644 --- a/src/Bundle/ChillMainBundle/Entity/User.php +++ b/src/Bundle/ChillMainBundle/Entity/User.php @@ -11,14 +11,15 @@ declare(strict_types=1); namespace Chill\MainBundle\Entity; +use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use RuntimeException; use Symfony\Component\Security\Core\User\AdvancedUserInterface; use Symfony\Component\Serializer\Annotation as Serializer; -use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Context\ExecutionContextInterface; use function in_array; /** @@ -40,6 +41,11 @@ class User implements AdvancedUserInterface */ protected ?int $id = null; + /** + * @ORM\Column(type="datetime", nullable=true) + */ + private ?DateTimeImmutable $absenceStart = null; + /** * Array where SAML attributes's data are stored. * @@ -175,6 +181,11 @@ class User implements AdvancedUserInterface { } + public function getAbsenceStart(): ?DateTimeImmutable + { + return $this->absenceStart; + } + /** * Get attributes. * @@ -295,6 +306,11 @@ class User implements AdvancedUserInterface return $this->usernameCanonical; } + public function isAbsent(): bool + { + return null !== $this->getAbsenceStart() ? true : false; + } + /** * @return bool */ @@ -359,6 +375,11 @@ class User implements AdvancedUserInterface } } + public function setAbsenceStart(?DateTimeImmutable $absenceStart): void + { + $this->absenceStart = $absenceStart; + } + public function setAttributeByDomain(string $domain, string $key, $value): self { $this->attributes[$domain][$key] = $value; diff --git a/src/Bundle/ChillMainBundle/Form/AbsenceType.php b/src/Bundle/ChillMainBundle/Form/AbsenceType.php new file mode 100644 index 000000000..1f43093e5 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Form/AbsenceType.php @@ -0,0 +1,36 @@ +add('absenceStart', ChillDateTimeType::class, [ + 'required' => true, + ]); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => User::class, + ]); + } +} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Menu/absence.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Menu/absence.html.twig new file mode 100644 index 000000000..37b4bdd2f --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Menu/absence.html.twig @@ -0,0 +1,10 @@ +{% if user.absenceStart is not null %} + +

Votre absence est indiqué à partier de {{ user.absenceStart|format_datetime() }}

+ +{% endif %} + +{{ form_start(form) }} +{{ form_row(form.absenceStart) }} +{{ form_row(form.submit, { 'attr' : { 'class' : 'btn btn-chill-green' } } ) }} +{{ form_end(form) }} diff --git a/src/Bundle/ChillMainBundle/Routing/MenuBuilder/UserMenuBuilder.php b/src/Bundle/ChillMainBundle/Routing/MenuBuilder/UserMenuBuilder.php index e0e8a782f..e91aa040b 100644 --- a/src/Bundle/ChillMainBundle/Routing/MenuBuilder/UserMenuBuilder.php +++ b/src/Bundle/ChillMainBundle/Routing/MenuBuilder/UserMenuBuilder.php @@ -78,6 +78,15 @@ class UserMenuBuilder implements LocalMenuBuilderInterface $nbNotifications = $this->notificationByUserCounter->countUnreadByUser($user); + //TODO add an icon? How exactly? For example a clock icon... + $menu + ->addChild($this->translator->trans('absence.Set absence date'), [ + 'route' => 'chill_absence_user', + ]) + ->setExtras([ + 'order' => -8888888, + ]); + $menu ->addChild( $this->translator->trans('notification.My notifications with counter', ['nb' => $nbNotifications]), diff --git a/src/Bundle/ChillMainBundle/config/routes.yaml b/src/Bundle/ChillMainBundle/config/routes.yaml index d25f2aaff..d1fde6f5c 100644 --- a/src/Bundle/ChillMainBundle/config/routes.yaml +++ b/src/Bundle/ChillMainBundle/config/routes.yaml @@ -94,3 +94,7 @@ login_check: logout: path: /logout + +chill_absence_user: + path: /{_locale}/absence + controller: Chill\MainBundle\Controller\AbsenceController::setAbsence diff --git a/src/Bundle/ChillMainBundle/migrations/Version20230111160610.php b/src/Bundle/ChillMainBundle/migrations/Version20230111160610.php new file mode 100644 index 000000000..1c5f74897 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20230111160610.php @@ -0,0 +1,36 @@ +addSql('ALTER TABLE users DROP absenceStart'); + } + + public function getDescription(): string + { + return 'Add absence property to user'; + } + + public function up(Schema $schema): void + { + $this->addSql('ALTER TABLE users ADD absenceStart TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL'); + } +} From b2924ede704863778e7d79543999c666b7c6114c Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 12 Jan 2023 11:10:41 +0100 Subject: [PATCH 002/106] FEAUTURE [routing][absence] use routing annotation instead of config file --- .../ChillMainBundle/Controller/AbsenceController.php | 10 ++++++++-- src/Bundle/ChillMainBundle/config/routes.yaml | 4 ---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Controller/AbsenceController.php b/src/Bundle/ChillMainBundle/Controller/AbsenceController.php index 97ca2979c..7241e15be 100644 --- a/src/Bundle/ChillMainBundle/Controller/AbsenceController.php +++ b/src/Bundle/ChillMainBundle/Controller/AbsenceController.php @@ -13,16 +13,22 @@ namespace Chill\MainBundle\Controller; use Chill\MainBundle\Form\AbsenceType; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Annotation\Route; class AbsenceController extends AbstractController { + /** + * @Route( + * "/{_locale}/absence", + * name="chill_absence_user", + * methods={"GET", "POST"} + * ) + */ public function setAbsence(Request $request) { $user = $this->getUser(); $form = $this->createForm(AbsenceType::class, $user); - $form->add('submit', SubmitType::class, ['label' => 'Create']); $form->handleRequest($request); diff --git a/src/Bundle/ChillMainBundle/config/routes.yaml b/src/Bundle/ChillMainBundle/config/routes.yaml index d1fde6f5c..d25f2aaff 100644 --- a/src/Bundle/ChillMainBundle/config/routes.yaml +++ b/src/Bundle/ChillMainBundle/config/routes.yaml @@ -94,7 +94,3 @@ login_check: logout: path: /logout - -chill_absence_user: - path: /{_locale}/absence - controller: Chill\MainBundle\Controller\AbsenceController::setAbsence From b93b78615bd4866439a33a9bce7e79931eee9ee9 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 12 Jan 2023 11:11:43 +0100 Subject: [PATCH 003/106] FIX [migration][absence] fix the typing in db for absence datetime immuatable --- src/Bundle/ChillMainBundle/Entity/User.php | 2 +- src/Bundle/ChillMainBundle/Form/AbsenceType.php | 1 + src/Bundle/ChillMainBundle/migrations/Version20230111160610.php | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Bundle/ChillMainBundle/Entity/User.php b/src/Bundle/ChillMainBundle/Entity/User.php index 10620dc0f..c38dd97c6 100644 --- a/src/Bundle/ChillMainBundle/Entity/User.php +++ b/src/Bundle/ChillMainBundle/Entity/User.php @@ -42,7 +42,7 @@ class User implements AdvancedUserInterface protected ?int $id = null; /** - * @ORM\Column(type="datetime", nullable=true) + * @ORM\Column(type="datetime_immutable", nullable=true) */ private ?DateTimeImmutable $absenceStart = null; diff --git a/src/Bundle/ChillMainBundle/Form/AbsenceType.php b/src/Bundle/ChillMainBundle/Form/AbsenceType.php index 1f43093e5..8600fd019 100644 --- a/src/Bundle/ChillMainBundle/Form/AbsenceType.php +++ b/src/Bundle/ChillMainBundle/Form/AbsenceType.php @@ -24,6 +24,7 @@ class AbsenceType extends AbstractType $builder ->add('absenceStart', ChillDateTimeType::class, [ 'required' => true, + 'input' => 'datetime_immutable', ]); } diff --git a/src/Bundle/ChillMainBundle/migrations/Version20230111160610.php b/src/Bundle/ChillMainBundle/migrations/Version20230111160610.php index 1c5f74897..3bdffa6ea 100644 --- a/src/Bundle/ChillMainBundle/migrations/Version20230111160610.php +++ b/src/Bundle/ChillMainBundle/migrations/Version20230111160610.php @@ -22,6 +22,7 @@ final class Version20230111160610 extends AbstractMigration public function down(Schema $schema): void { $this->addSql('ALTER TABLE users DROP absenceStart'); + $this->addSql('COMMENT ON COLUMN users.absenceStart IS \'(DC2Type:datetime_immutable)\''); } public function getDescription(): string From 68998c9156ec6764681b56d8e679c12d539ca07f Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 12 Jan 2023 11:50:06 +0100 Subject: [PATCH 004/106] FEATURE [translations][absence] add translations --- src/Bundle/ChillMainBundle/Form/AbsenceType.php | 9 +++++++++ src/Bundle/ChillMainBundle/config/services/form.yaml | 4 ++++ src/Bundle/ChillMainBundle/translations/messages.fr.yml | 7 +++++++ 3 files changed, 20 insertions(+) diff --git a/src/Bundle/ChillMainBundle/Form/AbsenceType.php b/src/Bundle/ChillMainBundle/Form/AbsenceType.php index 8600fd019..e5e92398e 100644 --- a/src/Bundle/ChillMainBundle/Form/AbsenceType.php +++ b/src/Bundle/ChillMainBundle/Form/AbsenceType.php @@ -16,15 +16,24 @@ use Chill\MainBundle\Form\Type\ChillDateTimeType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Contracts\Translation\TranslatorInterface; class AbsenceType extends AbstractType { + private TranslatorInterface $translator; + + public function __construct(TranslatorInterface $translator) + { + $this->translator = $translator; + } + public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('absenceStart', ChillDateTimeType::class, [ 'required' => true, 'input' => 'datetime_immutable', + 'label' => $this->translator->trans('absence.Absence start'), ]); } diff --git a/src/Bundle/ChillMainBundle/config/services/form.yaml b/src/Bundle/ChillMainBundle/config/services/form.yaml index 58f01883f..c8a46132f 100644 --- a/src/Bundle/ChillMainBundle/config/services/form.yaml +++ b/src/Bundle/ChillMainBundle/config/services/form.yaml @@ -138,6 +138,10 @@ services: autowire: true autoconfigure: true + Chill\MainBundle\Form\AbsenceType: + autowire: true + autoconfigure: true + Chill\MainBundle\Form\DataTransformer\IdToLocationDataTransformer: ~ Chill\MainBundle\Form\DataTransformer\IdToUserDataTransformer: ~ Chill\MainBundle\Form\DataTransformer\IdToUsersDataTransformer: ~ diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index b19a7a52f..b80171c8c 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -573,3 +573,10 @@ saved_export: Export is deleted: L'export est supprimé Saved export is saved!: L'export est enregistré Created on %date%: Créé le %date% + + +absence: + My absence: Mon absence + Unset absence: Supprimer la date d'absence + Set absence date: Indiquer date d'absence + Absence start: Absence à partir de From 44ef21f940b03e43d1b3d030d474d97d25511546 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 12 Jan 2023 11:51:42 +0100 Subject: [PATCH 005/106] FEATURE [delete][absence] add functionality to unset absence --- .../Controller/AbsenceController.php | 25 +++++++++- .../Resources/views/Menu/absence.html.twig | 50 ++++++++++++++++--- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Controller/AbsenceController.php b/src/Bundle/ChillMainBundle/Controller/AbsenceController.php index 7241e15be..9c3dc679d 100644 --- a/src/Bundle/ChillMainBundle/Controller/AbsenceController.php +++ b/src/Bundle/ChillMainBundle/Controller/AbsenceController.php @@ -37,7 +37,7 @@ class AbsenceController extends AbstractController $em->persist($user); $em->flush(); - return $this->redirect($this->generateUrl('chill_main_homepage')); + return $this->redirect($this->generateUrl('chill_absence_user')); } return $this->render('@ChillMain/Menu/absence.html.twig', [ @@ -45,4 +45,27 @@ class AbsenceController extends AbstractController 'form' => $form->createView(), ]); } + + /** + * @Route( + * "/{_locale}/absence/unset", + * name="chill_unset_absence_user", + * methods={"GET", "POST", "DELETE"} + * ) + */ + public function unsetAbsence(Request $request) + { + $user = $this->getUser(); + $form = $this->createForm(AbsenceType::class, $user); + + $user->setAbsenceStart(null); + $em = $this->getDoctrine()->getManager(); + $em->persist($user); + $em->flush(); + + return $this->render('@ChillMain/Menu/absence.html.twig', [ + 'user' => $user, + 'form' => $form->createView(), + ]); + } } diff --git a/src/Bundle/ChillMainBundle/Resources/views/Menu/absence.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Menu/absence.html.twig index 37b4bdd2f..8b27773e4 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Menu/absence.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Menu/absence.html.twig @@ -1,10 +1,46 @@ -{% if user.absenceStart is not null %} +{% extends '@ChillMain/Admin/layout.html.twig' %} -

Votre absence est indiqué à partier de {{ user.absenceStart|format_datetime() }}

+{% block title %} + {{ 'My absence'|trans }} +{% endblock title %} -{% endif %} +{% block content %} -{{ form_start(form) }} -{{ form_row(form.absenceStart) }} -{{ form_row(form.submit, { 'attr' : { 'class' : 'btn btn-chill-green' } } ) }} -{{ form_end(form) }} +
+

{{ 'absence.My absence'|trans }}

+ + {% if user.absenceStart is not null %} +
+

Votre absence est indiqué à partier de {{ user.absenceStart|format_datetime() }}.

+ +
+ {% else %} +
+

Aucune absence indiquer.

+
+ {% endif %} + +
+ {{ form_start(form) }} + {{ form_row(form.absenceStart) }} + +
    +
  • + +
  • +
+ + {{ form_end(form) }} +
+
+ + + +{% endblock %} From 2c5c815f68e7691b2903399768e4d4def74ee51b Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 12 Jan 2023 12:02:52 +0100 Subject: [PATCH 006/106] FEATURE [admin][absence] add possibility for admin to set absence of a user --- src/Bundle/ChillMainBundle/Form/UserType.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Bundle/ChillMainBundle/Form/UserType.php b/src/Bundle/ChillMainBundle/Form/UserType.php index b738cc8c7..532bc7b5b 100644 --- a/src/Bundle/ChillMainBundle/Form/UserType.php +++ b/src/Bundle/ChillMainBundle/Form/UserType.php @@ -15,6 +15,7 @@ use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\UserJob; +use Chill\MainBundle\Form\Type\ChillDateTimeType; use Chill\MainBundle\Form\Type\PickCivilityType; use Chill\MainBundle\Templating\TranslatableStringHelper; use Doctrine\ORM\EntityRepository; @@ -31,6 +32,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\Regex; +use Symfony\Contracts\Translation\TranslatorInterface; class UserType extends AbstractType { @@ -38,12 +40,16 @@ class UserType extends AbstractType private TranslatableStringHelper $translatableStringHelper; + private TranslatorInterface $translator; + public function __construct( TranslatableStringHelper $translatableStringHelper, - ParameterBagInterface $parameterBag + ParameterBagInterface $parameterBag, + TranslatorInterface $translator ) { $this->translatableStringHelper = $translatableStringHelper; $this->parameterBag = $parameterBag; + $this->translator = $translator; } public function buildForm(FormBuilderInterface $builder, array $options) @@ -110,6 +116,11 @@ class UserType extends AbstractType return $qb; }, + ]) + ->add('absenceStart', ChillDateTimeType::class, [ + 'required' => false, + 'input' => 'datetime_immutable', + 'label' => $this->translator->trans('absence.Absence start'), ]); // @phpstan-ignore-next-line From 5bbe5af12488944d587790273e6b8d09ecb5058a Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 12 Jan 2023 13:30:28 +0100 Subject: [PATCH 007/106] FEATURE [absence][render] add absence tag to renderbox and renderstring --- .../Resources/views/Entity/user.html.twig | 5 +++++ .../Templating/Entity/UserRender.php | 16 ++++++++++++++-- .../ChillMainBundle/translations/messages.fr.yml | 1 + 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Resources/views/Entity/user.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Entity/user.html.twig index 77dc959a2..12a1a618c 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Entity/user.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Entity/user.html.twig @@ -6,4 +6,9 @@ {%- if opts['main_scope'] and user.mainScope is not null %} ({{ user.mainScope.name|localize_translatable_string }}) {%- endif -%} + {%- if opts['absence'] and user.absenceStart is not null %} + {%- if date(user.absenceStart) < date() %} + ({{ 'absence.Absent'|trans }}) + {%- endif %} + {%- endif -%} diff --git a/src/Bundle/ChillMainBundle/Templating/Entity/UserRender.php b/src/Bundle/ChillMainBundle/Templating/Entity/UserRender.php index 141de2649..2cef950d5 100644 --- a/src/Bundle/ChillMainBundle/Templating/Entity/UserRender.php +++ b/src/Bundle/ChillMainBundle/Templating/Entity/UserRender.php @@ -13,8 +13,10 @@ namespace Chill\MainBundle\Templating\Entity; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Templating\TranslatableStringHelper; -use Symfony\Component\Templating\EngineInterface; +use DateTimeImmutable; +use Symfony\Component\Templating\EngineInterface; +use Symfony\Contracts\Translation\TranslatorInterface; use function array_merge; class UserRender implements ChillEntityRenderInterface @@ -22,16 +24,20 @@ class UserRender implements ChillEntityRenderInterface public const DEFAULT_OPTIONS = [ 'main_scope' => true, 'user_job' => true, + 'absence' => true, ]; private EngineInterface $engine; private TranslatableStringHelper $translatableStringHelper; - public function __construct(TranslatableStringHelper $translatableStringHelper, EngineInterface $engine) + private TranslatorInterface $translator; + + public function __construct(TranslatableStringHelper $translatableStringHelper, EngineInterface $engine, TranslatorInterface $translator) { $this->translatableStringHelper = $translatableStringHelper; $this->engine = $engine; + $this->translator = $translator; } public function renderBox($entity, array $options): string @@ -63,6 +69,12 @@ class UserRender implements ChillEntityRenderInterface ->localize($entity->getMainScope()->getName()) . ')'; } + $current_date = new DateTimeImmutable(); + + if (null !== $entity->getAbsenceStart() && $entity->getAbsenceStart() < $current_date && $opts['main_scope']) { + $str .= ' (' . $this->translator->trans('absence.Absent') . ')'; + } + return $str; } diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index b80171c8c..c7d376c48 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -580,3 +580,4 @@ absence: Unset absence: Supprimer la date d'absence Set absence date: Indiquer date d'absence Absence start: Absence à partir de + Absent: Absent From 6c1108b8aa020c7464a767ead127260788f22f4f Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 12 Jan 2023 13:50:29 +0100 Subject: [PATCH 008/106] FEATURE: [homepage][absence] display message to let user know they are still marked as absent --- .../Resources/views/Homepage/index.html.twig | 11 ++++++++--- .../ChillMainBundle/translations/messages.fr.yml | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Resources/views/Homepage/index.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Homepage/index.html.twig index aec38f3c3..5dc88faf3 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Homepage/index.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Homepage/index.html.twig @@ -1,15 +1,20 @@
- + {# vue component #} + {% if app.user.absenceStart is not null and date(app.user.absenceStart) < date() %} +

{{'absence.You are marked as being absent'|trans }}

+ {% endif %} +
- + {% include '@ChillMain/Homepage/fast_actions.html.twig' %}
+ {% block css %} {{ encore_entry_link_tags('page_homepage_widget') }} {% endblock %} {% block js %} {{ encore_entry_script_tags('page_homepage_widget') }} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index c7d376c48..cffb0d6f9 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -581,3 +581,4 @@ absence: Set absence date: Indiquer date d'absence Absence start: Absence à partir de Absent: Absent + You are marked as being absent: Vous êtes marquer comme absent. From de9d53936f805501c57ec8e6380f4aee946306a3 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 12 Jan 2023 19:51:43 +0100 Subject: [PATCH 009/106] FEATURE [styling][absence] give styling --- .../Resources/public/chill/scss/render_box.scss | 2 +- .../ChillMainBundle/Resources/views/Entity/user.html.twig | 6 +++++- .../Resources/views/Homepage/index.html.twig | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/render_box.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/render_box.scss index 4d0bfcf8e..92becc089 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/render_box.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/render_box.scss @@ -106,7 +106,7 @@ section.chill-entity { // used for comment-embeddable &.entity-comment-embeddable { width: 100%; - + /* already defined !! div.metadata { font-size: smaller; diff --git a/src/Bundle/ChillMainBundle/Resources/views/Entity/user.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Entity/user.html.twig index 12a1a618c..779be6740 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Entity/user.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Entity/user.html.twig @@ -8,7 +8,11 @@ {%- endif -%} {%- if opts['absence'] and user.absenceStart is not null %} {%- if date(user.absenceStart) < date() %} - ({{ 'absence.Absent'|trans }}) + + + A + + {%- endif %} {%- endif -%} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Homepage/index.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Homepage/index.html.twig index 5dc88faf3..60f6c88ff 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Homepage/index.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Homepage/index.html.twig @@ -2,7 +2,7 @@ {# vue component #} {% if app.user.absenceStart is not null and date(app.user.absenceStart) < date() %} -

{{'absence.You are marked as being absent'|trans }}

+ {% endif %}
From 7f9e045d5db27f0b627f1d8bef1433c073d63a28 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 26 Jan 2023 11:28:13 +0100 Subject: [PATCH 010/106] FEATURE [regroupment][exports] first commit to implement regroupment entity in exports --- .../ChillMainBundle/Controller/ExportController.php | 2 ++ .../Form/Type/Export/PickRegroupmentType.php | 10 ++++++++++ 2 files changed, 12 insertions(+) create mode 100644 src/Bundle/ChillMainBundle/Form/Type/Export/PickRegroupmentType.php diff --git a/src/Bundle/ChillMainBundle/Controller/ExportController.php b/src/Bundle/ChillMainBundle/Controller/ExportController.php index 84bf80c6b..caeec8327 100644 --- a/src/Bundle/ChillMainBundle/Controller/ExportController.php +++ b/src/Bundle/ChillMainBundle/Controller/ExportController.php @@ -298,6 +298,8 @@ class ExportController extends AbstractController 'csrf_protection' => $isGenerate ? false : true, ]); + // TODO: add a condition to be able to select a regroupment of centers? + if ('centers' === $step || 'generate_centers' === $step) { $builder->add('centers', PickCenterType::class, [ 'export_alias' => $alias, diff --git a/src/Bundle/ChillMainBundle/Form/Type/Export/PickRegroupmentType.php b/src/Bundle/ChillMainBundle/Form/Type/Export/PickRegroupmentType.php new file mode 100644 index 000000000..e60388fd3 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Form/Type/Export/PickRegroupmentType.php @@ -0,0 +1,10 @@ + Date: Thu, 26 Jan 2023 17:11:50 +0100 Subject: [PATCH 011/106] Fixed: [migration] fix the required comment for doctrine on new column --- src/Bundle/ChillMainBundle/migrations/Version20230111160610.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillMainBundle/migrations/Version20230111160610.php b/src/Bundle/ChillMainBundle/migrations/Version20230111160610.php index 3bdffa6ea..c00a3f521 100644 --- a/src/Bundle/ChillMainBundle/migrations/Version20230111160610.php +++ b/src/Bundle/ChillMainBundle/migrations/Version20230111160610.php @@ -22,7 +22,6 @@ final class Version20230111160610 extends AbstractMigration public function down(Schema $schema): void { $this->addSql('ALTER TABLE users DROP absenceStart'); - $this->addSql('COMMENT ON COLUMN users.absenceStart IS \'(DC2Type:datetime_immutable)\''); } public function getDescription(): string @@ -33,5 +32,6 @@ final class Version20230111160610 extends AbstractMigration public function up(Schema $schema): void { $this->addSql('ALTER TABLE users ADD absenceStart TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL'); + $this->addSql('COMMENT ON COLUMN users.absenceStart IS \'(DC2Type:datetime_immutable)\''); } } From 86b5f4dfac0a5bc6ac530dac68e00db2c0d58c7f Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Fri, 27 Jan 2023 10:44:25 +0100 Subject: [PATCH 012/106] FIX [template][translations] fixes to template and translations --- .../Resources/views/Menu/absence.html.twig | 8 ++++---- src/Bundle/ChillMainBundle/translations/messages.fr.yml | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Resources/views/Menu/absence.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Menu/absence.html.twig index 8b27773e4..d8508d8fd 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Menu/absence.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Menu/absence.html.twig @@ -1,7 +1,7 @@ {% extends '@ChillMain/Admin/layout.html.twig' %} {% block title %} - {{ 'My absence'|trans }} + {{ 'absence.My absence'|trans }} {% endblock title %} {% block content %} @@ -11,7 +11,7 @@ {% if user.absenceStart is not null %}
-

Votre absence est indiqué à partier de {{ user.absenceStart|format_datetime() }}.

+

{{ 'absence.You are listed as absent, as of'|trans }} {{ user.absenceStart|format_date('long') }}

  • {% else %}
    -

    Aucune absence indiquer.

    +

    {{ 'absence.No absence listed'|trans }}

    {% endif %} @@ -32,7 +32,7 @@
    diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index 8218bf827..94041b78d 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -589,6 +589,8 @@ absence: My absence: Mon absence Unset absence: Supprimer la date d'absence Set absence date: Indiquer date d'absence - Absence start: Absence à partir de + Absence start: Absence à partir du Absent: Absent - You are marked as being absent: Vous êtes marquer comme absent. + You are marked as being absent: Vous êtes indiqué absent. + You are listed as absent, as of: Votre absence est indiqué à partir du + No absence listed: Aucune absence indiquée. From f76c031ff311eea32ec79942640cb3ec35333b37 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Fri, 27 Jan 2023 10:51:59 +0100 Subject: [PATCH 013/106] FIX [translations][types] remove redundant translator and change datetime field to date field --- src/Bundle/ChillMainBundle/Form/AbsenceType.php | 12 +++--------- src/Bundle/ChillMainBundle/Form/UserType.php | 11 ++++------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Form/AbsenceType.php b/src/Bundle/ChillMainBundle/Form/AbsenceType.php index e5e92398e..e23e350c9 100644 --- a/src/Bundle/ChillMainBundle/Form/AbsenceType.php +++ b/src/Bundle/ChillMainBundle/Form/AbsenceType.php @@ -13,6 +13,7 @@ namespace Chill\MainBundle\Form; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Form\Type\ChillDateTimeType; +use Chill\MainBundle\Form\Type\ChillDateType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -20,20 +21,13 @@ use Symfony\Contracts\Translation\TranslatorInterface; class AbsenceType extends AbstractType { - private TranslatorInterface $translator; - - public function __construct(TranslatorInterface $translator) - { - $this->translator = $translator; - } - public function buildForm(FormBuilderInterface $builder, array $options) { $builder - ->add('absenceStart', ChillDateTimeType::class, [ + ->add('absenceStart', ChillDateType::class, [ 'required' => true, 'input' => 'datetime_immutable', - 'label' => $this->translator->trans('absence.Absence start'), + 'label' => 'absence.Absence start', ]); } diff --git a/src/Bundle/ChillMainBundle/Form/UserType.php b/src/Bundle/ChillMainBundle/Form/UserType.php index 532bc7b5b..f37c48a92 100644 --- a/src/Bundle/ChillMainBundle/Form/UserType.php +++ b/src/Bundle/ChillMainBundle/Form/UserType.php @@ -16,6 +16,7 @@ use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Form\Type\ChillDateTimeType; +use Chill\MainBundle\Form\Type\ChillDateType; use Chill\MainBundle\Form\Type\PickCivilityType; use Chill\MainBundle\Templating\TranslatableStringHelper; use Doctrine\ORM\EntityRepository; @@ -40,16 +41,12 @@ class UserType extends AbstractType private TranslatableStringHelper $translatableStringHelper; - private TranslatorInterface $translator; - public function __construct( TranslatableStringHelper $translatableStringHelper, - ParameterBagInterface $parameterBag, - TranslatorInterface $translator + ParameterBagInterface $parameterBag ) { $this->translatableStringHelper = $translatableStringHelper; $this->parameterBag = $parameterBag; - $this->translator = $translator; } public function buildForm(FormBuilderInterface $builder, array $options) @@ -117,10 +114,10 @@ class UserType extends AbstractType return $qb; }, ]) - ->add('absenceStart', ChillDateTimeType::class, [ + ->add('absenceStart', ChillDateType::class, [ 'required' => false, 'input' => 'datetime_immutable', - 'label' => $this->translator->trans('absence.Absence start'), + 'label' => 'absence.Absence start', ]); // @phpstan-ignore-next-line From bb7d072cc8af6b83574c10de87f6dfe3a7b7e80f Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Fri, 27 Jan 2023 11:10:17 +0100 Subject: [PATCH 014/106] FIX [render] use isAbsent method to render user as absent or not + place homepage msg above searchbar --- src/Bundle/ChillMainBundle/Entity/User.php | 2 +- .../ChillMainBundle/Resources/views/Entity/user.html.twig | 2 +- .../ChillMainBundle/Resources/views/Homepage/index.html.twig | 5 ----- src/Bundle/ChillMainBundle/Resources/views/layout.html.twig | 5 +++++ src/Bundle/ChillMainBundle/Templating/Entity/UserRender.php | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Entity/User.php b/src/Bundle/ChillMainBundle/Entity/User.php index 43a63c52d..24e88e7ba 100644 --- a/src/Bundle/ChillMainBundle/Entity/User.php +++ b/src/Bundle/ChillMainBundle/Entity/User.php @@ -304,7 +304,7 @@ class User implements UserInterface public function isAbsent(): bool { - return null !== $this->getAbsenceStart() ? true : false; + return (null !== $this->getAbsenceStart() && $this->getAbsenceStart() <= new DateTimeImmutable('now')) ? true : false; } /** diff --git a/src/Bundle/ChillMainBundle/Resources/views/Entity/user.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Entity/user.html.twig index 779be6740..6d6e095d5 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Entity/user.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Entity/user.html.twig @@ -6,7 +6,7 @@ {%- if opts['main_scope'] and user.mainScope is not null %} ({{ user.mainScope.name|localize_translatable_string }}) {%- endif -%} - {%- if opts['absence'] and user.absenceStart is not null %} + {%- if opts['absence'] and user.isAbsent %} {%- if date(user.absenceStart) < date() %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Homepage/index.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Homepage/index.html.twig index 60f6c88ff..98af21171 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Homepage/index.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Homepage/index.html.twig @@ -1,10 +1,5 @@
    - {# vue component #} - {% if app.user.absenceStart is not null and date(app.user.absenceStart) < date() %} - - {% endif %} -
    {% include '@ChillMain/Homepage/fast_actions.html.twig' %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig b/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig index 319adad81..9b4007266 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig @@ -69,6 +69,11 @@ {% block content %}
    -{% endblock %} \ No newline at end of file +{% endblock %} From c5fc6d4aadaaa7570ee33e3ca2f2928c38a36cb8 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Fri, 24 Feb 2023 17:25:28 +0100 Subject: [PATCH 043/106] [vue][suggested users] add suggested users in vue component and write create/remove logic --- .../public/module/pick-entity/index.js | 5 + .../public/vuejs/PickEntity/PickEntity.vue | 105 ++++++++++-------- 2 files changed, 64 insertions(+), 46 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Resources/public/module/pick-entity/index.js b/src/Bundle/ChillMainBundle/Resources/public/module/pick-entity/index.js index 890929c35..bb9cc744c 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/module/pick-entity/index.js +++ b/src/Bundle/ChillMainBundle/Resources/public/module/pick-entity/index.js @@ -23,8 +23,11 @@ function loadDynamicPicker(element) { (input.value === '[]' || input.value === '') ? null : [ JSON.parse(input.value) ] ) + suggested = null !== JSON.parse(el.dataset.suggested) ? JSON.parse(el.dataset.suggested) : null ; + console.log('suggested', suggested) + if (!isMultiple) { if (input.value === '[]'){ input.value = null; @@ -37,6 +40,7 @@ function loadDynamicPicker(element) { ':types="types" ' + ':picked="picked" ' + ':uniqid="uniqid" ' + + ':suggested="suggested" ' + '@addNewEntity="addNewEntity" ' + '@removeEntity="removeEntity">', components: { @@ -48,6 +52,7 @@ function loadDynamicPicker(element) { types: JSON.parse(el.dataset.types), picked: picked === null ? [] : picked, uniqid: el.dataset.uniqid, + suggested: suggested } }, methods: { diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/PickEntity.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/PickEntity.vue index e121ca950..02b53a6d3 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/PickEntity.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/PickEntity.vue @@ -17,6 +17,9 @@
+
    +
  • {{ s.text }}
  • +
From a16244a3f59f1bf9a12c7e823d3bd728d314755e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 28 Feb 2023 15:25:47 +0000 Subject: [PATCH 044/106] Feature: [docgen] generate documents in an async queue The documents are now generated in a queue, using symfony messenger. This queue should be configured: ```yaml # app/config/messenger.yaml framework: messenger: # reset services after consuming messages # reset_on_message: true failure_transport: failed transports: # https://symfony.com/doc/current/messenger.html#transport-configuration async: '%env(MESSENGER_TRANSPORT_DSN)%' priority: dsn: '%env(MESSENGER_TRANSPORT_DSN)%' failed: 'doctrine://default?queue_name=failed' routing: # ... other messages 'Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage': priority ``` `StoredObject`s now have additionnal properties: * status (pending, failure, ready (by default) ), which explain if the document is generated; * a generationTrialCounter, which is incremented on each generation trial, which prevent each generation more than 5 times; The generator computation is moved from the `DocGenTemplateController` to a `Generator` (implementing `GeneratorInterface`. There are new methods to `Context` which allow to normalize/denormalize context data to/from a messenger's `Message`. --- .../Service/DocGenerator/ActivityContext.php | 31 ++++ ...tActivitiesByAccompanyingPeriodContext.php | 10 ++ .../Service/DocGenerator/CalendarContext.php | 10 ++ .../DocGenerator/CalendarContextInterface.php | 4 + ...eneratorContextWithPublicFormInterface.php | 13 ++ .../DocGeneratorTemplateController.php | 144 ++++++---------- .../Email/on_generation_failed_email.txt.twig | 16 ++ .../Service/Generator/Generator.php | 73 ++++---- .../Service/Generator/GeneratorException.php | 8 + .../Service/Generator/GeneratorInterface.php | 27 +++ .../Service/Messenger/OnGenerationFails.php | 156 ++++++++++++++++++ .../Messenger/RequestGenerationHandler.php | 66 ++++++++ .../Messenger/RequestGenerationMessage.php | 26 ++- .../config/services.yaml | 16 +- .../migrations/Version20230214192558.php | 47 ++++++ .../Context/Generator/GeneratorTest.php | 19 ++- .../translations/messages.fr.yml | 12 +- .../Controller/StoredObjectApiController.php | 39 +++++ .../Entity/StoredObject.php | 62 ++++++- .../document_action_buttons_group/index.ts | 20 ++- .../Resources/public/types.ts | 12 +- .../vuejs/DocumentActionButtonsGroup.vue | 66 +++++++- .../vuejs/StoredObjectButton/helpers.ts | 14 ++ .../delete.html.twig | 5 - .../Resources/views/List/list_item.html.twig | 17 +- .../migrations/Version20230227161327.php | 26 +++ .../Phonenumber/PhonenumberHelper.php | 10 +- .../components/FormEvaluation.vue | 5 + .../vuejs/AccompanyingCourseWorkEdit/store.js | 17 +- .../AccompanyingPeriodContext.php | 30 ++++ .../AccompanyingPeriodWorkContext.php | 21 ++- ...ccompanyingPeriodWorkEvaluationContext.php | 12 ++ .../Service/DocGenerator/PersonContext.php | 38 +++++ .../DocGenerator/PersonContextInterface.php | 4 + .../PersonContextWithThirdParty.php | 27 ++- .../ChillWopiBundle/chill.webpack.config.js | 5 +- .../ChillWopiBundle/src/Controller/Editor.php | 28 ++++ .../Resources/public/module/pending/index.ts | 40 +++++ .../Editor/stored_object_failure.html.twig | 15 ++ .../Editor/stored_object_pending.html.twig | 36 ++++ 40 files changed, 1050 insertions(+), 177 deletions(-) create mode 100644 src/Bundle/ChillDocGeneratorBundle/Resources/views/Email/on_generation_failed_email.txt.twig create mode 100644 src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorInterface.php create mode 100644 src/Bundle/ChillDocGeneratorBundle/Service/Messenger/OnGenerationFails.php create mode 100644 src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationHandler.php create mode 100644 src/Bundle/ChillDocGeneratorBundle/migrations/Version20230214192558.php create mode 100644 src/Bundle/ChillDocStoreBundle/Controller/StoredObjectApiController.php create mode 100644 src/Bundle/ChillDocStoreBundle/migrations/Version20230227161327.php create mode 100644 src/Bundle/ChillWopiBundle/src/Resources/public/module/pending/index.ts create mode 100644 src/Bundle/ChillWopiBundle/src/Resources/views/Editor/stored_object_failure.html.twig create mode 100644 src/Bundle/ChillWopiBundle/src/Resources/views/Editor/stored_object_pending.html.twig diff --git a/src/Bundle/ChillActivityBundle/Service/DocGenerator/ActivityContext.php b/src/Bundle/ChillActivityBundle/Service/DocGenerator/ActivityContext.php index 4e2970138..ac832b34b 100644 --- a/src/Bundle/ChillActivityBundle/Service/DocGenerator/ActivityContext.php +++ b/src/Bundle/ChillActivityBundle/Service/DocGenerator/ActivityContext.php @@ -22,6 +22,7 @@ use Chill\DocStoreBundle\Repository\DocumentCategoryRepository; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; +use Chill\PersonBundle\Repository\PersonRepository; use Chill\PersonBundle\Templating\Entity\PersonRenderInterface; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bridge\Doctrine\Form\Type\EntityType; @@ -45,6 +46,8 @@ class ActivityContext implements private PersonRenderInterface $personRender; + private PersonRepository $personRepository; + private TranslatableStringHelperInterface $translatableStringHelper; private TranslatorInterface $translator; @@ -55,6 +58,7 @@ class ActivityContext implements TranslatableStringHelperInterface $translatableStringHelper, EntityManagerInterface $em, PersonRenderInterface $personRender, + PersonRepository $personRepository, TranslatorInterface $translator, BaseContextData $baseContextData ) { @@ -63,6 +67,7 @@ class ActivityContext implements $this->translatableStringHelper = $translatableStringHelper; $this->em = $em; $this->personRender = $personRender; + $this->personRepository = $personRepository; $this->translator = $translator; $this->baseContextData = $baseContextData; } @@ -206,6 +211,32 @@ class ActivityContext implements return $options['mainPerson'] || $options['person1'] || $options['person2']; } + public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array + { + $normalized = []; + + foreach (['mainPerson', 'person1', 'person2'] as $k) { + $normalized[$k] = null === $data[$k] ? null : $data[$k]->getId(); + } + + return $normalized; + } + + public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array + { + $denormalized = []; + + foreach (['mainPerson', 'person1', 'person2'] as $k) { + if (null !== ($id = ($data[$k] ?? null))) { + $denormalized[$k] = $this->personRepository->find($id); + } else { + $denormalized[$k] = null; + } + } + + return $denormalized; + } + /** * @param Activity $entity */ diff --git a/src/Bundle/ChillActivityBundle/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContext.php b/src/Bundle/ChillActivityBundle/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContext.php index 3189307f9..7e1873710 100644 --- a/src/Bundle/ChillActivityBundle/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContext.php +++ b/src/Bundle/ChillActivityBundle/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContext.php @@ -146,6 +146,16 @@ class ListActivitiesByAccompanyingPeriodContext implements return $this->accompanyingPeriodContext->hasPublicForm($template, $entity); } + public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array + { + return $this->accompanyingPeriodContext->contextGenerationDataNormalize($template, $entity, $data); + } + + public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array + { + return $this->accompanyingPeriodContext->contextGenerationDataDenormalize($template, $entity, $data); + } + public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void { $this->accompanyingPeriodContext->storeGenerated($template, $storedObject, $entity, $contextGenerationData); diff --git a/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContext.php b/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContext.php index 9ba9e36b4..4984c359a 100644 --- a/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContext.php +++ b/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContext.php @@ -226,6 +226,16 @@ final class CalendarContext implements CalendarContextInterface return true; } + public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array + { + // TODO: Implement publicFormTransform() method. + } + + public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array + { + // TODO: Implement publicFormReverseTransform() method. + } + /** * @param array{mainPerson?: Person, thirdParty?: ThirdParty, title: string} $contextGenerationData */ diff --git a/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContextInterface.php b/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContextInterface.php index d02cdc2c2..eeef3b417 100644 --- a/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContextInterface.php +++ b/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContextInterface.php @@ -56,6 +56,10 @@ interface CalendarContextInterface extends DocGeneratorContextWithPublicFormInte */ public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool; + public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array; + + public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array; + /** * @param Calendar $entity */ diff --git a/src/Bundle/ChillDocGeneratorBundle/Context/DocGeneratorContextWithPublicFormInterface.php b/src/Bundle/ChillDocGeneratorBundle/Context/DocGeneratorContextWithPublicFormInterface.php index f013c8435..4f58bd049 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Context/DocGeneratorContextWithPublicFormInterface.php +++ b/src/Bundle/ChillDocGeneratorBundle/Context/DocGeneratorContextWithPublicFormInterface.php @@ -23,6 +23,9 @@ interface DocGeneratorContextWithPublicFormInterface extends DocGeneratorContext */ public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void; + /** + * Fill the form with initial data + */ public function getFormData(DocGeneratorTemplate $template, $entity): array; /** @@ -31,4 +34,14 @@ interface DocGeneratorContextWithPublicFormInterface extends DocGeneratorContext * @param mixed $entity */ public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool; + + /** + * Transform the data from the form into serializable data, storable into messenger's message + */ + public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array; + + /** + * Reverse the data from the messenger's message into data usable for doc's generation + */ + public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array; } diff --git a/src/Bundle/ChillDocGeneratorBundle/Controller/DocGeneratorTemplateController.php b/src/Bundle/ChillDocGeneratorBundle/Controller/DocGeneratorTemplateController.php index d618f758a..6f153acd8 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Controller/DocGeneratorTemplateController.php +++ b/src/Bundle/ChillDocGeneratorBundle/Controller/DocGeneratorTemplateController.php @@ -16,67 +16,57 @@ use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface; use Chill\DocGeneratorBundle\Context\Exception\ContextNotFoundException; use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface; -use Chill\DocGeneratorBundle\GeneratorDriver\Exception\TemplateException; use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository; +use Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface; +use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Serializer\Model\Collection; use Doctrine\ORM\EntityManagerInterface; -use Exception; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\FileType; -use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; // TODO à mettre dans services use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; -use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Contracts\HttpClient\HttpClientInterface; -use Throwable; use function strlen; final class DocGeneratorTemplateController extends AbstractController { - private HttpClientInterface $client; - private ContextManager $contextManager; private DocGeneratorTemplateRepository $docGeneratorTemplateRepository; - private DriverInterface $driver; - private EntityManagerInterface $entityManager; - private LoggerInterface $logger; + private GeneratorInterface $generator; + + private MessageBusInterface $messageBus; private PaginatorFactory $paginatorFactory; - private StoredObjectManagerInterface $storedObjectManager; - public function __construct( ContextManager $contextManager, DocGeneratorTemplateRepository $docGeneratorTemplateRepository, - DriverInterface $driver, - LoggerInterface $logger, + GeneratorInterface $generator, + MessageBusInterface $messageBus, PaginatorFactory $paginatorFactory, - HttpClientInterface $client, - StoredObjectManagerInterface $storedObjectManager, EntityManagerInterface $entityManager ) { $this->contextManager = $contextManager; $this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository; - $this->driver = $driver; - $this->logger = $logger; + $this->generator = $generator; + $this->messageBus = $messageBus; $this->paginatorFactory = $paginatorFactory; - $this->client = $client; - $this->storedObjectManager = $storedObjectManager; $this->entityManager = $entityManager; } @@ -94,7 +84,6 @@ final class DocGeneratorTemplateController extends AbstractController ): Response { return $this->generateDocFromTemplate( $template, - $entityClassName, $entityId, $request, true @@ -115,7 +104,6 @@ final class DocGeneratorTemplateController extends AbstractController ): Response { return $this->generateDocFromTemplate( $template, - $entityClassName, $entityId, $request, false @@ -185,7 +173,6 @@ final class DocGeneratorTemplateController extends AbstractController private function generateDocFromTemplate( DocGeneratorTemplate $template, - string $entityClassName, int $entityId, Request $request, bool $isTest @@ -206,7 +193,7 @@ final class DocGeneratorTemplateController extends AbstractController if (null === $entity) { throw new NotFoundHttpException( - sprintf('Entity with classname %s and id %s is not found', $entityClassName, $entityId) + sprintf('Entity with classname %s and id %s is not found', $context->getEntityClass(), $entityId) ); } @@ -259,99 +246,68 @@ final class DocGeneratorTemplateController extends AbstractController } } - $document = $template->getFile(); - - if ($isTest && ($contextGenerationData['test_file'] instanceof File)) { - $dataDecrypted = file_get_contents($contextGenerationData['test_file']->getPathname()); - } else { - try { - $dataDecrypted = $this->storedObjectManager->read($document); - } catch (Throwable $exception) { - throw $exception; - } - } + // transform context generation data + $contextGenerationDataSanitized = + $context instanceof DocGeneratorContextWithPublicFormInterface ? + $context->contextGenerationDataNormalize($template, $entity, $contextGenerationData) + : []; + // if is test, render the data or generate the doc if ($isTest && isset($form) && $form['show_data']->getData()) { return $this->render('@ChillDocGenerator/Generator/debug_value.html.twig', [ 'datas' => json_encode($context->getData($template, $entity, $contextGenerationData), JSON_PRETTY_PRINT) ]); - } - - try { - $generatedResource = $this - ->driver - ->generateFromString( - $dataDecrypted, - $template->getFile()->getType(), - $context->getData($template, $entity, $contextGenerationData), - $template->getFile()->getFilename() - ); - } catch (TemplateException $e) { - return new Response( - implode("\n", $e->getErrors()), - 400, - [ - 'Content-Type' => 'text/plain', - ] + } elseif ($isTest) { + $generated = $this->generator->generateDocFromTemplate( + $template, + $entityId, + $contextGenerationDataSanitized, + null, + true, + isset($form) ? $form['test_file']->getData() : null ); - } - if ($isTest) { return new Response( - $generatedResource, + $generated, Response::HTTP_OK, [ 'Content-Transfer-Encoding', 'binary', 'Content-Type' => 'application/vnd.oasis.opendocument.text', 'Content-Disposition' => 'attachment; filename="generated.odt"', - 'Content-Length' => strlen($generatedResource), + 'Content-Length' => strlen($generated), ], ); } - /** @var StoredObject $storedObject */ - $storedObject = (new ObjectNormalizer()) - ->denormalize( - [ - 'type' => $template->getFile()->getType(), - 'filename' => sprintf('%s_odt', uniqid('doc_', true)), - ], - StoredObject::class - ); - - try { - $this->storedObjectManager->write($storedObject, $generatedResource); - } catch (Throwable $exception) { - throw $exception; - } + // this is not a test + // we prepare the object to store the document + $storedObject = (new StoredObject()) + ->setStatus(StoredObject::STATUS_PENDING) + ; $this->entityManager->persist($storedObject); - try { - $context - ->storeGenerated( - $template, - $storedObject, - $entity, - $contextGenerationData - ); - } catch (Exception $e) { - $this - ->logger - ->error( - 'Unable to store the associated document to entity', - [ - 'entityClassName' => $entityClassName, - 'entityId' => $entityId, - 'contextKey' => $context->getName(), - ] - ); - - throw $e; - } + // we store the generated document + $context + ->storeGenerated( + $template, + $storedObject, + $entity, + $contextGenerationData + ); $this->entityManager->flush(); + $this->messageBus->dispatch( + new RequestGenerationMessage( + $this->getUser(), + $template, + $entityId, + $storedObject, + $contextGenerationDataSanitized, + ) + ); + return $this ->redirectToRoute( 'chill_wopi_file_edit', diff --git a/src/Bundle/ChillDocGeneratorBundle/Resources/views/Email/on_generation_failed_email.txt.twig b/src/Bundle/ChillDocGeneratorBundle/Resources/views/Email/on_generation_failed_email.txt.twig new file mode 100644 index 000000000..c4ca7079d --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/Resources/views/Email/on_generation_failed_email.txt.twig @@ -0,0 +1,16 @@ +{{ creator.label }}, + +{{ 'docgen.failure_email.The generation of the document {template_name} failed'|trans({'{template_name}': template.name|localize_translatable_string}) }} + +{{ 'docgen.failure_email.Forward this email to your administrator for solving'|trans }} + +{{ 'docgen.failure_email.References'|trans }}: +{% if errors|length > 0 %} +{{ 'docgen.failure_email.The following errors were encoutered'|trans }}: + +{% for error in errors %} +- {{ error }} +{% endfor %} +{% endif %} +- template_id: {{ template.id }} +- stored_object_destination_id: {{ stored_object_id }} diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Generator/Generator.php b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/Generator.php index 755e608ba..0775cfb49 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Service/Generator/Generator.php +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/Generator.php @@ -3,6 +3,7 @@ namespace Chill\DocGeneratorBundle\Service\Generator; use Chill\DocGeneratorBundle\Context\ContextManagerInterface; +use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface; use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface; use Chill\DocGeneratorBundle\GeneratorDriver\Exception\TemplateException; @@ -12,7 +13,7 @@ use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\File\File; -class Generator +class Generator implements GeneratorInterface { private ContextManagerInterface $contextManager; @@ -24,6 +25,8 @@ class Generator private StoredObjectManagerInterface $storedObjectManager; + private const LOG_PREFIX = '[docgen generator] '; + public function __construct( ContextManagerInterface $contextManager, DriverInterface $driver, @@ -48,18 +51,23 @@ class Generator */ public function generateDocFromTemplate( DocGeneratorTemplate $template, - string $entityClassName, int $entityId, + array $contextGenerationDataNormalized, ?StoredObject $destinationStoredObject = null, bool $isTest = false, ?File $testFile = null ): ?string { if ($destinationStoredObject instanceof StoredObject && StoredObject::STATUS_PENDING !== $destinationStoredObject->getStatus()) { + $this->logger->info(self::LOG_PREFIX.'Aborting generation of an already generated document'); throw new ObjectReadyException(); } + $this->logger->info(self::LOG_PREFIX.'Starting generation of a document', [ + 'entity_id' => $entityId, + 'destination_stored_object' => $destinationStoredObject === null ? null : $destinationStoredObject->getId() + ]); + $context = $this->contextManager->getContextByDocGeneratorTemplate($template); - $contextGenerationData = ['test_file' => $testFile]; $entity = $this ->entityManager @@ -67,22 +75,38 @@ class Generator ; if (null === $entity) { - throw new RelatedEntityNotFoundException($entityClassName, $entityId); + throw new RelatedEntityNotFoundException($template->getEntity(), $entityId); + } + + $contextGenerationDataNormalized = array_merge( + $contextGenerationDataNormalized, + $context instanceof DocGeneratorContextWithPublicFormInterface ? + $context->contextGenerationDataDenormalize($template, $entity, $contextGenerationDataNormalized) + : [] + ); + + $data = $context->getData($template, $entity, $contextGenerationDataNormalized); + + $destinationStoredObjectId = $destinationStoredObject instanceof StoredObject ? $destinationStoredObject->getId() : null; + $this->entityManager->clear(); + gc_collect_cycles(); + if (null !== $destinationStoredObjectId) { + $destinationStoredObject = $this->entityManager->find(StoredObject::class, $destinationStoredObjectId); } if ($isTest && ($testFile instanceof File)) { - $dataDecrypted = file_get_contents($testFile->getPathname()); + $templateDecrypted = file_get_contents($testFile->getPathname()); } else { - $dataDecrypted = $this->storedObjectManager->read($template->getFile()); + $templateDecrypted = $this->storedObjectManager->read($template->getFile()); } try { $generatedResource = $this ->driver ->generateFromString( - $dataDecrypted, + $templateDecrypted, $template->getFile()->getType(), - $context->getData($template, $entity, $contextGenerationData), + $data, $template->getFile()->getFilename() ); } catch (TemplateException $e) { @@ -90,6 +114,11 @@ class Generator } if ($isTest) { + $this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [ + 'is_test' => true, + 'entity_id' => $entityId, + 'destination_stored_object' => $destinationStoredObject === null ? null : $destinationStoredObject->getId() + ]); return $generatedResource; } @@ -102,31 +131,13 @@ class Generator $this->storedObjectManager->write($destinationStoredObject, $generatedResource); - try { - $context - ->storeGenerated( - $template, - $destinationStoredObject, - $entity, - $contextGenerationData - ); - } catch (\Exception $e) { - $this - ->logger - ->error( - 'Unable to store the associated document to entity', - [ - 'entityClassName' => $entityClassName, - 'entityId' => $entityId, - 'contextKey' => $context->getName(), - ] - ); - - throw $e; - } - $this->entityManager->flush(); + $this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [ + 'entity_id' => $entityId, + 'destination_stored_object' => $destinationStoredObject->getId(), + ]); + return null; } } diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorException.php b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorException.php index 423d59dd4..0f3412859 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorException.php +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorException.php @@ -15,4 +15,12 @@ class GeneratorException extends \RuntimeException parent::__construct("Could not generate the document", 15252, $previous); } + + /** + * @return array + */ + public function getErrors(): array + { + return $this->errors; + } } diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorInterface.php b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorInterface.php new file mode 100644 index 000000000..385e1d010 --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorInterface.php @@ -0,0 +1,27 @@ +docGeneratorTemplateRepository = $docGeneratorTemplateRepository; + $this->entityManager = $entityManager; + $this->logger = $logger; + $this->mailer = $mailer; + $this->storedObjectRepository = $storedObjectRepository; + $this->translator = $translator; + $this->userRepository = $userRepository; + } + + + public static function getSubscribedEvents() + { + return [ + WorkerMessageFailedEvent::class => 'onMessageFailed' + ]; + } + + public function onMessageFailed(WorkerMessageFailedEvent $event): void + { + if ($event->willRetry()) { + return; + } + + if (!$event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) { + return; + } + + /** @var \Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage $message */ + $message = $event->getEnvelope()->getMessage(); + + $this->logger->error(self::LOG_PREFIX.'Docgen failed', [ + 'stored_object_id' => $message->getDestinationStoredObjectId(), + 'entity_id' => $message->getEntityId(), + 'template_id' => $message->getTemplateId(), + 'creator_id' => $message->getCreatorId(), + 'throwable_class' => get_class($event->getThrowable()), + ]); + + $this->markObjectAsFailed($message); + $this->warnCreator($message, $event); + } + + private function markObjectAsFailed(RequestGenerationMessage $message): void + { + $object = $this->storedObjectRepository->find($message->getDestinationStoredObjectId()); + + if (null === $object) { + $this->logger->error(self::LOG_PREFIX.'Stored object not found', ['stored_object_id', $message->getDestinationStoredObjectId()]); + } + + $object->setStatus(StoredObject::STATUS_FAILURE); + + $this->entityManager->flush(); + } + + private function warnCreator(RequestGenerationMessage $message, WorkerMessageFailedEvent $event): void + { + if (null === $creatorId = $message->getCreatorId()) { + $this->logger->info(self::LOG_PREFIX.'creator id is null'); + return; + } + + if (null === $creator = $this->userRepository->find($creatorId)) { + $this->logger->error(self::LOG_PREFIX.'Creator not found with given id', ['creator_id', $creatorId]); + return; + } + + if (null === $creator->getEmail() || '' === $creator->getEmail()) { + $this->logger->info(self::LOG_PREFIX.'Creator does not have any email', ['user' => $creator->getUsernameCanonical()]); + return; + } + + // if the exception is not a GeneratorException, we try the previous one... + $throwable = $event->getThrowable(); + if (!$throwable instanceof GeneratorException) { + $throwable = $throwable->getPrevious(); + } + + if ($throwable instanceof GeneratorException) { + $errors = $throwable->getErrors(); + } else { + $errors = [$throwable->getTraceAsString()]; + } + + if (null === $template = $this->docGeneratorTemplateRepository->find($message->getTemplateId())) { + $this->logger->info(self::LOG_PREFIX.'Template not found', ['template_id' => $message->getTemplateId()]); + return; + } + + $email = (new TemplatedEmail()) + ->to($creator->getEmail()) + ->subject($this->translator->trans('docgen.failure_email.The generation of a document failed')) + ->textTemplate('@ChillDocGenerator/Email/on_generation_failed_email.txt.twig') + ->context([ + 'errors' => $errors, + 'template' => $template, + 'creator' => $creator, + 'stored_object_id' => $message->getDestinationStoredObjectId(), + ]); + + $this->mailer->send($email); + } +} diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationHandler.php b/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationHandler.php new file mode 100644 index 000000000..981a3dde7 --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationHandler.php @@ -0,0 +1,66 @@ +docGeneratorTemplateRepository = $docGeneratorTemplateRepository; + $this->entityManager = $entityManager; + $this->generator = $generator; + $this->storedObjectRepository = $storedObjectRepository; + } + + public function __invoke(RequestGenerationMessage $message) + { + if (null === $template = $this->docGeneratorTemplateRepository->find($message->getTemplateId())) { + throw new \RuntimeException('template not found: ' . $message->getTemplateId()); + } + + if (null === $destinationStoredObject = $this->storedObjectRepository->find($message->getDestinationStoredObjectId())) { + throw new \RuntimeException('destination stored object not found : ' . $message->getDestinationStoredObjectId()); + } + + if ($destinationStoredObject->getGenerationTrialsCounter() >= self::AUTHORIZED_TRIALS) { + throw new UnrecoverableMessageHandlingException('maximum number of retry reached'); + } + + $destinationStoredObject->addGenerationTrial(); + $this->entityManager->createQuery('UPDATE '.StoredObject::class.' s SET s.generationTrialsCounter = s.generationTrialsCounter + 1 WHERE s.id = :id') + ->setParameter('id', $destinationStoredObject->getId()) + ->execute(); + + $this->generator->generateDocFromTemplate( + $template, + $message->getEntityId(), + $message->getContextGenerationData(), + $destinationStoredObject + ); + } +} diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationMessage.php b/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationMessage.php index 1489e2686..edba90ce4 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationMessage.php +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationMessage.php @@ -3,6 +3,7 @@ namespace Chill\DocGeneratorBundle\Service\Messenger; use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; +use Chill\DocStoreBundle\Entity\StoredObject; use Chill\MainBundle\Entity\User; class RequestGenerationMessage @@ -13,14 +14,22 @@ class RequestGenerationMessage private int $entityId; - private string $entityClassName; + private int $destinationStoredObjectId; - public function __construct(User $creator, DocGeneratorTemplate $template, int $entityId, string $entityClassName) - { + private array $contextGenerationData; + + public function __construct( + User $creator, + DocGeneratorTemplate $template, + int $entityId, + StoredObject $destinationStoredObject, + array $contextGenerationData + ) { $this->creatorId = $creator->getId(); $this->templateId = $template->getId(); $this->entityId = $entityId; - $this->entityClassName = $entityClassName; + $this->destinationStoredObjectId = $destinationStoredObject->getId(); + $this->contextGenerationData = $contextGenerationData; } public function getCreatorId(): int @@ -28,6 +37,11 @@ class RequestGenerationMessage return $this->creatorId; } + public function getDestinationStoredObjectId(): int + { + return $this->destinationStoredObjectId; + } + public function getTemplateId(): int { return $this->templateId; @@ -38,8 +52,8 @@ class RequestGenerationMessage return $this->entityId; } - public function getEntityClassName(): string + public function getContextGenerationData(): array { - return $this->entityClassName; + return $this->contextGenerationData; } } diff --git a/src/Bundle/ChillDocGeneratorBundle/config/services.yaml b/src/Bundle/ChillDocGeneratorBundle/config/services.yaml index 5bdfe2a11..5fef6fb22 100644 --- a/src/Bundle/ChillDocGeneratorBundle/config/services.yaml +++ b/src/Bundle/ChillDocGeneratorBundle/config/services.yaml @@ -20,10 +20,14 @@ services: resource: '../Serializer/Normalizer/' tags: - { name: 'serializer.normalizer', priority: -152 } + Chill\DocGeneratorBundle\Serializer\Normalizer\CollectionDocGenNormalizer: tags: - { name: 'serializer.normalizer', priority: -126 } + Chill\DocGeneratorBundle\Service\Context\: + resource: "../Service/Context" + Chill\DocGeneratorBundle\Controller\: resource: "../Controller" autowire: true @@ -34,18 +38,20 @@ services: autowire: true autoconfigure: true - Chill\DocGeneratorBundle\Service\Context\: - resource: "../Service/Context/" - autowire: true - autoconfigure: true - Chill\DocGeneratorBundle\GeneratorDriver\: resource: "../GeneratorDriver/" autowire: true autoconfigure: true + Chill\DocGeneratorBundle\Service\Messenger\: + resource: "../Service/Messenger/" + + Chill\DocGeneratorBundle\Service\Generator\Generator: ~ + Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface: '@Chill\DocGeneratorBundle\Service\Generator\Generator' + Chill\DocGeneratorBundle\Driver\RelatorioDriver: '@Chill\DocGeneratorBundle\Driver\DriverInterface' Chill\DocGeneratorBundle\Context\ContextManager: arguments: $contexts: !tagged_iterator { tag: chill_docgen.context, default_index_method: getKey } + Chill\DocGeneratorBundle\Context\ContextManagerInterface: '@Chill\DocGeneratorBundle\Context\ContextManager' diff --git a/src/Bundle/ChillDocGeneratorBundle/migrations/Version20230214192558.php b/src/Bundle/ChillDocGeneratorBundle/migrations/Version20230214192558.php new file mode 100644 index 000000000..de536ca84 --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/migrations/Version20230214192558.php @@ -0,0 +1,47 @@ +addSql('ALTER TABLE chill_doc.stored_object ADD template_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_doc.stored_object ADD status TEXT DEFAULT \'ready\' NOT NULL'); + $this->addSql('ALTER TABLE chill_doc.stored_object ADD createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL'); + $this->addSql('UPDATE chill_doc.stored_object SET createdAt = creation_date'); + $this->addSql('ALTER TABLE chill_doc.stored_object ADD createdBy_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_doc.stored_object DROP creation_date;'); + $this->addSql('ALTER TABLE chill_doc.stored_object ALTER type SET DEFAULT \'\''); + $this->addSql('ALTER TABLE chill_doc.stored_object ALTER title DROP DEFAULT'); + $this->addSql('COMMENT ON COLUMN chill_doc.stored_object.createdAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE chill_doc.stored_object ADD CONSTRAINT FK_49604E365DA0FB8 FOREIGN KEY (template_id) REFERENCES chill_docgen_template (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_doc.stored_object ADD CONSTRAINT FK_49604E363174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX IDX_49604E365DA0FB8 ON chill_doc.stored_object (template_id)'); + $this->addSql('CREATE INDEX IDX_49604E363174800F ON chill_doc.stored_object (createdBy_id)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_doc.stored_object DROP CONSTRAINT FK_49604E365DA0FB8'); + $this->addSql('ALTER TABLE chill_doc.stored_object DROP CONSTRAINT FK_49604E363174800F'); + $this->addSql('ALTER TABLE chill_doc.stored_object DROP template_id'); + $this->addSql('ALTER TABLE chill_doc.stored_object DROP status'); + $this->addSql('ALTER TABLE chill_doc.stored_object ADD creation_date TIMESTAMP(0) DEFAULT NOW()'); + $this->addSql('UPDATE chill_doc.stored_object SET creation_date = createdAt'); + $this->addSql('ALTER TABLE chill_doc.stored_object DROP createdAt'); + $this->addSql('ALTER TABLE chill_doc.stored_object DROP createdBy_id'); + $this->addSql('ALTER TABLE chill_doc.stored_object ALTER title SET DEFAULT \'\''); + $this->addSql('ALTER TABLE chill_doc.stored_object ALTER type DROP DEFAULT'); + } +} diff --git a/src/Bundle/ChillDocGeneratorBundle/tests/Service/Context/Generator/GeneratorTest.php b/src/Bundle/ChillDocGeneratorBundle/tests/Service/Context/Generator/GeneratorTest.php index 2e68b443c..066715b60 100644 --- a/src/Bundle/ChillDocGeneratorBundle/tests/Service/Context/Generator/GeneratorTest.php +++ b/src/Bundle/ChillDocGeneratorBundle/tests/Service/Context/Generator/GeneratorTest.php @@ -26,13 +26,14 @@ class GeneratorTest extends TestCase $template = (new DocGeneratorTemplate())->setFile($templateStoredObject = (new StoredObject()) ->setType('application/test')); $destinationStoredObject = (new StoredObject())->setStatus(StoredObject::STATUS_PENDING); + $reflection = new \ReflectionClass($destinationStoredObject); + $reflection->getProperty('id')->setAccessible(true); + $reflection->getProperty('id')->setValue($destinationStoredObject, 1); $entity = new class {}; $data = []; $context = $this->prophesize(DocGeneratorContextInterface::class); $context->getData($template, $entity, Argument::type('array'))->willReturn($data); - $context->storeGenerated($template, $destinationStoredObject, $entity, Argument::type('array')) - ->shouldBeCalled(); $context->getName()->willReturn('dummy_context'); $context->getEntityClass()->willReturn('DummyClass'); $context = $context->reveal(); @@ -46,8 +47,11 @@ class GeneratorTest extends TestCase ->willReturn('generated'); $entityManager = $this->prophesize(EntityManagerInterface::class); - $entityManager->find(Argument::type('string'), Argument::type('int')) + $entityManager->find(StoredObject::class, 1) + ->willReturn($destinationStoredObject); + $entityManager->find('DummyClass', Argument::type('int')) ->willReturn($entity); + $entityManager->clear()->shouldBeCalled(); $entityManager->flush()->shouldBeCalled(); $storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class); @@ -65,8 +69,8 @@ class GeneratorTest extends TestCase $generator->generateDocFromTemplate( $template, - 'DummyEntity', 1, + [], $destinationStoredObject ); } @@ -89,8 +93,8 @@ class GeneratorTest extends TestCase $generator->generateDocFromTemplate( $template, - 'DummyEntity', 1, + [], $destinationStoredObject ); } @@ -102,6 +106,9 @@ class GeneratorTest extends TestCase $template = (new DocGeneratorTemplate())->setFile($templateStoredObject = (new StoredObject()) ->setType('application/test')); $destinationStoredObject = (new StoredObject())->setStatus(StoredObject::STATUS_PENDING); + $reflection = new \ReflectionClass($destinationStoredObject); + $reflection->getProperty('id')->setAccessible(true); + $reflection->getProperty('id')->setValue($destinationStoredObject, 1); $context = $this->prophesize(DocGeneratorContextInterface::class); $context->getName()->willReturn('dummy_context'); @@ -126,8 +133,8 @@ class GeneratorTest extends TestCase $generator->generateDocFromTemplate( $template, - 'DummyEntity', 1, + [], $destinationStoredObject ); } diff --git a/src/Bundle/ChillDocGeneratorBundle/translations/messages.fr.yml b/src/Bundle/ChillDocGeneratorBundle/translations/messages.fr.yml index bf37ff838..d0950482e 100644 --- a/src/Bundle/ChillDocGeneratorBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillDocGeneratorBundle/translations/messages.fr.yml @@ -10,6 +10,16 @@ docgen: test generate: Tester la génération With context %name%: 'Avec le contexte "%name%"' + Doc generation failed: La génération de ce document a échoué + Doc generation is pending: La génération de ce document est en cours + Come back later: Revenir plus tard + + failure_email: + The generation of a document failed: La génération d'un document a échoué + The generation of the document {template_name} failed: La génération d'un document à partir du modèle {{ template_name }} a échoué. + The following errors were encoutered: Les erreurs suivantes ont été rencontrées + Forward this email to your administrator for solving: Faites suivre ce message vers votre administrateur pour la résolution du problème. + References: Références crud: docgen_template: @@ -19,4 +29,4 @@ crud: Show data instead of generating: Montrer les données au lieu de générer le document -Template file: Fichier modèle \ No newline at end of file +Template file: Fichier modèle diff --git a/src/Bundle/ChillDocStoreBundle/Controller/StoredObjectApiController.php b/src/Bundle/ChillDocStoreBundle/Controller/StoredObjectApiController.php new file mode 100644 index 000000000..29b978ffb --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Controller/StoredObjectApiController.php @@ -0,0 +1,39 @@ +security = $security; + } + + /** + * @Route("/api/1.0/doc-store/stored-object/{uuid}/is-ready") + */ + public function isDocumentReady(StoredObject $storedObject): Response + { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedHttpException(); + } + + return new JsonResponse( + [ + 'id' => $storedObject->getId(), + 'filename' => $storedObject->getFilename(), + 'status' => $storedObject->getStatus(), + 'type' => $storedObject->getType(), + ] + ); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php b/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php index 21abc1f56..8821ead7c 100644 --- a/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php +++ b/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php @@ -41,12 +41,6 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa use TrackCreationTrait; - /** - * @ORM\Column(type="datetime", name="creation_date") - * @Serializer\Groups({"read", "write"}) - */ - private DateTimeInterface $creationDate; - /** * @ORM\Column(type="json", name="datas") * @Serializer\Groups({"read", "write"}) @@ -87,7 +81,7 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa private string $title = ''; /** - * @ORM\Column(type="text", name="type") + * @ORM\Column(type="text", name="type", options={"default": ""}) * @Serializer\Groups({"read", "write"}) */ private string $type = ''; @@ -105,9 +99,20 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa /** * @ORM\Column(type="text", options={"default": "ready"}) + * @Serializer\Groups({"read"}) */ private string $status; + /** + * Store the number of times a generation has been tryied for this StoredObject. + * + * This is a workaround, as generation consume lot of memory, and out-of-memory errors + * are not handled by messenger. + * + * @ORM\Column(type="integer", options={"default": 0}) + */ + private int $generationTrialsCounter = 0; + /** * @param StoredObject::STATUS_* $status */ @@ -117,8 +122,16 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa $this->status = $status; } + public function addGenerationTrial(): self + { + $this->generationTrialsCounter++; + + return $this; + } + /** * @Serializer\Groups({"read", "write"}) + * @deprecated */ public function getCreationDate(): DateTime { @@ -135,6 +148,11 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa return $this->filename; } + public function getGenerationTrialsCounter(): int + { + return $this->generationTrialsCounter; + } + public function getId(): ?int { return $this->id; @@ -158,6 +176,9 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa return $this->getFilename(); } + /** + * @return StoredObject::STATUS_* + */ public function getStatus(): string { return $this->status; @@ -185,6 +206,7 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa /** * @Serializer\Groups({"write"}) + * @deprecated */ public function setCreationDate(DateTime $creationDate): self { @@ -244,4 +266,30 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa return $this; } + + public function getTemplate(): ?DocGeneratorTemplate + { + return $this->template; + } + + public function hasTemplate(): bool + { + return null !== $this->template; + } + + public function setTemplate(?DocGeneratorTemplate $template): StoredObject + { + $this->template = $template; + return $this; + } + + public function isPending(): bool + { + return self::STATUS_PENDING === $this->getStatus(); + } + + public function isFailure(): bool + { + return self::STATUS_FAILURE === $this->getStatus(); + } } diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/module/document_action_buttons_group/index.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/module/document_action_buttons_group/index.ts index ec1d50a86..e5912d38a 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/module/document_action_buttons_group/index.ts +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/module/document_action_buttons_group/index.ts @@ -1,7 +1,8 @@ import {_createI18n} from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n"; import DocumentActionButtonsGroup from "../../vuejs/DocumentActionButtonsGroup.vue"; import {createApp} from "vue"; -import {StoredObject} from "../../types"; +import {StoredObject, StoredObjectStatusChange} from "../../types"; +import {is_object_ready} from "../../vuejs/StoredObjectButton/helpers"; const i18n = _createI18n({}); @@ -19,7 +20,7 @@ window.addEventListener('DOMContentLoaded', function (e) { }; const - storedObject = JSON.parse(datasets.storedObject), + storedObject = JSON.parse(datasets.storedObject) as StoredObject, filename = datasets.filename, canEdit = datasets.canEdit === '1', small = datasets.small === '1' @@ -27,7 +28,20 @@ window.addEventListener('DOMContentLoaded', function (e) { return { storedObject, filename, canEdit, small }; }, - template: '', + template: '', + methods: { + onStoredObjectStatusChange: function(newStatus: StoredObjectStatusChange): void { + this.$data.storedObject.status = newStatus.status; + this.$data.storedObject.filename = newStatus.filename; + this.$data.storedObject.type = newStatus.type; + + // remove eventual div which inform pending status + document.querySelectorAll(`[data-docgen-is-pending="${this.$data.storedObject.id}"]`) + .forEach(function(el) { + el.remove(); + }); + } + } }); app.use(i18n).mount(el); diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts index 918526117..825055973 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts @@ -1,5 +1,7 @@ import {DateTime} from "../../../ChillMainBundle/Resources/public/types"; +export type StoredObjectStatus = "ready"|"failure"|"pending"; + export interface StoredObject { id: number, @@ -13,7 +15,15 @@ export interface StoredObject { keyInfos: object, title: string, type: string, - uuid: string + uuid: string, + status: StoredObjectStatus, +} + +export interface StoredObjectStatusChange { + id: number, + filename: string, + status: StoredObjectStatus, + type: string, } /** diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue index 60b368cd3..ac841f5cf 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue @@ -1,5 +1,5 @@