mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge branch 'master' into upgrade-php82
This commit is contained in:
commit
7dc07129f8
@ -22,6 +22,7 @@ use Chill\DocStoreBundle\Repository\DocumentCategoryRepository;
|
|||||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Chill\PersonBundle\Repository\PersonRepository;
|
||||||
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
|
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||||
@ -45,6 +46,8 @@ class ActivityContext implements
|
|||||||
|
|
||||||
private PersonRenderInterface $personRender;
|
private PersonRenderInterface $personRender;
|
||||||
|
|
||||||
|
private PersonRepository $personRepository;
|
||||||
|
|
||||||
private TranslatableStringHelperInterface $translatableStringHelper;
|
private TranslatableStringHelperInterface $translatableStringHelper;
|
||||||
|
|
||||||
private TranslatorInterface $translator;
|
private TranslatorInterface $translator;
|
||||||
@ -55,6 +58,7 @@ class ActivityContext implements
|
|||||||
TranslatableStringHelperInterface $translatableStringHelper,
|
TranslatableStringHelperInterface $translatableStringHelper,
|
||||||
EntityManagerInterface $em,
|
EntityManagerInterface $em,
|
||||||
PersonRenderInterface $personRender,
|
PersonRenderInterface $personRender,
|
||||||
|
PersonRepository $personRepository,
|
||||||
TranslatorInterface $translator,
|
TranslatorInterface $translator,
|
||||||
BaseContextData $baseContextData
|
BaseContextData $baseContextData
|
||||||
) {
|
) {
|
||||||
@ -63,6 +67,7 @@ class ActivityContext implements
|
|||||||
$this->translatableStringHelper = $translatableStringHelper;
|
$this->translatableStringHelper = $translatableStringHelper;
|
||||||
$this->em = $em;
|
$this->em = $em;
|
||||||
$this->personRender = $personRender;
|
$this->personRender = $personRender;
|
||||||
|
$this->personRepository = $personRepository;
|
||||||
$this->translator = $translator;
|
$this->translator = $translator;
|
||||||
$this->baseContextData = $baseContextData;
|
$this->baseContextData = $baseContextData;
|
||||||
}
|
}
|
||||||
@ -206,6 +211,32 @@ class ActivityContext implements
|
|||||||
return $options['mainPerson'] || $options['person1'] || $options['person2'];
|
return $options['mainPerson'] || $options['person1'] || $options['person2'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array
|
||||||
|
{
|
||||||
|
$normalized = [];
|
||||||
|
|
||||||
|
foreach (['mainPerson', 'person1', 'person2'] as $k) {
|
||||||
|
$normalized[$k] = null === $data[$k] ? null : $data[$k]->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array
|
||||||
|
{
|
||||||
|
$denormalized = [];
|
||||||
|
|
||||||
|
foreach (['mainPerson', 'person1', 'person2'] as $k) {
|
||||||
|
if (null !== ($id = ($data[$k] ?? null))) {
|
||||||
|
$denormalized[$k] = $this->personRepository->find($id);
|
||||||
|
} else {
|
||||||
|
$denormalized[$k] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $denormalized;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Activity $entity
|
* @param Activity $entity
|
||||||
*/
|
*/
|
||||||
|
@ -146,6 +146,16 @@ class ListActivitiesByAccompanyingPeriodContext implements
|
|||||||
return $this->accompanyingPeriodContext->hasPublicForm($template, $entity);
|
return $this->accompanyingPeriodContext->hasPublicForm($template, $entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array
|
||||||
|
{
|
||||||
|
return $this->accompanyingPeriodContext->contextGenerationDataNormalize($template, $entity, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array
|
||||||
|
{
|
||||||
|
return $this->accompanyingPeriodContext->contextGenerationDataDenormalize($template, $entity, $data);
|
||||||
|
}
|
||||||
|
|
||||||
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void
|
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void
|
||||||
{
|
{
|
||||||
$this->accompanyingPeriodContext->storeGenerated($template, $storedObject, $entity, $contextGenerationData);
|
$this->accompanyingPeriodContext->storeGenerated($template, $storedObject, $entity, $contextGenerationData);
|
||||||
|
@ -77,7 +77,7 @@ Choose a type: Choisir un type
|
|||||||
4 hours: 4 heures
|
4 hours: 4 heures
|
||||||
4 hours 30: 4 heures 30
|
4 hours 30: 4 heures 30
|
||||||
5 hours: 5 heures
|
5 hours: 5 heures
|
||||||
Concerned groups: Parties concernées
|
Concerned groups: Parties concernées par l'échange
|
||||||
Persons in accompanying course: Usagers du parcours
|
Persons in accompanying course: Usagers du parcours
|
||||||
Third persons: Tiers non-pro.
|
Third persons: Tiers non-pro.
|
||||||
Others persons: Usagers
|
Others persons: Usagers
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<div id="mainUser"></div> {# <=== vue component: mainUser #}
|
<div id="mainUser"></div> {# <=== vue component: mainUser #}
|
||||||
|
|
||||||
<h2 class="chill-red">{{ 'Concerned groups'|trans }}</h2>
|
<h2 class="chill-red">{{ 'Concerned groups calendar'|trans }}</h2>
|
||||||
|
|
||||||
{%- if form.persons is defined -%}
|
{%- if form.persons is defined -%}
|
||||||
{{ form_widget(form.persons) }}
|
{{ form_widget(form.persons) }}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<div id="mainUser"></div> {# <=== vue component: mainUser #}
|
<div id="mainUser"></div> {# <=== vue component: mainUser #}
|
||||||
|
|
||||||
<h2 class="chill-red">{{ 'Concerned groups'|trans }}</h2>
|
<h2 class="chill-red">{{ 'Concerned groups calendar'|trans }}</h2>
|
||||||
|
|
||||||
{%- if form.mainUser is defined -%}
|
{%- if form.mainUser is defined -%}
|
||||||
{{ form_row(form.mainUser) }}
|
{{ form_row(form.mainUser) }}
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
<dd>{{ entity.mainUser }}</dd>
|
<dd>{{ entity.mainUser }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<h2 class="chill-red">{{ 'Concerned groups'|trans }}</h2>
|
<h2 class="chill-red">{{ 'Concerned groups calendar'|trans }}</h2>
|
||||||
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {'context': 'calendar_' ~ context, 'render': 'bloc' } %}
|
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {'context': 'calendar_' ~ context, 'render': 'bloc' } %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -226,6 +226,16 @@ final class CalendarContext implements CalendarContextInterface
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array
|
||||||
|
{
|
||||||
|
// TODO: Implement publicFormTransform() method.
|
||||||
|
}
|
||||||
|
|
||||||
|
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array
|
||||||
|
{
|
||||||
|
// TODO: Implement publicFormReverseTransform() method.
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array{mainPerson?: Person, thirdParty?: ThirdParty, title: string} $contextGenerationData
|
* @param array{mainPerson?: Person, thirdParty?: ThirdParty, title: string} $contextGenerationData
|
||||||
*/
|
*/
|
||||||
|
@ -56,6 +56,10 @@ interface CalendarContextInterface extends DocGeneratorContextWithPublicFormInte
|
|||||||
*/
|
*/
|
||||||
public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool;
|
public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool;
|
||||||
|
|
||||||
|
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array;
|
||||||
|
|
||||||
|
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Calendar $entity
|
* @param Calendar $entity
|
||||||
*/
|
*/
|
||||||
|
@ -4,7 +4,7 @@ My calendar list: Mes rendez-vous
|
|||||||
There is no calendar items.: Il n'y a pas de rendez-vous
|
There is no calendar items.: Il n'y a pas de rendez-vous
|
||||||
Remove calendar item: Supprimer le rendez-vous
|
Remove calendar item: Supprimer le rendez-vous
|
||||||
Are you sure you want to remove the calendar item?: Êtes-vous sûr de vouloir supprimer le rendez-vous?
|
Are you sure you want to remove the calendar item?: Êtes-vous sûr de vouloir supprimer le rendez-vous?
|
||||||
Concerned groups: Parties concernées
|
Concerned groups calendar: Parties concernées
|
||||||
Calendar data: Données du rendez-vous
|
Calendar data: Données du rendez-vous
|
||||||
Update calendar: Modifier le rendez-vous
|
Update calendar: Modifier le rendez-vous
|
||||||
main user concerned: Utilisateur concerné
|
main user concerned: Utilisateur concerné
|
||||||
|
@ -23,6 +23,9 @@ interface DocGeneratorContextWithPublicFormInterface extends DocGeneratorContext
|
|||||||
*/
|
*/
|
||||||
public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void;
|
public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fill the form with initial data
|
||||||
|
*/
|
||||||
public function getFormData(DocGeneratorTemplate $template, $entity): array;
|
public function getFormData(DocGeneratorTemplate $template, $entity): array;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -31,4 +34,14 @@ interface DocGeneratorContextWithPublicFormInterface extends DocGeneratorContext
|
|||||||
* @param mixed $entity
|
* @param mixed $entity
|
||||||
*/
|
*/
|
||||||
public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool;
|
public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the data from the form into serializable data, storable into messenger's message
|
||||||
|
*/
|
||||||
|
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the data from the messenger's message into data usable for doc's generation
|
||||||
|
*/
|
||||||
|
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array;
|
||||||
}
|
}
|
||||||
|
@ -16,67 +16,57 @@ use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface;
|
|||||||
use Chill\DocGeneratorBundle\Context\Exception\ContextNotFoundException;
|
use Chill\DocGeneratorBundle\Context\Exception\ContextNotFoundException;
|
||||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||||
use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface;
|
use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface;
|
||||||
use Chill\DocGeneratorBundle\GeneratorDriver\Exception\TemplateException;
|
|
||||||
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
|
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
|
||||||
|
use Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface;
|
||||||
|
use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||||
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 Exception;
|
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
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\FileType;
|
||||||
use Symfony\Component\HttpFoundation\File\File;
|
|
||||||
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\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\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||||
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
|
|
||||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
use Throwable;
|
|
||||||
use function strlen;
|
use function strlen;
|
||||||
|
|
||||||
final class DocGeneratorTemplateController extends AbstractController
|
final class DocGeneratorTemplateController extends AbstractController
|
||||||
{
|
{
|
||||||
private HttpClientInterface $client;
|
|
||||||
|
|
||||||
private ContextManager $contextManager;
|
private ContextManager $contextManager;
|
||||||
|
|
||||||
private DocGeneratorTemplateRepository $docGeneratorTemplateRepository;
|
private DocGeneratorTemplateRepository $docGeneratorTemplateRepository;
|
||||||
|
|
||||||
private DriverInterface $driver;
|
|
||||||
|
|
||||||
private EntityManagerInterface $entityManager;
|
private EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
private LoggerInterface $logger;
|
private GeneratorInterface $generator;
|
||||||
|
|
||||||
|
private MessageBusInterface $messageBus;
|
||||||
|
|
||||||
private PaginatorFactory $paginatorFactory;
|
private PaginatorFactory $paginatorFactory;
|
||||||
|
|
||||||
private StoredObjectManagerInterface $storedObjectManager;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ContextManager $contextManager,
|
ContextManager $contextManager,
|
||||||
DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
|
DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
|
||||||
DriverInterface $driver,
|
GeneratorInterface $generator,
|
||||||
LoggerInterface $logger,
|
MessageBusInterface $messageBus,
|
||||||
PaginatorFactory $paginatorFactory,
|
PaginatorFactory $paginatorFactory,
|
||||||
HttpClientInterface $client,
|
|
||||||
StoredObjectManagerInterface $storedObjectManager,
|
|
||||||
EntityManagerInterface $entityManager
|
EntityManagerInterface $entityManager
|
||||||
) {
|
) {
|
||||||
$this->contextManager = $contextManager;
|
$this->contextManager = $contextManager;
|
||||||
$this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository;
|
$this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository;
|
||||||
$this->driver = $driver;
|
$this->generator = $generator;
|
||||||
$this->logger = $logger;
|
$this->messageBus = $messageBus;
|
||||||
$this->paginatorFactory = $paginatorFactory;
|
$this->paginatorFactory = $paginatorFactory;
|
||||||
$this->client = $client;
|
|
||||||
$this->storedObjectManager = $storedObjectManager;
|
|
||||||
$this->entityManager = $entityManager;
|
$this->entityManager = $entityManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,7 +84,6 @@ final class DocGeneratorTemplateController extends AbstractController
|
|||||||
): Response {
|
): Response {
|
||||||
return $this->generateDocFromTemplate(
|
return $this->generateDocFromTemplate(
|
||||||
$template,
|
$template,
|
||||||
$entityClassName,
|
|
||||||
$entityId,
|
$entityId,
|
||||||
$request,
|
$request,
|
||||||
true
|
true
|
||||||
@ -115,7 +104,6 @@ final class DocGeneratorTemplateController extends AbstractController
|
|||||||
): Response {
|
): Response {
|
||||||
return $this->generateDocFromTemplate(
|
return $this->generateDocFromTemplate(
|
||||||
$template,
|
$template,
|
||||||
$entityClassName,
|
|
||||||
$entityId,
|
$entityId,
|
||||||
$request,
|
$request,
|
||||||
false
|
false
|
||||||
@ -185,7 +173,6 @@ final class DocGeneratorTemplateController extends AbstractController
|
|||||||
|
|
||||||
private function generateDocFromTemplate(
|
private function generateDocFromTemplate(
|
||||||
DocGeneratorTemplate $template,
|
DocGeneratorTemplate $template,
|
||||||
string $entityClassName,
|
|
||||||
int $entityId,
|
int $entityId,
|
||||||
Request $request,
|
Request $request,
|
||||||
bool $isTest
|
bool $isTest
|
||||||
@ -206,7 +193,7 @@ final class DocGeneratorTemplateController extends AbstractController
|
|||||||
|
|
||||||
if (null === $entity) {
|
if (null === $entity) {
|
||||||
throw new NotFoundHttpException(
|
throw new NotFoundHttpException(
|
||||||
sprintf('Entity with classname %s and id %s is not found', $entityClassName, $entityId)
|
sprintf('Entity with classname %s and id %s is not found', $context->getEntityClass(), $entityId)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,99 +246,68 @@ final class DocGeneratorTemplateController extends AbstractController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$document = $template->getFile();
|
// transform context generation data
|
||||||
|
$contextGenerationDataSanitized =
|
||||||
if ($isTest && ($contextGenerationData['test_file'] instanceof File)) {
|
$context instanceof DocGeneratorContextWithPublicFormInterface ?
|
||||||
$dataDecrypted = file_get_contents($contextGenerationData['test_file']->getPathname());
|
$context->contextGenerationDataNormalize($template, $entity, $contextGenerationData)
|
||||||
} else {
|
: [];
|
||||||
try {
|
|
||||||
$dataDecrypted = $this->storedObjectManager->read($document);
|
|
||||||
} catch (Throwable $exception) {
|
|
||||||
throw $exception;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// if is test, render the data or generate the doc
|
||||||
if ($isTest && isset($form) && $form['show_data']->getData()) {
|
if ($isTest && isset($form) && $form['show_data']->getData()) {
|
||||||
return $this->render('@ChillDocGenerator/Generator/debug_value.html.twig', [
|
return $this->render('@ChillDocGenerator/Generator/debug_value.html.twig', [
|
||||||
'datas' => json_encode($context->getData($template, $entity, $contextGenerationData), JSON_PRETTY_PRINT)
|
'datas' => json_encode($context->getData($template, $entity, $contextGenerationData), JSON_PRETTY_PRINT)
|
||||||
]);
|
]);
|
||||||
}
|
} elseif ($isTest) {
|
||||||
|
$generated = $this->generator->generateDocFromTemplate(
|
||||||
try {
|
$template,
|
||||||
$generatedResource = $this
|
$entityId,
|
||||||
->driver
|
$contextGenerationDataSanitized,
|
||||||
->generateFromString(
|
null,
|
||||||
$dataDecrypted,
|
true,
|
||||||
$template->getFile()->getType(),
|
isset($form) ? $form['test_file']->getData() : null
|
||||||
$context->getData($template, $entity, $contextGenerationData),
|
|
||||||
$template->getFile()->getFilename()
|
|
||||||
);
|
|
||||||
} catch (TemplateException $e) {
|
|
||||||
return new Response(
|
|
||||||
implode("\n", $e->getErrors()),
|
|
||||||
400,
|
|
||||||
[
|
|
||||||
'Content-Type' => 'text/plain',
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if ($isTest) {
|
|
||||||
return new Response(
|
return new Response(
|
||||||
$generatedResource,
|
$generated,
|
||||||
Response::HTTP_OK,
|
Response::HTTP_OK,
|
||||||
[
|
[
|
||||||
'Content-Transfer-Encoding', 'binary',
|
'Content-Transfer-Encoding', 'binary',
|
||||||
'Content-Type' => 'application/vnd.oasis.opendocument.text',
|
'Content-Type' => 'application/vnd.oasis.opendocument.text',
|
||||||
'Content-Disposition' => 'attachment; filename="generated.odt"',
|
'Content-Disposition' => 'attachment; filename="generated.odt"',
|
||||||
'Content-Length' => strlen($generatedResource),
|
'Content-Length' => strlen($generated),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var StoredObject $storedObject */
|
// this is not a test
|
||||||
$storedObject = (new ObjectNormalizer())
|
// we prepare the object to store the document
|
||||||
->denormalize(
|
$storedObject = (new StoredObject())
|
||||||
[
|
->setStatus(StoredObject::STATUS_PENDING)
|
||||||
'type' => $template->getFile()->getType(),
|
;
|
||||||
'filename' => sprintf('%s_odt', uniqid('doc_', true)),
|
|
||||||
],
|
|
||||||
StoredObject::class
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
$this->storedObjectManager->write($storedObject, $generatedResource);
|
|
||||||
} catch (Throwable $exception) {
|
|
||||||
throw $exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->entityManager->persist($storedObject);
|
$this->entityManager->persist($storedObject);
|
||||||
|
|
||||||
try {
|
// we store the generated document
|
||||||
$context
|
$context
|
||||||
->storeGenerated(
|
->storeGenerated(
|
||||||
$template,
|
$template,
|
||||||
$storedObject,
|
$storedObject,
|
||||||
$entity,
|
$entity,
|
||||||
$contextGenerationData
|
$contextGenerationData
|
||||||
);
|
);
|
||||||
} catch (Exception $e) {
|
|
||||||
$this
|
|
||||||
->logger
|
|
||||||
->error(
|
|
||||||
'Unable to store the associated document to entity',
|
|
||||||
[
|
|
||||||
'entityClassName' => $entityClassName,
|
|
||||||
'entityId' => $entityId,
|
|
||||||
'contextKey' => $context->getName(),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->messageBus->dispatch(
|
||||||
|
new RequestGenerationMessage(
|
||||||
|
$this->getUser(),
|
||||||
|
$template,
|
||||||
|
$entityId,
|
||||||
|
$storedObject,
|
||||||
|
$contextGenerationDataSanitized,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
return $this
|
return $this
|
||||||
->redirectToRoute(
|
->redirectToRoute(
|
||||||
'chill_wopi_file_edit',
|
'chill_wopi_file_edit',
|
||||||
|
@ -53,6 +53,7 @@ final class RelatorioDriver implements DriverInterface
|
|||||||
$response = $this->client->request('POST', $this->url, [
|
$response = $this->client->request('POST', $this->url, [
|
||||||
'headers' => $form->getPreparedHeaders()->toArray(),
|
'headers' => $form->getPreparedHeaders()->toArray(),
|
||||||
'body' => $form->bodyToIterable(),
|
'body' => $form->bodyToIterable(),
|
||||||
|
'timeout' => '300',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $response->getContent();
|
return $response->getContent();
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
{{ creator.label }},
|
||||||
|
|
||||||
|
{{ 'docgen.failure_email.The generation of the document {template_name} failed'|trans({'{template_name}': template.name|localize_translatable_string}) }}
|
||||||
|
|
||||||
|
{{ 'docgen.failure_email.Forward this email to your administrator for solving'|trans }}
|
||||||
|
|
||||||
|
{{ 'docgen.failure_email.References'|trans }}:
|
||||||
|
{% if errors|length > 0 %}
|
||||||
|
{{ 'docgen.failure_email.The following errors were encoutered'|trans }}:
|
||||||
|
|
||||||
|
{% for error in errors %}
|
||||||
|
- {{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
- template_id: {{ template.id }}
|
||||||
|
- stored_object_destination_id: {{ stored_object_id }}
|
@ -0,0 +1,143 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Chill\DocGeneratorBundle\Service\Generator;
|
||||||
|
|
||||||
|
use Chill\DocGeneratorBundle\Context\ContextManagerInterface;
|
||||||
|
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface;
|
||||||
|
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||||
|
use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface;
|
||||||
|
use Chill\DocGeneratorBundle\GeneratorDriver\Exception\TemplateException;
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\File\File;
|
||||||
|
|
||||||
|
class Generator implements GeneratorInterface
|
||||||
|
{
|
||||||
|
private ContextManagerInterface $contextManager;
|
||||||
|
|
||||||
|
private DriverInterface $driver;
|
||||||
|
|
||||||
|
private EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
|
private LoggerInterface $logger;
|
||||||
|
|
||||||
|
private StoredObjectManagerInterface $storedObjectManager;
|
||||||
|
|
||||||
|
private const LOG_PREFIX = '[docgen generator] ';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
ContextManagerInterface $contextManager,
|
||||||
|
DriverInterface $driver,
|
||||||
|
EntityManagerInterface $entityManager,
|
||||||
|
LoggerInterface $logger,
|
||||||
|
StoredObjectManagerInterface $storedObjectManager
|
||||||
|
) {
|
||||||
|
$this->contextManager = $contextManager;
|
||||||
|
$this->driver = $driver;
|
||||||
|
$this->entityManager = $entityManager;
|
||||||
|
$this->logger = $logger;
|
||||||
|
$this->storedObjectManager = $storedObjectManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T of File|null
|
||||||
|
* @template B of bool
|
||||||
|
* @param B $isTest
|
||||||
|
* @param (B is true ? T : null) $testFile
|
||||||
|
* @psalm-return (B is true ? string : null)
|
||||||
|
* @throws \Symfony\Component\Serializer\Exception\ExceptionInterface|\Throwable
|
||||||
|
*/
|
||||||
|
public function generateDocFromTemplate(
|
||||||
|
DocGeneratorTemplate $template,
|
||||||
|
int $entityId,
|
||||||
|
array $contextGenerationDataNormalized,
|
||||||
|
?StoredObject $destinationStoredObject = null,
|
||||||
|
bool $isTest = false,
|
||||||
|
?File $testFile = null
|
||||||
|
): ?string {
|
||||||
|
if ($destinationStoredObject instanceof StoredObject && StoredObject::STATUS_PENDING !== $destinationStoredObject->getStatus()) {
|
||||||
|
$this->logger->info(self::LOG_PREFIX.'Aborting generation of an already generated document');
|
||||||
|
throw new ObjectReadyException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->info(self::LOG_PREFIX.'Starting generation of a document', [
|
||||||
|
'entity_id' => $entityId,
|
||||||
|
'destination_stored_object' => $destinationStoredObject === null ? null : $destinationStoredObject->getId()
|
||||||
|
]);
|
||||||
|
|
||||||
|
$context = $this->contextManager->getContextByDocGeneratorTemplate($template);
|
||||||
|
|
||||||
|
$entity = $this
|
||||||
|
->entityManager
|
||||||
|
->find($context->getEntityClass(), $entityId)
|
||||||
|
;
|
||||||
|
|
||||||
|
if (null === $entity) {
|
||||||
|
throw new RelatedEntityNotFoundException($template->getEntity(), $entityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$contextGenerationDataNormalized = array_merge(
|
||||||
|
$contextGenerationDataNormalized,
|
||||||
|
$context instanceof DocGeneratorContextWithPublicFormInterface ?
|
||||||
|
$context->contextGenerationDataDenormalize($template, $entity, $contextGenerationDataNormalized)
|
||||||
|
: []
|
||||||
|
);
|
||||||
|
|
||||||
|
$data = $context->getData($template, $entity, $contextGenerationDataNormalized);
|
||||||
|
|
||||||
|
$destinationStoredObjectId = $destinationStoredObject instanceof StoredObject ? $destinationStoredObject->getId() : null;
|
||||||
|
$this->entityManager->clear();
|
||||||
|
gc_collect_cycles();
|
||||||
|
if (null !== $destinationStoredObjectId) {
|
||||||
|
$destinationStoredObject = $this->entityManager->find(StoredObject::class, $destinationStoredObjectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isTest && ($testFile instanceof File)) {
|
||||||
|
$templateDecrypted = file_get_contents($testFile->getPathname());
|
||||||
|
} else {
|
||||||
|
$templateDecrypted = $this->storedObjectManager->read($template->getFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$generatedResource = $this
|
||||||
|
->driver
|
||||||
|
->generateFromString(
|
||||||
|
$templateDecrypted,
|
||||||
|
$template->getFile()->getType(),
|
||||||
|
$data,
|
||||||
|
$template->getFile()->getFilename()
|
||||||
|
);
|
||||||
|
} catch (TemplateException $e) {
|
||||||
|
throw new GeneratorException($e->getErrors(), $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isTest) {
|
||||||
|
$this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [
|
||||||
|
'is_test' => true,
|
||||||
|
'entity_id' => $entityId,
|
||||||
|
'destination_stored_object' => $destinationStoredObject === null ? null : $destinationStoredObject->getId()
|
||||||
|
]);
|
||||||
|
return $generatedResource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var StoredObject $storedObject */
|
||||||
|
$destinationStoredObject
|
||||||
|
->setType($template->getFile()->getType())
|
||||||
|
->setFilename(sprintf('%s_odt', uniqid('doc_', true)))
|
||||||
|
->setStatus(StoredObject::STATUS_READY)
|
||||||
|
;
|
||||||
|
|
||||||
|
$this->storedObjectManager->write($destinationStoredObject, $generatedResource);
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [
|
||||||
|
'entity_id' => $entityId,
|
||||||
|
'destination_stored_object' => $destinationStoredObject->getId(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Chill\DocGeneratorBundle\Service\Generator;
|
||||||
|
|
||||||
|
class GeneratorException extends \RuntimeException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
private array $errors;
|
||||||
|
|
||||||
|
public function __construct(array $errors = [], \Throwable $previous = null)
|
||||||
|
{
|
||||||
|
$this->errors = $errors;
|
||||||
|
parent::__construct("Could not generate the document", 15252,
|
||||||
|
$previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getErrors(): array
|
||||||
|
{
|
||||||
|
return $this->errors;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Chill\DocGeneratorBundle\Service\Generator;
|
||||||
|
|
||||||
|
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Symfony\Component\HttpFoundation\File\File;
|
||||||
|
|
||||||
|
interface GeneratorInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @template T of File|null
|
||||||
|
* @template B of bool
|
||||||
|
* @param B $isTest
|
||||||
|
* @param (B is true ? T : null) $testFile
|
||||||
|
* @psalm-return (B is true ? string : null)
|
||||||
|
* @throws \Symfony\Component\Serializer\Exception\ExceptionInterface|\Throwable
|
||||||
|
*/
|
||||||
|
public function generateDocFromTemplate(
|
||||||
|
DocGeneratorTemplate $template,
|
||||||
|
int $entityId,
|
||||||
|
array $contextGenerationDataNormalized,
|
||||||
|
?StoredObject $destinationStoredObject = null,
|
||||||
|
bool $isTest = false,
|
||||||
|
?File $testFile = null
|
||||||
|
): ?string;
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Chill\DocGeneratorBundle\Service\Generator;
|
||||||
|
|
||||||
|
class ObjectReadyException extends \RuntimeException
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct("object is already ready", 6698856);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Chill\DocGeneratorBundle\Service\Generator;
|
||||||
|
|
||||||
|
class RelatedEntityNotFoundException extends \RuntimeException
|
||||||
|
{
|
||||||
|
public function __construct(string $relatedEntityClass, int $relatedEntityId, Throwable $previous = null)
|
||||||
|
{
|
||||||
|
parent::__construct(
|
||||||
|
sprintf("Related entity not found: %s, %s", $relatedEntityClass, $relatedEntityId),
|
||||||
|
99876652,
|
||||||
|
$previous);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,156 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Chill\DocGeneratorBundle\Service\Messenger;
|
||||||
|
|
||||||
|
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
|
||||||
|
use Chill\DocGeneratorBundle\Service\Generator\GeneratorException;
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
|
||||||
|
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
|
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
final class OnGenerationFails implements EventSubscriberInterface
|
||||||
|
{
|
||||||
|
private DocGeneratorTemplateRepository $docGeneratorTemplateRepository;
|
||||||
|
|
||||||
|
private EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
|
private LoggerInterface $logger;
|
||||||
|
|
||||||
|
private MailerInterface $mailer;
|
||||||
|
|
||||||
|
private StoredObjectRepository $storedObjectRepository;
|
||||||
|
|
||||||
|
private TranslatorInterface $translator;
|
||||||
|
|
||||||
|
private UserRepositoryInterface $userRepository;
|
||||||
|
|
||||||
|
const LOG_PREFIX = '[docgen failed] ';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param DocGeneratorTemplateRepository $docGeneratorTemplateRepository
|
||||||
|
* @param EntityManagerInterface $entityManager
|
||||||
|
* @param LoggerInterface $logger
|
||||||
|
* @param MailerInterface $mailer
|
||||||
|
* @param StoredObjectRepository $storedObjectRepository
|
||||||
|
* @param TranslatorInterface $translator
|
||||||
|
* @param UserRepositoryInterface $userRepository
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
|
||||||
|
EntityManagerInterface $entityManager,
|
||||||
|
LoggerInterface $logger,
|
||||||
|
MailerInterface $mailer,
|
||||||
|
StoredObjectRepository $storedObjectRepository,
|
||||||
|
TranslatorInterface $translator,
|
||||||
|
UserRepositoryInterface $userRepository
|
||||||
|
) {
|
||||||
|
$this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository;
|
||||||
|
$this->entityManager = $entityManager;
|
||||||
|
$this->logger = $logger;
|
||||||
|
$this->mailer = $mailer;
|
||||||
|
$this->storedObjectRepository = $storedObjectRepository;
|
||||||
|
$this->translator = $translator;
|
||||||
|
$this->userRepository = $userRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static function getSubscribedEvents()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
WorkerMessageFailedEvent::class => 'onMessageFailed'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onMessageFailed(WorkerMessageFailedEvent $event): void
|
||||||
|
{
|
||||||
|
if ($event->willRetry()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var \Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage $message */
|
||||||
|
$message = $event->getEnvelope()->getMessage();
|
||||||
|
|
||||||
|
$this->logger->error(self::LOG_PREFIX.'Docgen failed', [
|
||||||
|
'stored_object_id' => $message->getDestinationStoredObjectId(),
|
||||||
|
'entity_id' => $message->getEntityId(),
|
||||||
|
'template_id' => $message->getTemplateId(),
|
||||||
|
'creator_id' => $message->getCreatorId(),
|
||||||
|
'throwable_class' => get_class($event->getThrowable()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->markObjectAsFailed($message);
|
||||||
|
$this->warnCreator($message, $event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function markObjectAsFailed(RequestGenerationMessage $message): void
|
||||||
|
{
|
||||||
|
$object = $this->storedObjectRepository->find($message->getDestinationStoredObjectId());
|
||||||
|
|
||||||
|
if (null === $object) {
|
||||||
|
$this->logger->error(self::LOG_PREFIX.'Stored object not found', ['stored_object_id', $message->getDestinationStoredObjectId()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$object->setStatus(StoredObject::STATUS_FAILURE);
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function warnCreator(RequestGenerationMessage $message, WorkerMessageFailedEvent $event): void
|
||||||
|
{
|
||||||
|
if (null === $creatorId = $message->getCreatorId()) {
|
||||||
|
$this->logger->info(self::LOG_PREFIX.'creator id is null');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $creator = $this->userRepository->find($creatorId)) {
|
||||||
|
$this->logger->error(self::LOG_PREFIX.'Creator not found with given id', ['creator_id', $creatorId]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $creator->getEmail() || '' === $creator->getEmail()) {
|
||||||
|
$this->logger->info(self::LOG_PREFIX.'Creator does not have any email', ['user' => $creator->getUsernameCanonical()]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the exception is not a GeneratorException, we try the previous one...
|
||||||
|
$throwable = $event->getThrowable();
|
||||||
|
if (!$throwable instanceof GeneratorException) {
|
||||||
|
$throwable = $throwable->getPrevious();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($throwable instanceof GeneratorException) {
|
||||||
|
$errors = $throwable->getErrors();
|
||||||
|
} else {
|
||||||
|
$errors = [$throwable->getTraceAsString()];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $template = $this->docGeneratorTemplateRepository->find($message->getTemplateId())) {
|
||||||
|
$this->logger->info(self::LOG_PREFIX.'Template not found', ['template_id' => $message->getTemplateId()]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = (new TemplatedEmail())
|
||||||
|
->to($creator->getEmail())
|
||||||
|
->subject($this->translator->trans('docgen.failure_email.The generation of a document failed'))
|
||||||
|
->textTemplate('@ChillDocGenerator/Email/on_generation_failed_email.txt.twig')
|
||||||
|
->context([
|
||||||
|
'errors' => $errors,
|
||||||
|
'template' => $template,
|
||||||
|
'creator' => $creator,
|
||||||
|
'stored_object_id' => $message->getDestinationStoredObjectId(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->mailer->send($email);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Chill\DocGeneratorBundle\Service\Messenger;
|
||||||
|
|
||||||
|
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
|
||||||
|
use Chill\DocGeneratorBundle\Service\Generator\Generator;
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
|
||||||
|
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the request of document generation
|
||||||
|
*/
|
||||||
|
class RequestGenerationHandler implements MessageHandlerInterface
|
||||||
|
{
|
||||||
|
private StoredObjectRepository $storedObjectRepository;
|
||||||
|
|
||||||
|
private DocGeneratorTemplateRepository $docGeneratorTemplateRepository;
|
||||||
|
|
||||||
|
private EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
|
private Generator $generator;
|
||||||
|
|
||||||
|
public const AUTHORIZED_TRIALS = 5;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
|
||||||
|
EntityManagerInterface $entityManager,
|
||||||
|
Generator $generator,
|
||||||
|
StoredObjectRepository $storedObjectRepository
|
||||||
|
) {
|
||||||
|
$this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository;
|
||||||
|
$this->entityManager = $entityManager;
|
||||||
|
$this->generator = $generator;
|
||||||
|
$this->storedObjectRepository = $storedObjectRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __invoke(RequestGenerationMessage $message)
|
||||||
|
{
|
||||||
|
if (null === $template = $this->docGeneratorTemplateRepository->find($message->getTemplateId())) {
|
||||||
|
throw new \RuntimeException('template not found: ' . $message->getTemplateId());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $destinationStoredObject = $this->storedObjectRepository->find($message->getDestinationStoredObjectId())) {
|
||||||
|
throw new \RuntimeException('destination stored object not found : ' . $message->getDestinationStoredObjectId());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($destinationStoredObject->getGenerationTrialsCounter() >= self::AUTHORIZED_TRIALS) {
|
||||||
|
throw new UnrecoverableMessageHandlingException('maximum number of retry reached');
|
||||||
|
}
|
||||||
|
|
||||||
|
$destinationStoredObject->addGenerationTrial();
|
||||||
|
$this->entityManager->createQuery('UPDATE '.StoredObject::class.' s SET s.generationTrialsCounter = s.generationTrialsCounter + 1 WHERE s.id = :id')
|
||||||
|
->setParameter('id', $destinationStoredObject->getId())
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
$this->generator->generateDocFromTemplate(
|
||||||
|
$template,
|
||||||
|
$message->getEntityId(),
|
||||||
|
$message->getContextGenerationData(),
|
||||||
|
$destinationStoredObject
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Chill\DocGeneratorBundle\Service\Messenger;
|
||||||
|
|
||||||
|
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
|
||||||
|
class RequestGenerationMessage
|
||||||
|
{
|
||||||
|
private int $creatorId;
|
||||||
|
|
||||||
|
private int $templateId;
|
||||||
|
|
||||||
|
private int $entityId;
|
||||||
|
|
||||||
|
private int $destinationStoredObjectId;
|
||||||
|
|
||||||
|
private array $contextGenerationData;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
User $creator,
|
||||||
|
DocGeneratorTemplate $template,
|
||||||
|
int $entityId,
|
||||||
|
StoredObject $destinationStoredObject,
|
||||||
|
array $contextGenerationData
|
||||||
|
) {
|
||||||
|
$this->creatorId = $creator->getId();
|
||||||
|
$this->templateId = $template->getId();
|
||||||
|
$this->entityId = $entityId;
|
||||||
|
$this->destinationStoredObjectId = $destinationStoredObject->getId();
|
||||||
|
$this->contextGenerationData = $contextGenerationData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatorId(): int
|
||||||
|
{
|
||||||
|
return $this->creatorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDestinationStoredObjectId(): int
|
||||||
|
{
|
||||||
|
return $this->destinationStoredObjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTemplateId(): int
|
||||||
|
{
|
||||||
|
return $this->templateId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntityId(): int
|
||||||
|
{
|
||||||
|
return $this->entityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContextGenerationData(): array
|
||||||
|
{
|
||||||
|
return $this->contextGenerationData;
|
||||||
|
}
|
||||||
|
}
|
@ -20,10 +20,14 @@ services:
|
|||||||
resource: '../Serializer/Normalizer/'
|
resource: '../Serializer/Normalizer/'
|
||||||
tags:
|
tags:
|
||||||
- { name: 'serializer.normalizer', priority: -152 }
|
- { name: 'serializer.normalizer', priority: -152 }
|
||||||
|
|
||||||
Chill\DocGeneratorBundle\Serializer\Normalizer\CollectionDocGenNormalizer:
|
Chill\DocGeneratorBundle\Serializer\Normalizer\CollectionDocGenNormalizer:
|
||||||
tags:
|
tags:
|
||||||
- { name: 'serializer.normalizer', priority: -126 }
|
- { name: 'serializer.normalizer', priority: -126 }
|
||||||
|
|
||||||
|
Chill\DocGeneratorBundle\Service\Context\:
|
||||||
|
resource: "../Service/Context"
|
||||||
|
|
||||||
Chill\DocGeneratorBundle\Controller\:
|
Chill\DocGeneratorBundle\Controller\:
|
||||||
resource: "../Controller"
|
resource: "../Controller"
|
||||||
autowire: true
|
autowire: true
|
||||||
@ -34,18 +38,20 @@ services:
|
|||||||
autowire: true
|
autowire: true
|
||||||
autoconfigure: true
|
autoconfigure: true
|
||||||
|
|
||||||
Chill\DocGeneratorBundle\Service\Context\:
|
|
||||||
resource: "../Service/Context/"
|
|
||||||
autowire: true
|
|
||||||
autoconfigure: true
|
|
||||||
|
|
||||||
Chill\DocGeneratorBundle\GeneratorDriver\:
|
Chill\DocGeneratorBundle\GeneratorDriver\:
|
||||||
resource: "../GeneratorDriver/"
|
resource: "../GeneratorDriver/"
|
||||||
autowire: true
|
autowire: true
|
||||||
autoconfigure: true
|
autoconfigure: true
|
||||||
|
|
||||||
|
Chill\DocGeneratorBundle\Service\Messenger\:
|
||||||
|
resource: "../Service/Messenger/"
|
||||||
|
|
||||||
|
Chill\DocGeneratorBundle\Service\Generator\Generator: ~
|
||||||
|
Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface: '@Chill\DocGeneratorBundle\Service\Generator\Generator'
|
||||||
|
|
||||||
Chill\DocGeneratorBundle\Driver\RelatorioDriver: '@Chill\DocGeneratorBundle\Driver\DriverInterface'
|
Chill\DocGeneratorBundle\Driver\RelatorioDriver: '@Chill\DocGeneratorBundle\Driver\DriverInterface'
|
||||||
|
|
||||||
Chill\DocGeneratorBundle\Context\ContextManager:
|
Chill\DocGeneratorBundle\Context\ContextManager:
|
||||||
arguments:
|
arguments:
|
||||||
$contexts: !tagged_iterator { tag: chill_docgen.context, default_index_method: getKey }
|
$contexts: !tagged_iterator { tag: chill_docgen.context, default_index_method: getKey }
|
||||||
|
Chill\DocGeneratorBundle\Context\ContextManagerInterface: '@Chill\DocGeneratorBundle\Context\ContextManager'
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\Migrations\DocGenerator;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20230214192558 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add status, template_id and fix defaults on chill_doc.stored_object';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object ADD template_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object ADD status TEXT DEFAULT \'ready\' NOT NULL');
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object ADD createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||||
|
$this->addSql('UPDATE chill_doc.stored_object SET createdAt = creation_date');
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object ADD createdBy_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object DROP creation_date;');
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object ALTER type SET DEFAULT \'\'');
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object ALTER title DROP DEFAULT');
|
||||||
|
$this->addSql('COMMENT ON COLUMN chill_doc.stored_object.createdAt IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object ADD CONSTRAINT FK_49604E365DA0FB8 FOREIGN KEY (template_id) REFERENCES chill_docgen_template (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object ADD CONSTRAINT FK_49604E363174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('CREATE INDEX IDX_49604E365DA0FB8 ON chill_doc.stored_object (template_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_49604E363174800F ON chill_doc.stored_object (createdBy_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object DROP CONSTRAINT FK_49604E365DA0FB8');
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object DROP CONSTRAINT FK_49604E363174800F');
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object DROP template_id');
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object DROP status');
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object ADD creation_date TIMESTAMP(0) DEFAULT NOW()');
|
||||||
|
$this->addSql('UPDATE chill_doc.stored_object SET creation_date = createdAt');
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object DROP createdAt');
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object DROP createdBy_id');
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object ALTER title SET DEFAULT \'\'');
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object ALTER type DROP DEFAULT');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,141 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Chill\DocGeneratorBundle\tests\Service\Context\Generator;
|
||||||
|
|
||||||
|
use Chill\DocGeneratorBundle\Context\ContextManagerInterface;
|
||||||
|
use Chill\DocGeneratorBundle\Context\DocGeneratorContextInterface;
|
||||||
|
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||||
|
use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface;
|
||||||
|
use Chill\DocGeneratorBundle\Service\Generator\Generator;
|
||||||
|
use Chill\DocGeneratorBundle\Service\Generator\ObjectReadyException;
|
||||||
|
use Chill\DocGeneratorBundle\Service\Generator\RelatedEntityNotFoundException;
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
|
||||||
|
class GeneratorTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
public function testSuccessfulGeneration(): void
|
||||||
|
{
|
||||||
|
$template = (new DocGeneratorTemplate())->setFile($templateStoredObject = (new StoredObject())
|
||||||
|
->setType('application/test'));
|
||||||
|
$destinationStoredObject = (new StoredObject())->setStatus(StoredObject::STATUS_PENDING);
|
||||||
|
$reflection = new \ReflectionClass($destinationStoredObject);
|
||||||
|
$reflection->getProperty('id')->setAccessible(true);
|
||||||
|
$reflection->getProperty('id')->setValue($destinationStoredObject, 1);
|
||||||
|
$entity = new class {};
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
$context = $this->prophesize(DocGeneratorContextInterface::class);
|
||||||
|
$context->getData($template, $entity, Argument::type('array'))->willReturn($data);
|
||||||
|
$context->getName()->willReturn('dummy_context');
|
||||||
|
$context->getEntityClass()->willReturn('DummyClass');
|
||||||
|
$context = $context->reveal();
|
||||||
|
|
||||||
|
$contextManagerInterface = $this->prophesize(ContextManagerInterface::class);
|
||||||
|
$contextManagerInterface->getContextByDocGeneratorTemplate($template)
|
||||||
|
->willReturn($context);
|
||||||
|
|
||||||
|
$driver = $this->prophesize(DriverInterface::class);
|
||||||
|
$driver->generateFromString('template', 'application/test', $data, Argument::any())
|
||||||
|
->willReturn('generated');
|
||||||
|
|
||||||
|
$entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
$entityManager->find(StoredObject::class, 1)
|
||||||
|
->willReturn($destinationStoredObject);
|
||||||
|
$entityManager->find('DummyClass', Argument::type('int'))
|
||||||
|
->willReturn($entity);
|
||||||
|
$entityManager->clear()->shouldBeCalled();
|
||||||
|
$entityManager->flush()->shouldBeCalled();
|
||||||
|
|
||||||
|
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
|
||||||
|
$storedObjectManager->read($templateStoredObject)->willReturn('template');
|
||||||
|
$storedObjectManager->write($destinationStoredObject, 'generated')->shouldBeCalled();
|
||||||
|
|
||||||
|
|
||||||
|
$generator = new Generator(
|
||||||
|
$contextManagerInterface->reveal(),
|
||||||
|
$driver->reveal(),
|
||||||
|
$entityManager->reveal(),
|
||||||
|
new NullLogger(),
|
||||||
|
$storedObjectManager->reveal()
|
||||||
|
);
|
||||||
|
|
||||||
|
$generator->generateDocFromTemplate(
|
||||||
|
$template,
|
||||||
|
1,
|
||||||
|
[],
|
||||||
|
$destinationStoredObject
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPreventRegenerateDocument(): void
|
||||||
|
{
|
||||||
|
$this->expectException(ObjectReadyException::class);
|
||||||
|
|
||||||
|
$generator = new Generator(
|
||||||
|
$this->prophesize(ContextManagerInterface::class)->reveal(),
|
||||||
|
$this->prophesize(DriverInterface::class)->reveal(),
|
||||||
|
$this->prophesize(EntityManagerInterface::class)->reveal(),
|
||||||
|
new NullLogger(),
|
||||||
|
$this->prophesize(StoredObjectManagerInterface::class)->reveal()
|
||||||
|
);
|
||||||
|
|
||||||
|
$template = (new DocGeneratorTemplate())->setFile($templateStoredObject = (new StoredObject())
|
||||||
|
->setType('application/test'));
|
||||||
|
$destinationStoredObject = (new StoredObject())->setStatus(StoredObject::STATUS_READY);
|
||||||
|
|
||||||
|
$generator->generateDocFromTemplate(
|
||||||
|
$template,
|
||||||
|
1,
|
||||||
|
[],
|
||||||
|
$destinationStoredObject
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRelatedEntityNotFound(): void
|
||||||
|
{
|
||||||
|
$this->expectException(RelatedEntityNotFoundException::class);
|
||||||
|
|
||||||
|
$template = (new DocGeneratorTemplate())->setFile($templateStoredObject = (new StoredObject())
|
||||||
|
->setType('application/test'));
|
||||||
|
$destinationStoredObject = (new StoredObject())->setStatus(StoredObject::STATUS_PENDING);
|
||||||
|
$reflection = new \ReflectionClass($destinationStoredObject);
|
||||||
|
$reflection->getProperty('id')->setAccessible(true);
|
||||||
|
$reflection->getProperty('id')->setValue($destinationStoredObject, 1);
|
||||||
|
|
||||||
|
$context = $this->prophesize(DocGeneratorContextInterface::class);
|
||||||
|
$context->getName()->willReturn('dummy_context');
|
||||||
|
$context->getEntityClass()->willReturn('DummyClass');
|
||||||
|
$context = $context->reveal();
|
||||||
|
|
||||||
|
$contextManagerInterface = $this->prophesize(ContextManagerInterface::class);
|
||||||
|
$contextManagerInterface->getContextByDocGeneratorTemplate($template)
|
||||||
|
->willReturn($context);
|
||||||
|
|
||||||
|
$entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
$entityManager->find(Argument::type('string'), Argument::type('int'))
|
||||||
|
->willReturn(null);
|
||||||
|
|
||||||
|
$generator = new Generator(
|
||||||
|
$contextManagerInterface->reveal(),
|
||||||
|
$this->prophesize(DriverInterface::class)->reveal(),
|
||||||
|
$entityManager->reveal(),
|
||||||
|
new NullLogger(),
|
||||||
|
$this->prophesize(StoredObjectManagerInterface::class)->reveal()
|
||||||
|
);
|
||||||
|
|
||||||
|
$generator->generateDocFromTemplate(
|
||||||
|
$template,
|
||||||
|
1,
|
||||||
|
[],
|
||||||
|
$destinationStoredObject
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,16 @@ docgen:
|
|||||||
test generate: Tester la génération
|
test generate: Tester la génération
|
||||||
With context %name%: 'Avec le contexte "%name%"'
|
With context %name%: 'Avec le contexte "%name%"'
|
||||||
|
|
||||||
|
Doc generation failed: La génération de ce document a échoué
|
||||||
|
Doc generation is pending: La génération de ce document est en cours
|
||||||
|
Come back later: Revenir plus tard
|
||||||
|
|
||||||
|
failure_email:
|
||||||
|
The generation of a document failed: La génération d'un document a échoué
|
||||||
|
The generation of the document {template_name} failed: La génération d'un document à partir du modèle {{ template_name }} a échoué.
|
||||||
|
The following errors were encoutered: Les erreurs suivantes ont été rencontrées
|
||||||
|
Forward this email to your administrator for solving: Faites suivre ce message vers votre administrateur pour la résolution du problème.
|
||||||
|
References: Références
|
||||||
|
|
||||||
crud:
|
crud:
|
||||||
docgen_template:
|
docgen_template:
|
||||||
@ -19,4 +29,4 @@ crud:
|
|||||||
|
|
||||||
|
|
||||||
Show data instead of generating: Montrer les données au lieu de générer le document
|
Show data instead of generating: Montrer les données au lieu de générer le document
|
||||||
Template file: Fichier modèle
|
Template file: Fichier modèle
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Chill\DocStoreBundle\Controller;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
|
class StoredObjectApiController
|
||||||
|
{
|
||||||
|
private Security $security;
|
||||||
|
|
||||||
|
public function __construct(Security $security)
|
||||||
|
{
|
||||||
|
$this->security = $security;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/api/1.0/doc-store/stored-object/{uuid}/is-ready")
|
||||||
|
*/
|
||||||
|
public function isDocumentReady(StoredObject $storedObject): Response
|
||||||
|
{
|
||||||
|
if (!$this->security->isGranted('ROLE_USER')) {
|
||||||
|
throw new AccessDeniedHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse(
|
||||||
|
[
|
||||||
|
'id' => $storedObject->getId(),
|
||||||
|
'filename' => $storedObject->getFilename(),
|
||||||
|
'status' => $storedObject->getStatus(),
|
||||||
|
'type' => $storedObject->getType(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -14,6 +14,9 @@ namespace Chill\DocStoreBundle\Entity;
|
|||||||
use ChampsLibres\AsyncUploaderBundle\Model\AsyncFileInterface;
|
use ChampsLibres\AsyncUploaderBundle\Model\AsyncFileInterface;
|
||||||
use ChampsLibres\AsyncUploaderBundle\Validator\Constraints\AsyncFileExists;
|
use ChampsLibres\AsyncUploaderBundle\Validator\Constraints\AsyncFileExists;
|
||||||
use ChampsLibres\WopiLib\Contract\Entity\Document;
|
use ChampsLibres\WopiLib\Contract\Entity\Document;
|
||||||
|
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||||
|
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||||
|
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
@ -30,13 +33,13 @@ use Symfony\Component\Serializer\Annotation as Serializer;
|
|||||||
* message="The file is not stored properly"
|
* message="The file is not stored properly"
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
class StoredObject implements AsyncFileInterface, Document
|
class StoredObject implements AsyncFileInterface, Document, TrackCreationInterface
|
||||||
{
|
{
|
||||||
/**
|
public const STATUS_READY = "ready";
|
||||||
* @ORM\Column(type="datetime", name="creation_date")
|
public const STATUS_PENDING = "pending";
|
||||||
* @Serializer\Groups({"read", "write"})
|
public const STATUS_FAILURE = "failure";
|
||||||
*/
|
|
||||||
private DateTimeInterface $creationDate;
|
use TrackCreationTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\Column(type="json", name="datas")
|
* @ORM\Column(type="json", name="datas")
|
||||||
@ -48,7 +51,7 @@ class StoredObject implements AsyncFileInterface, Document
|
|||||||
* @ORM\Column(type="text")
|
* @ORM\Column(type="text")
|
||||||
* @Serializer\Groups({"read", "write"})
|
* @Serializer\Groups({"read", "write"})
|
||||||
*/
|
*/
|
||||||
private $filename;
|
private string $filename = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\Id
|
* @ORM\Id
|
||||||
@ -56,7 +59,7 @@ class StoredObject implements AsyncFileInterface, Document
|
|||||||
* @ORM\Column(type="integer")
|
* @ORM\Column(type="integer")
|
||||||
* @Serializer\Groups({"read", "write"})
|
* @Serializer\Groups({"read", "write"})
|
||||||
*/
|
*/
|
||||||
private $id;
|
private ?int $id;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var int[]
|
* @var int[]
|
||||||
@ -78,7 +81,7 @@ class StoredObject implements AsyncFileInterface, Document
|
|||||||
private string $title = '';
|
private string $title = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\Column(type="text", name="type")
|
* @ORM\Column(type="text", name="type", options={"default": ""})
|
||||||
* @Serializer\Groups({"read", "write"})
|
* @Serializer\Groups({"read", "write"})
|
||||||
*/
|
*/
|
||||||
private string $type = '';
|
private string $type = '';
|
||||||
@ -89,28 +92,68 @@ class StoredObject implements AsyncFileInterface, Document
|
|||||||
*/
|
*/
|
||||||
private UuidInterface $uuid;
|
private UuidInterface $uuid;
|
||||||
|
|
||||||
public function __construct()
|
/**
|
||||||
|
* @ORM\ManyToOne(targetEntity=DocGeneratorTemplate::class)
|
||||||
|
*/
|
||||||
|
private ?DocGeneratorTemplate $template;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Column(type="text", options={"default": "ready"})
|
||||||
|
* @Serializer\Groups({"read"})
|
||||||
|
*/
|
||||||
|
private string $status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store the number of times a generation has been tryied for this StoredObject.
|
||||||
|
*
|
||||||
|
* This is a workaround, as generation consume lot of memory, and out-of-memory errors
|
||||||
|
* are not handled by messenger.
|
||||||
|
*
|
||||||
|
* @ORM\Column(type="integer", options={"default": 0})
|
||||||
|
*/
|
||||||
|
private int $generationTrialsCounter = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param StoredObject::STATUS_* $status
|
||||||
|
*/
|
||||||
|
public function __construct(string $status = "ready")
|
||||||
{
|
{
|
||||||
$this->creationDate = new DateTime();
|
|
||||||
$this->uuid = Uuid::uuid4();
|
$this->uuid = Uuid::uuid4();
|
||||||
|
$this->status = $status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addGenerationTrial(): self
|
||||||
|
{
|
||||||
|
$this->generationTrialsCounter++;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Serializer\Groups({"read", "write"})
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
public function getCreationDate(): DateTime
|
public function getCreationDate(): DateTime
|
||||||
{
|
{
|
||||||
return $this->creationDate;
|
return DateTime::createFromImmutable($this->createdAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDatas()
|
public function getDatas(): array
|
||||||
{
|
{
|
||||||
return $this->datas;
|
return $this->datas;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFilename()
|
public function getFilename(): string
|
||||||
{
|
{
|
||||||
return $this->filename;
|
return $this->filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId()
|
public function getGenerationTrialsCounter(): int
|
||||||
|
{
|
||||||
|
return $this->generationTrialsCounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
}
|
}
|
||||||
@ -133,6 +176,14 @@ class StoredObject implements AsyncFileInterface, Document
|
|||||||
return $this->getFilename();
|
return $this->getFilename();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return StoredObject::STATUS_*
|
||||||
|
*/
|
||||||
|
public function getStatus(): string
|
||||||
|
{
|
||||||
|
return $this->status;
|
||||||
|
}
|
||||||
|
|
||||||
public function getTitle()
|
public function getTitle()
|
||||||
{
|
{
|
||||||
return $this->title;
|
return $this->title;
|
||||||
@ -153,52 +204,92 @@ class StoredObject implements AsyncFileInterface, Document
|
|||||||
return (string) $this->uuid;
|
return (string) $this->uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setCreationDate(DateTime $creationDate)
|
/**
|
||||||
|
* @Serializer\Groups({"write"})
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
public function setCreationDate(DateTime $creationDate): self
|
||||||
{
|
{
|
||||||
$this->creationDate = $creationDate;
|
$this->createdAt = \DateTimeImmutable::createFromMutable($creationDate);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setDatas(?array $datas)
|
public function setDatas(?array $datas): self
|
||||||
{
|
{
|
||||||
$this->datas = (array) $datas;
|
$this->datas = (array) $datas;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setFilename(?string $filename)
|
public function setFilename(?string $filename): self
|
||||||
{
|
{
|
||||||
$this->filename = (string) $filename;
|
$this->filename = (string) $filename;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setIv(?array $iv)
|
public function setIv(?array $iv): self
|
||||||
{
|
{
|
||||||
$this->iv = (array) $iv;
|
$this->iv = (array) $iv;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setKeyInfos(?array $keyInfos)
|
public function setKeyInfos(?array $keyInfos): self
|
||||||
{
|
{
|
||||||
$this->keyInfos = (array) $keyInfos;
|
$this->keyInfos = (array) $keyInfos;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setTitle(?string $title)
|
/**
|
||||||
|
* @param StoredObject::STATUS_* $status
|
||||||
|
*/
|
||||||
|
public function setStatus(string $status): self
|
||||||
|
{
|
||||||
|
$this->status = $status;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTitle(?string $title): self
|
||||||
{
|
{
|
||||||
$this->title = (string) $title;
|
$this->title = (string) $title;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setType(?string $type)
|
public function setType(?string $type): self
|
||||||
{
|
{
|
||||||
$this->type = (string) $type;
|
$this->type = (string) $type;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getTemplate(): ?DocGeneratorTemplate
|
||||||
|
{
|
||||||
|
return $this->template;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasTemplate(): bool
|
||||||
|
{
|
||||||
|
return null !== $this->template;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTemplate(?DocGeneratorTemplate $template): StoredObject
|
||||||
|
{
|
||||||
|
$this->template = $template;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isPending(): bool
|
||||||
|
{
|
||||||
|
return self::STATUS_PENDING === $this->getStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isFailure(): bool
|
||||||
|
{
|
||||||
|
return self::STATUS_FAILURE === $this->getStatus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import {_createI18n} from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
|
import {_createI18n} from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
|
||||||
import DocumentActionButtonsGroup from "../../vuejs/DocumentActionButtonsGroup.vue";
|
import DocumentActionButtonsGroup from "../../vuejs/DocumentActionButtonsGroup.vue";
|
||||||
import {createApp} from "vue";
|
import {createApp} from "vue";
|
||||||
import {StoredObject} from "../../types";
|
import {StoredObject, StoredObjectStatusChange} from "../../types";
|
||||||
|
import {is_object_ready} from "../../vuejs/StoredObjectButton/helpers";
|
||||||
|
|
||||||
const i18n = _createI18n({});
|
const i18n = _createI18n({});
|
||||||
|
|
||||||
@ -19,7 +20,7 @@ window.addEventListener('DOMContentLoaded', function (e) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const
|
const
|
||||||
storedObject = JSON.parse(datasets.storedObject),
|
storedObject = JSON.parse(datasets.storedObject) as StoredObject,
|
||||||
filename = datasets.filename,
|
filename = datasets.filename,
|
||||||
canEdit = datasets.canEdit === '1',
|
canEdit = datasets.canEdit === '1',
|
||||||
small = datasets.small === '1'
|
small = datasets.small === '1'
|
||||||
@ -27,7 +28,20 @@ window.addEventListener('DOMContentLoaded', function (e) {
|
|||||||
|
|
||||||
return { storedObject, filename, canEdit, small };
|
return { storedObject, filename, canEdit, small };
|
||||||
},
|
},
|
||||||
template: '<document-action-buttons-group :can-edit="canEdit" :filename="filename" :stored-object="storedObject" :small="small"></document-action-buttons-group>',
|
template: '<document-action-buttons-group :can-edit="canEdit" :filename="filename" :stored-object="storedObject" :small="small" @on-stored-object-status-change="onStoredObjectStatusChange"></document-action-buttons-group>',
|
||||||
|
methods: {
|
||||||
|
onStoredObjectStatusChange: function(newStatus: StoredObjectStatusChange): void {
|
||||||
|
this.$data.storedObject.status = newStatus.status;
|
||||||
|
this.$data.storedObject.filename = newStatus.filename;
|
||||||
|
this.$data.storedObject.type = newStatus.type;
|
||||||
|
|
||||||
|
// remove eventual div which inform pending status
|
||||||
|
document.querySelectorAll(`[data-docgen-is-pending="${this.$data.storedObject.id}"]`)
|
||||||
|
.forEach(function(el) {
|
||||||
|
el.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(i18n).mount(el);
|
app.use(i18n).mount(el);
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import {DateTime} from "../../../ChillMainBundle/Resources/public/types";
|
import {DateTime} from "../../../ChillMainBundle/Resources/public/types";
|
||||||
|
|
||||||
|
export type StoredObjectStatus = "ready"|"failure"|"pending";
|
||||||
|
|
||||||
export interface StoredObject {
|
export interface StoredObject {
|
||||||
id: number,
|
id: number,
|
||||||
|
|
||||||
@ -13,7 +15,15 @@ export interface StoredObject {
|
|||||||
keyInfos: object,
|
keyInfos: object,
|
||||||
title: string,
|
title: string,
|
||||||
type: string,
|
type: string,
|
||||||
uuid: string
|
uuid: string,
|
||||||
|
status: StoredObjectStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoredObjectStatusChange {
|
||||||
|
id: number,
|
||||||
|
filename: string,
|
||||||
|
status: StoredObjectStatus,
|
||||||
|
type: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="dropdown">
|
<div v-if="'ready' === props.storedObject.status" class="dropdown">
|
||||||
<button :class="Object.assign({'btn': true, 'btn-outline-primary': true, 'dropdown-toggle': true, small: props.small})" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
<button :class="Object.assign({'btn': true, 'btn-outline-primary': true, 'dropdown-toggle': true, small: props.small})" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
Actions
|
Actions
|
||||||
</button>
|
</button>
|
||||||
@ -15,16 +15,27 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="'pending' === props.storedObject.status">
|
||||||
|
<div class="btn btn-outline-info">Génération en cours</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="'failure' === props.storedObject.status">
|
||||||
|
<div class="btn btn-outline-danger">La génération a échoué</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import {onMounted} from "vue";
|
||||||
import ConvertButton from "./StoredObjectButton/ConvertButton.vue";
|
import ConvertButton from "./StoredObjectButton/ConvertButton.vue";
|
||||||
import DownloadButton from "./StoredObjectButton/DownloadButton.vue";
|
import DownloadButton from "./StoredObjectButton/DownloadButton.vue";
|
||||||
import WopiEditButton from "./StoredObjectButton/WopiEditButton.vue";
|
import WopiEditButton from "./StoredObjectButton/WopiEditButton.vue";
|
||||||
import {is_extension_editable, is_extension_viewable} from "./StoredObjectButton/helpers";
|
import {is_extension_editable, is_extension_viewable, is_object_ready} from "./StoredObjectButton/helpers";
|
||||||
import {StoredObject, WopiEditButtonExecutableBeforeLeaveFunction} from "../types";
|
import {
|
||||||
|
StoredObject,
|
||||||
|
StoredObjectStatusChange,
|
||||||
|
WopiEditButtonExecutableBeforeLeaveFunction
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
interface DocumentActionButtonsGroupConfig {
|
interface DocumentActionButtonsGroupConfig {
|
||||||
storedObject: StoredObject,
|
storedObject: StoredObject,
|
||||||
@ -48,6 +59,10 @@ interface DocumentActionButtonsGroupConfig {
|
|||||||
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction,
|
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'onStoredObjectStatusChange', newStatus: StoredObjectStatusChange): void
|
||||||
|
}>();
|
||||||
|
|
||||||
const props = withDefaults(defineProps<DocumentActionButtonsGroupConfig>(), {
|
const props = withDefaults(defineProps<DocumentActionButtonsGroupConfig>(), {
|
||||||
small: false,
|
small: false,
|
||||||
canEdit: true,
|
canEdit: true,
|
||||||
@ -56,6 +71,51 @@ const props = withDefaults(defineProps<DocumentActionButtonsGroupConfig>(), {
|
|||||||
returnPath: window.location.pathname + window.location.search + window.location.hash,
|
returnPath: window.location.pathname + window.location.search + window.location.hash,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* counter for the number of times that we check for a new status
|
||||||
|
*/
|
||||||
|
let tryiesForReady = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* how many times we may check for a new status, once loaded
|
||||||
|
*/
|
||||||
|
const maxTryiesForReady = 120;
|
||||||
|
|
||||||
|
const checkForReady = function(): void {
|
||||||
|
if (
|
||||||
|
'ready' === props.storedObject.status
|
||||||
|
|| 'failure' === props.storedObject.status
|
||||||
|
// stop reloading if the page stays opened for a long time
|
||||||
|
|| tryiesForReady > maxTryiesForReady
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tryiesForReady = tryiesForReady + 1;
|
||||||
|
|
||||||
|
setTimeout(onObjectNewStatusCallback, 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onObjectNewStatusCallback = async function(): Promise<void> {
|
||||||
|
const new_status = await is_object_ready(props.storedObject);
|
||||||
|
if (props.storedObject.status !== new_status.status) {
|
||||||
|
emit('onStoredObjectStatusChange', new_status);
|
||||||
|
return Promise.resolve();
|
||||||
|
} else if ('failure' === new_status.status) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('ready' !== new_status.status) {
|
||||||
|
// we check for new status, unless it is ready
|
||||||
|
checkForReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
checkForReady();
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import {StoredObject, StoredObjectStatus, StoredObjectStatusChange} from "../../types";
|
||||||
|
|
||||||
const MIMES_EDIT = new Set([
|
const MIMES_EDIT = new Set([
|
||||||
'application/vnd.ms-powerpoint',
|
'application/vnd.ms-powerpoint',
|
||||||
@ -168,6 +169,18 @@ async function download_and_decrypt_doc(urlGenerator: string, keyData: JsonWebKe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function is_object_ready(storedObject: StoredObject): Promise<StoredObjectStatusChange>
|
||||||
|
{
|
||||||
|
const new_status_response = await window
|
||||||
|
.fetch( `/api/1.0/doc-store/stored-object/${storedObject.uuid}/is-ready`);
|
||||||
|
|
||||||
|
if (!new_status_response.ok) {
|
||||||
|
throw new Error("could not fetch the new status");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await new_status_response.json();
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
build_convert_link,
|
build_convert_link,
|
||||||
build_download_info_link,
|
build_download_info_link,
|
||||||
@ -176,4 +189,5 @@ export {
|
|||||||
download_doc,
|
download_doc,
|
||||||
is_extension_editable,
|
is_extension_editable,
|
||||||
is_extension_viewable,
|
is_extension_viewable,
|
||||||
|
is_object_ready,
|
||||||
};
|
};
|
||||||
|
@ -9,11 +9,6 @@
|
|||||||
<dt>{{ 'Title'|trans }}</dt>
|
<dt>{{ 'Title'|trans }}</dt>
|
||||||
<dd>{{ document.title }}</dd>
|
<dd>{{ document.title }}</dd>
|
||||||
|
|
||||||
{% if document.scope is not null %}
|
|
||||||
<dt>{{ 'Scope' | trans }}</dt>
|
|
||||||
<dd>{{ document.scope.name | localize_translatable_string }}</dd>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<dt>{{ 'Category'|trans }}</dt>
|
<dt>{{ 'Category'|trans }}</dt>
|
||||||
<dd>{{ document.category.name|localize_translatable_string }}</dd>
|
<dd>{{ document.category.name|localize_translatable_string }}</dd>
|
||||||
|
|
||||||
|
@ -5,18 +5,25 @@
|
|||||||
<div class="item-bloc">
|
<div class="item-bloc">
|
||||||
<div class="item-row">
|
<div class="item-row">
|
||||||
<div class="item-col" style="width: unset">
|
<div class="item-col" style="width: unset">
|
||||||
|
{% if document.object.isPending %}
|
||||||
|
<div class="badge text-bg-info" data-docgen-is-pending="{{ document.object.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
|
||||||
|
{% elseif document.object.isFailure %}
|
||||||
|
<div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
|
||||||
|
{% endif %}
|
||||||
<div class="denomination h2">
|
<div class="denomination h2">
|
||||||
{{ document.title }}
|
{{ document.title }}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{% if document.object.type is not empty %}
|
||||||
{{ mm.mimeIcon(document.object.type) }}
|
<div>
|
||||||
</div>
|
{{ mm.mimeIcon(document.object.type) }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div>
|
<div>
|
||||||
<p>{{ document.category.name|localize_translatable_string }}</p>
|
<p>{{ document.category.name|localize_translatable_string }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% if document.template is not null %}
|
{% if document.object.hasTemplate %}
|
||||||
<div>
|
<div>
|
||||||
<p>{{ document.template.name.fr }}</p>
|
<p>{{ document.object.template.name|localize_translatable_string }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\Migrations\DocStore;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20230227161327 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add a generation counter on doc store';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object ADD generationTrialsCounter INT DEFAULT 0 NOT NULL;');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_doc.stored_object DROP generationTrialsCounter');
|
||||||
|
}
|
||||||
|
}
|
@ -118,7 +118,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
|
|||||||
* Return true if the phonenumber is a landline or voip phone. Return always true
|
* Return true if the phonenumber is a landline or voip phone. Return always true
|
||||||
* if the validation is not configured.
|
* if the validation is not configured.
|
||||||
*
|
*
|
||||||
* @param string $phonenumber
|
* @param string|PhoneNumber $phonenumber
|
||||||
*/
|
*/
|
||||||
public function isValidPhonenumberAny($phonenumber): bool
|
public function isValidPhonenumberAny($phonenumber): bool
|
||||||
{
|
{
|
||||||
@ -138,7 +138,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
|
|||||||
* Return true if the phonenumber is a landline or voip phone. Return always true
|
* Return true if the phonenumber is a landline or voip phone. Return always true
|
||||||
* if the validation is not configured.
|
* if the validation is not configured.
|
||||||
*
|
*
|
||||||
* @param string $phonenumber
|
* @param string|PhoneNumber $phonenumber
|
||||||
*/
|
*/
|
||||||
public function isValidPhonenumberLandOrVoip($phonenumber): bool
|
public function isValidPhonenumberLandOrVoip($phonenumber): bool
|
||||||
{
|
{
|
||||||
@ -159,7 +159,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
|
|||||||
* REturn true if the phonenumber is a mobile phone. Return always true
|
* REturn true if the phonenumber is a mobile phone. Return always true
|
||||||
* if the validation is not configured.
|
* if the validation is not configured.
|
||||||
*
|
*
|
||||||
* @param string $phonenumber
|
* @param string|PhoneNumber $phonenumber
|
||||||
*/
|
*/
|
||||||
public function isValidPhonenumberMobile($phonenumber): bool
|
public function isValidPhonenumberMobile($phonenumber): bool
|
||||||
{
|
{
|
||||||
@ -182,6 +182,10 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($phonenumber instanceof PhoneNumber) {
|
||||||
|
$phonenumber = (string) $phonenumber;
|
||||||
|
}
|
||||||
|
|
||||||
// filter only number
|
// filter only number
|
||||||
$filtered = preg_replace('/[^0-9]/', '', (string) $phonenumber);
|
$filtered = preg_replace('/[^0-9]/', '', (string) $phonenumber);
|
||||||
|
|
||||||
|
@ -680,6 +680,11 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
|||||||
$this->proxyAccompanyingPeriodOpenState = false;
|
$this->proxyAccompanyingPeriodOpenState = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function countResources(): int
|
||||||
|
{
|
||||||
|
return $this->resources->count();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This public function is the same but return only true or false.
|
* This public function is the same but return only true or false.
|
||||||
*/
|
*/
|
||||||
@ -764,6 +769,18 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function countAccompanyingPeriodInvolved(
|
||||||
|
bool $asParticipantOpen = true,
|
||||||
|
bool $asRequestor = true
|
||||||
|
): int {
|
||||||
|
// TODO should be optimized to avoid loading accompanying period ?
|
||||||
|
return $this->getAccompanyingPeriodInvolved($asParticipantOpen, $asRequestor)
|
||||||
|
->filter(function (AccompanyingPeriod $p) {
|
||||||
|
return $p->getStep() !== AccompanyingPeriod::STEP_DRAFT;
|
||||||
|
})
|
||||||
|
->count();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get AccompanyingPeriodParticipations Collection.
|
* Get AccompanyingPeriodParticipations Collection.
|
||||||
*
|
*
|
||||||
|
@ -12,11 +12,14 @@ declare(strict_types=1);
|
|||||||
namespace Chill\PersonBundle\Menu;
|
namespace Chill\PersonBundle\Menu;
|
||||||
|
|
||||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Chill\PersonBundle\Repository\ResidentialAddressRepository;
|
||||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
|
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
|
||||||
use Knp\Menu\MenuItem;
|
use Knp\Menu\MenuItem;
|
||||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
use function count;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add menu entrie to person menu.
|
* Add menu entrie to person menu.
|
||||||
@ -35,18 +38,28 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
|
|||||||
|
|
||||||
protected TranslatorInterface $translator;
|
protected TranslatorInterface $translator;
|
||||||
|
|
||||||
|
private ResidentialAddressRepository $residentialAddressRepo;
|
||||||
|
|
||||||
private Security $security;
|
private Security $security;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ParameterBagInterface $parameterBag,
|
ParameterBagInterface $parameterBag,
|
||||||
Security $security,
|
Security $security,
|
||||||
TranslatorInterface $translator
|
TranslatorInterface $translator,
|
||||||
|
ResidentialAddressRepository $residentialAddressRepo
|
||||||
) {
|
) {
|
||||||
$this->showAccompanyingPeriod = $parameterBag->get('chill_person.accompanying_period');
|
$this->showAccompanyingPeriod = $parameterBag->get('chill_person.accompanying_period');
|
||||||
$this->security = $security;
|
$this->security = $security;
|
||||||
$this->translator = $translator;
|
$this->translator = $translator;
|
||||||
|
$this->residentialAddressRepo = $residentialAddressRepo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $menuId
|
||||||
|
* @param MenuItem $menu
|
||||||
|
* @param array{person: Person} $parameters
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
public function buildMenu($menuId, MenuItem $menu, array $parameters)
|
public function buildMenu($menuId, MenuItem $menu, array $parameters)
|
||||||
{
|
{
|
||||||
$menu->addChild($this->translator->trans('Person details'), [
|
$menu->addChild($this->translator->trans('Person details'), [
|
||||||
@ -67,6 +80,8 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
|
|||||||
])
|
])
|
||||||
->setExtras([
|
->setExtras([
|
||||||
'order' => 60,
|
'order' => 60,
|
||||||
|
'counter' => 0 < ($nbResidentials = $this->residentialAddressRepo->countByPerson($parameters['person'])) ?
|
||||||
|
$nbResidentials : null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$menu->addChild($this->translator->trans('person_resources_menu'), [
|
$menu->addChild($this->translator->trans('person_resources_menu'), [
|
||||||
@ -77,6 +92,7 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
|
|||||||
])
|
])
|
||||||
->setExtras([
|
->setExtras([
|
||||||
'order' => 70,
|
'order' => 70,
|
||||||
|
'counter' => 0 < ($nbResources = $parameters['person']->countResources()) ? $nbResources : null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$menu->addChild($this->translator->trans('household.person history'), [
|
$menu->addChild($this->translator->trans('household.person history'), [
|
||||||
@ -111,6 +127,8 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
|
|||||||
])
|
])
|
||||||
->setExtras([
|
->setExtras([
|
||||||
'order' => 100,
|
'order' => 100,
|
||||||
|
'counter' => 0 < ($nbAccompanyingPeriod = $parameters['person']->countAccompanyingPeriodInvolved())
|
||||||
|
? $nbAccompanyingPeriod : null,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,16 @@ class ResidentialAddressRepository extends ServiceEntityRepository
|
|||||||
parent::__construct($registry, ResidentialAddress::class);
|
parent::__construct($registry, ResidentialAddress::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function countByPerson(Person $person): int
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('ra')
|
||||||
|
->select('COUNT(ra)')
|
||||||
|
->where('ra.person = :person')
|
||||||
|
->setParameter('person', $person)
|
||||||
|
->getQuery()
|
||||||
|
->getSingleScalarResult();
|
||||||
|
}
|
||||||
|
|
||||||
public function buildQueryFindCurrentResidentialAddresses(Person $person, ?DateTimeImmutable $at = null): QueryBuilder
|
public function buildQueryFindCurrentResidentialAddresses(Person $person, ?DateTimeImmutable $at = null): QueryBuilder
|
||||||
{
|
{
|
||||||
$date = null === $at ? new DateTimeImmutable('today') : $at;
|
$date = null === $at ? new DateTimeImmutable('today') : $at;
|
||||||
|
@ -116,6 +116,7 @@
|
|||||||
:filename="d.title"
|
:filename="d.title"
|
||||||
:can-edit="true"
|
:can-edit="true"
|
||||||
:execute-before-leave="submitBeforeLeaveToEditor"
|
:execute-before-leave="submitBeforeLeaveToEditor"
|
||||||
|
@on-stored-object-status-change="onStatusDocumentChanged"
|
||||||
></document-action-buttons-group>
|
></document-action-buttons-group>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="d.workflows.length === 0">
|
<li v-if="d.workflows.length === 0">
|
||||||
@ -338,6 +339,10 @@ export default {
|
|||||||
this.$store.commit('removeDocument', {key: this.evaluation.key, document: document});
|
this.$store.commit('removeDocument', {key: this.evaluation.key, document: document});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onStatusDocumentChanged(newStatus) {
|
||||||
|
console.log('onStatusDocumentChanged', newStatus);
|
||||||
|
this.$store.commit('statusDocumentChanged', {key: this.evaluation.key, newStatus: newStatus});
|
||||||
|
},
|
||||||
goToGenerateWorkflowEvaluationDocument({event, link, workflowName, payload}) {
|
goToGenerateWorkflowEvaluationDocument({event, link, workflowName, payload}) {
|
||||||
const callback = (data) => {
|
const callback = (data) => {
|
||||||
let evaluation = data.accompanyingPeriodWorkEvaluations.find(e => e.key === this.evaluation.key);
|
let evaluation = data.accompanyingPeriodWorkEvaluations.find(e => e.key === this.evaluation.key);
|
||||||
|
@ -360,7 +360,22 @@ const store = createStore({
|
|||||||
state.evaluationsPicked.find(e => e.key === payload.evaluationKey)
|
state.evaluationsPicked.find(e => e.key === payload.evaluationKey)
|
||||||
.documents.find(d => d.id === payload.id).title = payload.title;
|
.documents.find(d => d.id === payload.id).title = payload.title;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
statusDocumentChanged(state, {newStatus, key}) {
|
||||||
|
const e = state.evaluationsPicked.find(e => e.key === key);
|
||||||
|
if (typeof e === 'undefined') {
|
||||||
|
console.error('evaluation not found for given key', {key});
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = e.documents.find(d => d.storedObject?.id === newStatus.id);
|
||||||
|
if (typeof doc === 'undefined') {
|
||||||
|
console.error('document not found', {newStatus});
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.storedObject.status = newStatus.status;
|
||||||
|
doc.storedObject.type = newStatus.type;
|
||||||
|
doc.storedObject.filename = newStatus.filename;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
updateThirdParty({ commit }, payload) {
|
updateThirdParty({ commit }, payload) {
|
||||||
|
@ -63,6 +63,41 @@
|
|||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
{%- if form.email is defined or form.phonenumber is defined or form.mobilenumber is defined or form.contactInfo is defined-%}
|
||||||
|
<fieldset>
|
||||||
|
<legend><h2>{{ 'Contact information'|trans }}</h2></legend>
|
||||||
|
{%- if form.email is defined -%}
|
||||||
|
<div id="personEmail">
|
||||||
|
{{ form_row(form.email, {'label': 'Email'}) }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{%- if form.acceptEmail is defined -%}
|
||||||
|
<div id="personAcceptEmail">
|
||||||
|
{{ form_row(form.acceptEmail, {'label' : 'Accept emails ?'}) }}
|
||||||
|
</div>
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
|
{%- if form.phonenumber is defined -%}
|
||||||
|
{{ form_row(form.phonenumber, {'label': 'Phonenumber'}) }}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- if form.mobilenumber is defined -%}
|
||||||
|
<div id="personPhoneNumber">
|
||||||
|
{{ form_row(form.mobilenumber, {'label': 'Mobilenumber'}) }}
|
||||||
|
</div>
|
||||||
|
<div id="personAcceptSMS">
|
||||||
|
{{ form_row(form.acceptSMS, {'label' : 'Accept short text message ?'}) }}
|
||||||
|
</div>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- if form.otherPhoneNumbers is defined -%}
|
||||||
|
{{ form_widget(form.otherPhoneNumbers) }}
|
||||||
|
{{ form_errors(form.otherPhoneNumbers) }}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- if form.contactInfo is defined -%}
|
||||||
|
{{ form_row(form.contactInfo, {'label': 'Notes on contact information'}) }}
|
||||||
|
{%- endif -%}
|
||||||
|
</fieldset>
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
{%- if form.nationality is defined or form.spokenLanguages is defined -%}
|
{%- if form.nationality is defined or form.spokenLanguages is defined -%}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend><h2>{{ 'Administrative information'|trans }}</h2></legend>
|
<legend><h2>{{ 'Administrative information'|trans }}</h2></legend>
|
||||||
@ -93,41 +128,6 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
{%- if form.email is defined or form.phonenumber is defined or form.mobilenumber is defined or form.contactInfo is defined-%}
|
|
||||||
<fieldset>
|
|
||||||
<legend><h2>{{ 'Contact information'|trans }}</h2></legend>
|
|
||||||
{%- if form.email is defined -%}
|
|
||||||
<div id="personEmail">
|
|
||||||
{{ form_row(form.email, {'label': 'Email'}) }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{%- if form.acceptEmail is defined -%}
|
|
||||||
<div id="personAcceptEmail">
|
|
||||||
{{ form_row(form.acceptEmail, {'label' : 'Accept emails ?'}) }}
|
|
||||||
</div>
|
|
||||||
{%- endif -%}
|
|
||||||
|
|
||||||
{%- if form.phonenumber is defined -%}
|
|
||||||
{{ form_row(form.phonenumber, {'label': 'Phonenumber'}) }}
|
|
||||||
{%- endif -%}
|
|
||||||
{%- if form.mobilenumber is defined -%}
|
|
||||||
<div id="personPhoneNumber">
|
|
||||||
{{ form_row(form.mobilenumber, {'label': 'Mobilenumber'}) }}
|
|
||||||
</div>
|
|
||||||
<div id="personAcceptSMS">
|
|
||||||
{{ form_row(form.acceptSMS, {'label' : 'Accept short text message ?'}) }}
|
|
||||||
</div>
|
|
||||||
{%- endif -%}
|
|
||||||
{%- if form.otherPhoneNumbers is defined -%}
|
|
||||||
{{ form_widget(form.otherPhoneNumbers) }}
|
|
||||||
{{ form_errors(form.otherPhoneNumbers) }}
|
|
||||||
{%- endif -%}
|
|
||||||
{%- if form.contactInfo is defined -%}
|
|
||||||
{{ form_row(form.contactInfo, {'label': 'Notes on contact information'}) }}
|
|
||||||
{%- endif -%}
|
|
||||||
</fieldset>
|
|
||||||
{%- endif -%}
|
|
||||||
|
|
||||||
{{ form_rest(form) }}
|
{{ form_rest(form) }}
|
||||||
|
|
||||||
<ul class="record_actions sticky-form-buttons">
|
<ul class="record_actions sticky-form-buttons">
|
||||||
|
@ -17,9 +17,13 @@
|
|||||||
|
|
||||||
<div class="list-group vertical-menu {{ 'menu-' ~ menus.name }}">
|
<div class="list-group vertical-menu {{ 'menu-' ~ menus.name }}">
|
||||||
{% for menu in menus %}
|
{% for menu in menus %}
|
||||||
<a class="list-group-item list-group-item-action"
|
|
||||||
|
<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center"
|
||||||
href="{{ menu.uri }}">
|
href="{{ menu.uri }}">
|
||||||
{{ menu.label|upper }}
|
{{ menu.label|upper }}
|
||||||
|
{% if menu.extras.counter is defined and menu.extras.counter is not null %}
|
||||||
|
<span class="badge rounded-pill bg-secondary notification-counter">{{ menu.extras.counter }}</span>
|
||||||
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,6 +24,7 @@ use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
|||||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
|
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
|
||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Chill\PersonBundle\Repository\PersonRepository;
|
||||||
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
|
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
@ -51,6 +52,8 @@ class AccompanyingPeriodContext implements
|
|||||||
|
|
||||||
private PersonRenderInterface $personRender;
|
private PersonRenderInterface $personRender;
|
||||||
|
|
||||||
|
private PersonRepository $personRepository;
|
||||||
|
|
||||||
private TranslatableStringHelperInterface $translatableStringHelper;
|
private TranslatableStringHelperInterface $translatableStringHelper;
|
||||||
|
|
||||||
private TranslatorInterface $translator;
|
private TranslatorInterface $translator;
|
||||||
@ -61,6 +64,7 @@ class AccompanyingPeriodContext implements
|
|||||||
TranslatableStringHelperInterface $translatableStringHelper,
|
TranslatableStringHelperInterface $translatableStringHelper,
|
||||||
EntityManagerInterface $em,
|
EntityManagerInterface $em,
|
||||||
PersonRenderInterface $personRender,
|
PersonRenderInterface $personRender,
|
||||||
|
PersonRepository $personRepository,
|
||||||
TranslatorInterface $translator,
|
TranslatorInterface $translator,
|
||||||
BaseContextData $baseContextData
|
BaseContextData $baseContextData
|
||||||
) {
|
) {
|
||||||
@ -69,6 +73,7 @@ class AccompanyingPeriodContext implements
|
|||||||
$this->translatableStringHelper = $translatableStringHelper;
|
$this->translatableStringHelper = $translatableStringHelper;
|
||||||
$this->em = $em;
|
$this->em = $em;
|
||||||
$this->personRender = $personRender;
|
$this->personRender = $personRender;
|
||||||
|
$this->personRepository = $personRepository;
|
||||||
$this->translator = $translator;
|
$this->translator = $translator;
|
||||||
$this->baseContextData = $baseContextData;
|
$this->baseContextData = $baseContextData;
|
||||||
}
|
}
|
||||||
@ -256,6 +261,31 @@ class AccompanyingPeriodContext implements
|
|||||||
return $options['mainPerson'] || $options['person1'] || $options['person2'];
|
return $options['mainPerson'] || $options['person1'] || $options['person2'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array
|
||||||
|
{
|
||||||
|
$normalized = [];
|
||||||
|
foreach (['mainPerson', 'person1', 'person2'] as $k) {
|
||||||
|
$normalized[$k] = null !== ($data[$k] ?? null) ? $data[$k]->getId() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array
|
||||||
|
{
|
||||||
|
$denormalized = [];
|
||||||
|
|
||||||
|
foreach (['mainPerson', 'person1', 'person2'] as $k) {
|
||||||
|
if (null !== ($id = ($data[$k] ?? null))) {
|
||||||
|
$denormalized[$k] = $this->personRepository->find($id);
|
||||||
|
} else {
|
||||||
|
$denormalized[$k] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $denormalized;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param AccompanyingPeriod $entity
|
* @param AccompanyingPeriod $entity
|
||||||
*/
|
*/
|
||||||
|
@ -11,6 +11,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\PersonBundle\Service\DocGenerator;
|
namespace Chill\PersonBundle\Service\DocGenerator;
|
||||||
|
|
||||||
|
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface;
|
||||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
|
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
|
||||||
@ -18,7 +19,13 @@ use Symfony\Component\Form\FormBuilderInterface;
|
|||||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
|
|
||||||
class AccompanyingPeriodWorkContext
|
/**
|
||||||
|
* Generate a context for an @link{AccompanyingPeriodWork}.
|
||||||
|
*
|
||||||
|
* Although there isn't any document associated to AccompanyingPeriodWork, this context
|
||||||
|
* is use by @link{AccompanyingPeriodWorkEvaluationContext}.
|
||||||
|
*/
|
||||||
|
class AccompanyingPeriodWorkContext implements DocGeneratorContextWithPublicFormInterface
|
||||||
{
|
{
|
||||||
private NormalizerInterface $normalizer;
|
private NormalizerInterface $normalizer;
|
||||||
|
|
||||||
@ -109,8 +116,18 @@ class AccompanyingPeriodWorkContext
|
|||||||
return $this->periodContext->hasPublicForm($template, $entity);
|
return $this->periodContext->hasPublicForm($template, $entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array
|
||||||
|
{
|
||||||
|
return $this->periodContext->contextGenerationDataNormalize($template, $entity, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array
|
||||||
|
{
|
||||||
|
return $this->periodContext->contextGenerationDataDenormalize($template, $entity, $data);
|
||||||
|
}
|
||||||
|
|
||||||
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void
|
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void
|
||||||
{
|
{
|
||||||
// TODO: Implement storeGenerated() method.
|
// currently, no document associated with a AccompanyingPeriodWork
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -174,6 +174,18 @@ class AccompanyingPeriodWorkEvaluationContext implements
|
|||||||
->hasPublicForm($template, $entity->getAccompanyingPeriodWork());
|
->hasPublicForm($template, $entity->getAccompanyingPeriodWork());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array
|
||||||
|
{
|
||||||
|
return $this->accompanyingPeriodWorkContext
|
||||||
|
->contextGenerationDataNormalize($template, $entity, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array
|
||||||
|
{
|
||||||
|
return $this->accompanyingPeriodWorkContext
|
||||||
|
->contextGenerationDataDenormalize($template, $entity, $data);
|
||||||
|
}
|
||||||
|
|
||||||
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void
|
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void
|
||||||
{
|
{
|
||||||
$doc = new AccompanyingPeriodWorkEvaluationDocument();
|
$doc = new AccompanyingPeriodWorkEvaluationDocument();
|
||||||
|
@ -21,11 +21,13 @@ use Chill\DocStoreBundle\Repository\DocumentCategoryRepository;
|
|||||||
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
|
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
|
||||||
use Chill\MainBundle\Entity\Scope;
|
use Chill\MainBundle\Entity\Scope;
|
||||||
use Chill\MainBundle\Form\Type\ScopePickerType;
|
use Chill\MainBundle\Form\Type\ScopePickerType;
|
||||||
|
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
|
||||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
||||||
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
|
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Chill\PersonBundle\Repository\PersonRepository;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
@ -55,6 +57,8 @@ final class PersonContext implements PersonContextInterface
|
|||||||
|
|
||||||
private NormalizerInterface $normalizer;
|
private NormalizerInterface $normalizer;
|
||||||
|
|
||||||
|
private ScopeRepositoryInterface $scopeRepository;
|
||||||
|
|
||||||
private Security $security;
|
private Security $security;
|
||||||
|
|
||||||
private bool $showScopes;
|
private bool $showScopes;
|
||||||
@ -71,6 +75,7 @@ final class PersonContext implements PersonContextInterface
|
|||||||
EntityManagerInterface $em,
|
EntityManagerInterface $em,
|
||||||
NormalizerInterface $normalizer,
|
NormalizerInterface $normalizer,
|
||||||
ParameterBagInterface $parameterBag,
|
ParameterBagInterface $parameterBag,
|
||||||
|
ScopeRepositoryInterface $scopeRepository,
|
||||||
Security $security,
|
Security $security,
|
||||||
TranslatorInterface $translator,
|
TranslatorInterface $translator,
|
||||||
TranslatableStringHelperInterface $translatableStringHelper
|
TranslatableStringHelperInterface $translatableStringHelper
|
||||||
@ -81,6 +86,7 @@ final class PersonContext implements PersonContextInterface
|
|||||||
$this->documentCategoryRepository = $documentCategoryRepository;
|
$this->documentCategoryRepository = $documentCategoryRepository;
|
||||||
$this->em = $em;
|
$this->em = $em;
|
||||||
$this->normalizer = $normalizer;
|
$this->normalizer = $normalizer;
|
||||||
|
$this->scopeRepository = $scopeRepository;
|
||||||
$this->security = $security;
|
$this->security = $security;
|
||||||
$this->showScopes = $parameterBag->get('chill_main')['acl']['form_show_scopes'];
|
$this->showScopes = $parameterBag->get('chill_main')['acl']['form_show_scopes'];
|
||||||
$this->translator = $translator;
|
$this->translator = $translator;
|
||||||
@ -211,6 +217,38 @@ final class PersonContext implements PersonContextInterface
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Person $entity
|
||||||
|
*/
|
||||||
|
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array
|
||||||
|
{
|
||||||
|
$scope = $data['scope'] ?? null;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'title' => $data['title'] ?? '',
|
||||||
|
'scope_id' => $scope instanceof Scope ? $scope->getId() : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Person $entity
|
||||||
|
*/
|
||||||
|
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array
|
||||||
|
{
|
||||||
|
if (!isset($data['scope'])) {
|
||||||
|
$scope = null;
|
||||||
|
} else {
|
||||||
|
if (null === $scope = $this->scopeRepository->find($data['scope'])) {
|
||||||
|
throw new \UnexpectedValueException('scope not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'title' => $data['title'] ?? '',
|
||||||
|
'scope' => $scope,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Person $entity
|
* @param Person $entity
|
||||||
*/
|
*/
|
||||||
|
@ -48,6 +48,10 @@ interface PersonContextInterface extends DocGeneratorContextWithAdminFormInterfa
|
|||||||
*/
|
*/
|
||||||
public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool;
|
public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool;
|
||||||
|
|
||||||
|
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array;
|
||||||
|
|
||||||
|
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Person $entity
|
* @param Person $entity
|
||||||
*/
|
*/
|
||||||
|
@ -17,6 +17,7 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
|||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||||
use Chill\ThirdPartyBundle\Form\Type\PickThirdpartyDynamicType;
|
use Chill\ThirdPartyBundle\Form\Type\PickThirdpartyDynamicType;
|
||||||
|
use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
@ -30,12 +31,16 @@ class PersonContextWithThirdParty implements DocGeneratorContextWithAdminFormInt
|
|||||||
|
|
||||||
private PersonContextInterface $personContext;
|
private PersonContextInterface $personContext;
|
||||||
|
|
||||||
|
private ThirdPartyRepository $thirdPartyRepository;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
PersonContextInterface $personContext,
|
PersonContextInterface $personContext,
|
||||||
NormalizerInterface $normalizer
|
NormalizerInterface $normalizer,
|
||||||
|
ThirdPartyRepository $thirdPartyRepository
|
||||||
) {
|
) {
|
||||||
$this->personContext = $personContext;
|
$this->personContext = $personContext;
|
||||||
$this->normalizer = $normalizer;
|
$this->normalizer = $normalizer;
|
||||||
|
$this->thirdPartyRepository = $thirdPartyRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function adminFormReverseTransform(array $data): array
|
public function adminFormReverseTransform(array $data): array
|
||||||
@ -123,6 +128,26 @@ class PersonContextWithThirdParty implements DocGeneratorContextWithAdminFormInt
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array
|
||||||
|
{
|
||||||
|
return array_merge(
|
||||||
|
[
|
||||||
|
'thirdParty' => null === $data['thirdParty'] ? null : $data['thirdParty']->getId(),
|
||||||
|
],
|
||||||
|
$this->personContext->contextGenerationDataNormalize($template, $entity, $data),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array
|
||||||
|
{
|
||||||
|
return array_merge(
|
||||||
|
[
|
||||||
|
'thirdParty' => null === $data['thirdParty'] ? null : $this->thirdPartyRepository->find($data['thirdParty']),
|
||||||
|
],
|
||||||
|
$this->personContext->contextGenerationDataDenormalize($template, $entity, $data),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void
|
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void
|
||||||
{
|
{
|
||||||
$this->personContext->storeGenerated($template, $storedObject, $entity, $contextGenerationData);
|
$this->personContext->storeGenerated($template, $storedObject, $entity, $contextGenerationData);
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
// this file loads all assets from the Chill person bundle
|
// this file loads all assets from the Chill person bundle
|
||||||
module.exports = function(encore, entries)
|
module.exports = function(encore, entries) {
|
||||||
{
|
|
||||||
encore.addEntry('page_wopi_editor', __dirname + '/src/Resources/public/page/editor/index.js');
|
encore.addEntry('page_wopi_editor', __dirname + '/src/Resources/public/page/editor/index.js');
|
||||||
//home/julien/dev/département-vendee/chill/vendor/chill-project/chill-bundles/src/Bundle/ChillWopiBundle/src/Resources/public/page/editor/index.js
|
encore.addEntry('mod_reload_page', __dirname + '/src/Resources/public/module/pending/index');
|
||||||
};
|
};
|
||||||
|
@ -26,6 +26,10 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
use Symfony\Component\Routing\RouterInterface;
|
use Symfony\Component\Routing\RouterInterface;
|
||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
|
use Symfony\Component\Templating\EngineInterface;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
@ -35,6 +39,8 @@ final class Editor
|
|||||||
{
|
{
|
||||||
private DocumentManagerInterface $documentManager;
|
private DocumentManagerInterface $documentManager;
|
||||||
|
|
||||||
|
private EngineInterface $engine;
|
||||||
|
|
||||||
private JWTTokenManagerInterface $JWTTokenManager;
|
private JWTTokenManagerInterface $JWTTokenManager;
|
||||||
|
|
||||||
private Psr17Interface $psr17;
|
private Psr17Interface $psr17;
|
||||||
@ -49,18 +55,24 @@ final class Editor
|
|||||||
|
|
||||||
private DiscoveryInterface $wopiDiscovery;
|
private DiscoveryInterface $wopiDiscovery;
|
||||||
|
|
||||||
|
private NormalizerInterface $normalizer;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ConfigurationInterface $wopiConfiguration,
|
ConfigurationInterface $wopiConfiguration,
|
||||||
DiscoveryInterface $wopiDiscovery,
|
DiscoveryInterface $wopiDiscovery,
|
||||||
DocumentManagerInterface $documentManager,
|
DocumentManagerInterface $documentManager,
|
||||||
|
EngineInterface $engine,
|
||||||
JWTTokenManagerInterface $JWTTokenManager,
|
JWTTokenManagerInterface $JWTTokenManager,
|
||||||
|
NormalizerInterface $normalizer,
|
||||||
ResponderInterface $responder,
|
ResponderInterface $responder,
|
||||||
Security $security,
|
Security $security,
|
||||||
Psr17Interface $psr17,
|
Psr17Interface $psr17,
|
||||||
RouterInterface $router
|
RouterInterface $router
|
||||||
) {
|
) {
|
||||||
$this->documentManager = $documentManager;
|
$this->documentManager = $documentManager;
|
||||||
|
$this->engine = $engine;
|
||||||
$this->JWTTokenManager = $JWTTokenManager;
|
$this->JWTTokenManager = $JWTTokenManager;
|
||||||
|
$this->normalizer = $normalizer;
|
||||||
$this->wopiConfiguration = $wopiConfiguration;
|
$this->wopiConfiguration = $wopiConfiguration;
|
||||||
$this->wopiDiscovery = $wopiDiscovery;
|
$this->wopiDiscovery = $wopiDiscovery;
|
||||||
$this->responder = $responder;
|
$this->responder = $responder;
|
||||||
@ -87,6 +99,22 @@ final class Editor
|
|||||||
throw new NotFoundHttpException(sprintf('Unable to find object %s', $fileId));
|
throw new NotFoundHttpException(sprintf('Unable to find object %s', $fileId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (StoredObject::STATUS_FAILURE === $storedObject->getStatus()) {
|
||||||
|
return new Response(
|
||||||
|
$this->engine
|
||||||
|
->render('@ChillWopi/Editor/stored_object_failure.html.twig')
|
||||||
|
);
|
||||||
|
} elseif (StoredObject::STATUS_PENDING === $storedObject->getStatus()) {
|
||||||
|
return new Response(
|
||||||
|
$this->engine
|
||||||
|
->render('@ChillWopi/Editor/stored_object_pending.html.twig', [
|
||||||
|
'storedObject' => $this->normalizer->normalize($storedObject, 'json', [
|
||||||
|
AbstractNormalizer::GROUPS => ['read']
|
||||||
|
])
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if ([] === $discoverExtension = $this->wopiDiscovery->discoverMimeType($storedObject->getType())) {
|
if ([] === $discoverExtension = $this->wopiDiscovery->discoverMimeType($storedObject->getType())) {
|
||||||
throw new Exception(sprintf('Unable to find mime type %s', $storedObject->getType()));
|
throw new Exception(sprintf('Unable to find mime type %s', $storedObject->getType()));
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
import {is_object_ready} from "../../../../../../ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/helpers";
|
||||||
|
import {
|
||||||
|
StoredObject,
|
||||||
|
StoredObjectStatus,
|
||||||
|
StoredObjectStatusChange
|
||||||
|
} from "../../../../../../ChillDocStoreBundle/Resources/public/types";
|
||||||
|
|
||||||
|
async function reload_if_needed(stored_object: StoredObject, i: number): Promise<void> {
|
||||||
|
let current_status = await is_object_ready(stored_object);
|
||||||
|
|
||||||
|
if (stored_object.status !== current_status.status) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
wait_before_reload(stored_object, i + 1);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
function wait_before_reload(stored_object: StoredObject, i: number): void {
|
||||||
|
/**
|
||||||
|
* value of the timeout. Set to 5 sec during the first 10 minutes, then every 1 minute
|
||||||
|
*/
|
||||||
|
let timeout = i < 1200 ? 5000 : 60000;
|
||||||
|
|
||||||
|
setTimeout(
|
||||||
|
reload_if_needed,
|
||||||
|
timeout,
|
||||||
|
stored_object,
|
||||||
|
i
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', async function (e) {
|
||||||
|
if (undefined === (<any>window).stored_object) {
|
||||||
|
console.error('window.stored_object is undefined');
|
||||||
|
throw Error('window.stored_object is undefined');
|
||||||
|
}
|
||||||
|
|
||||||
|
let stored_object = JSON.parse((<any>window).stored_object) as StoredObject;
|
||||||
|
reload_if_needed(stored_object, 0);
|
||||||
|
});
|
@ -0,0 +1,15 @@
|
|||||||
|
{% extends 'ChillMainBundle::layout.html.twig' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
{{ 'docgen.Doc generation failed'|trans }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="sticky-form-buttons record_actions">
|
||||||
|
<li class="cancel">
|
||||||
|
<a href="{{ chill_return_path_or('chill_main_homepage') }}" class="btn btn-cancel">
|
||||||
|
{{ 'Cancel'|trans|chill_return_path_label }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,36 @@
|
|||||||
|
{% extends '@ChillMain/layout.html.twig' %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
<script type="application/javascript">
|
||||||
|
window.stored_object = '{{ storedObject|json_encode|escape('js') }}'
|
||||||
|
</script>
|
||||||
|
{{ encore_entry_script_tags('mod_reload_page') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{{ encore_entry_link_tags('mod_reload_page') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="alert alert-danger text-center">
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{{ 'docgen.Doc generation is pending'|trans }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<i class="fa fa-cog fa-spin fa-3x fa-fw"></i>
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="sticky-form-buttons record_actions">
|
||||||
|
<li class="cancel">
|
||||||
|
<a href="{{ chill_return_path_or('chill_main_homepage') }}" class="btn btn-cancel">
|
||||||
|
{{ 'docgen.Come back later'|trans|chill_return_path_label }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% endblock content %}
|
Loading…
x
Reference in New Issue
Block a user