From 63f3010395dbed3e958918e40e106540ba6a17a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 20 Oct 2022 21:36:44 +0200 Subject: [PATCH] Feature: [calendar][docgen] generation context for Calendar --- .../ChillCalendarBundle/Entity/Invite.php | 4 +- .../Service/DocGenerator/CalendarContext.php | 249 ++++++++++++++++++ .../DocGenerator/CalendarContextInterface.php | 63 +++++ .../DocGenerator/CalendarContextTest.php | 236 +++++++++++++++++ .../translations/messages.fr.yml | 11 + 5 files changed, 561 insertions(+), 2 deletions(-) create mode 100644 src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContext.php create mode 100644 src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContextInterface.php create mode 100644 src/Bundle/ChillCalendarBundle/Tests/Service/DocGenerator/CalendarContextTest.php diff --git a/src/Bundle/ChillCalendarBundle/Entity/Invite.php b/src/Bundle/ChillCalendarBundle/Entity/Invite.php index 0489d0a00..c2d79aff2 100644 --- a/src/Bundle/ChillCalendarBundle/Entity/Invite.php +++ b/src/Bundle/ChillCalendarBundle/Entity/Invite.php @@ -73,14 +73,14 @@ class Invite implements TrackUpdateInterface, TrackCreationInterface /** * @ORM\Column(type="text", nullable=false, options={"default": "pending"}) - * @Serializer\Groups(groups={"calendar:read", "read"}) + * @Serializer\Groups(groups={"calendar:read", "read", "docgen:read"}) */ private string $status = self::PENDING; /** * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User") * @ORM\JoinColumn(nullable=false) - * @Serializer\Groups(groups={"calendar:read", "read"}) + * @Serializer\Groups(groups={"calendar:read", "read", "docgen:read"}) */ private ?User $user = null; diff --git a/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContext.php b/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContext.php new file mode 100644 index 000000000..9ba9e36b4 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContext.php @@ -0,0 +1,249 @@ +baseContextData = $baseContextData; + $this->entityManager = $entityManager; + $this->normalizer = $normalizer; + $this->personRender = $personRender; + $this->thirdPartyRender = $thirdPartyRender; + $this->translatableStringHelper = $translatableStringHelper; + } + + public function adminFormReverseTransform(array $data): array + { + return array_merge( + [ + 'trackDatetime' => true, + 'askMainPerson' => true, + 'mainPersonLabel' => 'docgen.calendar.Destinee', + 'askThirdParty' => false, + 'thirdPartyLabel' => 'Third party', + ], + $data + ); + } + + public function adminFormTransform(array $data): array + { + return $data; + } + + public function buildAdminForm(FormBuilderInterface $builder): void + { + $builder + ->add('trackDatetime', CheckboxType::class, [ + 'required' => false, + 'label' => 'docgen.calendar.Track changes on datetime and warn user if date time is updated after the doc generation', + ]) + ->add('askMainPerson', CheckboxType::class, [ + 'required' => false, + 'label' => 'docgen.calendar.Ask main person', + ]) + ->add('mainPersonLabel', TextType::class, [ + 'required' => false, + 'label' => 'docgen.calendar.Main person label', + ]) + ->add('askThirdParty', CheckboxType::class, [ + 'required' => false, + 'label' => 'docgen.calendar.Ask third party', + ]) + ->add('thirdPartyLabel', TextType::class, [ + 'required' => false, + 'label' => 'docgen.calendar.Third party label', + ]); + } + + public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void + { + $options = $this->getOptions($template); + + $builder->add('title', TextType::class, [ + 'required' => true, + 'label' => 'docgen.calendar.title of the generated document', + 'data' => $this->translatableStringHelper->localize($template->getName()), + ]); + + if ($options['askMainPerson']) { + $builder->add('mainPerson', EntityType::class, [ + 'class' => Person::class, + 'multiple' => false, + 'label' => $options['mainPersonLabel'] ?? 'docgen.calendar.Main person label', + 'required' => false, + 'choices' => $entity->getPersons(), + 'choice_label' => fn (Person $p) => $this->personRender->renderString($p, []), + 'expanded' => false, + ]); + } + + if ($options['askThirdParty']) { + $builder->add('thirdParty', EntityType::class, [ + 'class' => ThirdParty::class, + 'multiple' => false, + 'label' => $options['thirdPartyLabel'] ?? 'Third party', + 'choices' => $entity->getProfessionals(), + 'choice_label' => fn (ThirdParty $tp) => $this->thirdPartyRender->renderString($tp, []), + 'expanded' => false, + ]); + } + } + + /** + * @param array{mainPerson?: Person, thirdParty?: ThirdParty, title: string} $contextGenerationData + * @param mixed $entity + */ + public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array + { + $options = $this->getOptions($template); + + $data = array_merge( + $this->baseContextData->getData(), + [ + 'calendar' => $this->normalizer->normalize($entity, 'docgen', ['docgen:expects' => Calendar::class, 'groups' => ['docgen:read']]), + ] + ); + + if ($options['askMainPerson']) { + $data['mainPerson'] = $this->normalizer->normalize($contextGenerationData['mainPerson'] ?? null, 'docgen', [ + 'docgen:expects' => Person::class, + 'groups' => ['docgen:read'], + 'docgen:person:with-household' => true, + 'docgen:person:with-relations' => true, + 'docgen:person:with-budget' => true, + ]); + } + + if ($options['askThirdParty']) { + $data['thirdParty'] = $this->normalizer->normalize($contextGenerationData['thirdParty'] ?? null, 'docgen', [ + 'docgen:expects' => ThirdParty::class, + 'groups' => ['docgen:read'], + ]); + } + + return $data; + } + + public function getDescription(): string + { + return 'docgen.calendar.A base context for generating document on calendar'; + } + + public function getEntityClass(): string + { + return Calendar::class; + } + + public function getFormData(DocGeneratorTemplate $template, $entity): array + { + $options = $this->getOptions($template); + $data = []; + + if ($options['askMainPerson']) { + $data['mainPerson'] = null; + + if (1 === count($entity->getPersons())) { + $data['mainPerson'] = $entity->getPersons()->first(); + } + } + + if ($options['askThirdParty']) { + $data['thirdParty'] = null; + + if (1 === count($entity->getProfessionals())) { + $data['thirdParty'] = $entity->getProfessionals()->first(); + } + } + + return $data; + } + + public static function getKey(): string + { + return self::class; + } + + public function getName(): string + { + return 'docgen.calendar.Base context for calendar'; + } + + public function hasAdminForm(): bool + { + return true; + } + + public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool + { + return true; + } + + /** + * @param array{mainPerson?: Person, thirdParty?: ThirdParty, title: string} $contextGenerationData + */ + public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void + { + $options = $this->getOptions($template); + $storedObject->setTitle($contextGenerationData['title']); + $doc = new CalendarDoc($entity, $storedObject); + $doc->setTrackDateTimeVersion($options['trackDatetime']); + + $this->entityManager->persist($doc); + } + + /** + * @return array{askMainPerson: bool, mainPersonLabel: ?string, askThirdParty: bool, thirdPartyLabel: ?string, trackDateTime: bool} $options + */ + private function getOptions(DocGeneratorTemplate $template): array + { + return $template->getOptions(); + } +} diff --git a/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContextInterface.php b/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContextInterface.php new file mode 100644 index 000000000..d02cdc2c2 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContextInterface.php @@ -0,0 +1,63 @@ + true, + 'askMainPerson' => true, + 'mainPersonLabel' => 'docgen.calendar.Destinee', + 'askThirdParty' => false, + 'thirdPartyLabel' => 'Third party', + ]; + + $this->assertEqualsCanonicalizing($expected, $this->buildCalendarContext()->adminFormReverseTransform([])); + } + + public function testAdminFormTransform() + { + $expected = + [ + 'track_datetime' => true, + 'askMainPerson' => true, + 'mainPersonLabel' => 'docgen.calendar.Destinee', + 'askThirdParty' => false, + 'thirdPartyLabel' => 'Third party', + ]; + + $this->assertEqualsCanonicalizing($expected, $this->buildCalendarContext()->adminFormTransform($expected)); + } + + public function testBuildPublicForm() + { + $formBuilder = $this->prophesize(FormBuilderInterface::class); + $calendar = new Calendar(); + $calendar + ->addProfessional($tp1 = new ThirdParty()) + ->addProfessional($tp2 = new ThirdParty()) + ->addPerson($p1 = new Person()); + + // we will try once with askThirdParty = true, once with askPerson = true, and once with both + // so, we expect the call to be twice for each method + $formBuilder->add('thirdParty', EntityType::class, Argument::type('array')) + ->should(static function ($calls, $object, $method) use ($tp1, $tp2) { + if (2 !== count($calls)) { + throw new FailedPredictionException(sprintf('the $builder->add should be called exactly 2, %d receivved', count($calls))); + } + + $opts = $calls[0]->getArguments()[2]; + + if (!array_key_exists('label', $opts)) { + throw new FailedPredictionException('the $builder->add should have a label key'); + } + + if ('tplabel' !== $opts['label']) { + throw new FailedPredictionException('third party label not expected'); + } + + if (!$opts['choices']->contains($tp1) || !$opts['choices']->contains($tp2)) { + throw new FailedPredictionException('third party not present'); + } + }); + $formBuilder->add('mainPerson', EntityType::class, Argument::type('array')) + ->should(static function ($calls, $object, $method) use ($p1) { + if (2 !== count($calls)) { + throw new FailedPredictionException(sprintf('the $builder->add should be called exactly 2, %d receivved', count($calls))); + } + + $opts = $calls[0]->getArguments()[2]; + + if (!array_key_exists('label', $opts)) { + throw new FailedPredictionException('the $builder->add should have a label key'); + } + + if ('personLabel' !== $opts['label']) { + throw new FailedPredictionException('person label not expected'); + } + + if (!$opts['choices']->contains($p1)) { + throw new FailedPredictionException('person not present'); + } + }); + + $formBuilder->add('title', TextType::class, Argument::type('array')) + ->shouldBeCalledTimes(3); + + foreach ([ + ['askMainPerson' => true, 'mainPersonLabel' => 'personLabel', 'askThirdParty' => true, 'thirdPartyLabel' => 'tplabel'], + ['askMainPerson' => false, 'mainPersonLabel' => 'personLabel', 'askThirdParty' => true, 'thirdPartyLabel' => 'tplabel'], + ['askMainPerson' => true, 'mainPersonLabel' => 'personLabel', 'askThirdParty' => false, 'thirdPartyLabel' => 'tplabel'], + ] as $options) { + $template = new DocGeneratorTemplate(); + $template->setOptions($options); + + $this->buildCalendarContext()->buildPublicForm($formBuilder->reveal(), $template, $calendar); + } + } + + public function testGetData() + { + $calendar = (new Calendar()) + ->addPerson($p1 = new Person()) + ->addProfessional($t1 = new ThirdParty()); + $template = (new DocGeneratorTemplate())->setOptions( + ['askMainPerson' => true, 'mainPersonLabel' => 'personLabel', 'askThirdParty' => true, 'thirdPartyLabel' => 'tplabel'], + ); + $contextData = [ + 'mainPerson' => $p1, + 'thirdParty' => $t1, + ]; + + $normalizer = $this->prophesize(NormalizerInterface::class); + $normalizer->normalize($p1, 'docgen', Argument::type('array'))->willReturn(['person' => '1']); + $normalizer->normalize($t1, 'docgen', Argument::type('array'))->willReturn(['tp' => '1']); + $normalizer->normalize($calendar, 'docgen', Argument::type('array'))->willReturn(['calendar' => '1']); + + $actual = $this->buildCalendarContext(null, $normalizer->reveal()) + ->getData($template, $calendar, $contextData); + + $this->assertEqualsCanonicalizing([ + 'calendar' => ['calendar' => '1'], + 'mainPerson' => ['person' => '1'], + 'thirdParty' => ['tp' => '1'], + 'base_context' => 'data', + ], $actual); + } + + public function testStoreGenerated() + { + $calendar = new Calendar(); + $storedObject = new StoredObject(); + $contextData = ['title' => 'blabla']; + $template = (new DocGeneratorTemplate())->setOptions(['trackDatetime' => true]); + + $em = $this->prophesize(EntityManagerInterface::class); + $em->persist(Argument::type(CalendarDoc::class))->should( + static function ($calls, $object, $method) use ($storedObject) { + if (1 !== count($calls)) { + throw new FailedPredictionException('the persist method should be called once'); + } + + /** @var CalendarDoc $calendarDoc */ + $calendarDoc = $calls[0]->getArguments()[0]; + + if ($calendarDoc->getStoredObject() !== $storedObject) { + throw new FailedPredictionException('the stored object is not correct'); + } + + if ($calendarDoc->getStoredObject()->getTitle() !== 'blabla') { + throw new FailedPredictionException('the doc title should be the one provided'); + } + + if (!$calendarDoc->isTrackDateTimeVersion()) { + throw new FailedPredictionException('the track date time should be true'); + } + } + ); + + $this->buildCalendarContext($em->reveal())->storeGenerated($template, $storedObject, $calendar, $contextData); + } + + private function buildCalendarContext( + ?EntityManagerInterface $entityManager = null, + ?NormalizerInterface $normalizer = null + ): CalendarContext { + $baseContext = $this->prophesize(BaseContextData::class); + $baseContext->getData()->willReturn(['base_context' => 'data']); + + $personRender = $this->prophesize(PersonRender::class); + $personRender->renderString(Argument::type(Person::class), [])->willReturn('person name'); + + $thirdPartyRender = $this->prophesize(ThirdPartyRender::class); + $thirdPartyRender->renderString(Argument::type(ThirdParty::class), [])->willReturn('third party name'); + + $translatableStringHelper = $this->prophesize(TranslatableStringHelperInterface::class); + $translatableStringHelper->localize(Argument::type('array'))->willReturn('blabla'); + + if (null === $normalizer) { + $normalizer = $this->prophesize(NormalizerInterface::class)->reveal(); + } + + if (null === $entityManager) { + $entityManager = $this->prophesize(EntityManagerInterface::class)->reveal(); + } + + return new CalendarContext( + $baseContext->reveal(), + $entityManager, + $normalizer, + $personRender->reveal(), + $thirdPartyRender->reveal(), + $translatableStringHelper->reveal() + ); + } +} diff --git a/src/Bundle/ChillCalendarBundle/translations/messages.fr.yml b/src/Bundle/ChillCalendarBundle/translations/messages.fr.yml index a03dd4015..53bf3d337 100644 --- a/src/Bundle/ChillCalendarBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillCalendarBundle/translations/messages.fr.yml @@ -112,3 +112,14 @@ has calendar range: Dans une plage de disponibilité? Not made within a calendar range: Rendez-vous dans une plage de disponibilité Made within a calendar range: Rendez-vous en dehors d'une plage de disponibilité +docgen: + calendar: + Base context for calendar: 'Rendez-vous: contexte de base' + A base context for generating document on calendar: Contexte pour générer des documents à partir des rendez-vous + Track changes on datetime and warn user if date time is updated after the doc generation: Suivre les changements sur le document et prévenir les utilisateurs que la date et l'heure ont été modifiée après la génération du document + Ask main person: Demander de choisir une personne parmi les participants aux rendez-vous + Main person label: Label pour choisir la personne + Ask third party: Demander de choisir un tiers parmi les participants aux rendez-vous + Third party label: Label pour choisir le tiers + Destinee: Destinataire + None: Aucun choix