Improve admin UX for configuration of document template (document generation)

This commit is contained in:
2024-03-26 17:06:49 +00:00
parent 9ff7aef3fc
commit fc88a5f40d
33 changed files with 1116 additions and 181 deletions

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocGeneratorBundle\Service\Messenger;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent;
/**
* The OnAfterMessageHandledClearStoredObjectCache class is an event subscriber that clears the stored object cache
* after a specific message is handled or fails.
*/
final readonly class OnAfterMessageHandledClearStoredObjectCache implements EventSubscriberInterface
{
public function __construct(
private StoredObjectManagerInterface $storedObjectManager,
private LoggerInterface $logger,
) {
}
public static function getSubscribedEvents()
{
return [
WorkerMessageHandledEvent::class => [
['afterHandling', 0],
],
WorkerMessageFailedEvent::class => [
['afterFails', 0],
],
];
}
public function afterHandling(WorkerMessageHandledEvent $event): void
{
if ($event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) {
$this->clearStoredObjectCache();
}
}
public function afterFails(WorkerMessageFailedEvent $event): void
{
if ($event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) {
$this->clearStoredObjectCache();
}
}
private function clearStoredObjectCache(): void
{
$this->logger->debug('clear the cache after generation of a document');
$this->storedObjectManager->clearCache();
}
}

View File

