mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge branch '268-improve-ux-when-configuring-documents' into 'master'
Improve admin UX for configuration of document template (document generation) Closes #268 See merge request Chill-Projet/chill-bundles!670
This commit is contained in:
commit
9e667d4de4
5
.changes/unreleased/Feature-20240326-170418.yaml
Normal file
5
.changes/unreleased/Feature-20240326-170418.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
kind: Feature
|
||||||
|
body: Improve admin UX to configure document templates for document generation
|
||||||
|
time: 2024-03-26T17:04:18.351694753+01:00
|
||||||
|
custom:
|
||||||
|
Issue: "268"
|
@ -16,29 +16,42 @@ use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface;
|
|||||||
use Chill\DocGeneratorBundle\Context\Exception\ContextNotFoundException;
|
use Chill\DocGeneratorBundle\Context\Exception\ContextNotFoundException;
|
||||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||||
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
|
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
|
||||||
use Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface;
|
|
||||||
use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage;
|
use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||||
use Chill\MainBundle\Serializer\Model\Collection;
|
use Chill\MainBundle\Serializer\Model\Collection;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\Clock\ClockInterface;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
// TODO à mettre dans services
|
// TODO à mettre dans services
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||||
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
|
use Symfony\Component\Validator\Constraints\NotNull;
|
||||||
|
|
||||||
final class DocGeneratorTemplateController extends AbstractController
|
final class DocGeneratorTemplateController extends AbstractController
|
||||||
{
|
{
|
||||||
public function __construct(private readonly ContextManager $contextManager, private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository, private readonly GeneratorInterface $generator, private readonly MessageBusInterface $messageBus, private readonly PaginatorFactory $paginatorFactory, private readonly EntityManagerInterface $entityManager)
|
public function __construct(
|
||||||
{
|
private readonly ContextManager $contextManager,
|
||||||
|
private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
|
||||||
|
private readonly MessageBusInterface $messageBus,
|
||||||
|
private readonly PaginatorFactory $paginatorFactory,
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly ClockInterface $clock,
|
||||||
|
private readonly Security $security,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -163,9 +176,7 @@ final class DocGeneratorTemplateController extends AbstractController
|
|||||||
throw new NotFoundHttpException(sprintf('Entity with classname %s and id %s is not found', $context->getEntityClass(), $entityId));
|
throw new NotFoundHttpException(sprintf('Entity with classname %s and id %s is not found', $context->getEntityClass(), $entityId));
|
||||||
}
|
}
|
||||||
|
|
||||||
$contextGenerationData = [
|
$contextGenerationData = [];
|
||||||
'test_file' => null,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
$context instanceof DocGeneratorContextWithPublicFormInterface
|
$context instanceof DocGeneratorContextWithPublicFormInterface
|
||||||
@ -175,25 +186,39 @@ final class DocGeneratorTemplateController extends AbstractController
|
|||||||
$builder = $this->createFormBuilder(
|
$builder = $this->createFormBuilder(
|
||||||
array_merge(
|
array_merge(
|
||||||
$context->getFormData($template, $entity),
|
$context->getFormData($template, $entity),
|
||||||
$isTest ? ['test_file' => null, 'show_data' => false] : []
|
$isTest ? ['creator' => null, 'dump_only' => false, 'send_result_to' => ''] : []
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
$context->buildPublicForm($builder, $template, $entity);
|
$context->buildPublicForm($builder, $template, $entity);
|
||||||
} else {
|
} else {
|
||||||
$builder = $this->createFormBuilder(
|
$builder = $this->createFormBuilder(
|
||||||
['test_file' => null, 'show_data' => false]
|
['creator' => null, 'show_data' => false, 'send_result_to' => '']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($isTest) {
|
if ($isTest) {
|
||||||
$builder->add('test_file', FileType::class, [
|
$builder->add('dump_only', CheckboxType::class, [
|
||||||
'label' => 'Template file',
|
'label' => 'docgen.Show data instead of generating',
|
||||||
'required' => false,
|
'required' => false,
|
||||||
]);
|
]);
|
||||||
$builder->add('show_data', CheckboxType::class, [
|
$builder->add('send_result_to', EmailType::class, [
|
||||||
'label' => 'Show data instead of generating',
|
'label' => 'docgen.Send report to',
|
||||||
'required' => false,
|
'help' => 'docgen.Send report errors to this email address',
|
||||||
|
'empty_data' => '',
|
||||||
|
'required' => true,
|
||||||
|
'constraints' => [
|
||||||
|
new NotBlank(),
|
||||||
|
new NotNull(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$builder->add('creator', PickUserDynamicType::class, [
|
||||||
|
'label' => 'docgen.Generate as creator',
|
||||||
|
'help' => 'docgen.The document will be generated as the given creator',
|
||||||
|
'multiple' => false,
|
||||||
|
'constraints' => [
|
||||||
|
new NotNull(),
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,8 +229,10 @@ final class DocGeneratorTemplateController extends AbstractController
|
|||||||
} elseif (!$form->isSubmitted() || ($form->isSubmitted() && !$form->isValid())) {
|
} elseif (!$form->isSubmitted() || ($form->isSubmitted() && !$form->isValid())) {
|
||||||
$templatePath = '@ChillDocGenerator/Generator/basic_form.html.twig';
|
$templatePath = '@ChillDocGenerator/Generator/basic_form.html.twig';
|
||||||
$templateOptions = [
|
$templateOptions = [
|
||||||
'entity' => $entity, 'form' => $form->createView(),
|
'entity' => $entity,
|
||||||
'template' => $template, 'context' => $context,
|
'form' => $form->createView(),
|
||||||
|
'template' => $template,
|
||||||
|
'context' => $context,
|
||||||
];
|
];
|
||||||
|
|
||||||
return $this->render($templatePath, $templateOptions);
|
return $this->render($templatePath, $templateOptions);
|
||||||
@ -218,43 +245,21 @@ final class DocGeneratorTemplateController extends AbstractController
|
|||||||
$context->contextGenerationDataNormalize($template, $entity, $contextGenerationData)
|
$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),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
if ($isTest) {
|
|
||||||
$generated = $this->generator->generateDocFromTemplate(
|
|
||||||
$template,
|
|
||||||
$entityId,
|
|
||||||
$contextGenerationDataSanitized,
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
isset($form) ? $form['test_file']->getData() : null
|
|
||||||
);
|
|
||||||
|
|
||||||
return new Response(
|
|
||||||
$generated,
|
|
||||||
Response::HTTP_OK,
|
|
||||||
[
|
|
||||||
'Content-Transfer-Encoding', 'binary',
|
|
||||||
'Content-Type' => 'application/vnd.oasis.opendocument.text',
|
|
||||||
'Content-Disposition' => 'attachment; filename="generated.odt"',
|
|
||||||
'Content-Length' => \strlen($generated),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is not a test
|
|
||||||
// we prepare the object to store the document
|
// we prepare the object to store the document
|
||||||
$storedObject = (new StoredObject())
|
$storedObject = (new StoredObject())
|
||||||
->setStatus(StoredObject::STATUS_PENDING)
|
->setStatus(StoredObject::STATUS_PENDING)
|
||||||
;
|
;
|
||||||
|
|
||||||
|
if ($isTest) {
|
||||||
|
// document will be stored during 15 days, if generation is a test
|
||||||
|
$storedObject->setDeleteAt($this->clock->now()->add(new \DateInterval('P15D')));
|
||||||
|
}
|
||||||
|
|
||||||
$this->entityManager->persist($storedObject);
|
$this->entityManager->persist($storedObject);
|
||||||
|
|
||||||
// we store the generated document
|
// we store the generated document (associate with the original entity, etc.)
|
||||||
|
// but only if this is not a test
|
||||||
|
if (!$isTest) {
|
||||||
$context
|
$context
|
||||||
->storeGenerated(
|
->storeGenerated(
|
||||||
$template,
|
$template,
|
||||||
@ -262,16 +267,35 @@ final class DocGeneratorTemplateController extends AbstractController
|
|||||||
$entity,
|
$entity,
|
||||||
$contextGenerationData
|
$contextGenerationData
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
if ($isTest) {
|
||||||
|
$creator = $contextGenerationData['creator'];
|
||||||
|
$sendResultTo = ($form ?? null)?->get('send_result_to')?->getData() ?? null;
|
||||||
|
$dumpOnly = ($form ?? null)?->get('dump_only')?->getData() ?? false;
|
||||||
|
} else {
|
||||||
|
$creator = $this->security->getUser();
|
||||||
|
|
||||||
|
if (!$creator instanceof User) {
|
||||||
|
throw new AccessDeniedHttpException('only authenticated user can request a generation');
|
||||||
|
}
|
||||||
|
|
||||||
|
$sendResultTo = null;
|
||||||
|
$dumpOnly = false;
|
||||||
|
}
|
||||||
|
|
||||||
$this->messageBus->dispatch(
|
$this->messageBus->dispatch(
|
||||||
new RequestGenerationMessage(
|
new RequestGenerationMessage(
|
||||||
$this->getUser(),
|
$creator,
|
||||||
$template,
|
$template,
|
||||||
$entityId,
|
$entityId,
|
||||||
$storedObject,
|
$storedObject,
|
||||||
$contextGenerationDataSanitized,
|
$contextGenerationDataSanitized,
|
||||||
|
$isTest,
|
||||||
|
$sendResultTo,
|
||||||
|
$dumpOnly,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ class DocGeneratorTemplate
|
|||||||
*
|
*
|
||||||
* @Serializer\Groups({"read"})
|
* @Serializer\Groups({"read"})
|
||||||
*/
|
*/
|
||||||
private int $id;
|
private ?int $id = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\Column(type="json")
|
* @ORM\Column(type="json")
|
||||||
|
@ -14,10 +14,9 @@ namespace Chill\DocGeneratorBundle\Repository;
|
|||||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
use Doctrine\Persistence\ObjectRepository;
|
|
||||||
use Symfony\Component\HttpFoundation\RequestStack;
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
|
||||||
final class DocGeneratorTemplateRepository implements ObjectRepository
|
final class DocGeneratorTemplateRepository implements DocGeneratorTemplateRepositoryInterface
|
||||||
{
|
{
|
||||||
private readonly EntityRepository $repository;
|
private readonly EntityRepository $repository;
|
||||||
|
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
<?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\Repository;
|
||||||
|
|
||||||
|
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||||
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ObjectRepository<DocGeneratorTemplate>
|
||||||
|
*/
|
||||||
|
interface DocGeneratorTemplateRepositoryInterface extends ObjectRepository
|
||||||
|
{
|
||||||
|
public function countByEntity(string $entity): int;
|
||||||
|
}
|
@ -1,5 +1,16 @@
|
|||||||
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
|
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
{{ parent() }}
|
||||||
|
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{{ parent() }}
|
||||||
|
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block admin_content %}
|
{% block admin_content %}
|
||||||
{% embed '@ChillMain/CRUD/_index.html.twig' %}
|
{% embed '@ChillMain/CRUD/_index.html.twig' %}
|
||||||
{% block table_entities_thead_tr %}
|
{% block table_entities_thead_tr %}
|
||||||
@ -11,6 +22,47 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block table_entities_tbody %}
|
{% block table_entities_tbody %}
|
||||||
|
{% if entities|length == 0 %}
|
||||||
|
<p class="chill-no-data-statement">{{ 'docgen.Any template configured'|trans }}</p>
|
||||||
|
{% else %}
|
||||||
|
<div class="flex-table">
|
||||||
|
{% for entity in entities %}
|
||||||
|
<div class="item-bloc">
|
||||||
|
<div class="item-row">
|
||||||
|
<div class="item-col" style="flex-basis:100%;">
|
||||||
|
<h2>{{ entity.name|localize_translatable_string }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-row">
|
||||||
|
<p><span class="badge bg-chill-green-dark">{{ contextManager.getContextByKey(entity.context).name|trans }}</span></p>
|
||||||
|
</div>
|
||||||
|
<div class="item-row">
|
||||||
|
<div class="item-col"></div>
|
||||||
|
<ul class="record_actions item-col flex-shrink-1">
|
||||||
|
<li>
|
||||||
|
<form method="get" action="{{ path('chill_docgenerator_test_generate_redirect') }}">
|
||||||
|
<input type="hidden" name="returnPath" value="{{ app.request.query.get('returnPath', app.request.uri)|e('html_attr') }}" />
|
||||||
|
<input type="hidden" name="template" value="{{ entity.id|e('html_attr') }}" />
|
||||||
|
<input type="hidden" name="entityClassName" value="{{ contextManager.getContextByKey(entity.context).entityClass|e('html_attr') }}" />
|
||||||
|
<input type="text" name="entityId" placeholder="{{ 'docgen.entity_id_placeholder'|trans }}" required />
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-mini btn-misc"><i class="fa fa-cog"></i>{{ 'docgen.test generate'|trans }}</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{{ entity.file|chill_document_button_group('Template file', true) }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ chill_path_add_return_path('chill_crud_docgen_template_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'Edit'|trans }}"></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{% for entity in entities %}
|
{% for entity in entities %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ entity.id }}</td>
|
<td>{{ entity.id }}</td>
|
||||||
@ -18,7 +70,7 @@
|
|||||||
<td>{{ contextManager.getContextByKey(entity.context).name|trans }}</td>
|
<td>{{ contextManager.getContextByKey(entity.context).name|trans }}</td>
|
||||||
<td>
|
<td>
|
||||||
<form method="get" action="{{ path('chill_docgenerator_test_generate_redirect') }}">
|
<form method="get" action="{{ path('chill_docgenerator_test_generate_redirect') }}">
|
||||||
<input type="hidden" name="returnPath" value="{{ app.request.query.get('returnPath', '/')|e('html_attr') }}" />
|
<input type="hidden" name="returnPath" value="{{ app.request.query.get('returnPath', app.request.uri)|e('html_attr') }}" />
|
||||||
<input type="hidden" name="template" value="{{ entity.id|e('html_attr') }}" />
|
<input type="hidden" name="template" value="{{ entity.id|e('html_attr') }}" />
|
||||||
<input type="hidden" name="entityClassName" value="{{ contextManager.getContextByKey(entity.context).entityClass|e('html_attr') }}" />
|
<input type="hidden" name="entityClassName" value="{{ contextManager.getContextByKey(entity.context).entityClass|e('html_attr') }}" />
|
||||||
<input type="text" name="entityId" />
|
<input type="text" name="entityId" />
|
||||||
@ -27,7 +79,14 @@
|
|||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
<ul class="record_actions">
|
||||||
|
<li>
|
||||||
|
{{ entity.file|chill_document_button_group('Template file', true, {small: true}) }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
<a href="{{ chill_path_add_return_path('chill_crud_docgen_template_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'Edit'|trans }}"></a>
|
<a href="{{ chill_path_add_return_path('chill_crud_docgen_template_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'Edit'|trans }}"></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -6,20 +6,22 @@
|
|||||||
<div class="col-md-10 col-xxl">
|
<div class="col-md-10 col-xxl">
|
||||||
|
|
||||||
<h1>{{ block('title') }}</h1>
|
<h1>{{ block('title') }}</h1>
|
||||||
<div class="container">
|
<div class="container overflow-hidden">
|
||||||
{% for key, context in contexts %}
|
{% for key, context in contexts %}
|
||||||
<div class="row">
|
<div class="row g-3" style="margin-top: 1rem;">
|
||||||
<div class="col-md-4">
|
<div class="col-4 offset-1 text-center">
|
||||||
<a
|
<a
|
||||||
href="{{ path('chill_crud_docgen_template_new', { 'context': key }) }}"
|
href="{{ path('chill_crud_docgen_template_new', { 'context': key }) }}"
|
||||||
class="btn btn-outline-chill-green-dark">
|
class="btn btn-outline-chill-green-dark">
|
||||||
{{ context.name|trans }}
|
{{ context.name|trans }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-8">
|
<div class="col">
|
||||||
|
<div>
|
||||||
{{ context.description|trans|nl2br }}
|
{{ context.description|trans|nl2br }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{{ creator.label }},
|
{% if creator is not same as null %}{{ creator.label }},{% endif %}
|
||||||
|
|
||||||
{{ 'docgen.failure_email.The generation of the document {template_name} failed'|trans({'{template_name}': template.name|localize_translatable_string}) }}
|
{{ '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.Forward this email to your administrator for solving'|trans }}
|
||||||
|
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
{{ 'docgen.data_dump_email.Dear'|trans }}
|
||||||
|
|
||||||
|
{{ 'docgen.data_dump_email.data_dump_ready_and_link'|trans }}
|
||||||
|
|
||||||
|
{{ link }}
|
||||||
|
|
||||||
|
{{ 'docgen.data_dump_email.link_valid_until'|trans({validity: validity}) }}
|
@ -17,54 +17,88 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
|||||||
use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface;
|
use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface;
|
||||||
use Chill\DocGeneratorBundle\GeneratorDriver\Exception\TemplateException;
|
use Chill\DocGeneratorBundle\GeneratorDriver\Exception\TemplateException;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\HttpFoundation\File\File;
|
use Symfony\Component\Yaml\Yaml;
|
||||||
|
|
||||||
class Generator implements GeneratorInterface
|
class Generator implements GeneratorInterface
|
||||||
{
|
{
|
||||||
private const LOG_PREFIX = '[docgen generator] ';
|
private const LOG_PREFIX = '[docgen generator] ';
|
||||||
|
|
||||||
public function __construct(private readonly ContextManagerInterface $contextManager, private readonly DriverInterface $driver, private readonly EntityManagerInterface $entityManager, private readonly LoggerInterface $logger, private readonly StoredObjectManagerInterface $storedObjectManager)
|
public function __construct(
|
||||||
{
|
private readonly ContextManagerInterface $contextManager,
|
||||||
|
private readonly DriverInterface $driver,
|
||||||
|
private readonly ManagerRegistry $objectManagerRegistry,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
private readonly StoredObjectManagerInterface $storedObjectManager
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateDataDump(
|
||||||
|
DocGeneratorTemplate $template,
|
||||||
|
int $entityId,
|
||||||
|
array $contextGenerationDataNormalized,
|
||||||
|
StoredObject $destinationStoredObject,
|
||||||
|
User $creator,
|
||||||
|
bool $clearEntityManagerDuringProcess = true,
|
||||||
|
): StoredObject {
|
||||||
|
return $this->generateFromTemplate(
|
||||||
|
$template,
|
||||||
|
$entityId,
|
||||||
|
$contextGenerationDataNormalized,
|
||||||
|
$destinationStoredObject,
|
||||||
|
$creator,
|
||||||
|
$clearEntityManagerDuringProcess,
|
||||||
|
true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @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(
|
public function generateDocFromTemplate(
|
||||||
DocGeneratorTemplate $template,
|
DocGeneratorTemplate $template,
|
||||||
int $entityId,
|
int $entityId,
|
||||||
array $contextGenerationDataNormalized,
|
array $contextGenerationDataNormalized,
|
||||||
?StoredObject $destinationStoredObject = null,
|
StoredObject $destinationStoredObject,
|
||||||
bool $isTest = false,
|
User $creator,
|
||||||
?File $testFile = null,
|
bool $clearEntityManagerDuringProcess = true,
|
||||||
?User $creator = null
|
): StoredObject {
|
||||||
): ?string {
|
return $this->generateFromTemplate(
|
||||||
if ($destinationStoredObject instanceof StoredObject && StoredObject::STATUS_PENDING !== $destinationStoredObject->getStatus()) {
|
$template,
|
||||||
|
$entityId,
|
||||||
|
$contextGenerationDataNormalized,
|
||||||
|
$destinationStoredObject,
|
||||||
|
$creator,
|
||||||
|
$clearEntityManagerDuringProcess,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateFromTemplate(
|
||||||
|
DocGeneratorTemplate $template,
|
||||||
|
int $entityId,
|
||||||
|
array $contextGenerationDataNormalized,
|
||||||
|
StoredObject $destinationStoredObject,
|
||||||
|
User $creator,
|
||||||
|
bool $clearEntityManagerDuringProcess = true,
|
||||||
|
bool $generateDumpOnly = false,
|
||||||
|
): StoredObject {
|
||||||
|
if (StoredObject::STATUS_PENDING !== $destinationStoredObject->getStatus()) {
|
||||||
$this->logger->info(self::LOG_PREFIX.'Aborting generation of an already generated document');
|
$this->logger->info(self::LOG_PREFIX.'Aborting generation of an already generated document');
|
||||||
throw new ObjectReadyException();
|
throw new ObjectReadyException();
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->logger->info(self::LOG_PREFIX.'Starting generation of a document', [
|
$this->logger->info(self::LOG_PREFIX.'Starting generation of a document', [
|
||||||
'entity_id' => $entityId,
|
'entity_id' => $entityId,
|
||||||
'destination_stored_object' => null === $destinationStoredObject ? null : $destinationStoredObject->getId(),
|
'destination_stored_object' => $destinationStoredObject->getId(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$context = $this->contextManager->getContextByDocGeneratorTemplate($template);
|
$context = $this->contextManager->getContextByDocGeneratorTemplate($template);
|
||||||
|
|
||||||
$entity = $this
|
$entity = $this
|
||||||
->entityManager
|
->objectManagerRegistry
|
||||||
|
->getManagerForClass($context->getEntityClass())
|
||||||
->find($context->getEntityClass(), $entityId)
|
->find($context->getEntityClass(), $entityId)
|
||||||
;
|
;
|
||||||
|
|
||||||
@ -82,17 +116,47 @@ class Generator implements GeneratorInterface
|
|||||||
|
|
||||||
$data = $context->getData($template, $entity, $contextGenerationDataNormalized);
|
$data = $context->getData($template, $entity, $contextGenerationDataNormalized);
|
||||||
|
|
||||||
$destinationStoredObjectId = $destinationStoredObject instanceof StoredObject ? $destinationStoredObject->getId() : null;
|
$destinationStoredObjectId = $destinationStoredObject->getId();
|
||||||
$this->entityManager->clear();
|
|
||||||
|
if ($clearEntityManagerDuringProcess) {
|
||||||
|
// we clean the entity manager
|
||||||
|
$this->objectManagerRegistry->getManagerForClass($context->getEntityClass())?->clear();
|
||||||
|
|
||||||
|
// this will force php to clean the memory
|
||||||
gc_collect_cycles();
|
gc_collect_cycles();
|
||||||
if (null !== $destinationStoredObjectId) {
|
|
||||||
$destinationStoredObject = $this->entityManager->find(StoredObject::class, $destinationStoredObjectId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($isTest && ($testFile instanceof File)) {
|
// as we potentially deleted the storedObject from memory, we have to restore it
|
||||||
$templateDecrypted = file_get_contents($testFile->getPathname());
|
$destinationStoredObject = $this->objectManagerRegistry
|
||||||
} else {
|
->getManagerForClass(StoredObject::class)
|
||||||
|
->find(StoredObject::class, $destinationStoredObjectId);
|
||||||
|
|
||||||
|
if ($generateDumpOnly) {
|
||||||
|
$content = Yaml::dump($data, 6);
|
||||||
|
/* @var StoredObject $destinationStoredObject */
|
||||||
|
$destinationStoredObject
|
||||||
|
->setType('application/yaml')
|
||||||
|
->setFilename(sprintf('%s_yaml', uniqid('doc_', true)))
|
||||||
|
->setStatus(StoredObject::STATUS_READY)
|
||||||
|
;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->storedObjectManager->write($destinationStoredObject, $content);
|
||||||
|
} catch (StoredObjectManagerException $e) {
|
||||||
|
$destinationStoredObject->addGenerationErrors($e->getMessage());
|
||||||
|
|
||||||
|
throw new GeneratorException([$e->getMessage()], $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $destinationStoredObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
$templateDecrypted = $this->storedObjectManager->read($template->getFile());
|
$templateDecrypted = $this->storedObjectManager->read($template->getFile());
|
||||||
|
} catch (StoredObjectManagerException $e) {
|
||||||
|
$destinationStoredObject->addGenerationErrors($e->getMessage());
|
||||||
|
|
||||||
|
throw new GeneratorException([$e->getMessage()], $e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -105,19 +169,10 @@ class Generator implements GeneratorInterface
|
|||||||
$template->getFile()->getFilename()
|
$template->getFile()->getFilename()
|
||||||
);
|
);
|
||||||
} catch (TemplateException $e) {
|
} catch (TemplateException $e) {
|
||||||
|
$destinationStoredObject->addGenerationErrors(implode("\n", $e->getErrors()));
|
||||||
throw new GeneratorException($e->getErrors(), $e);
|
throw new GeneratorException($e->getErrors(), $e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (true === $isTest) {
|
|
||||||
$this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [
|
|
||||||
'is_test' => true,
|
|
||||||
'entity_id' => $entityId,
|
|
||||||
'destination_stored_object' => null === $destinationStoredObject ? null : $destinationStoredObject->getId(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $generatedResource;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @var StoredObject $destinationStoredObject */
|
/* @var StoredObject $destinationStoredObject */
|
||||||
$destinationStoredObject
|
$destinationStoredObject
|
||||||
->setType($template->getFile()->getType())
|
->setType($template->getFile()->getType())
|
||||||
@ -125,15 +180,19 @@ class Generator implements GeneratorInterface
|
|||||||
->setStatus(StoredObject::STATUS_READY)
|
->setStatus(StoredObject::STATUS_READY)
|
||||||
;
|
;
|
||||||
|
|
||||||
|
try {
|
||||||
$this->storedObjectManager->write($destinationStoredObject, $generatedResource);
|
$this->storedObjectManager->write($destinationStoredObject, $generatedResource);
|
||||||
|
} catch (StoredObjectManagerException $e) {
|
||||||
|
$destinationStoredObject->addGenerationErrors($e->getMessage());
|
||||||
|
|
||||||
$this->entityManager->flush();
|
throw new GeneratorException([$e->getMessage()], $e);
|
||||||
|
}
|
||||||
|
|
||||||
$this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [
|
$this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [
|
||||||
'entity_id' => $entityId,
|
'entity_id' => $entityId,
|
||||||
'destination_stored_object' => $destinationStoredObject->getId(),
|
'destination_stored_object' => $destinationStoredObject->getId(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return null;
|
return $destinationStoredObject;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,29 +13,48 @@ namespace Chill\DocGeneratorBundle\Service\Generator;
|
|||||||
|
|
||||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Symfony\Component\HttpFoundation\File\File;
|
|
||||||
|
|
||||||
interface GeneratorInterface
|
interface GeneratorInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @template T of File|null
|
* Generate a document and store the document on disk.
|
||||||
* @template B of bool
|
|
||||||
*
|
*
|
||||||
* @param B $isTest
|
* The given $destinationStoredObject will be updated with filename, status, and eventually errors will be stored
|
||||||
* @param (B is true ? T : null) $testFile
|
* into the object. The number of generation trial will also be incremented.
|
||||||
*
|
*
|
||||||
* @psalm-return (B is true ? string : null)
|
* This process requires a huge amount of data. For this reason, the entity manager will be cleaned during the process,
|
||||||
|
* unless the paarameter `$clearEntityManagerDuringProcess` is set on false.
|
||||||
*
|
*
|
||||||
* @throws \Symfony\Component\Serializer\Exception\ExceptionInterface|\Throwable
|
* As the entity manager might be cleaned, the new instance of the stored object will be returned by this method.
|
||||||
|
*
|
||||||
|
* Ensure to store change in the database after each generation trial (call `EntityManagerInterface::flush`).
|
||||||
|
*
|
||||||
|
* @phpstan-impure
|
||||||
|
*
|
||||||
|
* @param StoredObject $destinationStoredObject will be update with filename, status and incremented of generation trials
|
||||||
|
*
|
||||||
|
* @throws StoredObjectManagerException if unable to decrypt the template or store the document
|
||||||
*/
|
*/
|
||||||
public function generateDocFromTemplate(
|
public function generateDocFromTemplate(
|
||||||
DocGeneratorTemplate $template,
|
DocGeneratorTemplate $template,
|
||||||
int $entityId,
|
int $entityId,
|
||||||
array $contextGenerationDataNormalized,
|
array $contextGenerationDataNormalized,
|
||||||
?StoredObject $destinationStoredObject = null,
|
StoredObject $destinationStoredObject,
|
||||||
bool $isTest = false,
|
User $creator,
|
||||||
?File $testFile = null,
|
bool $clearEntityManagerDuringProcess = true,
|
||||||
?User $creator = null
|
): StoredObject;
|
||||||
): ?string;
|
|
||||||
|
/**
|
||||||
|
* Generate a data dump, and store it within the `$destinationStoredObject`.
|
||||||
|
*/
|
||||||
|
public function generateDataDump(
|
||||||
|
DocGeneratorTemplate $template,
|
||||||
|
int $entityId,
|
||||||
|
array $contextGenerationDataNormalized,
|
||||||
|
StoredObject $destinationStoredObject,
|
||||||
|
User $creator,
|
||||||
|
bool $clearEntityManagerDuringProcess = true,
|
||||||
|
): StoredObject;
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -11,10 +11,11 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\DocGeneratorBundle\Service\Messenger;
|
namespace Chill\DocGeneratorBundle\Service\Messenger;
|
||||||
|
|
||||||
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
|
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface;
|
||||||
use Chill\DocGeneratorBundle\Service\Generator\GeneratorException;
|
use Chill\DocGeneratorBundle\Service\Generator\GeneratorException;
|
||||||
|
use Chill\DocGeneratorBundle\tests\Service\Messenger\OnGenerationFailsTest;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
|
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
|
||||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
@ -24,12 +25,22 @@ use Symfony\Component\Mailer\MailerInterface;
|
|||||||
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see OnGenerationFailsTest for test suite
|
||||||
|
*/
|
||||||
final readonly class OnGenerationFails implements EventSubscriberInterface
|
final readonly class OnGenerationFails implements EventSubscriberInterface
|
||||||
{
|
{
|
||||||
public const LOG_PREFIX = '[docgen failed] ';
|
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()
|
public static function getSubscribedEvents()
|
||||||
@ -45,13 +56,12 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) {
|
$message = $event->getEnvelope()->getMessage();
|
||||||
|
|
||||||
|
if (!$message instanceof RequestGenerationMessage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var RequestGenerationMessage $message */
|
|
||||||
$message = $event->getEnvelope()->getMessage();
|
|
||||||
|
|
||||||
$this->logger->error(self::LOG_PREFIX.'Docgen failed', [
|
$this->logger->error(self::LOG_PREFIX.'Docgen failed', [
|
||||||
'stored_object_id' => $message->getDestinationStoredObjectId(),
|
'stored_object_id' => $message->getDestinationStoredObjectId(),
|
||||||
'entity_id' => $message->getEntityId(),
|
'entity_id' => $message->getEntityId(),
|
||||||
@ -79,16 +89,8 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
|
|||||||
|
|
||||||
private function warnCreator(RequestGenerationMessage $message, WorkerMessageFailedEvent $event): void
|
private function warnCreator(RequestGenerationMessage $message, WorkerMessageFailedEvent $event): void
|
||||||
{
|
{
|
||||||
$creatorId = $message->getCreatorId();
|
if (null === $message->getSendResultToEmail() || '' === $message->getSendResultToEmail()) {
|
||||||
|
$this->logger->info(self::LOG_PREFIX.'No email associated with this request generation');
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
@ -96,7 +98,7 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
|
|||||||
// if the exception is not a GeneratorException, we try the previous one...
|
// if the exception is not a GeneratorException, we try the previous one...
|
||||||
$throwable = $event->getThrowable();
|
$throwable = $event->getThrowable();
|
||||||
if (!$throwable instanceof GeneratorException) {
|
if (!$throwable instanceof GeneratorException) {
|
||||||
$throwable = $throwable->getPrevious();
|
$throwable = $throwable->getPrevious() ?? $throwable;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($throwable instanceof GeneratorException) {
|
if ($throwable instanceof GeneratorException) {
|
||||||
@ -111,8 +113,14 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (null === $creator = $this->userRepository->find($message->getCreatorId())) {
|
||||||
|
$this->logger->error(self::LOG_PREFIX.'Creator not found');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$email = (new TemplatedEmail())
|
$email = (new TemplatedEmail())
|
||||||
->to($creator->getEmail())
|
->to($message->getSendResultToEmail())
|
||||||
->subject($this->translator->trans('docgen.failure_email.The generation of a document failed'))
|
->subject($this->translator->trans('docgen.failure_email.The generation of a document failed'))
|
||||||
->textTemplate('@ChillDocGenerator/Email/on_generation_failed_email.txt.twig')
|
->textTemplate('@ChillDocGenerator/Email/on_generation_failed_email.txt.twig')
|
||||||
->context([
|
->context([
|
||||||
|
@ -11,15 +11,21 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\DocGeneratorBundle\Service\Messenger;
|
namespace Chill\DocGeneratorBundle\Service\Messenger;
|
||||||
|
|
||||||
|
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
|
||||||
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
|
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
|
||||||
use Chill\DocGeneratorBundle\Service\Generator\Generator;
|
use Chill\DocGeneratorBundle\Service\Generator\Generator;
|
||||||
|
use Chill\DocGeneratorBundle\Service\Generator\GeneratorException;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||||
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
|
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
|
||||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
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\Exception\UnrecoverableMessageHandlingException;
|
||||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle the request of document generation.
|
* Handle the request of document generation.
|
||||||
@ -30,8 +36,17 @@ class RequestGenerationHandler implements MessageHandlerInterface
|
|||||||
|
|
||||||
private const LOG_PREFIX = '[docgen message handler] ';
|
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)
|
public function __invoke(RequestGenerationMessage $message)
|
||||||
@ -45,30 +60,83 @@ class RequestGenerationHandler implements MessageHandlerInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($destinationStoredObject->getGenerationTrialsCounter() >= self::AUTHORIZED_TRIALS) {
|
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');
|
throw new UnrecoverableMessageHandlingException('maximum number of retry reached');
|
||||||
}
|
}
|
||||||
|
|
||||||
$creator = $this->userRepository->find($message->getCreatorId());
|
$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();
|
$destinationStoredObject->addGenerationTrial();
|
||||||
$this->entityManager->createQuery('UPDATE '.StoredObject::class.' s SET s.generationTrialsCounter = s.generationTrialsCounter + 1 WHERE s.id = :id')
|
$this->entityManager->createQuery('UPDATE '.StoredObject::class.' s SET s.generationTrialsCounter = s.generationTrialsCounter + 1 WHERE s.id = :id')
|
||||||
->setParameter('id', $destinationStoredObject->getId())
|
->setParameter('id', $destinationStoredObject->getId())
|
||||||
->execute();
|
->execute();
|
||||||
|
|
||||||
$this->generator->generateDocFromTemplate(
|
try {
|
||||||
|
if ($message->isDumpOnly()) {
|
||||||
|
$destinationStoredObject = $this->generator->generateDataDump(
|
||||||
$template,
|
$template,
|
||||||
$message->getEntityId(),
|
$message->getEntityId(),
|
||||||
$message->getContextGenerationData(),
|
$message->getContextGenerationData(),
|
||||||
$destinationStoredObject,
|
$destinationStoredObject,
|
||||||
false,
|
|
||||||
null,
|
|
||||||
$creator
|
$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', [
|
$this->logger->info(self::LOG_PREFIX.'Request generation finished', [
|
||||||
'template_id' => $message->getTemplateId(),
|
'template_id' => $message->getTemplateId(),
|
||||||
'destination_stored_object' => $message->getDestinationStoredObjectId(),
|
'destination_stored_object' => $message->getDestinationStoredObjectId(),
|
||||||
'duration_int' => (new \DateTimeImmutable('now'))->getTimestamp() - $message->getCreatedAt()->getTimestamp(),
|
'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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,27 +15,33 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
|||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Chill\MainBundle\Entity\User;
|
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(
|
public function __construct(
|
||||||
User $creator,
|
User $creator,
|
||||||
DocGeneratorTemplate $template,
|
DocGeneratorTemplate $template,
|
||||||
private readonly int $entityId,
|
private int $entityId,
|
||||||
StoredObject $destinationStoredObject,
|
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->creatorId = $creator->getId();
|
||||||
$this->templateId = $template->getId();
|
$this->templateId = $template->getId();
|
||||||
$this->destinationStoredObjectId = $destinationStoredObject->getId();
|
$this->destinationStoredObjectId = $destinationStoredObject->getId();
|
||||||
$this->createdAt = new \DateTimeImmutable('now');
|
$this->createdAt = new \DateTimeImmutable('now');
|
||||||
|
$this->sendResultToEmail = $sendResultToEmail ?? $creator->getEmail();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCreatorId(): int
|
public function getCreatorId(): int
|
||||||
@ -67,4 +73,19 @@ class RequestGenerationMessage
|
|||||||
{
|
{
|
||||||
return $this->createdAt;
|
return $this->createdAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isTest(): bool
|
||||||
|
{
|
||||||
|
return $this->isTest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSendResultToEmail(): ?string
|
||||||
|
{
|
||||||
|
return $this->sendResultToEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isDumpOnly(): bool
|
||||||
|
{
|
||||||
|
return $this->dumpOnly;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,9 @@ use Chill\DocGeneratorBundle\Service\Generator\ObjectReadyException;
|
|||||||
use Chill\DocGeneratorBundle\Service\Generator\RelatedEntityNotFoundException;
|
use Chill\DocGeneratorBundle\Service\Generator\RelatedEntityNotFoundException;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
use Prophecy\PhpUnit\ProphecyTrait;
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
@ -66,7 +68,11 @@ class GeneratorTest extends TestCase
|
|||||||
$entityManager->find('DummyClass', Argument::type('int'))
|
$entityManager->find('DummyClass', Argument::type('int'))
|
||||||
->willReturn($entity);
|
->willReturn($entity);
|
||||||
$entityManager->clear()->shouldBeCalled();
|
$entityManager->clear()->shouldBeCalled();
|
||||||
$entityManager->flush()->shouldBeCalled();
|
$entityManager->flush()->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$managerRegistry = $this->prophesize(ManagerRegistry::class);
|
||||||
|
$managerRegistry->getManagerForClass('DummyClass')->willReturn($entityManager->reveal());
|
||||||
|
$managerRegistry->getManagerForClass(StoredObject::class)->willReturn($entityManager->reveal());
|
||||||
|
|
||||||
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
|
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
|
||||||
$storedObjectManager->read($templateStoredObject)->willReturn('template');
|
$storedObjectManager->read($templateStoredObject)->willReturn('template');
|
||||||
@ -75,7 +81,7 @@ class GeneratorTest extends TestCase
|
|||||||
$generator = new Generator(
|
$generator = new Generator(
|
||||||
$contextManagerInterface->reveal(),
|
$contextManagerInterface->reveal(),
|
||||||
$driver->reveal(),
|
$driver->reveal(),
|
||||||
$entityManager->reveal(),
|
$managerRegistry->reveal(),
|
||||||
new NullLogger(),
|
new NullLogger(),
|
||||||
$storedObjectManager->reveal()
|
$storedObjectManager->reveal()
|
||||||
);
|
);
|
||||||
@ -84,7 +90,8 @@ class GeneratorTest extends TestCase
|
|||||||
$template,
|
$template,
|
||||||
1,
|
1,
|
||||||
[],
|
[],
|
||||||
$destinationStoredObject
|
$destinationStoredObject,
|
||||||
|
new User()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,7 +102,7 @@ class GeneratorTest extends TestCase
|
|||||||
$generator = new Generator(
|
$generator = new Generator(
|
||||||
$this->prophesize(ContextManagerInterface::class)->reveal(),
|
$this->prophesize(ContextManagerInterface::class)->reveal(),
|
||||||
$this->prophesize(DriverInterface::class)->reveal(),
|
$this->prophesize(DriverInterface::class)->reveal(),
|
||||||
$this->prophesize(EntityManagerInterface::class)->reveal(),
|
$this->prophesize(ManagerRegistry::class)->reveal(),
|
||||||
new NullLogger(),
|
new NullLogger(),
|
||||||
$this->prophesize(StoredObjectManagerInterface::class)->reveal()
|
$this->prophesize(StoredObjectManagerInterface::class)->reveal()
|
||||||
);
|
);
|
||||||
@ -108,7 +115,8 @@ class GeneratorTest extends TestCase
|
|||||||
$template,
|
$template,
|
||||||
1,
|
1,
|
||||||
[],
|
[],
|
||||||
$destinationStoredObject
|
$destinationStoredObject,
|
||||||
|
new User()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,10 +144,14 @@ class GeneratorTest extends TestCase
|
|||||||
$entityManager->find(Argument::type('string'), Argument::type('int'))
|
$entityManager->find(Argument::type('string'), Argument::type('int'))
|
||||||
->willReturn(null);
|
->willReturn(null);
|
||||||
|
|
||||||
|
$managerRegistry = $this->prophesize(ManagerRegistry::class);
|
||||||
|
$managerRegistry->getManagerForClass('DummyClass')->willReturn($entityManager->reveal());
|
||||||
|
$managerRegistry->getManagerForClass(StoredObject::class)->willReturn($entityManager->reveal());
|
||||||
|
|
||||||
$generator = new Generator(
|
$generator = new Generator(
|
||||||
$contextManagerInterface->reveal(),
|
$contextManagerInterface->reveal(),
|
||||||
$this->prophesize(DriverInterface::class)->reveal(),
|
$this->prophesize(DriverInterface::class)->reveal(),
|
||||||
$entityManager->reveal(),
|
$managerRegistry->reveal(),
|
||||||
new NullLogger(),
|
new NullLogger(),
|
||||||
$this->prophesize(StoredObjectManagerInterface::class)->reveal()
|
$this->prophesize(StoredObjectManagerInterface::class)->reveal()
|
||||||
);
|
);
|
||||||
@ -148,7 +160,8 @@ class GeneratorTest extends TestCase
|
|||||||
$template,
|
$template,
|
||||||
1,
|
1,
|
||||||
[],
|
[],
|
||||||
$destinationStoredObject
|
$destinationStoredObject,
|
||||||
|
new User()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,107 @@
|
|||||||
|
<?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\tests\Service\Messenger;
|
||||||
|
|
||||||
|
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||||
|
use Chill\DocGeneratorBundle\Service\Messenger\OnAfterMessageHandledClearStoredObjectCache;
|
||||||
|
use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage;
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
use Symfony\Component\Messenger\Envelope;
|
||||||
|
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
||||||
|
use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class OnAfterMessageHandledClearStoredObjectCacheTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
public function testThatNotGenerationMessageDoesNotCallAClearCache(): void
|
||||||
|
{
|
||||||
|
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
|
||||||
|
$storedObjectManager->clearCache()->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$eventSubscriber = $this->buildEventSubscriber($storedObjectManager->reveal());
|
||||||
|
|
||||||
|
$eventSubscriber->afterHandling($this->buildEventSuccess(new \stdClass()));
|
||||||
|
$eventSubscriber->afterFails($this->buildEventFailed(new \stdClass()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThatConcernedEventCallAClearCache(): void
|
||||||
|
{
|
||||||
|
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
|
||||||
|
$storedObjectManager->clearCache()->shouldBeCalledTimes(2);
|
||||||
|
|
||||||
|
$eventSubscriber = $this->buildEventSubscriber($storedObjectManager->reveal());
|
||||||
|
|
||||||
|
$eventSubscriber->afterHandling($this->buildEventSuccess($this->buildRequestGenerationMessage()));
|
||||||
|
$eventSubscriber->afterFails($this->buildEventFailed($this->buildRequestGenerationMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildRequestGenerationMessage(
|
||||||
|
): RequestGenerationMessage {
|
||||||
|
$creator = new User();
|
||||||
|
$creator->setEmail('fake@example.com');
|
||||||
|
|
||||||
|
$class = new \ReflectionClass($creator);
|
||||||
|
$property = $class->getProperty('id');
|
||||||
|
$property->setAccessible(true);
|
||||||
|
$property->setValue($creator, 1);
|
||||||
|
|
||||||
|
$template ??= new DocGeneratorTemplate();
|
||||||
|
$class = new \ReflectionClass($template);
|
||||||
|
$property = $class->getProperty('id');
|
||||||
|
$property->setAccessible(true);
|
||||||
|
$property->setValue($template, 2);
|
||||||
|
|
||||||
|
$destinationStoredObject = new StoredObject();
|
||||||
|
$class = new \ReflectionClass($destinationStoredObject);
|
||||||
|
$property = $class->getProperty('id');
|
||||||
|
$property->setAccessible(true);
|
||||||
|
$property->setValue($destinationStoredObject, 3);
|
||||||
|
|
||||||
|
return new RequestGenerationMessage(
|
||||||
|
$creator,
|
||||||
|
$template,
|
||||||
|
1,
|
||||||
|
$destinationStoredObject,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildEventSubscriber(StoredObjectManagerInterface $storedObjectManager): OnAfterMessageHandledClearStoredObjectCache
|
||||||
|
{
|
||||||
|
return new OnAfterMessageHandledClearStoredObjectCache($storedObjectManager, new NullLogger());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildEventFailed(object $message): WorkerMessageFailedEvent
|
||||||
|
{
|
||||||
|
$envelope = new Envelope($message);
|
||||||
|
|
||||||
|
return new WorkerMessageFailedEvent($envelope, 'testing', new \RuntimeException());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildEventSuccess(object $message): WorkerMessageHandledEvent
|
||||||
|
{
|
||||||
|
$envelope = new Envelope($message);
|
||||||
|
|
||||||
|
return new WorkerMessageHandledEvent($envelope, 'test_receiver');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,226 @@
|
|||||||
|
<?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\tests\Service\Messenger;
|
||||||
|
|
||||||
|
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||||
|
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface;
|
||||||
|
use Chill\DocGeneratorBundle\Service\Messenger\OnGenerationFails;
|
||||||
|
use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage;
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
|
use Symfony\Component\Messenger\Envelope;
|
||||||
|
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
use Symfony\Component\Mime\RawMessage;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class OnGenerationFailsTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
public function testNotConcernedMessageAreNotHandled(): void
|
||||||
|
{
|
||||||
|
$entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
$entityManager->flush()->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$mailer = $this->prophesize(MailerInterface::class);
|
||||||
|
$mailer->send()->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$eventSubscriber = $this->buildOnGenerationFailsEventSubscriber(
|
||||||
|
entityManager: $entityManager->reveal(),
|
||||||
|
mailer: $mailer->reveal()
|
||||||
|
);
|
||||||
|
|
||||||
|
$event = $this->buildEvent(new \stdClass());
|
||||||
|
|
||||||
|
$eventSubscriber->onMessageFailed($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMessageThatWillBeRetriedAreNotHandled(): void
|
||||||
|
{
|
||||||
|
$storedObject = new StoredObject();
|
||||||
|
|
||||||
|
$entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
$entityManager->flush()->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$mailer = $this->prophesize(MailerInterface::class);
|
||||||
|
$mailer->send()->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$eventSubscriber = $this->buildOnGenerationFailsEventSubscriber(
|
||||||
|
entityManager: $entityManager->reveal(),
|
||||||
|
mailer: $mailer->reveal()
|
||||||
|
);
|
||||||
|
|
||||||
|
$event = $this->buildEvent($this->buildRequestGenerationMessage($storedObject));
|
||||||
|
$event->setForRetry();
|
||||||
|
|
||||||
|
$eventSubscriber->onMessageFailed($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThatANotRetriyableEventWillMarkObjectAsFailed(): void
|
||||||
|
{
|
||||||
|
$storedObject = new StoredObject();
|
||||||
|
|
||||||
|
$entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
$entityManager->flush()->shouldBeCalled();
|
||||||
|
|
||||||
|
$mailer = $this->prophesize(MailerInterface::class);
|
||||||
|
$mailer->send(Argument::type(RawMessage::class), Argument::any())->shouldBeCalled();
|
||||||
|
|
||||||
|
$eventSubscriber = $this->buildOnGenerationFailsEventSubscriber(
|
||||||
|
entityManager: $entityManager->reveal(),
|
||||||
|
mailer: $mailer->reveal(),
|
||||||
|
storedObject: $storedObject
|
||||||
|
);
|
||||||
|
|
||||||
|
$event = $this->buildEvent($this->buildRequestGenerationMessage($storedObject));
|
||||||
|
|
||||||
|
$eventSubscriber->onMessageFailed($event);
|
||||||
|
|
||||||
|
self::assertEquals(StoredObject::STATUS_FAILURE, $storedObject->getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThatANonRetryableEventSendAnEmail(): void
|
||||||
|
{
|
||||||
|
$storedObject = new StoredObject();
|
||||||
|
|
||||||
|
$entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
$entityManager->flush()->shouldBeCalled();
|
||||||
|
|
||||||
|
$mailer = $this->prophesize(MailerInterface::class);
|
||||||
|
$mailer->send(
|
||||||
|
Argument::that(function ($arg): bool {
|
||||||
|
if (!$arg instanceof Email) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($arg->getTo() as $to) {
|
||||||
|
if ('test@test.com' === $to->getAddress()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}),
|
||||||
|
Argument::any()
|
||||||
|
)
|
||||||
|
->shouldBeCalled();
|
||||||
|
|
||||||
|
$eventSubscriber = $this->buildOnGenerationFailsEventSubscriber(
|
||||||
|
entityManager: $entityManager->reveal(),
|
||||||
|
mailer: $mailer->reveal(),
|
||||||
|
storedObject: $storedObject
|
||||||
|
);
|
||||||
|
|
||||||
|
$event = $this->buildEvent($this->buildRequestGenerationMessage($storedObject, sendResultToEmail: 'test@test.com'));
|
||||||
|
|
||||||
|
$eventSubscriber->onMessageFailed($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildRequestGenerationMessage(
|
||||||
|
StoredObject $destinationStoredObject,
|
||||||
|
?User $creator = null,
|
||||||
|
?DocGeneratorTemplate $template = null,
|
||||||
|
array $contextGenerationData = [],
|
||||||
|
bool $isTest = false,
|
||||||
|
?string $sendResultToEmail = null,
|
||||||
|
): RequestGenerationMessage {
|
||||||
|
if (null === $creator) {
|
||||||
|
$creator = new User();
|
||||||
|
$creator->setEmail('fake@example.com');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $creator->getId()) {
|
||||||
|
$class = new \ReflectionClass($creator);
|
||||||
|
$property = $class->getProperty('id');
|
||||||
|
$property->setAccessible(true);
|
||||||
|
$property->setValue($creator, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$template ??= new DocGeneratorTemplate();
|
||||||
|
$class = new \ReflectionClass($template);
|
||||||
|
$property = $class->getProperty('id');
|
||||||
|
$property->setAccessible(true);
|
||||||
|
$property->setValue($template, 2);
|
||||||
|
|
||||||
|
$class = new \ReflectionClass($destinationStoredObject);
|
||||||
|
$property = $class->getProperty('id');
|
||||||
|
$property->setAccessible(true);
|
||||||
|
$property->setValue($destinationStoredObject, 3);
|
||||||
|
|
||||||
|
return new RequestGenerationMessage(
|
||||||
|
$creator,
|
||||||
|
$template,
|
||||||
|
1,
|
||||||
|
$destinationStoredObject,
|
||||||
|
$contextGenerationData,
|
||||||
|
$isTest,
|
||||||
|
$sendResultToEmail
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildOnGenerationFailsEventSubscriber(
|
||||||
|
?StoredObject $storedObject = null,
|
||||||
|
?EntityManagerInterface $entityManager = null,
|
||||||
|
?MailerInterface $mailer = null,
|
||||||
|
): OnGenerationFails {
|
||||||
|
$storedObjectRepository = $this->prophesize(StoredObjectRepositoryInterface::class);
|
||||||
|
$storedObjectRepository->find(Argument::type('int'))->willReturn($storedObject ?? new StoredObject());
|
||||||
|
|
||||||
|
if (null === $entityManager) {
|
||||||
|
$entityManagerProphecy = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $mailer) {
|
||||||
|
$mailerProphecy = $this->prophesize(MailerInterface::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
$translator = $this->prophesize(TranslatorInterface::class);
|
||||||
|
$translator->trans(Argument::type('string'))->will(fn ($args) => $args[0]);
|
||||||
|
|
||||||
|
$userRepository = $this->prophesize(UserRepositoryInterface::class);
|
||||||
|
$userRepository->find(Argument::type('int'))->willReturn(new User());
|
||||||
|
|
||||||
|
$docGeneratorTemplateRepository = $this->prophesize(DocGeneratorTemplateRepositoryInterface::class);
|
||||||
|
$docGeneratorTemplateRepository->find(Argument::type('int'))->willReturn(new DocGeneratorTemplate());
|
||||||
|
|
||||||
|
return new OnGenerationFails(
|
||||||
|
$docGeneratorTemplateRepository->reveal(),
|
||||||
|
$entityManager ?? $entityManagerProphecy->reveal(),
|
||||||
|
new NullLogger(),
|
||||||
|
$mailer ?? $mailerProphecy->reveal(),
|
||||||
|
$storedObjectRepository->reveal(),
|
||||||
|
$translator->reveal(),
|
||||||
|
$userRepository->reveal()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildEvent(object $message): WorkerMessageFailedEvent
|
||||||
|
{
|
||||||
|
$envelope = new Envelope($message);
|
||||||
|
|
||||||
|
return new WorkerMessageFailedEvent($envelope, 'testing', new \RuntimeException());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
docgen:
|
||||||
|
data_dump_email:
|
||||||
|
link_valid_until: >-
|
||||||
|
Ce lien est valide jusqu'au {validity, date, full}, {validity, time, medium}
|
@ -14,13 +14,31 @@ docgen:
|
|||||||
Doc generation is pending: La génération de ce document est en cours
|
Doc generation is pending: La génération de ce document est en cours
|
||||||
Come back later: Revenir plus tard
|
Come back later: Revenir plus tard
|
||||||
|
|
||||||
|
Send report to: Envoyer le rapport à
|
||||||
|
Send report errors to this email address: Les rapports d'erreurs seront envoyés à l'adresse email indiquée
|
||||||
|
Generate as creator: Générer en tant que
|
||||||
|
The document will be generated as the given creator: Le document sera généré à la place de l'utilisateur indiqué
|
||||||
|
Show data instead of generating: Montrer les données au lieu de générer le document
|
||||||
|
|
||||||
|
Any template configured: Aucun gabarit de document configuré
|
||||||
|
|
||||||
|
entity_id_placeholder: Identifiant de l'entité
|
||||||
|
|
||||||
failure_email:
|
failure_email:
|
||||||
The generation of a document failed: La génération d'un document a échoué
|
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 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
|
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.
|
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
|
References: Références
|
||||||
|
|
||||||
|
data_dump_email:
|
||||||
|
subject: Contenu des données de génération de document disponible
|
||||||
|
Dear: Cher
|
||||||
|
data_dump_ready_and_link: >-
|
||||||
|
Le contenu des données est disponible. Vous pouvez le télécharger à l'aide du lien suivant:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
crud:
|
crud:
|
||||||
docgen_template:
|
docgen_template:
|
||||||
index:
|
index:
|
||||||
@ -28,5 +46,4 @@ crud:
|
|||||||
add_new: Créer
|
add_new: Créer
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
@ -25,6 +25,11 @@ use Symfony\Component\Serializer\Annotation as Serializer;
|
|||||||
/**
|
/**
|
||||||
* Represent a document stored in an object store.
|
* Represent a document stored in an object store.
|
||||||
*
|
*
|
||||||
|
* StoredObjects 's content should be read and written using the @see{StoredObjectManagerInterface}.
|
||||||
|
*
|
||||||
|
* The property `$deleteAt` allow a deletion of the document after the given date. But this property should
|
||||||
|
* be set before the document is actually written by the StoredObjectManager.
|
||||||
|
*
|
||||||
* @ORM\Entity
|
* @ORM\Entity
|
||||||
*
|
*
|
||||||
* @ORM\Table("chill_doc.stored_object")
|
* @ORM\Table("chill_doc.stored_object")
|
||||||
@ -117,6 +122,16 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa
|
|||||||
*/
|
*/
|
||||||
private int $generationTrialsCounter = 0;
|
private int $generationTrialsCounter = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Column(type="datetime_immutable", nullable=true, options={"default": null})
|
||||||
|
*/
|
||||||
|
private ?\DateTimeImmutable $deleteAt = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Column(type="text", nullable=false, options={"default": ""})
|
||||||
|
*/
|
||||||
|
private string $generationErrors = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param StoredObject::STATUS_* $status
|
* @param StoredObject::STATUS_* $status
|
||||||
*/
|
*/
|
||||||
@ -144,6 +159,11 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa
|
|||||||
*/
|
*/
|
||||||
public function getCreationDate(): \DateTime
|
public function getCreationDate(): \DateTime
|
||||||
{
|
{
|
||||||
|
if (null === $this->createdAt) {
|
||||||
|
// this scenario will quite never happens
|
||||||
|
return new \DateTime('now');
|
||||||
|
}
|
||||||
|
|
||||||
return \DateTime::createFromImmutable($this->createdAt);
|
return \DateTime::createFromImmutable($this->createdAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,4 +323,37 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa
|
|||||||
{
|
{
|
||||||
return self::STATUS_FAILURE === $this->getStatus();
|
return self::STATUS_FAILURE === $this->getStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getDeleteAt(): ?\DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->deleteAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDeleteAt(?\DateTimeImmutable $deleteAt): StoredObject
|
||||||
|
{
|
||||||
|
$this->deleteAt = $deleteAt;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGenerationErrors(): string
|
||||||
|
{
|
||||||
|
return $this->generationErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds generation errors to the stored object.
|
||||||
|
*
|
||||||
|
* The existing generation errors are not removed
|
||||||
|
*
|
||||||
|
* @param string $generationErrors the generation errors to be added
|
||||||
|
*
|
||||||
|
* @return StoredObject the modified StoredObject instance
|
||||||
|
*/
|
||||||
|
public function addGenerationErrors(string $generationErrors): StoredObject
|
||||||
|
{
|
||||||
|
$this->generationErrors = $this->generationErrors.$generationErrors."\n";
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,11 +14,10 @@ namespace Chill\DocStoreBundle\Repository;
|
|||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
use Doctrine\Persistence\ObjectRepository;
|
|
||||||
|
|
||||||
final class StoredObjectRepository implements ObjectRepository
|
final readonly class StoredObjectRepository implements StoredObjectRepositoryInterface
|
||||||
{
|
{
|
||||||
private readonly EntityRepository $repository;
|
private EntityRepository $repository;
|
||||||
|
|
||||||
public function __construct(EntityManagerInterface $entityManager)
|
public function __construct(EntityManagerInterface $entityManager)
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
<?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\DocStoreBundle\Repository;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ObjectRepository<StoredObject>
|
||||||
|
*/
|
||||||
|
interface StoredObjectRepositoryInterface extends ObjectRepository
|
||||||
|
{
|
||||||
|
}
|
@ -104,6 +104,12 @@ final class StoredObjectManager implements StoredObjectManagerInterface
|
|||||||
)
|
)
|
||||||
: $clearContent;
|
: $clearContent;
|
||||||
|
|
||||||
|
$headers = [];
|
||||||
|
|
||||||
|
if (null !== $document->getDeleteAt()) {
|
||||||
|
$headers['X-Delete-At'] = $document->getDeleteAt()->getTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$response = $this
|
$response = $this
|
||||||
->client
|
->client
|
||||||
@ -118,6 +124,7 @@ final class StoredObjectManager implements StoredObjectManagerInterface
|
|||||||
->url,
|
->url,
|
||||||
[
|
[
|
||||||
'body' => $encryptedContent,
|
'body' => $encryptedContent,
|
||||||
|
'headers' => $headers,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
} catch (TransportExceptionInterface $exception) {
|
} catch (TransportExceptionInterface $exception) {
|
||||||
@ -129,6 +136,11 @@ final class StoredObjectManager implements StoredObjectManagerInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function clearCache(): void
|
||||||
|
{
|
||||||
|
$this->inMemory = [];
|
||||||
|
}
|
||||||
|
|
||||||
private function extractLastModifiedFromResponse(ResponseInterface $response): \DateTimeImmutable
|
private function extractLastModifiedFromResponse(ResponseInterface $response): \DateTimeImmutable
|
||||||
{
|
{
|
||||||
$lastModifiedString = (($response->getHeaders()['last-modified'] ?? [])[0] ?? '');
|
$lastModifiedString = (($response->getHeaders()['last-modified'] ?? [])[0] ?? '');
|
||||||
|
@ -12,6 +12,7 @@ declare(strict_types=1);
|
|||||||
namespace Chill\DocStoreBundle\Service;
|
namespace Chill\DocStoreBundle\Service;
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||||
|
|
||||||
interface StoredObjectManagerInterface
|
interface StoredObjectManagerInterface
|
||||||
{
|
{
|
||||||
@ -23,6 +24,8 @@ interface StoredObjectManagerInterface
|
|||||||
* @param StoredObject $document the document
|
* @param StoredObject $document the document
|
||||||
*
|
*
|
||||||
* @return string the retrieved content in clear
|
* @return string the retrieved content in clear
|
||||||
|
*
|
||||||
|
* @throws StoredObjectManagerException if unable to read or decrypt the content
|
||||||
*/
|
*/
|
||||||
public function read(StoredObject $document): string;
|
public function read(StoredObject $document): string;
|
||||||
|
|
||||||
@ -31,6 +34,10 @@ interface StoredObjectManagerInterface
|
|||||||
*
|
*
|
||||||
* @param StoredObject $document the document
|
* @param StoredObject $document the document
|
||||||
* @param $clearContent The content to store in clear
|
* @param $clearContent The content to store in clear
|
||||||
|
*
|
||||||
|
* @throws StoredObjectManagerException
|
||||||
*/
|
*/
|
||||||
public function write(StoredObject $document, string $clearContent): void;
|
public function write(StoredObject $document, string $clearContent): void;
|
||||||
|
|
||||||
|
public function clearCache(): void;
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ declare(strict_types=1);
|
|||||||
* the LICENSE file that was distributed with this source code.
|
* the LICENSE file that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace Chill\DocStoreBundle\Tests;
|
namespace Chill\DocStoreBundle\Tests\Service;
|
||||||
|
|
||||||
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
|
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
@ -117,6 +117,41 @@ final class StoredObjectManagerTest extends TestCase
|
|||||||
self::assertEquals($clearContent, $storedObjectManager->read($storedObject));
|
self::assertEquals($clearContent, $storedObjectManager->read($storedObject));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testWriteWithDeleteAt()
|
||||||
|
{
|
||||||
|
$storedObject = new StoredObject();
|
||||||
|
|
||||||
|
$expectedRequests = [
|
||||||
|
function ($method, $url, $options): MockResponse {
|
||||||
|
self::assertEquals('PUT', $method);
|
||||||
|
self::assertArrayHasKey('headers', $options);
|
||||||
|
self::assertIsArray($options['headers']);
|
||||||
|
self::assertCount(0, array_filter($options['headers'], fn (string $header) => str_contains($header, 'X-Delete-At')));
|
||||||
|
|
||||||
|
return new MockResponse('', ['http_code' => 201]);
|
||||||
|
},
|
||||||
|
|
||||||
|
function ($method, $url, $options): MockResponse {
|
||||||
|
self::assertEquals('PUT', $method);
|
||||||
|
self::assertArrayHasKey('headers', $options);
|
||||||
|
self::assertIsArray($options['headers']);
|
||||||
|
self::assertCount(1, array_filter($options['headers'], fn (string $header) => str_contains($header, 'X-Delete-At')));
|
||||||
|
self::assertContains('X-Delete-At: 1711014260', $options['headers']);
|
||||||
|
|
||||||
|
return new MockResponse('', ['http_code' => 201]);
|
||||||
|
},
|
||||||
|
];
|
||||||
|
$client = new MockHttpClient($expectedRequests);
|
||||||
|
|
||||||
|
$manager = new StoredObjectManager($client, $this->getTempUrlGenerator($storedObject));
|
||||||
|
|
||||||
|
$manager->write($storedObject, 'ok');
|
||||||
|
|
||||||
|
// with a deletedAt date
|
||||||
|
$storedObject->setDeleteAt(\DateTimeImmutable::createFromFormat('U', '1711014260'));
|
||||||
|
$manager->write($storedObject, 'ok');
|
||||||
|
}
|
||||||
|
|
||||||
private function getHttpClient(string $encodedContent): HttpClientInterface
|
private function getHttpClient(string $encodedContent): HttpClientInterface
|
||||||
{
|
{
|
||||||
$callback = static function ($method, $url, $options) use ($encodedContent) {
|
$callback = static function ($method, $url, $options) use ($encodedContent) {
|
@ -0,0 +1,36 @@
|
|||||||
|
<?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\Migrations\DocStore;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20240322100107 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'StoredObject: add deleteAt and generationErrors columns';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object ADD deleteAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object ADD generationErrors TEXT DEFAULT \'\' NOT NULL');
|
||||||
|
$this->addSql('COMMENT ON COLUMN chill_doc.stored_object.deleteAt IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object DROP deleteAt');
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object DROP generationErrors');
|
||||||
|
}
|
||||||
|
}
|
@ -227,7 +227,7 @@ class AccompanyingPeriodContext implements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($options['thirdParty']) {
|
if ($options['thirdParty'] ?? false) {
|
||||||
$data['thirdParty'] = $this->normalizer->normalize($contextGenerationData['thirdParty'], 'docgen', [
|
$data['thirdParty'] = $this->normalizer->normalize($contextGenerationData['thirdParty'], 'docgen', [
|
||||||
'docgen:expects' => ThirdParty::class,
|
'docgen:expects' => ThirdParty::class,
|
||||||
'groups' => 'docgen:read',
|
'groups' => 'docgen:read',
|
||||||
|
@ -15,7 +15,6 @@ use ChampsLibres\WopiLib\Contract\Service\Configuration\ConfigurationInterface;
|
|||||||
use ChampsLibres\WopiLib\Contract\Service\Discovery\DiscoveryInterface;
|
use ChampsLibres\WopiLib\Contract\Service\Discovery\DiscoveryInterface;
|
||||||
use ChampsLibres\WopiLib\Contract\Service\DocumentManagerInterface;
|
use ChampsLibres\WopiLib\Contract\Service\DocumentManagerInterface;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Chill\MainBundle\Entity\User;
|
|
||||||
use Chill\WopiBundle\Service\Controller\ResponderInterface;
|
use Chill\WopiBundle\Service\Controller\ResponderInterface;
|
||||||
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
|
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
|
||||||
use loophp\psr17\Psr17Interface;
|
use loophp\psr17\Psr17Interface;
|
||||||
@ -43,13 +42,11 @@ final readonly class Editor
|
|||||||
|
|
||||||
public function __invoke(string $fileId, Request $request): Response
|
public function __invoke(string $fileId, Request $request): Response
|
||||||
{
|
{
|
||||||
if (null === $user = $this->security->getUser()) {
|
if (!($this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN'))) {
|
||||||
throw new AccessDeniedHttpException('Please authenticate to access this feature');
|
throw new AccessDeniedHttpException('Please authenticate to access this feature');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$user instanceof User) {
|
$user = $this->security->getUser();
|
||||||
throw new AccessDeniedHttpException('Please authenticate as a user to access this feature');
|
|
||||||
}
|
|
||||||
|
|
||||||
$configuration = $this->wopiConfiguration->jsonSerialize();
|
$configuration = $this->wopiConfiguration->jsonSerialize();
|
||||||
/** @var StoredObject $storedObject */
|
/** @var StoredObject $storedObject */
|
||||||
@ -77,7 +74,12 @@ final readonly class Editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ([] === $discoverExtension = $this->wopiDiscovery->discoverMimeType($storedObject->getType())) {
|
if ([] === $discoverExtension = $this->wopiDiscovery->discoverMimeType($storedObject->getType())) {
|
||||||
throw new \Exception(sprintf('Unable to find mime type %s', $storedObject->getType()));
|
return new Response(
|
||||||
|
$this->engine
|
||||||
|
->render('@ChillWopi/Editor/unable_to_edit_such_document.html.twig', [
|
||||||
|
'document' => $storedObject,
|
||||||
|
])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$configuration['favIconUrl'] = '';
|
$configuration['favIconUrl'] = '';
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
{% extends '@ChillMain/layout.html.twig' %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
{{ parent() }}
|
||||||
|
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{{ parent() }}
|
||||||
|
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="alert alert-danger text-center">
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{{ 'wopi_editor.document unsupported for edition'|trans }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>{{ document|chill_document_button_group(document.title|default('Document'), false) }}</p>
|
||||||
|
</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">
|
||||||
|
{{ 'Cancel'|trans|chill_return_path_label }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% endblock content %}
|
@ -38,7 +38,7 @@ class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\Authori
|
|||||||
|
|
||||||
$user = $this->security->getUser();
|
$user = $this->security->getUser();
|
||||||
|
|
||||||
if (!$user instanceof User) {
|
if (!($user instanceof User || $this->security->isGranted('ROLE_ADMIN'))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +25,10 @@ class UserManager implements \ChampsLibres\WopiBundle\Contracts\UserManagerInter
|
|||||||
{
|
{
|
||||||
$user = $this->security->getUser();
|
$user = $this->security->getUser();
|
||||||
|
|
||||||
|
if (!$user instanceof User && $this->security->isGranted('ROLE_ADMIN')) {
|
||||||
|
return $user->getUsername();
|
||||||
|
}
|
||||||
|
|
||||||
if (!$user instanceof User) {
|
if (!$user instanceof User) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -36,6 +40,10 @@ class UserManager implements \ChampsLibres\WopiBundle\Contracts\UserManagerInter
|
|||||||
{
|
{
|
||||||
$user = $this->security->getUser();
|
$user = $this->security->getUser();
|
||||||
|
|
||||||
|
if (!$user instanceof User && $this->security->isGranted('ROLE_ADMIN')) {
|
||||||
|
return $user->getUsername();
|
||||||
|
}
|
||||||
|
|
||||||
if (!$user instanceof User) {
|
if (!$user instanceof User) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
wopi_editor:
|
||||||
|
document unsupported for edition: Ce format de document n'est pas éditable
|
Loading…
x
Reference in New Issue
Block a user