mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-31 17:28:23 +00:00 
			
		
		
		
	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`.
			
			
This commit is contained in:
		| @@ -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 | ||||
|      */ | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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 | ||||
|      */ | ||||
|   | ||||
| @@ -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 | ||||
|      */ | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|   | ||||
| @@ -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', | ||||
|   | ||||
| @@ -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 }} | ||||
| @@ -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; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,27 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Chill\DocGeneratorBundle\Service\Generator; | ||||
|  | ||||
| use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Symfony\Component\HttpFoundation\File\File; | ||||
|  | ||||
| interface GeneratorInterface | ||||
| { | ||||
|     /** | ||||
|      * @template T of File|null | ||||
|      * @template B of bool | ||||
|      * @param B $isTest | ||||
|      * @param (B is true ? T : null) $testFile | ||||
|      * @psalm-return (B is true ? string : null) | ||||
|      * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface|\Throwable | ||||
|      */ | ||||
|     public function generateDocFromTemplate( | ||||
|         DocGeneratorTemplate $template, | ||||
|         int                  $entityId, | ||||
|         array                $contextGenerationDataNormalized, | ||||
|         ?StoredObject        $destinationStoredObject = null, | ||||
|         bool                 $isTest = false, | ||||
|         ?File                $testFile = null | ||||
|     ): ?string; | ||||
| } | ||||
| @@ -0,0 +1,156 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Chill\DocGeneratorBundle\Service\Messenger; | ||||
|  | ||||
| use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository; | ||||
| use Chill\DocGeneratorBundle\Service\Generator\GeneratorException; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Repository\StoredObjectRepository; | ||||
| use Chill\MainBundle\Repository\UserRepositoryInterface; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Psr\Log\LoggerInterface; | ||||
| use Symfony\Bridge\Twig\Mime\TemplatedEmail; | ||||
| use Symfony\Component\EventDispatcher\EventSubscriberInterface; | ||||
| use Symfony\Component\Mailer\MailerInterface; | ||||
| use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; | ||||
| use Symfony\Contracts\Translation\TranslatorInterface; | ||||
|  | ||||
| final class OnGenerationFails implements EventSubscriberInterface | ||||
| { | ||||
|     private DocGeneratorTemplateRepository $docGeneratorTemplateRepository; | ||||
|  | ||||
|     private EntityManagerInterface $entityManager; | ||||
|  | ||||
|     private LoggerInterface $logger; | ||||
|  | ||||
|     private MailerInterface $mailer; | ||||
|  | ||||
|     private StoredObjectRepository $storedObjectRepository; | ||||
|  | ||||
|     private TranslatorInterface $translator; | ||||
|  | ||||
|     private UserRepositoryInterface $userRepository; | ||||
|  | ||||
|     const LOG_PREFIX = '[docgen failed] '; | ||||
|  | ||||
|     /** | ||||
|      * @param DocGeneratorTemplateRepository $docGeneratorTemplateRepository | ||||
|      * @param EntityManagerInterface $entityManager | ||||
|      * @param LoggerInterface $logger | ||||
|      * @param MailerInterface $mailer | ||||
|      * @param StoredObjectRepository $storedObjectRepository | ||||
|      * @param TranslatorInterface $translator | ||||
|      * @param UserRepositoryInterface $userRepository | ||||
|      */ | ||||
|     public function __construct( | ||||
|         DocGeneratorTemplateRepository $docGeneratorTemplateRepository, | ||||
|         EntityManagerInterface $entityManager, | ||||
|         LoggerInterface $logger, | ||||
|         MailerInterface $mailer, | ||||
|         StoredObjectRepository $storedObjectRepository, | ||||
|         TranslatorInterface $translator, | ||||
|         UserRepositoryInterface $userRepository | ||||
|     ) { | ||||
|         $this->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); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,66 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Chill\DocGeneratorBundle\Service\Messenger; | ||||
|  | ||||
| use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository; | ||||
| use Chill\DocGeneratorBundle\Service\Generator\Generator; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Repository\StoredObjectRepository; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; | ||||
| use Symfony\Component\Messenger\Handler\MessageHandlerInterface; | ||||
|  | ||||
| /** | ||||
|  * Handle the request of document generation | ||||
|  */ | ||||
| class RequestGenerationHandler implements MessageHandlerInterface | ||||
| { | ||||
|     private StoredObjectRepository $storedObjectRepository; | ||||
|  | ||||
|     private DocGeneratorTemplateRepository $docGeneratorTemplateRepository; | ||||
|  | ||||
|     private EntityManagerInterface $entityManager; | ||||
|  | ||||
|     private Generator $generator; | ||||
|  | ||||
|     public const AUTHORIZED_TRIALS = 5; | ||||
|  | ||||
|     public function __construct( | ||||
|         DocGeneratorTemplateRepository $docGeneratorTemplateRepository, | ||||
|         EntityManagerInterface $entityManager, | ||||
|         Generator $generator, | ||||
|         StoredObjectRepository $storedObjectRepository, | ||||
|     ) { | ||||
|         $this->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 | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| @@ -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; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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' | ||||
|   | ||||
| @@ -0,0 +1,47 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Chill\Migrations\DocGenerator; | ||||
|  | ||||
| use Doctrine\DBAL\Schema\Schema; | ||||
| use Doctrine\Migrations\AbstractMigration; | ||||
|  | ||||
| final class Version20230214192558 extends AbstractMigration | ||||
| { | ||||
|     public function getDescription(): string | ||||
|     { | ||||
|         return 'Add status, template_id and fix defaults on chill_doc.stored_object'; | ||||
|     } | ||||
|  | ||||
|     public function up(Schema $schema): void | ||||
|     { | ||||
|         $this->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'); | ||||
|     } | ||||
| } | ||||
| @@ -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 | ||||
|         ); | ||||
|     } | ||||
|   | ||||
| @@ -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 | ||||
| Template file: Fichier modèle | ||||
|   | ||||
| @@ -0,0 +1,39 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Controller; | ||||
|  | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Symfony\Component\HttpFoundation\JsonResponse; | ||||
| use Symfony\Component\HttpFoundation\Response; | ||||
| use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; | ||||
| use Symfony\Component\Routing\Annotation\Route; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
|  | ||||
| class StoredObjectApiController | ||||
| { | ||||
|     private Security $security; | ||||
|  | ||||
|     public function __construct(Security $security) | ||||
|     { | ||||
|         $this->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(), | ||||
|             ] | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| @@ -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(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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: '<document-action-buttons-group :can-edit="canEdit" :filename="filename" :stored-object="storedObject" :small="small"></document-action-buttons-group>', | ||||
|        template: '<document-action-buttons-group :can-edit="canEdit" :filename="filename" :stored-object="storedObject" :small="small" @on-stored-object-status-change="onStoredObjectStatusChange"></document-action-buttons-group>', | ||||
|        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); | ||||
|   | ||||
| @@ -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, | ||||
| } | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| <template> | ||||
|   <div class="dropdown"> | ||||
|   <div v-if="'ready' === props.storedObject.status" class="dropdown"> | ||||
|     <button :class="Object.assign({'btn': true, 'btn-outline-primary': true, 'dropdown-toggle': true, small: props.small})" type="button" data-bs-toggle="dropdown" aria-expanded="false"> | ||||
|       Actions | ||||
|     </button> | ||||
| @@ -15,16 +15,27 @@ | ||||
|       </li> | ||||
|     </ul> | ||||
|   </div> | ||||
|   <div v-else-if="'pending' === props.storedObject.status"> | ||||
|     <div class="btn btn-outline-info">Génération en cours</div> | ||||
|   </div> | ||||
|   <div v-else-if="'failure' === props.storedObject.status"> | ||||
|     <div class="btn btn-outline-danger">La génération a échoué</div> | ||||
|   </div> | ||||
|  | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
|  | ||||
| import {onMounted} from "vue"; | ||||
| import ConvertButton from "./StoredObjectButton/ConvertButton.vue"; | ||||
| import DownloadButton from "./StoredObjectButton/DownloadButton.vue"; | ||||
| import WopiEditButton from "./StoredObjectButton/WopiEditButton.vue"; | ||||
| import {is_extension_editable, is_extension_viewable} from "./StoredObjectButton/helpers"; | ||||
| import {StoredObject, WopiEditButtonExecutableBeforeLeaveFunction} from "../types"; | ||||
| import {is_extension_editable, is_extension_viewable, is_object_ready} from "./StoredObjectButton/helpers"; | ||||
| import { | ||||
|   StoredObject, | ||||
|   StoredObjectStatusChange, | ||||
|   WopiEditButtonExecutableBeforeLeaveFunction | ||||
| } from "../types"; | ||||
|  | ||||
| interface DocumentActionButtonsGroupConfig { | ||||
|   storedObject: StoredObject, | ||||
| @@ -48,6 +59,10 @@ interface DocumentActionButtonsGroupConfig { | ||||
|   executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction, | ||||
| } | ||||
|  | ||||
| const emit = defineEmits<{ | ||||
|   (e: 'onStoredObjectStatusChange', newStatus: StoredObjectStatusChange): void | ||||
| }>(); | ||||
|  | ||||
| const props = withDefaults(defineProps<DocumentActionButtonsGroupConfig>(), { | ||||
|   small: false, | ||||
|   canEdit: true, | ||||
| @@ -56,6 +71,51 @@ const props = withDefaults(defineProps<DocumentActionButtonsGroupConfig>(), { | ||||
|   returnPath: window.location.pathname + window.location.search + window.location.hash, | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * counter for the number of times that we check for a new status | ||||
|  */ | ||||
| let tryiesForReady = 0; | ||||
|  | ||||
| /** | ||||
|  * how many times we may check for a new status, once loaded | ||||
|  */ | ||||
| const maxTryiesForReady = 120; | ||||
|  | ||||
| const checkForReady = function(): void { | ||||
|   if ( | ||||
|     'ready' === props.storedObject.status | ||||
|     || 'failure' === props.storedObject.status | ||||
|     // stop reloading if the page stays opened for a long time | ||||
|     || tryiesForReady > maxTryiesForReady | ||||
|   ) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   tryiesForReady = tryiesForReady + 1; | ||||
|  | ||||
|   setTimeout(onObjectNewStatusCallback, 5000); | ||||
| }; | ||||
|  | ||||
| const onObjectNewStatusCallback = async function(): Promise<void> { | ||||
|   const new_status = await is_object_ready(props.storedObject); | ||||
|   if (props.storedObject.status !== new_status.status) { | ||||
|     emit('onStoredObjectStatusChange', new_status); | ||||
|     return Promise.resolve(); | ||||
|   } else if ('failure' === new_status.status) { | ||||
|     return Promise.resolve(); | ||||
|   } | ||||
|  | ||||
|   if ('ready' !== new_status.status) { | ||||
|     // we check for new status, unless it is ready | ||||
|     checkForReady(); | ||||
|   } | ||||
|  | ||||
|   return Promise.resolve(); | ||||
| }; | ||||
|  | ||||
| onMounted(() => { | ||||
|   checkForReady(); | ||||
| }) | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import {StoredObject, StoredObjectStatus, StoredObjectStatusChange} from "../../types"; | ||||
|  | ||||
| const MIMES_EDIT = new Set([ | ||||
|   'application/vnd.ms-powerpoint', | ||||
| @@ -168,6 +169,18 @@ async function download_and_decrypt_doc(urlGenerator: string, keyData: JsonWebKe | ||||
|    } | ||||
| } | ||||
|  | ||||
| async function is_object_ready(storedObject: StoredObject): Promise<StoredObjectStatusChange> | ||||
| { | ||||
|     const new_status_response = await window | ||||
|       .fetch( `/api/1.0/doc-store/stored-object/${storedObject.uuid}/is-ready`); | ||||
|  | ||||
|     if (!new_status_response.ok) { | ||||
|       throw new Error("could not fetch the new status"); | ||||
|     } | ||||
|  | ||||
|     return await new_status_response.json(); | ||||
| } | ||||
|  | ||||
| export { | ||||
|   build_convert_link, | ||||
|   build_download_info_link, | ||||
| @@ -176,4 +189,5 @@ export { | ||||
|   download_doc, | ||||
|   is_extension_editable, | ||||
|   is_extension_viewable, | ||||
|   is_object_ready, | ||||
| }; | ||||
|   | ||||
| @@ -9,11 +9,6 @@ | ||||
|         <dt>{{ 'Title'|trans }}</dt> | ||||
|         <dd>{{ document.title }}</dd> | ||||
|  | ||||
|         {% if document.scope is not null %} | ||||
|             <dt>{{ 'Scope' | trans }}</dt> | ||||
|             <dd>{{ document.scope.name | localize_translatable_string }}</dd> | ||||
|         {% endif %} | ||||
|  | ||||
|         <dt>{{ 'Category'|trans }}</dt> | ||||
|         <dd>{{ document.category.name|localize_translatable_string }}</dd> | ||||
|  | ||||
|   | ||||
| @@ -5,18 +5,25 @@ | ||||
| <div class="item-bloc"> | ||||
|     <div class="item-row"> | ||||
|         <div class="item-col" style="width: unset"> | ||||
|             {% if document.object.isPending %} | ||||
|                 <div class="badge text-bg-info" data-docgen-is-pending="{{ document.object.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div> | ||||
|             {% elseif document.object.isFailure %} | ||||
|                 <div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div> | ||||
|             {% endif %} | ||||
|             <div class="denomination h2"> | ||||
|                 {{ document.title }} | ||||
|             </div> | ||||
|             <div> | ||||
|                 {{ mm.mimeIcon(document.object.type) }} | ||||
|             </div> | ||||
|             {% if document.object.type is not empty %} | ||||
|                 <div> | ||||
|                     {{ mm.mimeIcon(document.object.type) }} | ||||
|                 </div> | ||||
|             {% endif %} | ||||
|             <div> | ||||
|                 <p>{{ document.category.name|localize_translatable_string }}</p> | ||||
|             </div> | ||||
|             {% if document.template is not null %} | ||||
|             {% if document.object.hasTemplate %} | ||||
|                 <div> | ||||
|                     <p>{{ document.template.name.fr }}</p> | ||||
|                     <p>{{ document.object.template.name|localize_translatable_string }}</p> | ||||
|                 </div> | ||||
|             {% endif %} | ||||
|         </div> | ||||
|   | ||||
| @@ -0,0 +1,26 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Chill\Migrations\DocStore; | ||||
|  | ||||
| use Doctrine\DBAL\Schema\Schema; | ||||
| use Doctrine\Migrations\AbstractMigration; | ||||
|  | ||||
| final class Version20230227161327 extends AbstractMigration | ||||
| { | ||||
|     public function getDescription(): string | ||||
|     { | ||||
|         return 'Add a generation counter on doc store'; | ||||
|     } | ||||
|  | ||||
|     public function up(Schema $schema): void | ||||
|     { | ||||
|         $this->addSql('ALTER TABLE chill_doc.stored_object ADD generationTrialsCounter INT DEFAULT 0 NOT NULL;'); | ||||
|     } | ||||
|  | ||||
|     public function down(Schema $schema): void | ||||
|     { | ||||
|         $this->addSql('ALTER TABLE chill_doc.stored_object DROP generationTrialsCounter'); | ||||
|     } | ||||
| } | ||||
| @@ -118,7 +118,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface | ||||
|      * Return true if the phonenumber is a landline or voip phone.  Return always true | ||||
|      * if the validation is not configured. | ||||
|      * | ||||
|      * @param string $phonenumber | ||||
|      * @param string|PhoneNumber $phonenumber | ||||
|      */ | ||||
|     public function isValidPhonenumberAny($phonenumber): bool | ||||
|     { | ||||
| @@ -138,7 +138,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface | ||||
|      * Return true if the phonenumber is a landline or voip phone.  Return always true | ||||
|      * if the validation is not configured. | ||||
|      * | ||||
|      * @param string $phonenumber | ||||
|      * @param string|PhoneNumber $phonenumber | ||||
|      */ | ||||
|     public function isValidPhonenumberLandOrVoip($phonenumber): bool | ||||
|     { | ||||
| @@ -159,7 +159,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface | ||||
|      * REturn true if the phonenumber is a mobile phone. Return always true | ||||
|      * if the validation is not configured. | ||||
|      * | ||||
|      * @param string $phonenumber | ||||
|      * @param string|PhoneNumber $phonenumber | ||||
|      */ | ||||
|     public function isValidPhonenumberMobile($phonenumber): bool | ||||
|     { | ||||
| @@ -182,6 +182,10 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         if ($phonenumber instanceof PhoneNumber) { | ||||
|             $phonenumber = (string) $phonenumber; | ||||
|         } | ||||
|  | ||||
|         // filter only number | ||||
|         $filtered = preg_replace('/[^0-9]/', '', $phonenumber); | ||||
|  | ||||
|   | ||||
| @@ -116,6 +116,7 @@ | ||||
|                          :filename="d.title" | ||||
|                          :can-edit="true" | ||||
|                          :execute-before-leave="submitBeforeLeaveToEditor" | ||||
|                          @on-stored-object-status-change="onStatusDocumentChanged" | ||||
|                      ></document-action-buttons-group> | ||||
|                    </li> | ||||
|                    <li v-if="d.workflows.length === 0"> | ||||
| @@ -338,6 +339,10 @@ export default { | ||||
|             this.$store.commit('removeDocument', {key: this.evaluation.key, document: document}); | ||||
|          } | ||||
|       }, | ||||
|       onStatusDocumentChanged(newStatus) { | ||||
|         console.log('onStatusDocumentChanged', newStatus); | ||||
|         this.$store.commit('statusDocumentChanged', {key: this.evaluation.key, newStatus: newStatus}); | ||||
|       }, | ||||
|       goToGenerateWorkflowEvaluationDocument({event, link, workflowName, payload}) { | ||||
|          const callback = (data) => { | ||||
|             let evaluation = data.accompanyingPeriodWorkEvaluations.find(e => e.key === this.evaluation.key); | ||||
|   | ||||
| @@ -360,7 +360,22 @@ const store = createStore({ | ||||
|         state.evaluationsPicked.find(e => e.key === payload.evaluationKey) | ||||
|           .documents.find(d => d.id === payload.id).title = payload.title; | ||||
|       } | ||||
|     } | ||||
|     }, | ||||
|     statusDocumentChanged(state, {newStatus, key}) { | ||||
|       const e = state.evaluationsPicked.find(e => e.key === key); | ||||
|       if (typeof e === 'undefined') { | ||||
|         console.error('evaluation not found for given key', {key}); | ||||
|       } | ||||
|  | ||||
|       const doc = e.documents.find(d => d.storedObject?.id === newStatus.id); | ||||
|       if (typeof doc === 'undefined') { | ||||
|         console.error('document not found', {newStatus}); | ||||
|       } | ||||
|  | ||||
|       doc.storedObject.status = newStatus.status; | ||||
|       doc.storedObject.type = newStatus.type; | ||||
|       doc.storedObject.filename = newStatus.filename; | ||||
|     }, | ||||
|   }, | ||||
|   actions: { | ||||
|     updateThirdParty({ commit }, payload) { | ||||
|   | ||||
| @@ -24,6 +24,7 @@ use Chill\MainBundle\Templating\TranslatableStringHelperInterface; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Chill\PersonBundle\Repository\PersonRepository; | ||||
| use Chill\PersonBundle\Templating\Entity\PersonRenderInterface; | ||||
| use DateTime; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| @@ -51,6 +52,8 @@ class AccompanyingPeriodContext implements | ||||
|  | ||||
|     private PersonRenderInterface $personRender; | ||||
|  | ||||
|     private PersonRepository $personRepository; | ||||
|  | ||||
|     private TranslatableStringHelperInterface $translatableStringHelper; | ||||
|  | ||||
|     private TranslatorInterface $translator; | ||||
| @@ -61,6 +64,7 @@ class AccompanyingPeriodContext implements | ||||
|         TranslatableStringHelperInterface $translatableStringHelper, | ||||
|         EntityManagerInterface $em, | ||||
|         PersonRenderInterface $personRender, | ||||
|         PersonRepository $personRepository, | ||||
|         TranslatorInterface $translator, | ||||
|         BaseContextData $baseContextData | ||||
|     ) { | ||||
| @@ -69,6 +73,7 @@ class AccompanyingPeriodContext implements | ||||
|         $this->translatableStringHelper = $translatableStringHelper; | ||||
|         $this->em = $em; | ||||
|         $this->personRender = $personRender; | ||||
|         $this->personRepository = $personRepository; | ||||
|         $this->translator = $translator; | ||||
|         $this->baseContextData = $baseContextData; | ||||
|     } | ||||
| @@ -256,6 +261,31 @@ class AccompanyingPeriodContext 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() : null; | ||||
|         } | ||||
|  | ||||
|         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 AccompanyingPeriod $entity | ||||
|      */ | ||||
|   | ||||
| @@ -11,6 +11,7 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Chill\PersonBundle\Service\DocGenerator; | ||||
|  | ||||
| use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface; | ||||
| use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; | ||||
| @@ -18,7 +19,13 @@ use Symfony\Component\Form\FormBuilderInterface; | ||||
| use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; | ||||
| use Symfony\Component\Serializer\Normalizer\NormalizerInterface; | ||||
|  | ||||
| class AccompanyingPeriodWorkContext | ||||
| /** | ||||
|  * Generate a context for an @link{AccompanyingPeriodWork}. | ||||
|  * | ||||
|  * Although there isn't any document associated to AccompanyingPeriodWork, this context | ||||
|  * is use by @link{AccompanyingPeriodWorkEvaluationContext}. | ||||
|  */ | ||||
| class AccompanyingPeriodWorkContext implements DocGeneratorContextWithPublicFormInterface | ||||
| { | ||||
|     private NormalizerInterface $normalizer; | ||||
|  | ||||
| @@ -109,8 +116,18 @@ class AccompanyingPeriodWorkContext | ||||
|         return $this->periodContext->hasPublicForm($template, $entity); | ||||
|     } | ||||
|  | ||||
|     public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array | ||||
|     { | ||||
|         return $this->periodContext->contextGenerationDataNormalize($template, $entity, $data); | ||||
|     } | ||||
|  | ||||
|     public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array | ||||
|     { | ||||
|         return $this->periodContext->contextGenerationDataDenormalize($template, $entity, $data); | ||||
|     } | ||||
|  | ||||
|     public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void | ||||
|     { | ||||
|         // TODO: Implement storeGenerated() method. | ||||
|         // currently, no document associated with a AccompanyingPeriodWork | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -174,6 +174,18 @@ class AccompanyingPeriodWorkEvaluationContext implements | ||||
|             ->hasPublicForm($template, $entity->getAccompanyingPeriodWork()); | ||||
|     } | ||||
|  | ||||
|     public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array | ||||
|     { | ||||
|         return $this->accompanyingPeriodWorkContext | ||||
|             ->contextGenerationDataNormalize($template, $entity, $data); | ||||
|     } | ||||
|  | ||||
|     public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array | ||||
|     { | ||||
|         return $this->accompanyingPeriodWorkContext | ||||
|             ->contextGenerationDataDenormalize($template, $entity, $data); | ||||
|     } | ||||
|  | ||||
|     public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void | ||||
|     { | ||||
|         $doc = new AccompanyingPeriodWorkEvaluationDocument(); | ||||
|   | ||||
| @@ -21,11 +21,13 @@ use Chill\DocStoreBundle\Repository\DocumentCategoryRepository; | ||||
| use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter; | ||||
| use Chill\MainBundle\Entity\Scope; | ||||
| use Chill\MainBundle\Form\Type\ScopePickerType; | ||||
| use Chill\MainBundle\Repository\ScopeRepositoryInterface; | ||||
| use Chill\MainBundle\Security\Authorization\AuthorizationHelper; | ||||
| use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface; | ||||
| use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; | ||||
| use Chill\MainBundle\Templating\TranslatableStringHelperInterface; | ||||
| use Chill\PersonBundle\Entity\Person; | ||||
| use Chill\PersonBundle\Repository\PersonRepository; | ||||
| use DateTime; | ||||
| use Doctrine\ORM\EntityManagerInterface; | ||||
| use Doctrine\ORM\EntityRepository; | ||||
| @@ -55,6 +57,8 @@ final class PersonContext implements PersonContextInterface | ||||
|  | ||||
|     private NormalizerInterface $normalizer; | ||||
|  | ||||
|     private ScopeRepositoryInterface $scopeRepository; | ||||
|  | ||||
|     private Security $security; | ||||
|  | ||||
|     private bool $showScopes; | ||||
| @@ -71,6 +75,7 @@ final class PersonContext implements PersonContextInterface | ||||
|         EntityManagerInterface $em, | ||||
|         NormalizerInterface $normalizer, | ||||
|         ParameterBagInterface $parameterBag, | ||||
|         ScopeRepositoryInterface $scopeRepository, | ||||
|         Security $security, | ||||
|         TranslatorInterface $translator, | ||||
|         TranslatableStringHelperInterface $translatableStringHelper | ||||
| @@ -81,6 +86,7 @@ final class PersonContext implements PersonContextInterface | ||||
|         $this->documentCategoryRepository = $documentCategoryRepository; | ||||
|         $this->em = $em; | ||||
|         $this->normalizer = $normalizer; | ||||
|         $this->scopeRepository = $scopeRepository; | ||||
|         $this->security = $security; | ||||
|         $this->showScopes = $parameterBag->get('chill_main')['acl']['form_show_scopes']; | ||||
|         $this->translator = $translator; | ||||
| @@ -211,6 +217,38 @@ final class PersonContext implements PersonContextInterface | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param Person $entity | ||||
|      */ | ||||
|     public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array | ||||
|     { | ||||
|         $scope = $data['scope'] ?? null; | ||||
|  | ||||
|         return [ | ||||
|             'title' => $data['title'] ?? '', | ||||
|             'scope_id' => $scope instanceof Scope ? $scope->getId() : null, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param Person $entity | ||||
|      */ | ||||
|     public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array | ||||
|     { | ||||
|         if (!isset($data['scope'])) { | ||||
|             $scope = null; | ||||
|         } else { | ||||
|             if (null === $scope = $this->scopeRepository->find($data['scope'])) { | ||||
|                 throw new \UnexpectedValueException('scope not found'); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return [ | ||||
|             'title' => $data['title'] ?? '', | ||||
|             'scope' => $scope, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param Person $entity | ||||
|      */ | ||||
|   | ||||
| @@ -48,6 +48,10 @@ interface PersonContextInterface extends DocGeneratorContextWithAdminFormInterfa | ||||
|      */ | ||||
|     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 Person $entity | ||||
|      */ | ||||
|   | ||||
| @@ -17,6 +17,7 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\ThirdPartyBundle\Entity\ThirdParty; | ||||
| use Chill\ThirdPartyBundle\Form\Type\PickThirdpartyDynamicType; | ||||
| use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository; | ||||
| use Symfony\Component\Form\Extension\Core\Type\TextType; | ||||
| use Symfony\Component\Form\FormBuilderInterface; | ||||
| use Symfony\Component\Serializer\Normalizer\NormalizerInterface; | ||||
| @@ -30,12 +31,16 @@ class PersonContextWithThirdParty implements DocGeneratorContextWithAdminFormInt | ||||
|  | ||||
|     private PersonContextInterface $personContext; | ||||
|  | ||||
|     private ThirdPartyRepository $thirdPartyRepository; | ||||
|  | ||||
|     public function __construct( | ||||
|         PersonContextInterface $personContext, | ||||
|         NormalizerInterface $normalizer | ||||
|         NormalizerInterface $normalizer, | ||||
|         ThirdPartyRepository $thirdPartyRepository | ||||
|     ) { | ||||
|         $this->personContext = $personContext; | ||||
|         $this->normalizer = $normalizer; | ||||
|         $this->thirdPartyRepository = $thirdPartyRepository; | ||||
|     } | ||||
|  | ||||
|     public function adminFormReverseTransform(array $data): array | ||||
| @@ -123,6 +128,26 @@ class PersonContextWithThirdParty implements DocGeneratorContextWithAdminFormInt | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array | ||||
|     { | ||||
|         return array_merge( | ||||
|             [ | ||||
|                 'thirdParty' => null === $data['thirdParty'] ? null : $data['thirdParty']->getId(), | ||||
|             ], | ||||
|             $this->personContext->contextGenerationDataNormalize($template, $entity, $data), | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array | ||||
|     { | ||||
|         return array_merge( | ||||
|             [ | ||||
|                 'thirdParty' => null === $data['thirdParty'] ? null : $this->thirdPartyRepository->find($data['thirdParty']), | ||||
|             ], | ||||
|             $this->personContext->contextGenerationDataDenormalize($template, $entity, $data), | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void | ||||
|     { | ||||
|         $this->personContext->storeGenerated($template, $storedObject, $entity, $contextGenerationData); | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| // this file loads all assets from the Chill person bundle | ||||
| module.exports = function(encore, entries) | ||||
| { | ||||
| module.exports = function(encore, entries) { | ||||
|     encore.addEntry('page_wopi_editor', __dirname + '/src/Resources/public/page/editor/index.js'); | ||||
|     //home/julien/dev/département-vendee/chill/vendor/chill-project/chill-bundles/src/Bundle/ChillWopiBundle/src/Resources/public/page/editor/index.js | ||||
|     encore.addEntry('mod_reload_page', __dirname + '/src/Resources/public/module/pending/index'); | ||||
| }; | ||||
|   | ||||
| @@ -26,6 +26,10 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; | ||||
| use Symfony\Component\Routing\Generator\UrlGeneratorInterface; | ||||
| use Symfony\Component\Routing\RouterInterface; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
| use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; | ||||
| use Symfony\Component\Serializer\Normalizer\NormalizerInterface; | ||||
| use Symfony\Component\Templating\EngineInterface; | ||||
| use Twig\Environment; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
| @@ -35,6 +39,8 @@ final class Editor | ||||
| { | ||||
|     private DocumentManagerInterface $documentManager; | ||||
|  | ||||
|     private EngineInterface $engine; | ||||
|  | ||||
|     private JWTTokenManagerInterface $JWTTokenManager; | ||||
|  | ||||
|     private Psr17Interface $psr17; | ||||
| @@ -49,18 +55,24 @@ final class Editor | ||||
|  | ||||
|     private DiscoveryInterface $wopiDiscovery; | ||||
|  | ||||
|     private NormalizerInterface $normalizer; | ||||
|  | ||||
|     public function __construct( | ||||
|         ConfigurationInterface $wopiConfiguration, | ||||
|         DiscoveryInterface $wopiDiscovery, | ||||
|         DocumentManagerInterface $documentManager, | ||||
|         EngineInterface $engine, | ||||
|         JWTTokenManagerInterface $JWTTokenManager, | ||||
|         NormalizerInterface $normalizer, | ||||
|         ResponderInterface $responder, | ||||
|         Security $security, | ||||
|         Psr17Interface $psr17, | ||||
|         RouterInterface $router | ||||
|     ) { | ||||
|         $this->documentManager = $documentManager; | ||||
|         $this->engine = $engine; | ||||
|         $this->JWTTokenManager = $JWTTokenManager; | ||||
|         $this->normalizer = $normalizer; | ||||
|         $this->wopiConfiguration = $wopiConfiguration; | ||||
|         $this->wopiDiscovery = $wopiDiscovery; | ||||
|         $this->responder = $responder; | ||||
| @@ -87,6 +99,22 @@ final class Editor | ||||
|             throw new NotFoundHttpException(sprintf('Unable to find object %s', $fileId)); | ||||
|         } | ||||
|  | ||||
|         if (StoredObject::STATUS_FAILURE === $storedObject->getStatus()) { | ||||
|             return new Response( | ||||
|                 $this->engine | ||||
|                     ->render('@ChillWopi/Editor/stored_object_failure.html.twig') | ||||
|             ); | ||||
|         } elseif (StoredObject::STATUS_PENDING === $storedObject->getStatus()) { | ||||
|             return new Response( | ||||
|                 $this->engine | ||||
|                     ->render('@ChillWopi/Editor/stored_object_pending.html.twig', [ | ||||
|                         'storedObject' => $this->normalizer->normalize($storedObject, 'json', [ | ||||
|                             AbstractNormalizer::GROUPS => ['read'] | ||||
|                         ]) | ||||
|                     ]) | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         if ([] === $discoverExtension = $this->wopiDiscovery->discoverMimeType($storedObject->getType())) { | ||||
|             throw new Exception(sprintf('Unable to find mime type %s', $storedObject->getType())); | ||||
|         } | ||||
|   | ||||
| @@ -0,0 +1,40 @@ | ||||
| import {is_object_ready} from "../../../../../../ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/helpers"; | ||||
| import { | ||||
|     StoredObject, | ||||
|     StoredObjectStatus, | ||||
|     StoredObjectStatusChange | ||||
| } from "../../../../../../ChillDocStoreBundle/Resources/public/types"; | ||||
|  | ||||
| async function reload_if_needed(stored_object: StoredObject, i: number): Promise<void> { | ||||
|     let current_status = await is_object_ready(stored_object); | ||||
|  | ||||
|     if (stored_object.status !== current_status.status) { | ||||
|         window.location.reload(); | ||||
|     } | ||||
|     wait_before_reload(stored_object, i + 1); | ||||
|     return Promise.resolve(); | ||||
| } | ||||
|  | ||||
| function wait_before_reload(stored_object: StoredObject, i: number): void { | ||||
|     /** | ||||
|      * value of the timeout. Set to 5 sec during the first 10 minutes, then every 1 minute | ||||
|      */ | ||||
|     let timeout = i < 1200 ? 5000 : 60000; | ||||
|  | ||||
|     setTimeout( | ||||
|        reload_if_needed, | ||||
|        timeout, | ||||
|        stored_object, | ||||
|        i | ||||
|    ); | ||||
| } | ||||
|  | ||||
| window.addEventListener('DOMContentLoaded', async function (e) { | ||||
|     if (undefined === (<any>window).stored_object) { | ||||
|         console.error('window.stored_object is undefined'); | ||||
|         throw Error('window.stored_object is undefined'); | ||||
|     } | ||||
|  | ||||
|     let stored_object = JSON.parse((<any>window).stored_object) as StoredObject; | ||||
|     reload_if_needed(stored_object, 0); | ||||
| }); | ||||
| @@ -0,0 +1,15 @@ | ||||
| {% extends 'ChillMainBundle::layout.html.twig' %} | ||||
|  | ||||
| {% block content %} | ||||
|     <div class="alert alert-danger"> | ||||
|         {{ 'docgen.Doc generation failed'|trans }} | ||||
|     </div> | ||||
|  | ||||
|     <ul class="sticky-form-buttons record_actions"> | ||||
|         <li class="cancel"> | ||||
|             <a href="{{ chill_return_path_or('chill_main_homepage') }}" class="btn btn-cancel"> | ||||
|                 {{ 'Cancel'|trans|chill_return_path_label }} | ||||
|             </a> | ||||
|         </li> | ||||
|     </ul> | ||||
| {% endblock %} | ||||
| @@ -0,0 +1,36 @@ | ||||
| {% extends '@ChillMain/layout.html.twig' %} | ||||
|  | ||||
| {% block js %} | ||||
|     <script type="application/javascript"> | ||||
|         window.stored_object = '{{ storedObject|json_encode|escape('js') }}' | ||||
|     </script> | ||||
|     {{ encore_entry_script_tags('mod_reload_page') }} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block css %} | ||||
|     {{ encore_entry_link_tags('mod_reload_page') }} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block content %} | ||||
|     <div class="alert alert-danger text-center"> | ||||
|         <div> | ||||
|             <p> | ||||
|                 {{ 'docgen.Doc generation is pending'|trans }} | ||||
|             </p> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|             <i class="fa fa-cog fa-spin fa-3x fa-fw"></i> | ||||
|             <span class="sr-only">Loading...</span> | ||||
|         </div> | ||||
|  | ||||
|     </div> | ||||
|  | ||||
|     <ul class="sticky-form-buttons record_actions"> | ||||
|         <li class="cancel"> | ||||
|             <a href="{{ chill_return_path_or('chill_main_homepage') }}" class="btn btn-cancel"> | ||||
|                 {{ 'docgen.Come back later'|trans|chill_return_path_label }} | ||||
|             </a> | ||||
|         </li> | ||||
|     </ul> | ||||
| {% endblock content %} | ||||
		Reference in New Issue
	
	Block a user