@@ -11,10 +11,11 @@ declare(strict_types=1);
namespace Chill\DocGeneratorBundle\Service\Messenger;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface;
use Chill\DocGeneratorBundle\Service\Generator\GeneratorException;
use Chill\DocGeneratorBundle\tests\Service\Messenger\OnGenerationFailsTest;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
@@ -24,12 +25,22 @@ use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @see OnGenerationFailsTest for test suite
*/
final readonly class OnGenerationFails implements EventSubscriberInterface
{
public const LOG_PREFIX = '[docgen failed] ';
public function __construct(private DocGeneratorTemplateRepository $docGeneratorTemplateRepository, private EntityManagerInterface $entityManager, private LoggerInterface $logger, private MailerInterface $mailer, private StoredObjectRepository $storedObjectRepository, private TranslatorInterface $translator, private UserRepositoryInterface $userRepository)
{
public function __construct(
private DocGeneratorTemplateRepositoryInterface $docGeneratorTemplateRepository,
private EntityManagerInterface $entityManager,
private LoggerInterface $logger,
private MailerInterface $mailer,
private StoredObjectRepositoryInterface $storedObjectRepository,
private TranslatorInterface $translator,
private UserRepositoryInterface $userRepository
) {
}
public static function getSubscribedEvents()
@@ -45,13 +56,12 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
return;
}
if (!$event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) {
$message = $event->getEnvelope()->getMessage();
if (!$message instanceof RequestGenerationMessage) {
return;
}
/** @var RequestGenerationMessage $message */
$message = $event->getEnvelope()->getMessage();
$this->logger->error(self::LOG_PREFIX.'Docgen failed', [
'stored_object_id' => $message->getDestinationStoredObjectId(),
'entity_id' => $message->getEntityId(),
@@ -79,16 +89,8 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
private function warnCreator(RequestGenerationMessage $message, WorkerMessageFailedEvent $event): void
{
$creatorId = $message->getCreatorId();
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()]);
if (null === $message->getSendResultToEmail() || '' === $message->getSendResultToEmail()) {
$this->logger->info(self::LOG_PREFIX.'No email associated with this request generation');
return;
}
@@ -96,7 +98,7 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
// if the exception is not a GeneratorException, we try the previous one...
$throwable = $event->getThrowable();
if (!$throwable instanceof GeneratorException) {
$throwable = $throwable->getPrevious();
$throwable = $throwable->getPrevious() ?? $throwable;
}
if ($throwable instanceof GeneratorException) {
@@ -111,8 +113,14 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
return;
}
if (null === $creator = $this->userRepository->find($message->getCreatorId())) {
$this->logger->error(self::LOG_PREFIX.'Creator not found');
return;
}
$email = (new TemplatedEmail())
->to($creator->getEmail())
->to($message->getSendResultToEmail())
->subject($this->translator->trans('docgen.failure_email.The generation of a document failed'))
->textTemplate('@ChillDocGenerator/Email/on_generation_failed_email.txt.twig')
->context([

View File

@@ -11,15 +11,21 @@ declare(strict_types=1);
namespace Chill\DocGeneratorBundle\Service\Messenger;
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
use Chill\DocGeneratorBundle\Service\Generator\Generator;
use Chill\DocGeneratorBundle\Service\Generator\GeneratorException;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
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\Mailer\MailerInterface;
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Handle the request of document generation.
@@ -30,8 +36,17 @@ class RequestGenerationHandler implements MessageHandlerInterface
private const LOG_PREFIX = '[docgen message handler] ';
public function __construct(private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository, private readonly EntityManagerInterface $entityManager, private readonly Generator $generator, private readonly LoggerInterface $logger, private readonly StoredObjectRepository $storedObjectRepository, private readonly UserRepositoryInterface $userRepository)
{
public function __construct(
private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
private readonly EntityManagerInterface $entityManager,
private readonly Generator $generator,
private readonly LoggerInterface $logger,
private readonly StoredObjectRepository $storedObjectRepository,
private readonly UserRepositoryInterface $userRepository,
private readonly MailerInterface $mailer,
private readonly TempUrlGeneratorInterface $tempUrlGenerator,
private readonly TranslatorInterface $translator,
) {
}
public function __invoke(RequestGenerationMessage $message)
@@ -45,25 +60,59 @@ class RequestGenerationHandler implements MessageHandlerInterface
}
if ($destinationStoredObject->getGenerationTrialsCounter() >= self::AUTHORIZED_TRIALS) {
$this->logger->error(self::LOG_PREFIX.'Request generation abandoned: maximum number of retry reached', [
'template_id' => $message->getTemplateId(),
'destination_stored_object' => $message->getDestinationStoredObjectId(),
'trial' => $destinationStoredObject->getGenerationTrialsCounter(),
]);
throw new UnrecoverableMessageHandlingException('maximum number of retry reached');
}
$creator = $this->userRepository->find($message->getCreatorId());
// we increase the number of generation trial in the object, and, in the same time, update the counter
// on the database side. This ensure that, if the script fails for any reason (memory limit reached), the
// counter is inscreased
$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,
false,
null,
$creator
);
try {
if ($message->isDumpOnly()) {
$destinationStoredObject = $this->generator->generateDataDump(
$template,
$message->getEntityId(),
$message->getContextGenerationData(),
$destinationStoredObject,
$creator
);
$this->sendDataDump($destinationStoredObject, $message);
} else {
$destinationStoredObject = $this->generator->generateDocFromTemplate(
$template,
$message->getEntityId(),
$message->getContextGenerationData(),
$destinationStoredObject,
$creator
);
}
} catch (StoredObjectManagerException|GeneratorException $e) {
$this->entityManager->flush();
$this->logger->error(self::LOG_PREFIX.'Request generation failed', [
'template_id' => $message->getTemplateId(),
'destination_stored_object' => $message->getDestinationStoredObjectId(),
'trial' => $destinationStoredObject->getGenerationTrialsCounter(),
'error' => $e->getTraceAsString(),
]);
throw $e;
}
$this->entityManager->flush();
$this->logger->info(self::LOG_PREFIX.'Request generation finished', [
'template_id' => $message->getTemplateId(),
@@ -71,4 +120,23 @@ class RequestGenerationHandler implements MessageHandlerInterface
'duration_int' => (new \DateTimeImmutable('now'))->getTimestamp() - $message->getCreatedAt()->getTimestamp(),
]);
}
private function sendDataDump(StoredObject $destinationStoredObject, RequestGenerationMessage $message): void
{
$url = $this->tempUrlGenerator->generate('GET', $destinationStoredObject->getFilename(), 3600);
$parts = [];
parse_str(parse_url((string) $url->url)['query'], $parts);
$validity = \DateTimeImmutable::createFromFormat('U', $parts['temp_url_expires']);
$email = (new TemplatedEmail())
->to($message->getSendResultToEmail())
->textTemplate('@ChillDocGenerator/Email/send_data_dump_to_admin.txt.twig')
->context([
'link' => $url->url,
'validity' => $validity,
])
->subject($this->translator->trans('docgen.data_dump_email.subject'));
$this->mailer->send($email);
}
}

View File

@@ -15,27 +15,33 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Entity\User;
class RequestGenerationMessage
final readonly class RequestGenerationMessage
{
private readonly int $creatorId;
private int $creatorId;
private readonly int $templateId;
private int $templateId;
private readonly int $destinationStoredObjectId;
private int $destinationStoredObjectId;
private readonly \DateTimeImmutable $createdAt;
private \DateTimeImmutable $createdAt;
private ?string $sendResultToEmail;
public function __construct(
User $creator,
DocGeneratorTemplate $template,
private readonly int $entityId,
private int $entityId,
StoredObject $destinationStoredObject,
private readonly array $contextGenerationData
private array $contextGenerationData,
private bool $isTest = false,
?string $sendResultToEmail = null,
private bool $dumpOnly = false,
) {
$this->creatorId = $creator->getId();
$this->templateId = $template->getId();
$this->destinationStoredObjectId = $destinationStoredObject->getId();
$this->createdAt = new \DateTimeImmutable('now');
$this->sendResultToEmail = $sendResultToEmail ?? $creator->getEmail();
}
public function getCreatorId(): int
@@ -67,4 +73,19 @@ class RequestGenerationMessage
{
return $this->createdAt;
}
public function isTest(): bool
{
return $this->isTest;
}
public function getSendResultToEmail(): ?string
{
return $this->sendResultToEmail;
}
public function isDumpOnly(): bool
{
return $this->dumpOnly;
}
}