mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge remote-tracking branch 'origin/master' into user_absences
This commit is contained in:
commit
f1ebc089c3
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,6 +5,7 @@ composer.lock
|
||||
docs/build/
|
||||
node_modules/*
|
||||
.php_cs.cache
|
||||
.cache/*
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
/.env.local
|
||||
|
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\ActivityBundle\Export\Filter\ACPFilters;
|
||||
|
||||
use Chill\ActivityBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Form\Type\PickUserLocationType;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class LocationFilter implements FilterInterface
|
||||
{
|
||||
private TranslatableStringHelper $translatableStringHelper;
|
||||
|
||||
public function __construct(TranslatableStringHelper $translatableStringHelper)
|
||||
{
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data)
|
||||
{
|
||||
$qb->andWhere(
|
||||
$qb->expr()->in('activity.location', ':location')
|
||||
);
|
||||
|
||||
$qb->setParameter('location', $data['accepted_location']);
|
||||
}
|
||||
|
||||
public function applyOn(): string
|
||||
{
|
||||
return Declarations::ACTIVITY_ACP;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('accepted_location', PickUserLocationType::class, [
|
||||
'multiple' => true,
|
||||
'label' => 'pick location'
|
||||
]);
|
||||
}
|
||||
|
||||
public function describeAction($data, $format = 'string'): array
|
||||
{
|
||||
$locations = [];
|
||||
|
||||
foreach ($data['accepted_location'] as $location) {
|
||||
$locations[] = $location->getName();
|
||||
}
|
||||
|
||||
return ['Filtered activity by location: only %locations%', [
|
||||
'%locations%' => implode(', ', $locations),
|
||||
]];
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'Filter activity by location';
|
||||
}
|
||||
}
|
@ -168,7 +168,7 @@
|
||||
{% if entity.documents|length > 0 %}
|
||||
<ul>
|
||||
{% for d in entity.documents %}
|
||||
<li>{{ d.title }}{{ m.download_button(d) }}</li>
|
||||
<li>{{ d.title }} {{ d|chill_document_button_group() }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
|
@ -8,12 +8,14 @@
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
|
||||
{{ encore_entry_script_tags('mod_async_upload') }}
|
||||
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_notification_toggle_read_status') }}
|
||||
{{ encore_entry_link_tags('mod_async_upload') }}
|
||||
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
||||
{% import 'ChillActivityBundle:ActivityReason:macro.html.twig' as m %}
|
||||
|
@ -7,13 +7,13 @@
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
|
||||
{{ encore_entry_link_tags('mod_async_upload') }}
|
||||
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_notification_toggle_read_status') }}
|
||||
{{ encore_entry_link_tags('mod_async_upload') }}
|
||||
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
||||
{% import 'ChillActivityBundle:ActivityReason:macro.html.twig' as m %}
|
||||
|
@ -22,6 +22,7 @@ use Chill\DocStoreBundle\Repository\DocumentCategoryRepository;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Repository\PersonRepository;
|
||||
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
@ -45,6 +46,8 @@ class ActivityContext implements
|
||||
|
||||
private PersonRenderInterface $personRender;
|
||||
|
||||
private PersonRepository $personRepository;
|
||||
|
||||
private TranslatableStringHelperInterface $translatableStringHelper;
|
||||
|
||||
private TranslatorInterface $translator;
|
||||
@ -55,6 +58,7 @@ class ActivityContext implements
|
||||
TranslatableStringHelperInterface $translatableStringHelper,
|
||||
EntityManagerInterface $em,
|
||||
PersonRenderInterface $personRender,
|
||||
PersonRepository $personRepository,
|
||||
TranslatorInterface $translator,
|
||||
BaseContextData $baseContextData
|
||||
) {
|
||||
@ -63,6 +67,7 @@ class ActivityContext implements
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
$this->em = $em;
|
||||
$this->personRender = $personRender;
|
||||
$this->personRepository = $personRepository;
|
||||
$this->translator = $translator;
|
||||
$this->baseContextData = $baseContextData;
|
||||
}
|
||||
@ -206,6 +211,32 @@ class ActivityContext implements
|
||||
return $options['mainPerson'] || $options['person1'] || $options['person2'];
|
||||
}
|
||||
|
||||
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array
|
||||
{
|
||||
$normalized = [];
|
||||
|
||||
foreach (['mainPerson', 'person1', 'person2'] as $k) {
|
||||
$normalized[$k] = null === $data[$k] ? null : $data[$k]->getId();
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array
|
||||
{
|
||||
$denormalized = [];
|
||||
|
||||
foreach (['mainPerson', 'person1', 'person2'] as $k) {
|
||||
if (null !== ($id = ($data[$k] ?? null))) {
|
||||
$denormalized[$k] = $this->personRepository->find($id);
|
||||
} else {
|
||||
$denormalized[$k] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $denormalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity $entity
|
||||
*/
|
||||
|
@ -146,6 +146,16 @@ class ListActivitiesByAccompanyingPeriodContext implements
|
||||
return $this->accompanyingPeriodContext->hasPublicForm($template, $entity);
|
||||
}
|
||||
|
||||
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array
|
||||
{
|
||||
return $this->accompanyingPeriodContext->contextGenerationDataNormalize($template, $entity, $data);
|
||||
}
|
||||
|
||||
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array
|
||||
{
|
||||
return $this->accompanyingPeriodContext->contextGenerationDataDenormalize($template, $entity, $data);
|
||||
}
|
||||
|
||||
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void
|
||||
{
|
||||
$this->accompanyingPeriodContext->storeGenerated($template, $storedObject, $entity, $contextGenerationData);
|
||||
|
@ -79,6 +79,11 @@ services:
|
||||
tags:
|
||||
- { name: chill.export_filter, alias: 'accompanyingcourse_activitytype_filter' }
|
||||
|
||||
chill.activity.export.location_filter:
|
||||
class: Chill\ActivityBundle\Export\Filter\ACPFilters\LocationFilter
|
||||
tags:
|
||||
- { name: chill.export_filter, alias: 'activity_location_filter' }
|
||||
|
||||
chill.activity.export.locationtype_filter:
|
||||
class: Chill\ActivityBundle\Export\Filter\ACPFilters\LocationTypeFilter
|
||||
tags:
|
||||
|
@ -77,7 +77,7 @@ Choose a type: Choisir un type
|
||||
4 hours: 4 heures
|
||||
4 hours 30: 4 heures 30
|
||||
5 hours: 5 heures
|
||||
Concerned groups: Parties concernées
|
||||
Concerned groups: Parties concernées par l'échange
|
||||
Persons in accompanying course: Usagers du parcours
|
||||
Third persons: Tiers non-pro.
|
||||
Others persons: Usagers
|
||||
@ -252,6 +252,8 @@ Activity reasons for those activities: Sujets de ces activités
|
||||
|
||||
Filter by activity type: Filtrer les activités par type
|
||||
|
||||
Filter activity by location: Filtrer les activités par localisation
|
||||
'Filtered activity by location: only %locations%': "Filtré par localisation: uniquement %locations%"
|
||||
Filter activity by locationtype: Filtrer les activités par type de localisation
|
||||
'Filtered activity by locationtype: only %types%': "Filtré par type de localisation: uniquement %types%"
|
||||
Accepted locationtype: Types de localisation
|
||||
|
@ -53,19 +53,15 @@ class ByActivityTypeAggregator implements AggregatorInterface
|
||||
|
||||
public function getLabels($key, array $values, $data)
|
||||
{
|
||||
$this->asideActivityCategoryRepository->findBy(['id' => $values]);
|
||||
|
||||
return function ($value): string {
|
||||
if ('_header' === $value) {
|
||||
return 'export.aggregator.Aside activity type';
|
||||
}
|
||||
|
||||
if (null === $value) {
|
||||
if (null === $value || null === $t = $this->asideActivityCategoryRepository->find($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$t = $this->asideActivityCategoryRepository->find($value);
|
||||
|
||||
return $this->translatableStringHelper->localize($t->getTitle());
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\AsideActivityBundle\Export\Aggregator;
|
||||
|
||||
use Chill\AsideActivityBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Export\AggregatorInterface;
|
||||
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
use function in_array;
|
||||
|
||||
class ByUserJobAggregator implements AggregatorInterface
|
||||
{
|
||||
private TranslatableStringHelperInterface $translatableStringHelper;
|
||||
|
||||
private UserJobRepositoryInterface $userJobRepository;
|
||||
|
||||
public function __construct(UserJobRepositoryInterface $userJobRepository, TranslatableStringHelperInterface $translatableStringHelper)
|
||||
{
|
||||
$this->userJobRepository = $userJobRepository;
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data)
|
||||
{
|
||||
if (!in_array('aside_user', $qb->getAllAliases(), true)) {
|
||||
$qb->leftJoin('aside.agent', 'aside_user');
|
||||
}
|
||||
|
||||
$qb
|
||||
->addSelect('IDENTITY(aside_user.userJob) AS aside_activity_user_job_aggregator')
|
||||
->addGroupBy('aside_activity_user_job_aggregator');
|
||||
}
|
||||
|
||||
public function applyOn()
|
||||
{
|
||||
return Declarations::ASIDE_ACTIVITY_TYPE;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
// nothing to add in the form
|
||||
}
|
||||
|
||||
public function getLabels($key, array $values, $data)
|
||||
{
|
||||
return function ($value): string {
|
||||
if ('_header' === $value) {
|
||||
return 'Users \'s job';
|
||||
}
|
||||
|
||||
if (null === $value || '' === $value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$j = $this->userJobRepository->find($value);
|
||||
|
||||
return $this->translatableStringHelper->localize(
|
||||
$j->getLabel()
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
public function getQueryKeys($data): array
|
||||
{
|
||||
return ['aside_activity_user_job_aggregator'];
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return 'export.aggregator.Aggregate by user job';
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\AsideActivityBundle\Export\Aggregator;
|
||||
|
||||
use Chill\AsideActivityBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Export\AggregatorInterface;
|
||||
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
use function in_array;
|
||||
|
||||
class ByUserScopeAggregator implements AggregatorInterface
|
||||
{
|
||||
private ScopeRepositoryInterface $scopeRepository;
|
||||
|
||||
private TranslatableStringHelperInterface $translatableStringHelper;
|
||||
|
||||
public function __construct(ScopeRepositoryInterface $scopeRepository, TranslatableStringHelperInterface $translatableStringHelper)
|
||||
{
|
||||
$this->scopeRepository = $scopeRepository;
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data)
|
||||
{
|
||||
if (!in_array('aside_user', $qb->getAllAliases(), true)) {
|
||||
$qb->leftJoin('aside.agent', 'aside_user');
|
||||
}
|
||||
|
||||
$qb
|
||||
->addSelect('IDENTITY(aside_user.mainScope) AS aside_activity_user_scope_aggregator')
|
||||
->addGroupBy('aside_activity_user_scope_aggregator');
|
||||
}
|
||||
|
||||
public function applyOn()
|
||||
{
|
||||
return Declarations::ASIDE_ACTIVITY_TYPE;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
// nothing to add in the form
|
||||
}
|
||||
|
||||
public function getLabels($key, array $values, $data)
|
||||
{
|
||||
return function ($value): string {
|
||||
if ('_header' === $value) {
|
||||
return 'Users \'s scope';
|
||||
}
|
||||
|
||||
if (null === $value || '' === $value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$s = $this->scopeRepository->find($value);
|
||||
|
||||
return $this->translatableStringHelper->localize(
|
||||
$s->getName()
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
public function getQueryKeys($data): array
|
||||
{
|
||||
return ['aside_activity_user_scope_aggregator'];
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return 'export.aggregator.Aggregate by user scope';
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\AsideActivityBundle\Export\Export;
|
||||
|
||||
use Chill\AsideActivityBundle\Export\Declarations;
|
||||
use Chill\AsideActivityBundle\Repository\AsideActivityRepository;
|
||||
use Chill\AsideActivityBundle\Security\AsideActivityVoter;
|
||||
use Chill\MainBundle\Export\ExportInterface;
|
||||
use Chill\MainBundle\Export\FormatterInterface;
|
||||
use Chill\MainBundle\Export\GroupedExportInterface;
|
||||
use Doctrine\ORM\Query;
|
||||
use LogicException;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class AvgAsideActivityDuration implements ExportInterface, GroupedExportInterface
|
||||
{
|
||||
private AsideActivityRepository $repository;
|
||||
|
||||
public function __construct(
|
||||
AsideActivityRepository $repository
|
||||
) {
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
}
|
||||
|
||||
public function getAllowedFormattersTypes(): array
|
||||
{
|
||||
return [FormatterInterface::TYPE_TABULAR];
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'export.Average aside activities duration';
|
||||
}
|
||||
|
||||
public function getGroup(): string
|
||||
{
|
||||
return 'export.Exports of aside activities';
|
||||
}
|
||||
|
||||
public function getLabels($key, array $values, $data)
|
||||
{
|
||||
if ('export_avg_aside_activity_duration' !== $key) {
|
||||
throw new LogicException("the key {$key} is not used by this export");
|
||||
}
|
||||
|
||||
return static fn ($value) => '_header' === $value ? 'Average duration aside activities' : $value;
|
||||
}
|
||||
|
||||
public function getQueryKeys($data): array
|
||||
{
|
||||
return ['export_avg_aside_activity_duration'];
|
||||
}
|
||||
|
||||
public function getResult($query, $data)
|
||||
{
|
||||
return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'export.Average aside activities duration';
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return Declarations::ASIDE_ACTIVITY_TYPE;
|
||||
}
|
||||
|
||||
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('aside');
|
||||
|
||||
$qb
|
||||
->select('AVG(aside.duration) as export_avg_aside_activity_duration')
|
||||
->andWhere($qb->expr()->isNotNull('aside.duration'));
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function requiredRole(): string
|
||||
{
|
||||
return AsideActivityVoter::STATS;
|
||||
}
|
||||
|
||||
public function supportsModifiers(): array
|
||||
{
|
||||
return [Declarations::ASIDE_ACTIVITY_TYPE];
|
||||
}
|
||||
}
|
@ -11,12 +11,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\AsideActivityBundle\Export\Export;
|
||||
|
||||
use Chill\AsideActivityBundle\Export\Declarations;
|
||||
use Chill\AsideActivityBundle\Repository\AsideActivityRepository;
|
||||
use Chill\AsideActivityBundle\Security\AsideActivityVoter;
|
||||
use Chill\MainBundle\Export\ExportInterface;
|
||||
use Chill\MainBundle\Export\FormatterInterface;
|
||||
use Chill\MainBundle\Export\GroupedExportInterface;
|
||||
use ChillAsideActivityBundle\Export\Declarations;
|
||||
use Doctrine\ORM\Query;
|
||||
use LogicException;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
@ -100,6 +100,8 @@ class CountAsideActivity implements ExportInterface, GroupedExportInterface
|
||||
|
||||
public function supportsModifiers(): array
|
||||
{
|
||||
return [];
|
||||
return [
|
||||
Declarations::ASIDE_ACTIVITY_TYPE,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,236 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\AsideActivityBundle\Export\Export;
|
||||
|
||||
use Chill\AsideActivityBundle\Entity\AsideActivity;
|
||||
use Chill\AsideActivityBundle\Export\Declarations;
|
||||
use Chill\AsideActivityBundle\Form\AsideActivityCategoryType;
|
||||
use Chill\AsideActivityBundle\Repository\AsideActivityCategoryRepository;
|
||||
use Chill\AsideActivityBundle\Security\AsideActivityVoter;
|
||||
use Chill\AsideActivityBundle\Templating\Entity\CategoryRender;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Export\FormatterInterface;
|
||||
use Chill\MainBundle\Export\GroupedExportInterface;
|
||||
use Chill\MainBundle\Export\Helper\DateTimeHelper;
|
||||
use Chill\MainBundle\Export\Helper\UserHelper;
|
||||
use Chill\MainBundle\Export\ListInterface;
|
||||
use Chill\MainBundle\Repository\CenterRepositoryInterface;
|
||||
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Closure;
|
||||
use Doctrine\ORM\AbstractQuery;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
final class ListAsideActivity implements ListInterface, GroupedExportInterface
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private UserHelper $userHelper;
|
||||
|
||||
private DateTimeHelper $dateTimeHelper;
|
||||
|
||||
private ScopeRepositoryInterface $scopeRepository;
|
||||
|
||||
private CenterRepositoryInterface $centerRepository;
|
||||
|
||||
private AsideActivityCategoryRepository $asideActivityCategoryRepository;
|
||||
|
||||
private CategoryRender $categoryRender;
|
||||
|
||||
private TranslatableStringHelperInterface $translatableStringHelper;
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $em,
|
||||
DateTimeHelper $dateTimeHelper,
|
||||
UserHelper $userHelper,
|
||||
ScopeRepositoryInterface $scopeRepository,
|
||||
CenterRepositoryInterface $centerRepository,
|
||||
AsideActivityCategoryRepository $asideActivityCategoryRepository,
|
||||
CategoryRender $categoryRender,
|
||||
TranslatableStringHelperInterface $translatableStringHelper
|
||||
) {
|
||||
$this->em = $em;
|
||||
$this->dateTimeHelper = $dateTimeHelper;
|
||||
$this->userHelper = $userHelper;
|
||||
$this->scopeRepository = $scopeRepository;
|
||||
$this->centerRepository = $centerRepository;
|
||||
$this->asideActivityCategoryRepository = $asideActivityCategoryRepository;
|
||||
$this->categoryRender = $categoryRender;
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
}
|
||||
|
||||
public function getAllowedFormattersTypes()
|
||||
{
|
||||
return [FormatterInterface::TYPE_LIST];
|
||||
}
|
||||
|
||||
public function getDescription()
|
||||
{
|
||||
return 'export.aside_activity.List of aside activities';
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return 'export.aside_activity.List of aside activities';
|
||||
}
|
||||
|
||||
public function getGroup(): string
|
||||
{
|
||||
return 'export.Exports of aside activities';
|
||||
}
|
||||
|
||||
public function getLabels($key, array $values, $data)
|
||||
{
|
||||
switch ($key) {
|
||||
case 'id':
|
||||
case 'note':
|
||||
return function ($value) use ($key) {
|
||||
if ('_header' === $value) {
|
||||
return 'export.aside_activity.' . $key;
|
||||
}
|
||||
|
||||
return $value ?? '';
|
||||
};
|
||||
case 'duration':
|
||||
return function ($value) use ($key) {
|
||||
if ('_header' === $value) {
|
||||
return 'export.aside_activity.' . $key;
|
||||
}
|
||||
|
||||
if (null === $value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($value instanceof \DateTimeInterface) {
|
||||
return $value->format('H:i:s');
|
||||
}
|
||||
|
||||
return $value;
|
||||
};
|
||||
|
||||
case 'createdAt':
|
||||
case 'updatedAt':
|
||||
case 'date':
|
||||
return $this->dateTimeHelper->getLabel('export.aside_activity.'.$key);
|
||||
|
||||
case 'agent_id':
|
||||
case 'creator_id':
|
||||
return $this->userHelper->getLabel($key, $values, 'export.aside_activity.' . $key);
|
||||
|
||||
case 'aside_activity_type':
|
||||
return function ($value) {
|
||||
if ('_header' === $value) {
|
||||
return 'export.aside_activity.aside_activity_type';
|
||||
}
|
||||
|
||||
if (null === $value || '' === $value || null === $c = $this->asideActivityCategoryRepository->find($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->categoryRender->renderString($c, []);
|
||||
};
|
||||
|
||||
case 'main_scope':
|
||||
return function ($value) {
|
||||
if ('_header' === $value) {
|
||||
return 'export.aside_activity.main_scope';
|
||||
}
|
||||
|
||||
if (null === $value || '' === $value || null === $c = $this->scopeRepository->find($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->translatableStringHelper->localize($c->getName());
|
||||
};
|
||||
|
||||
case 'main_center':
|
||||
return function ($value) {
|
||||
if ('_header' === $value) {
|
||||
return 'export.aside_activity.main_center';
|
||||
}
|
||||
|
||||
/** @var Center $c */
|
||||
if (null === $value || '' === $value || null === $c = $this->centerRepository->find($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $c->getName();
|
||||
};
|
||||
|
||||
default:
|
||||
throw new \LogicException('this key is not supported : ' . $key);
|
||||
}
|
||||
}
|
||||
|
||||
public function getQueryKeys($data)
|
||||
{
|
||||
return [
|
||||
'id',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'agent_id',
|
||||
'creator_id',
|
||||
'main_scope',
|
||||
'main_center',
|
||||
'aside_activity_type',
|
||||
'date',
|
||||
'duration',
|
||||
'note'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param QueryBuilder $query
|
||||
* @param array $data
|
||||
*/
|
||||
public function getResult($query, $data): array
|
||||
{
|
||||
return $query->getQuery()->getResult(AbstractQuery::HYDRATE_ARRAY);
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return Declarations::ASIDE_ACTIVITY_TYPE;
|
||||
}
|
||||
|
||||
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
|
||||
{
|
||||
$qb = $this->em->createQueryBuilder()
|
||||
->from(AsideActivity::class, 'aside')
|
||||
->leftJoin('aside.agent', 'agent')
|
||||
;
|
||||
|
||||
$qb
|
||||
->addSelect('aside.id AS id')
|
||||
->addSelect('aside.createdAt AS createdAt')
|
||||
->addSelect('aside.updatedAt AS updatedAt')
|
||||
->addSelect('IDENTITY(aside.agent) AS agent_id')
|
||||
->addSelect('IDENTITY(aside.createdBy) AS creator_id')
|
||||
->addSelect('IDENTITY(agent.mainScope) AS main_scope')
|
||||
->addSelect('IDENTITY(agent.mainCenter) AS main_center')
|
||||
->addSelect('IDENTITY(aside.type) AS aside_activity_type')
|
||||
->addSelect('aside.date')
|
||||
->addSelect('aside.duration')
|
||||
->addSelect('aside.note')
|
||||
;
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function requiredRole(): string
|
||||
{
|
||||
return AsideActivityVoter::STATS;
|
||||
}
|
||||
|
||||
public function supportsModifiers()
|
||||
{
|
||||
return [Declarations::ASIDE_ACTIVITY_TYPE];
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\AsideActivityBundle\Export\Export;
|
||||
|
||||
use Chill\AsideActivityBundle\Export\Declarations;
|
||||
use Chill\AsideActivityBundle\Repository\AsideActivityRepository;
|
||||
use Chill\AsideActivityBundle\Security\AsideActivityVoter;
|
||||
use Chill\MainBundle\Export\ExportInterface;
|
||||
use Chill\MainBundle\Export\FormatterInterface;
|
||||
use Chill\MainBundle\Export\GroupedExportInterface;
|
||||
use Doctrine\ORM\Query;
|
||||
use LogicException;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class SumAsideActivityDuration implements ExportInterface, GroupedExportInterface
|
||||
{
|
||||
private AsideActivityRepository $repository;
|
||||
|
||||
public function __construct(
|
||||
AsideActivityRepository $repository
|
||||
) {
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
}
|
||||
|
||||
public function getAllowedFormattersTypes(): array
|
||||
{
|
||||
return [FormatterInterface::TYPE_TABULAR];
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'export.Sum aside activities duration';
|
||||
}
|
||||
|
||||
public function getGroup(): string
|
||||
{
|
||||
return 'export.Exports of aside activities';
|
||||
}
|
||||
|
||||
public function getLabels($key, array $values, $data)
|
||||
{
|
||||
if ('export_sum_aside_activity_duration' !== $key) {
|
||||
throw new LogicException("the key {$key} is not used by this export");
|
||||
}
|
||||
|
||||
return static fn ($value) => '_header' === $value ? 'Sum duration aside activities' : $value;
|
||||
}
|
||||
|
||||
public function getQueryKeys($data): array
|
||||
{
|
||||
return ['export_sum_aside_activity_duration'];
|
||||
}
|
||||
|
||||
public function getResult($query, $data)
|
||||
{
|
||||
return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'export.Sum aside activities duration';
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return Declarations::ASIDE_ACTIVITY_TYPE;
|
||||
}
|
||||
|
||||
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
|
||||
{
|
||||
$qb = $this->repository
|
||||
->createQueryBuilder('aside');
|
||||
|
||||
$qb->select('SUM(aside.duration) as export_sum_aside_activity_duration')
|
||||
->andWhere($qb->expr()->isNotNull('aside.duration'));
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function requiredRole(): string
|
||||
{
|
||||
return AsideActivityVoter::STATS;
|
||||
}
|
||||
|
||||
public function supportsModifiers(): array
|
||||
{
|
||||
return [Declarations::ASIDE_ACTIVITY_TYPE];
|
||||
}
|
||||
}
|
@ -80,8 +80,8 @@ class ByActivityTypeFilter implements FilterInterface
|
||||
public function describeAction($data, $format = 'string'): array
|
||||
{
|
||||
$types = array_map(
|
||||
fn (AsideActivityCategory $t): string => $this->translatableStringHelper->localize($t->getName()),
|
||||
$this->asideActivityTypeRepository->findBy(['id' => $data['types']->toArray()])
|
||||
fn (AsideActivityCategory $t): string => $this->translatableStringHelper->localize($t->getTitle()),
|
||||
$data['types']->toArray()
|
||||
);
|
||||
|
||||
return ['export.filter.Filtered by aside activity type: only %type%', [
|
||||
|
@ -46,25 +46,18 @@ class ByDateFilter implements FilterInterface
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data)
|
||||
{
|
||||
$where = $qb->getDQLPart('where');
|
||||
$clause = $qb->expr()->between(
|
||||
'aside.date',
|
||||
':date_from',
|
||||
':date_to'
|
||||
);
|
||||
|
||||
if ($where instanceof Andx) {
|
||||
$where->add($clause);
|
||||
} else {
|
||||
$where = $qb->expr()->andX($clause);
|
||||
}
|
||||
$qb->andWhere($clause);
|
||||
|
||||
$qb->add('where', $where);
|
||||
$qb->setParameter(
|
||||
'date_from',
|
||||
$this->rollingDateConverter->convert($data['date_from'])
|
||||
);
|
||||
$qb->setParameter(
|
||||
)->setParameter(
|
||||
'date_to',
|
||||
$this->rollingDateConverter->convert($data['date_to'])
|
||||
);
|
||||
|
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\AsideActivityBundle\Export\Filter;
|
||||
|
||||
use Chill\AsideActivityBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||
use Chill\MainBundle\Templating\Entity\UserRender;
|
||||
use Doctrine\ORM\Query\Expr\Andx;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class ByUserFilter implements FilterInterface
|
||||
{
|
||||
private UserRender $userRender;
|
||||
|
||||
public function __construct(UserRender $userRender)
|
||||
{
|
||||
$this->userRender = $userRender;
|
||||
}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data)
|
||||
{
|
||||
$clause = $qb->expr()->in('aside.agent', ':users');
|
||||
|
||||
$qb
|
||||
->andWhere($clause)
|
||||
->setParameter('users', $data['accepted_users']);
|
||||
}
|
||||
|
||||
public function applyOn(): string
|
||||
{
|
||||
return Declarations::ASIDE_ACTIVITY_TYPE;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('accepted_users', PickUserDynamicType::class, [
|
||||
'multiple' => true,
|
||||
'label' => 'Creators',
|
||||
]);
|
||||
}
|
||||
|
||||
public function describeAction($data, $format = 'string'): array
|
||||
{
|
||||
$users = [];
|
||||
|
||||
foreach ($data['accepted_users'] as $u) {
|
||||
$users[] = $this->userRender->renderString($u, []);
|
||||
}
|
||||
|
||||
return ['export.filter.Filtered aside activity by user: only %users%', [
|
||||
'%users%' => implode(', ', $users),
|
||||
]];
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'export.filter.Filter aside activity by user';
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\AsideActivityBundle\Export\Filter;
|
||||
|
||||
use Chill\AsideActivityBundle\Entity\AsideActivity;
|
||||
use Chill\AsideActivityBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Entity\UserJob;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class ByUserJobFilter implements FilterInterface
|
||||
{
|
||||
private TranslatableStringHelperInterface $translatableStringHelper;
|
||||
|
||||
public function __construct(TranslatableStringHelperInterface $translatableStringHelper)
|
||||
{
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data)
|
||||
{
|
||||
$qb
|
||||
->andWhere(
|
||||
$qb->expr()->exists(
|
||||
'SELECT 1 FROM ' . AsideActivity::class . ' aside_activity_user_job_filter_act
|
||||
JOIN aside_activity_user_job_filter_act.agent aside_activity_user_job_filter_user WHERE aside_activity_user_job_filter_user.userJob IN (:aside_activity_user_job_filter_jobs) AND aside_activity_user_job_filter_act = aside'
|
||||
)
|
||||
)
|
||||
->setParameter('aside_activity_user_job_filter_jobs', $data['jobs']);
|
||||
}
|
||||
|
||||
public function applyOn()
|
||||
{
|
||||
return Declarations::ASIDE_ACTIVITY_TYPE;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('jobs', EntityType::class, [
|
||||
'class' => UserJob::class,
|
||||
'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()),
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function describeAction($data, $format = 'string')
|
||||
{
|
||||
return ['export.filter.Filtered aside activities by user jobs: only %jobs%', [
|
||||
'%jobs%' => implode(
|
||||
', ',
|
||||
array_map(
|
||||
fn (UserJob $job) => $this->translatableStringHelper->localize($job->getLabel()),
|
||||
$data['jobs']->toArray()
|
||||
)
|
||||
),
|
||||
]];
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return 'export.filter.Filter by user jobs';
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\AsideActivityBundle\Export\Filter;
|
||||
|
||||
use Chill\AsideActivityBundle\Entity\AsideActivity;
|
||||
use Chill\AsideActivityBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class ByUserScopeFilter implements FilterInterface
|
||||
{
|
||||
private ScopeRepositoryInterface $scopeRepository;
|
||||
|
||||
private TranslatableStringHelperInterface $translatableStringHelper;
|
||||
|
||||
public function __construct(
|
||||
ScopeRepositoryInterface $scopeRepository,
|
||||
TranslatableStringHelperInterface $translatableStringHelper
|
||||
) {
|
||||
$this->scopeRepository = $scopeRepository;
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function alterQuery(QueryBuilder $qb, $data)
|
||||
{
|
||||
$qb
|
||||
->andWhere(
|
||||
$qb->expr()->exists(
|
||||
'SELECT 1 FROM ' . AsideActivity::class . ' aside_activity_user_scope_filter_act
|
||||
JOIN aside_activity_user_scope_filter_act.agent aside_activity_user_scope_filter_user WHERE aside_activity_user_scope_filter_user.mainScope IN (:aside_activity_user_scope_filter_scopes) AND aside_activity_user_scope_filter_act = aside '
|
||||
)
|
||||
)
|
||||
->setParameter('aside_activity_user_scope_filter_scopes', $data['scopes']);
|
||||
}
|
||||
|
||||
public function applyOn()
|
||||
{
|
||||
return Declarations::ASIDE_ACTIVITY_TYPE;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('scopes', EntityType::class, [
|
||||
'class' => Scope::class,
|
||||
'choices' => $this->scopeRepository->findAllActive(),
|
||||
'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()),
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function describeAction($data, $format = 'string')
|
||||
{
|
||||
return ['export.filter.Filtered aside activities by user scope: only %scopes%', [
|
||||
'%scopes%' => implode(
|
||||
', ',
|
||||
array_map(
|
||||
fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()),
|
||||
$data['scopes']->toArray()
|
||||
)
|
||||
),
|
||||
]];
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return 'export.filter.Filter by user scope';
|
||||
}
|
||||
}
|
@ -20,33 +20,3 @@ services:
|
||||
resource: "../Controller"
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
|
||||
## Exports
|
||||
|
||||
# indicators
|
||||
Chill\AsideActivityBundle\Export\Export\CountAsideActivity:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
tags:
|
||||
- { name: chill.export, alias: count_asideactivity }
|
||||
|
||||
# filters
|
||||
Chill\AsideActivityBundle\Export\Filter\ByDateFilter:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
tags:
|
||||
- { name: chill.export_filter, alias: asideactivity_bydate_filter }
|
||||
|
||||
Chill\AsideActivityBundle\Export\Filter\ByActivityTypeFilter:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
tags:
|
||||
- { name: chill.export_filter, alias: asideactivity_activitytype_filter }
|
||||
|
||||
# aggregators
|
||||
Chill\AsideActivityBundle\Export\Aggregator\ByActivityTypeAggregator:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
tags:
|
||||
- { name: chill.export_aggregator, alias: asideactivity_activitytype_aggregator }
|
||||
|
@ -3,11 +3,23 @@ services:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\AsideActivityBundle\Export\Export\ListAsideActivity:
|
||||
tags:
|
||||
- { name: chill.export, alias: 'list_aside_activity' }
|
||||
|
||||
## Indicators
|
||||
Chill\AsideActivityBundle\Export\Export\CountAsideActivity:
|
||||
tags:
|
||||
- { name: chill.export, alias: 'count_aside_activity' }
|
||||
|
||||
Chill\AsideActivityBundle\Export\Export\SumAsideActivityDuration:
|
||||
tags:
|
||||
- { name: chill.export, alias: 'sum_aside_activity_duration' }
|
||||
|
||||
Chill\AsideActivityBundle\Export\Export\AvgAsideActivityDuration:
|
||||
tags:
|
||||
- { name: chill.export, alias: 'avg_aside_activity_duration' }
|
||||
|
||||
## Filters
|
||||
chill.aside_activity.export.date_filter:
|
||||
class: Chill\AsideActivityBundle\Export\Filter\ByDateFilter
|
||||
@ -19,9 +31,34 @@ services:
|
||||
tags:
|
||||
- { name: chill.export_filter, alias: 'aside_activity_type_filter' }
|
||||
|
||||
chill.aside_activity.export.user_job_filter:
|
||||
class: Chill\AsideActivityBundle\Export\Filter\ByUserJobFilter
|
||||
tags:
|
||||
- { name: chill.export_filter, alias: 'aside_activity_user_job_filter' }
|
||||
|
||||
chill.aside_activity.export.user_scope_filter:
|
||||
class: Chill\AsideActivityBundle\Export\Filter\ByUserScopeFilter
|
||||
tags:
|
||||
- { name: chill.export_filter, alias: 'aside_activity_user_scope_filter' }
|
||||
|
||||
chill.aside_activity.export.user_filter:
|
||||
class: Chill\AsideActivityBundle\Export\Filter\ByUserFilter
|
||||
tags:
|
||||
- { name: chill.export_filter, alias: 'aside_activity_user_filter' }
|
||||
|
||||
## Aggregators
|
||||
|
||||
chill.aside_activity.export.type_aggregator:
|
||||
class: Chill\AsideActivityBundle\Export\Aggregator\ByActivityTypeAggregator
|
||||
tags:
|
||||
- { name: chill.export_aggregator, alias: activity_type_aggregator }
|
||||
- { name: chill.export_aggregator, alias: activity_type_aggregator }
|
||||
|
||||
chill.aside_activity.export.user_job_aggregator:
|
||||
class: Chill\AsideActivityBundle\Export\Aggregator\ByUserJobAggregator
|
||||
tags:
|
||||
- { name: chill.export_aggregator, alias: aside_activity_user_job_aggregator }
|
||||
|
||||
chill.aside_activity.export.user_scope_aggregator:
|
||||
class: Chill\AsideActivityBundle\Export\Aggregator\ByUserScopeAggregator
|
||||
tags:
|
||||
- { name: chill.export_aggregator, alias: aside_activity_user_scope_aggregator }
|
||||
|
@ -29,16 +29,16 @@ location: Lieu
|
||||
|
||||
# Crud
|
||||
crud:
|
||||
aside_activity:
|
||||
title_view: Détail de l'activité annexe
|
||||
title_new: Nouvelle activité annexe
|
||||
title_edit: Édition d'une activité annexe
|
||||
title_delete: Supprimer une activité annexe
|
||||
button_delete: Supprimer
|
||||
confirm_message_delete: Êtes-vous sûr de vouloir supprimer cette activité annexe?
|
||||
aside_activity_category:
|
||||
title_new: Nouvelle catégorie d'activité annexe
|
||||
title_edit: Édition d'une catégorie de type d'activité
|
||||
aside_activity:
|
||||
title_view: Détail de l'activité annexe
|
||||
title_new: Nouvelle activité annexe
|
||||
title_edit: Édition d'une activité annexe
|
||||
title_delete: Supprimer une activité annexe
|
||||
button_delete: Supprimer
|
||||
confirm_message_delete: Êtes-vous sûr de vouloir supprimer cette activité annexe?
|
||||
aside_activity_category:
|
||||
title_new: Nouvelle catégorie d'activité annexe
|
||||
title_edit: Édition d'une catégorie de type d'activité
|
||||
|
||||
#forms
|
||||
Create a new aside activity type: Nouvelle categorie d'activité annexe
|
||||
@ -169,19 +169,43 @@ Aside activity configuration: Configuration des activités annexes
|
||||
|
||||
# exports
|
||||
export:
|
||||
Exports of aside activities: Exports des activités annexes
|
||||
Count aside activities: Nombre d'activités annexes
|
||||
Count aside activities by various parameters.: Compte le nombre d'activités annexes selon divers critères
|
||||
filter:
|
||||
Filter by aside activity date: Filtrer les activités annexes par date
|
||||
Filter by aside activity type: Filtrer les activités annexes par type d'activité
|
||||
'Filtered by aside activity type: only %type%': "Filtré par type d'activité annexe: uniquement %type%"
|
||||
This date should be after the date given in "Implied in an aside activity after this date" field: Cette date devrait être postérieure à la date donnée dans le champ "activités annexes après cette date"
|
||||
Aside activities after this date: Actvitités annexes après cette date
|
||||
Aside activities before this date: Actvitités annexes avant cette date
|
||||
aggregator:
|
||||
Group by aside activity type: Grouper les activités annexes par type d'activité
|
||||
Aside activity type: Type d'activité annexe
|
||||
aside_activity:
|
||||
List of aside activities: Liste des activités annexes
|
||||
createdAt: Création
|
||||
updatedAt: Dernière mise à jour
|
||||
agent_id: Utilisateur
|
||||
creator_id: Créateur
|
||||
main_scope: Service principal de l'utilisateur
|
||||
main_center: Centre principal de l'utilisteur
|
||||
aside_activity_type: Catégorie d'activité annexe
|
||||
date: Date
|
||||
duration: Durée
|
||||
note: Note
|
||||
|
||||
Exports of aside activities: Exports des activités annexes
|
||||
Count aside activities: Nombre d'activités annexes
|
||||
Count aside activities by various parameters.: Compte le nombre d'activités annexes selon divers critères
|
||||
Average aside activities duration: Durée moyenne des activités annexes
|
||||
Sum aside activities duration: Durée des activités annexes
|
||||
filter:
|
||||
Filter by aside activity date: Filtrer les activités annexes par date
|
||||
Filter by aside activity type: Filtrer les activités annexes par type d'activité
|
||||
'Filtered by aside activity type: only %type%': "Filtré par type d'activité annexe: uniquement %type%"
|
||||
Filtered by aside activities between %dateFrom% and %dateTo%: Filtré par date d'activité annexe, entre %dateFrom% et %dateTo%
|
||||
This date should be after the date given in "Implied in an aside activity after this date" field: Cette date devrait être postérieure à la date donnée dans le champ "activités annexes après cette date"
|
||||
Aside activities after this date: Actvitités annexes après cette date
|
||||
Aside activities before this date: Actvitités annexes avant cette date
|
||||
'Filtered aside activity by user: only %users%': "Filtré par utilisateur: uniquement %users%"
|
||||
Filter aside activity by user: Filtrer par utilisateur
|
||||
'Filtered aside activities by user jobs: only %jobs%': "Filtré par métier des utilisateurs: uniquement %jobs%"
|
||||
Filter by user jobs: Filtrer les activités annexes par métier des utilisateurs
|
||||
'Filtered aside activities by user scope: only %scopes%': "Filtré par service des utilisateur: uniquement %scopes%"
|
||||
Filter by user scope: Filtrer les activités annexes par service d'utilisateur
|
||||
aggregator:
|
||||
Group by aside activity type: Grouper les activités annexes par type d'activité
|
||||
Aside activity type: Type d'activité annexe
|
||||
Aggregate by user job: Grouper les activités annexes par métier des utilisateurs
|
||||
Aggregate by user scope: Grouper les activités annexes par service des utilisateurs
|
||||
|
||||
# ROLES
|
||||
CHILL_ASIDE_ACTIVITY_STATS: Statistiques pour les activités annexes
|
||||
|
@ -1,102 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\BudgetBundle\Config;
|
||||
|
||||
class ConfigRepository
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $charges;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $resources;
|
||||
|
||||
public function __construct($resources, $charges)
|
||||
{
|
||||
$this->resources = $resources;
|
||||
$this->charges = $charges;
|
||||
}
|
||||
|
||||
public function getChargesKeys(bool $onlyActive = false): array
|
||||
{
|
||||
return array_map(static function ($element) {
|
||||
return $element['key'];
|
||||
}, $this->getCharges($onlyActive));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array where keys are the resource'key and label the ressource label
|
||||
*/
|
||||
public function getChargesLabels(bool $onlyActive = false)
|
||||
{
|
||||
$charges = [];
|
||||
|
||||
foreach ($this->getCharges($onlyActive) as $definition) {
|
||||
$charges[$definition['key']] = $this->normalizeLabel($definition['labels']);
|
||||
}
|
||||
|
||||
return $charges;
|
||||
}
|
||||
|
||||
public function getResourcesKeys(bool $onlyActive = false): array
|
||||
{
|
||||
return array_map(static function ($element) {
|
||||
return $element['key'];
|
||||
}, $this->getResources($onlyActive));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array where keys are the resource'key and label the ressource label
|
||||
*/
|
||||
public function getResourcesLabels(bool $onlyActive = false)
|
||||
{
|
||||
$resources = [];
|
||||
|
||||
foreach ($this->getResources($onlyActive) as $definition) {
|
||||
$resources[$definition['key']] = $this->normalizeLabel($definition['labels']);
|
||||
}
|
||||
|
||||
return $resources;
|
||||
}
|
||||
|
||||
private function getCharges(bool $onlyActive = false): array
|
||||
{
|
||||
return $onlyActive ?
|
||||
array_filter($this->charges, static function ($el) {
|
||||
return $el['active'];
|
||||
})
|
||||
: $this->charges;
|
||||
}
|
||||
|
||||
private function getResources(bool $onlyActive = false): array
|
||||
{
|
||||
return $onlyActive ?
|
||||
array_filter($this->resources, static function ($el) {
|
||||
return $el['active'];
|
||||
})
|
||||
: $this->resources;
|
||||
}
|
||||
|
||||
private function normalizeLabel($labels)
|
||||
{
|
||||
$normalizedLabels = [];
|
||||
|
||||
foreach ($labels as $labelDefinition) {
|
||||
$normalizedLabels[$labelDefinition['lang']] = $labelDefinition['label'];
|
||||
}
|
||||
|
||||
return $normalizedLabels;
|
||||
}
|
||||
}
|
@ -35,7 +35,6 @@ class ChillBudgetExtension extends Extension implements PrependExtensionInterfac
|
||||
$config = $this->processConfiguration($configuration, $configs);
|
||||
|
||||
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../config'));
|
||||
$loader->load('services/config.yaml');
|
||||
$loader->load('services/form.yaml');
|
||||
$loader->load('services/repository.yaml');
|
||||
$loader->load('services/security.yaml');
|
||||
|
@ -75,7 +75,7 @@ abstract class AbstractElement
|
||||
/**
|
||||
* @ORM\Column(name="type", type="string", length=255)
|
||||
*/
|
||||
private string $type;
|
||||
private string $type = '';
|
||||
|
||||
/*Getters and Setters */
|
||||
|
||||
|
@ -12,12 +12,17 @@ declare(strict_types=1);
|
||||
namespace Chill\BudgetBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* Type of charge.
|
||||
*
|
||||
* @ORM\Table(name="chill_budget.charge_type")
|
||||
* @ORM\Table(name="chill_budget.charge_type",
|
||||
* uniqueConstraints={@ORM\UniqueConstraint(name="charge_kind_unique_type_idx", fields={"kind"})}
|
||||
* )
|
||||
* @ORM\Entity
|
||||
* @UniqueEntity(fields={"kind"})
|
||||
*/
|
||||
class ChargeKind
|
||||
{
|
||||
@ -35,6 +40,8 @@ class ChargeKind
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255, options={"default": ""}, nullable=false)
|
||||
* @Assert\Regex(pattern="/^[a-z0-9\-_]{1,}$/", message="budget.admin.form.kind.only_alphanumeric")
|
||||
* @Assert\Length(min=3)
|
||||
*/
|
||||
private string $kind = '';
|
||||
|
||||
|
@ -12,12 +12,17 @@ declare(strict_types=1);
|
||||
namespace Chill\BudgetBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* Type of resource.
|
||||
*
|
||||
* @ORM\Table(name="chill_budget.resource_type")
|
||||
* @ORM\Table(name="chill_budget.resource_type", uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(name="resource_kind_unique_type_idx", fields={"kind"})
|
||||
* })
|
||||
* @ORM\Entity
|
||||
* @UniqueEntity(fields={"kind"})
|
||||
*/
|
||||
class ResourceKind
|
||||
{
|
||||
@ -35,6 +40,8 @@ class ResourceKind
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255, nullable=false, options={"default": ""})
|
||||
* @Assert\Regex(pattern="/^[a-z0-9\-_]{1,}$/", message="budget.admin.form.kind.only_alphanumeric")
|
||||
* @Assert\Length(min=3)
|
||||
*/
|
||||
private string $kind = '';
|
||||
|
||||
|
@ -16,6 +16,7 @@ use Chill\MainBundle\Form\Type\TranslatableStringFormType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
@ -25,7 +26,11 @@ class ChargeKindType extends AbstractType
|
||||
{
|
||||
$builder
|
||||
->add('name', TranslatableStringFormType::class, [
|
||||
'label' => 'Nom',
|
||||
'label' => 'Title',
|
||||
])
|
||||
->add('kind', TextType::class, [
|
||||
'label' => 'budget.admin.form.Charge_kind_key',
|
||||
'help' => 'budget.admin.form.This kind must contains only alphabeticals characters, and dashes. This string is in use during document generation. Changes may have side effect on document'
|
||||
])
|
||||
->add('ordering', NumberType::class)
|
||||
->add('isActive', CheckboxType::class, [
|
||||
|
@ -16,6 +16,7 @@ use Chill\MainBundle\Form\Type\TranslatableStringFormType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
@ -25,7 +26,11 @@ class ResourceKindType extends AbstractType
|
||||
{
|
||||
$builder
|
||||
->add('name', TranslatableStringFormType::class, [
|
||||
'label' => 'Nom',
|
||||
'label' => 'Title',
|
||||
])
|
||||
->add('kind', TextType::class, [
|
||||
'label' => 'budget.admin.form.Resource_kind_key',
|
||||
'help' => 'budget.admin.form.This kind must contains only alphabeticals characters, and dashes. This string is in use during document generation. Changes may have side effect on document'
|
||||
])
|
||||
->add('ordering', NumberType::class)
|
||||
->add('isActive', CheckboxType::class, [
|
||||
|
@ -11,7 +11,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\BudgetBundle\Form;
|
||||
|
||||
use Chill\BudgetBundle\Config\ConfigRepository;
|
||||
use Chill\BudgetBundle\Entity\Charge;
|
||||
use Chill\BudgetBundle\Entity\ChargeKind;
|
||||
use Chill\BudgetBundle\Repository\ChargeKindRepository;
|
||||
@ -25,13 +24,9 @@ use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use function array_flip;
|
||||
use function asort;
|
||||
|
||||
class ChargeType extends AbstractType
|
||||
{
|
||||
protected ConfigRepository $configRepository;
|
||||
|
||||
protected TranslatableStringHelperInterface $translatableStringHelper;
|
||||
|
||||
private ChargeKindRepository $repository;
|
||||
@ -39,12 +34,10 @@ class ChargeType extends AbstractType
|
||||
private TranslatorInterface $translator;
|
||||
|
||||
public function __construct(
|
||||
ConfigRepository $configRepository,
|
||||
TranslatableStringHelperInterface $translatableStringHelper,
|
||||
ChargeKindRepository $repository,
|
||||
TranslatorInterface $translator
|
||||
) {
|
||||
$this->configRepository = $configRepository;
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
$this->repository = $repository;
|
||||
$this->translator = $translator;
|
||||
@ -116,19 +109,4 @@ class ChargeType extends AbstractType
|
||||
{
|
||||
return 'chill_budgetbundle_charge';
|
||||
}
|
||||
|
||||
private function getTypes()
|
||||
{
|
||||
$charges = $this->configRepository
|
||||
->getChargesLabels(true);
|
||||
|
||||
// rewrite labels to filter in language
|
||||
foreach ($charges as $key => $labels) {
|
||||
$charges[$key] = $this->translatableStringHelper->localize($labels);
|
||||
}
|
||||
|
||||
asort($charges);
|
||||
|
||||
return array_flip($charges);
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\BudgetBundle\Form;
|
||||
|
||||
use Chill\BudgetBundle\Config\ConfigRepository;
|
||||
use Chill\BudgetBundle\Entity\Resource;
|
||||
use Chill\BudgetBundle\Entity\ResourceKind;
|
||||
use Chill\BudgetBundle\Repository\ResourceKindRepository;
|
||||
@ -24,12 +23,9 @@ use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use function array_flip;
|
||||
|
||||
class ResourceType extends AbstractType
|
||||
{
|
||||
protected ConfigRepository $configRepository;
|
||||
|
||||
protected TranslatableStringHelperInterface $translatableStringHelper;
|
||||
|
||||
private ResourceKindRepository $repository;
|
||||
@ -37,12 +33,10 @@ class ResourceType extends AbstractType
|
||||
private TranslatorInterface $translator;
|
||||
|
||||
public function __construct(
|
||||
ConfigRepository $configRepository,
|
||||
TranslatableStringHelperInterface $translatableStringHelper,
|
||||
ResourceKindRepository $repository,
|
||||
TranslatorInterface $translator
|
||||
) {
|
||||
$this->configRepository = $configRepository;
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
$this->repository = $repository;
|
||||
$this->translator = $translator;
|
||||
@ -98,19 +92,4 @@ class ResourceType extends AbstractType
|
||||
{
|
||||
return 'chill_budgetbundle_resource';
|
||||
}
|
||||
|
||||
private function getTypes()
|
||||
{
|
||||
$resources = $this->configRepository
|
||||
->getResourcesLabels(true);
|
||||
|
||||
// rewrite labels to filter in language
|
||||
foreach ($resources as $key => $labels) {
|
||||
$resources[$key] = $this->translatableStringHelper->localize($labels);
|
||||
}
|
||||
|
||||
asort($resources);
|
||||
|
||||
return array_flip($resources);
|
||||
}
|
||||
}
|
||||
|
@ -14,9 +14,8 @@ namespace Chill\BudgetBundle\Repository;
|
||||
use Chill\BudgetBundle\Entity\ChargeKind;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
class ChargeKindRepository implements ObjectRepository
|
||||
final class ChargeKindRepository implements ChargeKindRepositoryInterface
|
||||
{
|
||||
private EntityRepository $repository;
|
||||
|
||||
@ -50,7 +49,8 @@ class ChargeKindRepository implements ObjectRepository
|
||||
->where($qb->expr()->eq('c.isActive', 'true'))
|
||||
->orderBy('c.ordering', 'ASC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -77,6 +77,11 @@ class ChargeKindRepository implements ObjectRepository
|
||||
return $this->repository->findOneBy($criteria);
|
||||
}
|
||||
|
||||
public function findOneByKind(string $kind): ?ChargeKind
|
||||
{
|
||||
return $this->repository->findOneBy(['kind' => $kind]);
|
||||
}
|
||||
|
||||
public function getClassName(): string
|
||||
{
|
||||
return ChargeKind::class;
|
||||
|
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\BudgetBundle\Repository;
|
||||
|
||||
use Chill\BudgetBundle\Entity\ChargeKind;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
interface ChargeKindRepositoryInterface extends ObjectRepository
|
||||
{
|
||||
public function find($id): ?ChargeKind;
|
||||
|
||||
/**
|
||||
* @return ChargeType[]
|
||||
*/
|
||||
public function findAll(): array;
|
||||
|
||||
/**
|
||||
* @return ChargeType[]
|
||||
*/
|
||||
public function findAllActive(): array;
|
||||
|
||||
public function findOneByKind(string $kind): ?ChargeKind;
|
||||
|
||||
/**
|
||||
* @return ChargeType[]
|
||||
*/
|
||||
public function findAllByType(string $type): array;
|
||||
|
||||
/**
|
||||
* @param mixed|null $limit
|
||||
* @param mixed|null $offset
|
||||
*
|
||||
* @return ChargeType[]
|
||||
*/
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;
|
||||
|
||||
public function findOneBy(array $criteria): ?ChargeKind;
|
||||
|
||||
public function getClassName(): string;
|
||||
}
|
@ -14,9 +14,8 @@ namespace Chill\BudgetBundle\Repository;
|
||||
use Chill\BudgetBundle\Entity\ResourceKind;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
class ResourceKindRepository implements ObjectRepository
|
||||
final class ResourceKindRepository implements ResourceKindRepositoryInterface
|
||||
{
|
||||
private EntityRepository $repository;
|
||||
|
||||
@ -50,7 +49,8 @@ class ResourceKindRepository implements ObjectRepository
|
||||
->where($qb->expr()->eq('r.isActive', 'true'))
|
||||
->orderBy('r.ordering', 'ASC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -77,6 +77,11 @@ class ResourceKindRepository implements ObjectRepository
|
||||
return $this->repository->findOneBy($criteria);
|
||||
}
|
||||
|
||||
public function findOneByKind(string $kind): ?ResourceKind
|
||||
{
|
||||
return $this->repository->findOneBy(['kind' => $kind]);
|
||||
}
|
||||
|
||||
public function getClassName(): string
|
||||
{
|
||||
return ResourceKind::class;
|
||||
|
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\BudgetBundle\Repository;
|
||||
|
||||
use Chill\BudgetBundle\Entity\ResourceKind;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
interface ResourceKindRepositoryInterface extends ObjectRepository
|
||||
{
|
||||
public function find($id): ?ResourceKind;
|
||||
|
||||
/**
|
||||
* @return ResourceType[]
|
||||
*/
|
||||
public function findAll(): array;
|
||||
|
||||
/**
|
||||
* @return ResourceType[]
|
||||
*/
|
||||
public function findAllActive(): array;
|
||||
|
||||
/**
|
||||
* @return ResourceType[]
|
||||
*/
|
||||
public function findAllByType(string $type): array;
|
||||
|
||||
/**
|
||||
* @param mixed|null $limit
|
||||
* @param mixed|null $offset
|
||||
*
|
||||
* @return ResourceType[]
|
||||
*/
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;
|
||||
|
||||
public function findOneBy(array $criteria): ?ResourceKind;
|
||||
|
||||
public function findOneByKind(string $kind): ?ResourceKind;
|
||||
|
||||
public function getClassName(): string;
|
||||
}
|
@ -34,7 +34,7 @@ class ResourceRepository extends EntityRepository
|
||||
//->andWhere('c.startDate < :date')
|
||||
// TODO: there is a misconception here, the end date must be lower or null. startDate are never null
|
||||
//->andWhere('c.startDate < :date OR c.startDate IS NULL');
|
||||
;
|
||||
;
|
||||
|
||||
if (null !== $sort) {
|
||||
$qb->orderBy($sort);
|
||||
|
@ -19,7 +19,10 @@
|
||||
{% for entity in entities %}
|
||||
<tr>
|
||||
<td>{{ entity.ordering }}</td>
|
||||
<td>{{ entity|chill_entity_render_box }}</td>
|
||||
<td>
|
||||
{{ entity|chill_entity_render_box }}<br/>
|
||||
<strong>{{ 'budget.admin.form.Charge_kind_key'|trans }} :</strong> <code>{{ entity.kind }}</code>
|
||||
</td>
|
||||
<td style="text-align:center;">
|
||||
{%- if entity.isActive -%}
|
||||
<i class="fa fa-check-square-o"></i>
|
||||
@ -39,6 +42,8 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{ chill_pagination(paginator) }}
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li>
|
||||
<a href="{{ path('chill_crud_charge_kind_new') }}" class="btn btn-create">
|
||||
|
@ -19,7 +19,10 @@
|
||||
{% for entity in entities %}
|
||||
<tr>
|
||||
<td>{{ entity.ordering }}</td>
|
||||
<td>{{ entity|chill_entity_render_box }}</td>
|
||||
<td>
|
||||
{{ entity|chill_entity_render_box }}<br/>
|
||||
<strong>{{ 'budget.admin.form.Resource_kind_key'|trans }} :</strong> <code>{{ entity.kind }}</code>
|
||||
</td>
|
||||
<td style="text-align:center;">
|
||||
{%- if entity.isActive -%}
|
||||
<i class="fa fa-check-square-o"></i>
|
||||
@ -39,6 +42,8 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{ chill_pagination(paginator) }}
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li>
|
||||
<a href="{{ path('chill_crud_resource_kind_new') }}" class="btn btn-create">
|
||||
|
@ -11,45 +11,52 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\BudgetBundle\Service\Summary;
|
||||
|
||||
use Chill\BudgetBundle\Config\ConfigRepository;
|
||||
use Chill\BudgetBundle\Entity\ChargeKind;
|
||||
use Chill\BudgetBundle\Entity\ResourceKind;
|
||||
use Chill\BudgetBundle\Repository\ChargeKindRepository;
|
||||
use Chill\BudgetBundle\Repository\ChargeKindRepositoryInterface;
|
||||
use Chill\BudgetBundle\Repository\ResourceKindRepository;
|
||||
use Chill\BudgetBundle\Repository\ResourceKindRepositoryInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Chill\PersonBundle\Entity\Household\Household;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use LogicException;
|
||||
use RuntimeException;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* Helps to find a summary of the budget: the sum of resources and charges.
|
||||
*/
|
||||
class SummaryBudget implements SummaryBudgetInterface
|
||||
final class SummaryBudget implements SummaryBudgetInterface
|
||||
{
|
||||
private const QUERY_CHARGE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, type FROM chill_budget.charge WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
|
||||
private const QUERY_CHARGE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, charge_id AS kind_id FROM chill_budget.charge WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY charge_id';
|
||||
|
||||
private const QUERY_CHARGE_BY_PERSON = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, type FROM chill_budget.charge WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
|
||||
private const QUERY_CHARGE_BY_PERSON = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, charge_id AS kind_id FROM chill_budget.charge WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY charge_id';
|
||||
|
||||
private const QUERY_RESOURCE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, type FROM chill_budget.resource WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
|
||||
private const QUERY_RESOURCE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, resource_id AS kind_id FROM chill_budget.resource WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY resource_id';
|
||||
|
||||
private const QUERY_RESOURCE_BY_PERSON = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, type FROM chill_budget.resource WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
|
||||
private const QUERY_RESOURCE_BY_PERSON = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, resource_id AS kind_id FROM chill_budget.resource WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY resource_id';
|
||||
|
||||
private array $chargeLabels;
|
||||
|
||||
private ConfigRepository $configRepository;
|
||||
private ChargeKindRepositoryInterface $chargeKindRepository;
|
||||
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private array $resourcesLabels;
|
||||
private ResourceKindRepositoryInterface $resourceKindRepository;
|
||||
|
||||
private TranslatableStringHelperInterface $translatableStringHelper;
|
||||
|
||||
public function __construct(EntityManagerInterface $em, ConfigRepository $configRepository, TranslatableStringHelperInterface $translatableStringHelper)
|
||||
{
|
||||
public function __construct(
|
||||
EntityManagerInterface $em,
|
||||
TranslatableStringHelperInterface $translatableStringHelper,
|
||||
ResourceKindRepositoryInterface $resourceKindRepository,
|
||||
ChargeKindRepositoryInterface $chargeKindRepository
|
||||
) {
|
||||
$this->em = $em;
|
||||
$this->configRepository = $configRepository;
|
||||
$this->chargeLabels = $configRepository->getChargesLabels();
|
||||
$this->resourcesLabels = $configRepository->getResourcesLabels();
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
$this->resourceKindRepository = $resourceKindRepository;
|
||||
$this->chargeKindRepository = $chargeKindRepository;
|
||||
}
|
||||
|
||||
public function getSummaryForHousehold(?Household $household): array
|
||||
@ -112,7 +119,7 @@ class SummaryBudget implements SummaryBudgetInterface
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm
|
||||
->addScalarResult('sum', 'sum')
|
||||
->addScalarResult('type', 'type')
|
||||
->addScalarResult('kind_id', 'kind_id')
|
||||
->addScalarResult('comment', 'comment');
|
||||
|
||||
return $rsm;
|
||||
@ -120,51 +127,62 @@ class SummaryBudget implements SummaryBudgetInterface
|
||||
|
||||
private function getEmptyChargeArray(): array
|
||||
{
|
||||
$keys = $this->configRepository->getChargesKeys();
|
||||
$labels = $this->chargeLabels;
|
||||
$keys = array_map(static fn (ChargeKind $kind) => $kind->getKind(), $this->chargeKindRepository->findAll());
|
||||
|
||||
return array_combine($keys, array_map(function ($i) use ($labels) {
|
||||
return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($labels[$i]), 'comment' => ''];
|
||||
return array_combine($keys, array_map(function ($kind) {
|
||||
return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($this->chargeKindRepository->findOneByKind($kind)->getName()), 'comment' => ''];
|
||||
}, $keys));
|
||||
}
|
||||
|
||||
private function getEmptyResourceArray(): array
|
||||
{
|
||||
$keys = $this->configRepository->getResourcesKeys();
|
||||
$labels = $this->resourcesLabels;
|
||||
$keys = array_map(static fn (ResourceKind $kind) => $kind->getKind(), $this->resourceKindRepository->findAll());
|
||||
|
||||
return array_combine($keys, array_map(function ($i) use ($labels) {
|
||||
return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($labels[$i]), 'comment' => ''];
|
||||
return array_combine($keys, array_map(function ($kind) {
|
||||
return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($this->resourceKindRepository->findOneByKind($kind)->getName()), 'comment' => ''];
|
||||
}, $keys));
|
||||
}
|
||||
|
||||
private function rowToArray(array $rows, string $kind): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
switch ($kind) {
|
||||
case 'charge':
|
||||
$label = $this->chargeLabels;
|
||||
foreach ($rows as $row) {
|
||||
$chargeKind = $this->chargeKindRepository->find($row['kind_id']);
|
||||
|
||||
break;
|
||||
if (null === $chargeKind) {
|
||||
throw new RuntimeException('charge kind not found: ' . $row['kind_id']);
|
||||
}
|
||||
$result[$chargeKind->getKind()] = [
|
||||
'sum' => (float) $row['sum'],
|
||||
'label' => $this->translatableStringHelper->localize($chargeKind->getName()),
|
||||
'comment' => (string) $row['comment'],
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
||||
case 'resource':
|
||||
$label = $this->resourcesLabels;
|
||||
foreach ($rows as $row) {
|
||||
$resourceKind = $this->resourceKindRepository->find($row['kind_id']);
|
||||
|
||||
break;
|
||||
if (null === $resourceKind) {
|
||||
throw new RuntimeException('charge kind not found: ' . $row['kind_id']);
|
||||
}
|
||||
|
||||
$result[$resourceKind->getKind()] = [
|
||||
'sum' => (float) $row['sum'],
|
||||
'label' => $this->translatableStringHelper->localize($resourceKind->getName()),
|
||||
'comment' => (string) $row['comment'],
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
||||
default:
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$result[$row['type']] = [
|
||||
'sum' => (float) $row['sum'],
|
||||
'label' => $this->translatableStringHelper->localize($label[$row['type']]),
|
||||
'comment' => (string) $row['comment'],
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
@ -1,65 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\BudgetBundle\Templating;
|
||||
|
||||
use Chill\BudgetBundle\Config\ConfigRepository;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
use UnexpectedValueException;
|
||||
|
||||
class Twig extends AbstractExtension
|
||||
{
|
||||
/**
|
||||
* @var ConfigRepository
|
||||
*/
|
||||
protected $configRepository;
|
||||
|
||||
/**
|
||||
* @var TranslatableStringHelper
|
||||
*/
|
||||
protected $translatableStringHelper;
|
||||
|
||||
public function __construct(
|
||||
ConfigRepository $configRepository,
|
||||
TranslatableStringHelper $translatableStringHelper
|
||||
) {
|
||||
$this->configRepository = $configRepository;
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
}
|
||||
|
||||
public function displayLink($link, $family)
|
||||
{
|
||||
switch ($family) {
|
||||
case 'resource':
|
||||
return $this->translatableStringHelper->localize(
|
||||
$this->configRepository->getResourcesLabels()[$link]
|
||||
);
|
||||
|
||||
case 'charge':
|
||||
return $this->translatableStringHelper->localize(
|
||||
$this->configRepository->getChargesLabels()[$link]
|
||||
);
|
||||
|
||||
default:
|
||||
throw new UnexpectedValueException("This family of element: {$family} is not "
|
||||
. "supported. Supported families are 'resource', 'charge'");
|
||||
}
|
||||
}
|
||||
|
||||
public function getFilters()
|
||||
{
|
||||
return [
|
||||
new TwigFilter('budget_element_type_display', [$this, 'displayLink'], ['is_safe' => ['html']]),
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\BudgetBundle\Tests\Service\Summary;
|
||||
|
||||
use Chill\BudgetBundle\Entity\ChargeKind;
|
||||
use Chill\BudgetBundle\Entity\ResourceKind;
|
||||
use Chill\BudgetBundle\Repository\ChargeKindRepositoryInterface;
|
||||
use Chill\BudgetBundle\Repository\ResourceKindRepositoryInterface;
|
||||
use Chill\BudgetBundle\Service\Summary\SummaryBudget;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Chill\PersonBundle\Entity\Household\Household;
|
||||
use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Doctrine\ORM\AbstractQuery;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Query;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
final class SummaryBudgetTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testGenerateSummaryForPerson(): void
|
||||
{
|
||||
$queryCharges = $this->prophesize(AbstractQuery::class);
|
||||
$queryCharges->getResult()->willReturn([
|
||||
[
|
||||
'sum' => 250.0,
|
||||
'comment' => '',
|
||||
'kind_id' => 1, // kind: rental
|
||||
],
|
||||
]);
|
||||
$queryCharges->setParameters(Argument::type('array'))
|
||||
->will(function ($args, $query) {
|
||||
return $query;
|
||||
})
|
||||
;
|
||||
|
||||
$queryResources = $this->prophesize(AbstractQuery::class);
|
||||
$queryResources->getResult()->willReturn([
|
||||
[
|
||||
'sum' => 1500.0,
|
||||
'comment' => '',
|
||||
'kind_id' => 2, // kind: 'salary',
|
||||
],
|
||||
]);
|
||||
$queryResources->setParameters(Argument::type('array'))
|
||||
->will(function ($args, $query) {
|
||||
return $query;
|
||||
})
|
||||
;
|
||||
|
||||
$em = $this->prophesize(EntityManagerInterface::class);
|
||||
$em->createNativeQuery(Argument::type('string'), Argument::type(Query\ResultSetMapping::class))
|
||||
->will(function ($args) use ($queryResources, $queryCharges) {
|
||||
if (false !== strpos($args[0], 'chill_budget.resource')) {
|
||||
return $queryResources->reveal();
|
||||
}
|
||||
if (false !== strpos($args[0], 'chill_budget.charge')) {
|
||||
return $queryCharges->reveal();
|
||||
}
|
||||
throw new \RuntimeException('this query does not have a stub counterpart: '.$args[0]);
|
||||
})
|
||||
;
|
||||
|
||||
$chargeRepository = $this->prophesize(ChargeKindRepositoryInterface::class);
|
||||
$chargeRepository->findAll()->willReturn([
|
||||
$rental = (new ChargeKind())->setKind('rental')->setName(['fr' => 'Rental']),
|
||||
$other = (new ChargeKind())->setKind('other')->setName(['fr' => 'Other']),
|
||||
]);
|
||||
$chargeRepository->find(1)->willReturn($rental);
|
||||
$chargeRepository->findOneByKind('rental')->willReturn($rental);
|
||||
$chargeRepository->findOneByKind('other')->willReturn($other);
|
||||
|
||||
$resourceRepository = $this->prophesize(ResourceKindRepositoryInterface::class);
|
||||
$resourceRepository->findAll()->willReturn([
|
||||
$salary = (new ResourceKind())->setKind('salary')->setName(['fr' => 'Salary']),
|
||||
$misc = (new ResourceKind())->setKind('misc')->setName(['fr' => 'Misc']),
|
||||
]);
|
||||
$resourceRepository->find(2)->willReturn($salary);
|
||||
$resourceRepository->findOneByKind('salary')->willReturn($salary);
|
||||
$resourceRepository->findOneByKind('misc')->willReturn($misc);
|
||||
|
||||
$translatableStringHelper = $this->prophesize(TranslatableStringHelperInterface::class);
|
||||
$translatableStringHelper->localize(Argument::type('array'))->will(function ($arg) {
|
||||
return $arg[0]['fr'];
|
||||
});
|
||||
|
||||
$person = new Person();
|
||||
$personReflection = new \ReflectionClass($person);
|
||||
$personIdReflection = $personReflection->getProperty('id');
|
||||
$personIdReflection->setAccessible(true);
|
||||
$personIdReflection->setValue($person, 1);
|
||||
|
||||
$household = new Household();
|
||||
$householdReflection = new \ReflectionClass($household);
|
||||
$householdId = $householdReflection->getProperty('id');
|
||||
$householdId->setAccessible(true);
|
||||
$householdId->setValue($household, 1);
|
||||
$householdMember = (new HouseholdMember())->setPerson($person)
|
||||
->setStartDate(new \DateTimeImmutable('1 month ago'))
|
||||
;
|
||||
$household->addMember($householdMember);
|
||||
|
||||
$summaryBudget = new SummaryBudget(
|
||||
$em->reveal(),
|
||||
$translatableStringHelper->reveal(),
|
||||
$resourceRepository->reveal(),
|
||||
$chargeRepository->reveal()
|
||||
);
|
||||
|
||||
$summary = $summaryBudget->getSummaryForPerson($person);
|
||||
$summaryForHousehold = $summaryBudget->getSummaryForHousehold($household);
|
||||
|
||||
// we check the structure for the summary. The structure is the same for household
|
||||
// and persons
|
||||
|
||||
$expected = [
|
||||
'charges' => [
|
||||
'rental' => ['sum' => 250.0, 'comment' => '', 'label' => 'Rental'],
|
||||
'other' => ['sum' => 0.0, 'comment' => '', 'label' => 'Other'],
|
||||
],
|
||||
'resources' => [
|
||||
'salary' => ['sum' => 1500.0, 'comment' => '', 'label' => 'Salary'],
|
||||
'misc' => ['sum' => 0.0, 'comment' => '', 'label' => 'Misc'],
|
||||
],
|
||||
];
|
||||
|
||||
foreach ([$summaryForHousehold, $summary] as $summary) {
|
||||
$this->assertIsArray($summary);
|
||||
$this->assertEqualsCanonicalizing(['charges', 'resources'], array_keys($summary));
|
||||
$this->assertEqualsCanonicalizing(['rental', 'other'], array_keys($summary['charges']));
|
||||
$this->assertEqualsCanonicalizing(['salary', 'misc'], array_keys($summary['resources']));
|
||||
|
||||
foreach ($expected as $resCha => $contains) {
|
||||
foreach ($contains as $kind => $row) {
|
||||
$this->assertEqualsCanonicalizing($row, $summary[$resCha][$kind]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
services:
|
||||
Chill\BudgetBundle\Config\ConfigRepository:
|
||||
arguments:
|
||||
$resources: '%chill_budget.resources%'
|
||||
$charges: '%chill_budget.charges%'
|
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\Migrations\Budget;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20230201131008 extends AbstractMigration
|
||||
{
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this shouldn't be undone.
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Fix the comment on tags column in resource and charge type';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('COMMENT ON COLUMN chill_budget.resource_type.tags IS \'(DC2Type:json)\'');
|
||||
$this->addSql('COMMENT ON COLUMN chill_budget.charge_type.tags IS \'(DC2Type:json)\'');
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Budget;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20230209161546 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Budget: add unique constraint on kind for charge_kind and resource_kind';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql("UPDATE chill_budget.resource_type SET kind=md5(random()::text) WHERE kind = ''");
|
||||
$this->addSql("UPDATE chill_budget.charge_type SET kind=md5(random()::text) WHERE kind = ''");
|
||||
$this->addSql('CREATE UNIQUE INDEX resource_kind_unique_type_idx ON chill_budget.resource_type (kind);');
|
||||
$this->addSql('CREATE UNIQUE INDEX charge_kind_unique_type_idx ON chill_budget.charge_type (kind);');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX resource_kind_unique_type_idx');
|
||||
$this->addSql('DROP INDEX charge_kind_unique_type_idx');
|
||||
}
|
||||
}
|
@ -77,6 +77,13 @@ The balance: Différence entre ressources et charges
|
||||
Valid since %startDate% until %endDate%: Valide depuis le %startDate% jusqu'au %endDate%
|
||||
Valid since %startDate%: Valide depuis le %startDate%
|
||||
|
||||
budget:
|
||||
admin:
|
||||
form:
|
||||
Charge_kind_key: Clé d'identification
|
||||
Resource_kind_key: Clé d'identification
|
||||
This kind must contains only alphabeticals characters, and dashes. This string is in use during document generation. Changes may have side effect on document: Cette clé sert à identifier le type de charge ou de revenu lors de la génération de document. Seuls les caractères alpha-numériques sont autorisés. Modifier cette clé peut avoir un effet lors de la génération de nouveaux documents.
|
||||
|
||||
# ROLES
|
||||
Budget elements: Budget
|
||||
CHILL_BUDGET_ELEMENT_CREATE: Créer une ressource/charge
|
||||
|
@ -1,2 +1,8 @@
|
||||
The amount cannot be empty: Le montant ne peut pas être vide ou égal à zéro
|
||||
The budget element's end date must be after the start date: La date de fin doit être après la date de début
|
||||
The budget element's end date must be after the start date: La date de fin doit être après la date de début
|
||||
|
||||
budget:
|
||||
admin:
|
||||
form:
|
||||
kind:
|
||||
only_alphanumeric
|
||||
|
@ -61,7 +61,7 @@ class LoadCalendarRange extends Fixture implements FixtureGroupInterface, Ordere
|
||||
->setEmail('centreA@test.chill.social')
|
||||
->setLocationType($type = new LocationType())
|
||||
->setPhonenumber1(PhoneNumberUtil::getInstance()->parse('+3287653812'));
|
||||
$type->setTitle('Service');
|
||||
$type->setTitle(['fr' => 'Service']);
|
||||
$address->setStreet('Rue des Épaules')->setStreetNumber('14')
|
||||
->setPostcode($postCode = new PostalCode());
|
||||
$postCode->setCode('4145')->setName('Houte-Si-Plout')->setCountry(
|
||||
|
@ -12,6 +12,8 @@ declare(strict_types=1);
|
||||
namespace Chill\CalendarBundle\DataFixtures\ORM;
|
||||
|
||||
use Chill\CalendarBundle\Entity\Invite;
|
||||
use Chill\MainBundle\DataFixtures\ORM\LoadUsers;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
@ -33,14 +35,21 @@ class LoadInvite extends Fixture implements FixtureGroupInterface
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$arr = [
|
||||
['name' => ['fr' => 'Rendez-vous décliné']],
|
||||
['name' => ['fr' => 'Rendez-vous accepté']],
|
||||
[
|
||||
'name' => ['fr' => 'Rendez-vous décliné'],
|
||||
'status' => Invite::DECLINED,
|
||||
],
|
||||
[
|
||||
'name' => ['fr' => 'Rendez-vous accepté'],
|
||||
'status' => Invite::ACCEPTED,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($arr as $a) {
|
||||
echo 'Creating calendar invite : ' . $a['name']['fr'] . "\n";
|
||||
$invite = (new Invite())
|
||||
->setStatus($a['name']);
|
||||
->setStatus($a['status'])
|
||||
->setUser($this->getRandomUser());
|
||||
$manager->persist($invite);
|
||||
$reference = 'Invite_' . $a['name']['fr'];
|
||||
$this->addReference($reference, $invite);
|
||||
@ -49,4 +58,11 @@ class LoadInvite extends Fixture implements FixtureGroupInterface
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
private function getRandomUser(): User
|
||||
{
|
||||
$userRef = array_rand(LoadUsers::$refs);
|
||||
|
||||
return $this->getReference($userRef);
|
||||
}
|
||||
}
|
||||
|
@ -17,30 +17,20 @@
|
||||
<td class="eval">
|
||||
<ul class="eval_title">
|
||||
<li>
|
||||
{{ mm.mimeIcon(d.storedObject.type) }}
|
||||
{{ d.storedObject.title }}
|
||||
{% if d.dateTimeVersion < d.calendar.dateTimeVersion %}
|
||||
<span class="badge bg-danger">{{ 'chill_calendar.Document outdated'|trans }}</span>
|
||||
{% endif %}
|
||||
|
||||
<ul class="record_actions small inline">
|
||||
{% if chill_document_is_editable(d.storedObject) and is_granted('CHILL_CALENDAR_DOC_EDIT', d) %}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_calendar_calendardoc_delete', {'id': d.id})}}" class="btn btn-delete"></a>
|
||||
</li>
|
||||
<li>
|
||||
{{ d.storedObject|chill_document_edit_button }}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_CALENDAR_DOC_EDIT', d) %}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_calendar_calendardoc_edit', {'id': d.id})}}" class="btn btn-edit"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
{{ m.download_button(d.storedObject, d.storedObject.title) }}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="row">
|
||||
<div class="col text-start">
|
||||
{{ d.storedObject.title }}
|
||||
{% if d.dateTimeVersion < d.calendar.dateTimeVersion %}
|
||||
<span class="badge bg-danger">{{ 'chill_calendar.Document outdated'|trans }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-auto text-center">
|
||||
{{ mm.mimeIcon(d.storedObject.type) }}
|
||||
</div>
|
||||
<div class="col col-lg-4 text-end">
|
||||
{{ d.storedObject|chill_document_button_group(d.storedObject.title, is_granted('CHILL_CALENDAR_DOC_EDIT', d), {'small': true}) }}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
<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 -%}
|
||||
{{ form_widget(form.persons) }}
|
||||
|
@ -10,13 +10,13 @@
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_answer') }}
|
||||
{{ encore_entry_script_tags('mod_async_upload') }}
|
||||
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_answer') }}
|
||||
{{ encore_entry_link_tags('mod_async_upload') }}
|
||||
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
<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 -%}
|
||||
{{ form_row(form.mainUser) }}
|
||||
|
@ -14,7 +14,7 @@
|
||||
<dd>{{ entity.mainUser }}</dd>
|
||||
</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' } %}
|
||||
|
||||
|
||||
|
@ -18,8 +18,10 @@ use Chill\DocGeneratorBundle\Service\Context\BaseContextData;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Repository\PersonRepository;
|
||||
use Chill\PersonBundle\Templating\Entity\PersonRender;
|
||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||
use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository;
|
||||
use Chill\ThirdPartyBundle\Templating\Entity\ThirdPartyRender;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
@ -39,6 +41,10 @@ final class CalendarContext implements CalendarContextInterface
|
||||
|
||||
private PersonRender $personRender;
|
||||
|
||||
private PersonRepository $personRepository;
|
||||
|
||||
private ThirdPartyRepository $thirdPartyRepository;
|
||||
|
||||
private ThirdPartyRender $thirdPartyRender;
|
||||
|
||||
private TranslatableStringHelperInterface $translatableStringHelper;
|
||||
@ -48,14 +54,18 @@ final class CalendarContext implements CalendarContextInterface
|
||||
EntityManagerInterface $entityManager,
|
||||
NormalizerInterface $normalizer,
|
||||
PersonRender $personRender,
|
||||
PersonRepository $personRepository,
|
||||
ThirdPartyRender $thirdPartyRender,
|
||||
ThirdPartyRepository $thirdPartyRepository,
|
||||
TranslatableStringHelperInterface $translatableStringHelper
|
||||
) {
|
||||
$this->baseContextData = $baseContextData;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->normalizer = $normalizer;
|
||||
$this->personRender = $personRender;
|
||||
$this->personRepository = $personRepository;
|
||||
$this->thirdPartyRender = $thirdPartyRender;
|
||||
$this->thirdPartyRepository = $thirdPartyRepository;
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
}
|
||||
|
||||
@ -226,8 +236,44 @@ final class CalendarContext implements CalendarContextInterface
|
||||
return true;
|
||||
}
|
||||
|
||||
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array
|
||||
{
|
||||
$normalized = [];
|
||||
$normalized['title'] = $data['title'] ?? '';
|
||||
|
||||
foreach (['mainPerson', 'thirdParty'] as $k) {
|
||||
if (isset($data[$k])) {
|
||||
$normalized[$k] = $data[$k]->getId();
|
||||
}
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array
|
||||
{
|
||||
$denormalized = [];
|
||||
$denormalized['title'] = $data['title'];
|
||||
|
||||
if (null !== ($data['mainPerson'] ?? null)) {
|
||||
if (null === $person = $this->personRepository->find($data['mainPerson'])) {
|
||||
throw new \RuntimeException('person not found');
|
||||
}
|
||||
$denormalized['mainPerson'] = $person;
|
||||
}
|
||||
|
||||
if (null !== ($data['thirdParty'] ?? null)) {
|
||||
if (null === $thirdParty = $this->thirdPartyRepository->find($data['thirdParty'])) {
|
||||
throw new \RuntimeException('third party not found');
|
||||
}
|
||||
$denormalized['thirdParty'] = $thirdParty;
|
||||
}
|
||||
|
||||
return $denormalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{mainPerson?: Person, thirdParty?: ThirdParty, title: string} $contextGenerationData
|
||||
* param array{mainPerson?: Person, thirdParty?: ThirdParty, title: string} $contextGenerationData
|
||||
*/
|
||||
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void
|
||||
{
|
||||
|
@ -56,6 +56,10 @@ interface CalendarContextInterface extends DocGeneratorContextWithPublicFormInte
|
||||
*/
|
||||
public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool;
|
||||
|
||||
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array;
|
||||
|
||||
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array;
|
||||
|
||||
/**
|
||||
* @param Calendar $entity
|
||||
*/
|
||||
|
@ -4,7 +4,7 @@ My calendar list: Mes rendez-vous
|
||||
There is no calendar items.: Il n'y a pas de 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?
|
||||
Concerned groups: Parties concernées
|
||||
Concerned groups calendar: Parties concernées
|
||||
Calendar data: Données du rendez-vous
|
||||
Update calendar: Modifier le rendez-vous
|
||||
main user concerned: Utilisateur concerné
|
||||
|
@ -23,6 +23,9 @@ interface DocGeneratorContextWithPublicFormInterface extends DocGeneratorContext
|
||||
*/
|
||||
public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void;
|
||||
|
||||
/**
|
||||
* Fill the form with initial data
|
||||
*/
|
||||
public function getFormData(DocGeneratorTemplate $template, $entity): array;
|
||||
|
||||
/**
|
||||
@ -31,4 +34,14 @@ interface DocGeneratorContextWithPublicFormInterface extends DocGeneratorContext
|
||||
* @param mixed $entity
|
||||
*/
|
||||
public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool;
|
||||
|
||||
/**
|
||||
* Transform the data from the form into serializable data, storable into messenger's message
|
||||
*/
|
||||
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array;
|
||||
|
||||
/**
|
||||
* Reverse the data from the messenger's message into data usable for doc's generation
|
||||
*/
|
||||
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array;
|
||||
}
|
||||
|
@ -16,67 +16,57 @@ use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface;
|
||||
use Chill\DocGeneratorBundle\Context\Exception\ContextNotFoundException;
|
||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||
use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface;
|
||||
use Chill\DocGeneratorBundle\GeneratorDriver\Exception\TemplateException;
|
||||
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
|
||||
use Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface;
|
||||
use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Serializer\Model\Collection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Exception;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||
use Symfony\Component\HttpFoundation\File\File;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
// TODO à mettre dans services
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Throwable;
|
||||
use function strlen;
|
||||
|
||||
final class DocGeneratorTemplateController extends AbstractController
|
||||
{
|
||||
private HttpClientInterface $client;
|
||||
|
||||
private ContextManager $contextManager;
|
||||
|
||||
private DocGeneratorTemplateRepository $docGeneratorTemplateRepository;
|
||||
|
||||
private DriverInterface $driver;
|
||||
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
private LoggerInterface $logger;
|
||||
private GeneratorInterface $generator;
|
||||
|
||||
private MessageBusInterface $messageBus;
|
||||
|
||||
private PaginatorFactory $paginatorFactory;
|
||||
|
||||
private StoredObjectManagerInterface $storedObjectManager;
|
||||
|
||||
public function __construct(
|
||||
ContextManager $contextManager,
|
||||
DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
|
||||
DriverInterface $driver,
|
||||
LoggerInterface $logger,
|
||||
GeneratorInterface $generator,
|
||||
MessageBusInterface $messageBus,
|
||||
PaginatorFactory $paginatorFactory,
|
||||
HttpClientInterface $client,
|
||||
StoredObjectManagerInterface $storedObjectManager,
|
||||
EntityManagerInterface $entityManager
|
||||
) {
|
||||
$this->contextManager = $contextManager;
|
||||
$this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository;
|
||||
$this->driver = $driver;
|
||||
$this->logger = $logger;
|
||||
$this->generator = $generator;
|
||||
$this->messageBus = $messageBus;
|
||||
$this->paginatorFactory = $paginatorFactory;
|
||||
$this->client = $client;
|
||||
$this->storedObjectManager = $storedObjectManager;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
@ -94,7 +84,6 @@ final class DocGeneratorTemplateController extends AbstractController
|
||||
): Response {
|
||||
return $this->generateDocFromTemplate(
|
||||
$template,
|
||||
$entityClassName,
|
||||
$entityId,
|
||||
$request,
|
||||
true
|
||||
@ -115,7 +104,6 @@ final class DocGeneratorTemplateController extends AbstractController
|
||||
): Response {
|
||||
return $this->generateDocFromTemplate(
|
||||
$template,
|
||||
$entityClassName,
|
||||
$entityId,
|
||||
$request,
|
||||
false
|
||||
@ -185,7 +173,6 @@ final class DocGeneratorTemplateController extends AbstractController
|
||||
|
||||
private function generateDocFromTemplate(
|
||||
DocGeneratorTemplate $template,
|
||||
string $entityClassName,
|
||||
int $entityId,
|
||||
Request $request,
|
||||
bool $isTest
|
||||
@ -206,7 +193,7 @@ final class DocGeneratorTemplateController extends AbstractController
|
||||
|
||||
if (null === $entity) {
|
||||
throw new NotFoundHttpException(
|
||||
sprintf('Entity with classname %s and id %s is not found', $entityClassName, $entityId)
|
||||
sprintf('Entity with classname %s and id %s is not found', $context->getEntityClass(), $entityId)
|
||||
);
|
||||
}
|
||||
|
||||
@ -259,98 +246,68 @@ final class DocGeneratorTemplateController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
$document = $template->getFile();
|
||||
|
||||
if ($isTest && ($contextGenerationData['test_file'] instanceof File)) {
|
||||
$dataDecrypted = file_get_contents($contextGenerationData['test_file']->getPathname());
|
||||
} else {
|
||||
try {
|
||||
$dataDecrypted = $this->storedObjectManager->read($document);
|
||||
} catch (Throwable $exception) {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
// transform context generation data
|
||||
$contextGenerationDataSanitized =
|
||||
$context instanceof DocGeneratorContextWithPublicFormInterface ?
|
||||
$context->contextGenerationDataNormalize($template, $entity, $contextGenerationData)
|
||||
: [];
|
||||
|
||||
// if is test, render the data or generate the doc
|
||||
if ($isTest && isset($form) && $form['show_data']->getData()) {
|
||||
// very ugly hack...
|
||||
dd($context->getData($template, $entity, $contextGenerationData));
|
||||
}
|
||||
|
||||
try {
|
||||
$generatedResource = $this
|
||||
->driver
|
||||
->generateFromString(
|
||||
$dataDecrypted,
|
||||
$template->getFile()->getType(),
|
||||
$context->getData($template, $entity, $contextGenerationData),
|
||||
$template->getFile()->getFilename()
|
||||
);
|
||||
} catch (TemplateException $e) {
|
||||
return new Response(
|
||||
implode("\n", $e->getErrors()),
|
||||
400,
|
||||
[
|
||||
'Content-Type' => 'text/plain',
|
||||
]
|
||||
return $this->render('@ChillDocGenerator/Generator/debug_value.html.twig', [
|
||||
'datas' => json_encode($context->getData($template, $entity, $contextGenerationData), JSON_PRETTY_PRINT)
|
||||
]);
|
||||
} elseif ($isTest) {
|
||||
$generated = $this->generator->generateDocFromTemplate(
|
||||
$template,
|
||||
$entityId,
|
||||
$contextGenerationDataSanitized,
|
||||
null,
|
||||
true,
|
||||
isset($form) ? $form['test_file']->getData() : null
|
||||
);
|
||||
}
|
||||
|
||||
if ($isTest) {
|
||||
return new Response(
|
||||
$generatedResource,
|
||||
$generated,
|
||||
Response::HTTP_OK,
|
||||
[
|
||||
'Content-Transfer-Encoding', 'binary',
|
||||
'Content-Type' => 'application/vnd.oasis.opendocument.text',
|
||||
'Content-Disposition' => 'attachment; filename="generated.odt"',
|
||||
'Content-Length' => strlen($generatedResource),
|
||||
'Content-Length' => strlen($generated),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/** @var StoredObject $storedObject */
|
||||
$storedObject = (new ObjectNormalizer())
|
||||
->denormalize(
|
||||
[
|
||||
'type' => $template->getFile()->getType(),
|
||||
'filename' => sprintf('%s_odt', uniqid('doc_', true)),
|
||||
],
|
||||
StoredObject::class
|
||||
);
|
||||
|
||||
try {
|
||||
$this->storedObjectManager->write($storedObject, $generatedResource);
|
||||
} catch (Throwable $exception) {
|
||||
throw $exception;
|
||||
}
|
||||
// this is not a test
|
||||
// we prepare the object to store the document
|
||||
$storedObject = (new StoredObject())
|
||||
->setStatus(StoredObject::STATUS_PENDING)
|
||||
;
|
||||
|
||||
$this->entityManager->persist($storedObject);
|
||||
|
||||
try {
|
||||
$context
|
||||
->storeGenerated(
|
||||
$template,
|
||||
$storedObject,
|
||||
$entity,
|
||||
$contextGenerationData
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
$this
|
||||
->logger
|
||||
->error(
|
||||
'Unable to store the associated document to entity',
|
||||
[
|
||||
'entityClassName' => $entityClassName,
|
||||
'entityId' => $entityId,
|
||||
'contextKey' => $context->getName(),
|
||||
]
|
||||
);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
// we store the generated document
|
||||
$context
|
||||
->storeGenerated(
|
||||
$template,
|
||||
$storedObject,
|
||||
$entity,
|
||||
$contextGenerationData
|
||||
);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->messageBus->dispatch(
|
||||
new RequestGenerationMessage(
|
||||
$this->getUser(),
|
||||
$template,
|
||||
$entityId,
|
||||
$storedObject,
|
||||
$contextGenerationDataSanitized,
|
||||
)
|
||||
);
|
||||
|
||||
return $this
|
||||
->redirectToRoute(
|
||||
'chill_wopi_file_edit',
|
||||
|
@ -53,6 +53,7 @@ final class RelatorioDriver implements DriverInterface
|
||||
$response = $this->client->request('POST', $this->url, [
|
||||
'headers' => $form->getPreparedHeaders()->toArray(),
|
||||
'body' => $form->bodyToIterable(),
|
||||
'timeout' => '300',
|
||||
]);
|
||||
|
||||
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,8 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ 'Doc generator debug'|trans }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<pre>{{ datas }}</pre>
|
||||
</body>
|
||||
</html>
|
@ -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/'
|
||||
tags:
|
||||
- { name: 'serializer.normalizer', priority: -152 }
|
||||
|
||||
Chill\DocGeneratorBundle\Serializer\Normalizer\CollectionDocGenNormalizer:
|
||||
tags:
|
||||
- { name: 'serializer.normalizer', priority: -126 }
|
||||
|
||||
Chill\DocGeneratorBundle\Service\Context\:
|
||||
resource: "../Service/Context"
|
||||
|
||||
Chill\DocGeneratorBundle\Controller\:
|
||||
resource: "../Controller"
|
||||
autowire: true
|
||||
@ -34,18 +38,20 @@ services:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\DocGeneratorBundle\Service\Context\:
|
||||
resource: "../Service/Context/"
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\DocGeneratorBundle\GeneratorDriver\:
|
||||
resource: "../GeneratorDriver/"
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\DocGeneratorBundle\Service\Messenger\:
|
||||
resource: "../Service/Messenger/"
|
||||
|
||||
Chill\DocGeneratorBundle\Service\Generator\Generator: ~
|
||||
Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface: '@Chill\DocGeneratorBundle\Service\Generator\Generator'
|
||||
|
||||
Chill\DocGeneratorBundle\Driver\RelatorioDriver: '@Chill\DocGeneratorBundle\Driver\DriverInterface'
|
||||
|
||||
Chill\DocGeneratorBundle\Context\ContextManager:
|
||||
arguments:
|
||||
$contexts: !tagged_iterator { tag: chill_docgen.context, default_index_method: getKey }
|
||||
Chill\DocGeneratorBundle\Context\ContextManagerInterface: '@Chill\DocGeneratorBundle\Context\ContextManager'
|
||||
|
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\DocGenerator;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20230214192558 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add status, template_id and fix defaults on chill_doc.stored_object';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object ADD template_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object ADD status TEXT DEFAULT \'ready\' NOT NULL');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object ADD createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||
$this->addSql('UPDATE chill_doc.stored_object SET createdAt = creation_date');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object ADD createdBy_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object DROP creation_date;');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object ALTER type SET DEFAULT \'\'');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object ALTER title DROP DEFAULT');
|
||||
$this->addSql('COMMENT ON COLUMN chill_doc.stored_object.createdAt IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object ADD CONSTRAINT FK_49604E365DA0FB8 FOREIGN KEY (template_id) REFERENCES chill_docgen_template (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object ADD CONSTRAINT FK_49604E363174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('CREATE INDEX IDX_49604E365DA0FB8 ON chill_doc.stored_object (template_id)');
|
||||
$this->addSql('CREATE INDEX IDX_49604E363174800F ON chill_doc.stored_object (createdBy_id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object DROP CONSTRAINT FK_49604E365DA0FB8');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object DROP CONSTRAINT FK_49604E363174800F');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object DROP template_id');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object DROP status');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object ADD creation_date TIMESTAMP(0) DEFAULT NOW()');
|
||||
$this->addSql('UPDATE chill_doc.stored_object SET creation_date = createdAt');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object DROP createdAt');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object DROP createdBy_id');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object ALTER title SET DEFAULT \'\'');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object ALTER type DROP DEFAULT');
|
||||
}
|
||||
}
|
@ -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
|
||||
With context %name%: 'Avec le contexte "%name%"'
|
||||
|
||||
Doc generation failed: La génération de ce document a échoué
|
||||
Doc generation is pending: La génération de ce document est en cours
|
||||
Come back later: Revenir plus tard
|
||||
|
||||
failure_email:
|
||||
The generation of a document failed: La génération d'un document a échoué
|
||||
The generation of the document {template_name} failed: La génération d'un document à partir du modèle {{ template_name }} a échoué.
|
||||
The following errors were encoutered: Les erreurs suivantes ont été rencontrées
|
||||
Forward this email to your administrator for solving: Faites suivre ce message vers votre administrateur pour la résolution du problème.
|
||||
References: Références
|
||||
|
||||
crud:
|
||||
docgen_template:
|
||||
@ -19,4 +29,4 @@ crud:
|
||||
|
||||
|
||||
Show data instead of generating: Montrer les données au lieu de générer le document
|
||||
Template file: Fichier modèle
|
||||
Template file: Fichier modèle
|
||||
|
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\DocStoreBundle\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
class StoredObjectApiController
|
||||
{
|
||||
private Security $security;
|
||||
|
||||
public function __construct(Security $security)
|
||||
{
|
||||
$this->security = $security;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/api/1.0/doc-store/stored-object/{uuid}/is-ready")
|
||||
*/
|
||||
public function isDocumentReady(StoredObject $storedObject): Response
|
||||
{
|
||||
if (!$this->security->isGranted('ROLE_USER')) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
return new JsonResponse(
|
||||
[
|
||||
'id' => $storedObject->getId(),
|
||||
'filename' => $storedObject->getFilename(),
|
||||
'status' => $storedObject->getStatus(),
|
||||
'type' => $storedObject->getType(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
@ -14,6 +14,9 @@ namespace Chill\DocStoreBundle\Entity;
|
||||
use ChampsLibres\AsyncUploaderBundle\Model\AsyncFileInterface;
|
||||
use ChampsLibres\AsyncUploaderBundle\Validator\Constraints\AsyncFileExists;
|
||||
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 DateTimeInterface;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
@ -30,13 +33,13 @@ use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
* message="The file is not stored properly"
|
||||
* )
|
||||
*/
|
||||
class StoredObject implements AsyncFileInterface, Document
|
||||
class StoredObject implements AsyncFileInterface, Document, TrackCreationInterface
|
||||
{
|
||||
/**
|
||||
* @ORM\Column(type="datetime", name="creation_date")
|
||||
* @Serializer\Groups({"read", "write"})
|
||||
*/
|
||||
private DateTimeInterface $creationDate;
|
||||
public const STATUS_READY = "ready";
|
||||
public const STATUS_PENDING = "pending";
|
||||
public const STATUS_FAILURE = "failure";
|
||||
|
||||
use TrackCreationTrait;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="json", name="datas")
|
||||
@ -48,7 +51,7 @@ class StoredObject implements AsyncFileInterface, Document
|
||||
* @ORM\Column(type="text")
|
||||
* @Serializer\Groups({"read", "write"})
|
||||
*/
|
||||
private $filename;
|
||||
private string $filename = '';
|
||||
|
||||
/**
|
||||
* @ORM\Id
|
||||
@ -56,7 +59,7 @@ class StoredObject implements AsyncFileInterface, Document
|
||||
* @ORM\Column(type="integer")
|
||||
* @Serializer\Groups({"read", "write"})
|
||||
*/
|
||||
private $id;
|
||||
private ?int $id;
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
@ -78,7 +81,7 @@ class StoredObject implements AsyncFileInterface, Document
|
||||
private string $title = '';
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="text", name="type")
|
||||
* @ORM\Column(type="text", name="type", options={"default": ""})
|
||||
* @Serializer\Groups({"read", "write"})
|
||||
*/
|
||||
private string $type = '';
|
||||
@ -89,28 +92,68 @@ class StoredObject implements AsyncFileInterface, Document
|
||||
*/
|
||||
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->status = $status;
|
||||
}
|
||||
|
||||
public function addGenerationTrial(): self
|
||||
{
|
||||
$this->generationTrialsCounter++;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read", "write"})
|
||||
* @deprecated
|
||||
*/
|
||||
public function getCreationDate(): DateTime
|
||||
{
|
||||
return $this->creationDate;
|
||||
return DateTime::createFromImmutable($this->createdAt);
|
||||
}
|
||||
|
||||
public function getDatas()
|
||||
public function getDatas(): array
|
||||
{
|
||||
return $this->datas;
|
||||
}
|
||||
|
||||
public function getFilename()
|
||||
public function getFilename(): string
|
||||
{
|
||||
return $this->filename;
|
||||
}
|
||||
|
||||
public function getId()
|
||||
public function getGenerationTrialsCounter(): int
|
||||
{
|
||||
return $this->generationTrialsCounter;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
@ -133,6 +176,14 @@ class StoredObject implements AsyncFileInterface, Document
|
||||
return $this->getFilename();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return StoredObject::STATUS_*
|
||||
*/
|
||||
public function getStatus(): string
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
@ -153,52 +204,92 @@ class StoredObject implements AsyncFileInterface, Document
|
||||
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;
|
||||
}
|
||||
|
||||
public function setDatas(?array $datas)
|
||||
public function setDatas(?array $datas): self
|
||||
{
|
||||
$this->datas = (array) $datas;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setFilename(?string $filename)
|
||||
public function setFilename(?string $filename): self
|
||||
{
|
||||
$this->filename = (string) $filename;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setIv(?array $iv)
|
||||
public function setIv(?array $iv): self
|
||||
{
|
||||
$this->iv = (array) $iv;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setKeyInfos(?array $keyInfos)
|
||||
public function setKeyInfos(?array $keyInfos): self
|
||||
{
|
||||
$this->keyInfos = (array) $keyInfos;
|
||||
|
||||
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;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setType(?string $type)
|
||||
public function setType(?string $type): self
|
||||
{
|
||||
$this->type = (string) $type;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,49 @@
|
||||
import {_createI18n} from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
|
||||
import DocumentActionButtonsGroup from "../../vuejs/DocumentActionButtonsGroup.vue";
|
||||
import {createApp} from "vue";
|
||||
import {StoredObject, StoredObjectStatusChange} from "../../types";
|
||||
import {is_object_ready} from "../../vuejs/StoredObjectButton/helpers";
|
||||
|
||||
const i18n = _createI18n({});
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function (e) {
|
||||
document.querySelectorAll<HTMLDivElement>('div[data-download-buttons]').forEach((el) => {
|
||||
const app = createApp({
|
||||
components: {DocumentActionButtonsGroup},
|
||||
data() {
|
||||
|
||||
const datasets = el.dataset as {
|
||||
filename: string,
|
||||
canEdit: string,
|
||||
storedObject: string,
|
||||
buttonSmall: string,
|
||||
};
|
||||
|
||||
const
|
||||
storedObject = JSON.parse(datasets.storedObject) as StoredObject,
|
||||
filename = datasets.filename,
|
||||
canEdit = datasets.canEdit === '1',
|
||||
small = datasets.buttonSmall === '1'
|
||||
;
|
||||
|
||||
return { storedObject, filename, canEdit, small };
|
||||
},
|
||||
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);
|
||||
})
|
||||
});
|
35
src/Bundle/ChillDocStoreBundle/Resources/public/types.ts
Normal file
35
src/Bundle/ChillDocStoreBundle/Resources/public/types.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import {DateTime} from "../../../ChillMainBundle/Resources/public/types";
|
||||
|
||||
export type StoredObjectStatus = "ready"|"failure"|"pending";
|
||||
|
||||
export interface StoredObject {
|
||||
id: number,
|
||||
|
||||
/**
|
||||
* filename of the object in the object storage
|
||||
*/
|
||||
filename: string,
|
||||
creationDate: DateTime,
|
||||
datas: object,
|
||||
iv: number[],
|
||||
keyInfos: object,
|
||||
title: string,
|
||||
type: string,
|
||||
uuid: string,
|
||||
status: StoredObjectStatus,
|
||||
}
|
||||
|
||||
export interface StoredObjectStatusChange {
|
||||
id: number,
|
||||
filename: string,
|
||||
status: StoredObjectStatus,
|
||||
type: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Function executed by the WopiEditButton component.
|
||||
*/
|
||||
export type WopiEditButtonExecutableBeforeLeaveFunction = {
|
||||
(): Promise<void>
|
||||
}
|
||||
|
@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div v-if="'ready' === props.storedObject.status" class="dropdown">
|
||||
<button :class="Object.assign({'btn': true, 'btn-outline-primary': true, 'dropdown-toggle': true, 'btn-sm': props.small})" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Actions
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li v-if="props.canEdit && is_extension_editable(props.storedObject.type)">
|
||||
<wopi-edit-button :stored-object="props.storedObject" :classes="{'dropdown-item': true}" :execute-before-leave="props.executeBeforeLeave"></wopi-edit-button>
|
||||
</li>
|
||||
<li v-if="props.storedObject.type != 'application/pdf' && is_extension_viewable(props.storedObject.type) && props.canConvertPdf">
|
||||
<convert-button :stored-object="props.storedObject" :filename="filename" :classes="{'dropdown-item': true}"></convert-button>
|
||||
</li>
|
||||
<li v-if="props.canDownload">
|
||||
<download-button :stored-object="props.storedObject" :filename="filename" :classes="{'dropdown-item': true}"></download-button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else-if="'pending' === props.storedObject.status">
|
||||
<div class="btn btn-outline-info">Génération en cours</div>
|
||||
</div>
|
||||
<div v-else-if="'failure' === props.storedObject.status">
|
||||
<div class="btn btn-outline-danger">La génération a échoué</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
import {onMounted} from "vue";
|
||||
import ConvertButton from "./StoredObjectButton/ConvertButton.vue";
|
||||
import DownloadButton from "./StoredObjectButton/DownloadButton.vue";
|
||||
import WopiEditButton from "./StoredObjectButton/WopiEditButton.vue";
|
||||
import {is_extension_editable, is_extension_viewable, is_object_ready} from "./StoredObjectButton/helpers";
|
||||
import {
|
||||
StoredObject,
|
||||
StoredObjectStatusChange,
|
||||
WopiEditButtonExecutableBeforeLeaveFunction
|
||||
} from "../types";
|
||||
|
||||
interface DocumentActionButtonsGroupConfig {
|
||||
storedObject: StoredObject,
|
||||
small?: boolean,
|
||||
canEdit?: boolean,
|
||||
canDownload?: boolean,
|
||||
canConvertPdf?: boolean,
|
||||
returnPath?: string,
|
||||
|
||||
/**
|
||||
* Will be the filename displayed to the user when he·she download the document
|
||||
* (the document will be saved on his disk with this name)
|
||||
*
|
||||
* If not set, 'document' will be used.
|
||||
*/
|
||||
filename?: string,
|
||||
|
||||
/**
|
||||
* If set, will execute this function before leaving to the editor
|
||||
*/
|
||||
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction,
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'onStoredObjectStatusChange', newStatus: StoredObjectStatusChange): void
|
||||
}>();
|
||||
|
||||
const props = withDefaults(defineProps<DocumentActionButtonsGroupConfig>(), {
|
||||
small: false,
|
||||
canEdit: true,
|
||||
canDownload: true,
|
||||
canConvertPdf: true,
|
||||
returnPath: window.location.pathname + window.location.search + window.location.hash,
|
||||
});
|
||||
|
||||
/**
|
||||
* counter for the number of times that we check for a new status
|
||||
*/
|
||||
let tryiesForReady = 0;
|
||||
|
||||
/**
|
||||
* how many times we may check for a new status, once loaded
|
||||
*/
|
||||
const maxTryiesForReady = 120;
|
||||
|
||||
const checkForReady = function(): void {
|
||||
if (
|
||||
'ready' === props.storedObject.status
|
||||
|| 'failure' === props.storedObject.status
|
||||
// stop reloading if the page stays opened for a long time
|
||||
|| tryiesForReady > maxTryiesForReady
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
tryiesForReady = tryiesForReady + 1;
|
||||
|
||||
setTimeout(onObjectNewStatusCallback, 5000);
|
||||
};
|
||||
|
||||
const onObjectNewStatusCallback = async function(): Promise<void> {
|
||||
const new_status = await is_object_ready(props.storedObject);
|
||||
if (props.storedObject.status !== new_status.status) {
|
||||
emit('onStoredObjectStatusChange', new_status);
|
||||
return Promise.resolve();
|
||||
} else if ('failure' === new_status.status) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if ('ready' !== new_status.status) {
|
||||
// we check for new status, unless it is ready
|
||||
checkForReady();
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
checkForReady();
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -0,0 +1,5 @@
|
||||
# About buttons and components available
|
||||
|
||||
## DocumentActionButtonsGroup
|
||||
|
||||
This is an component to use to render a group of button with actions linked to a document.
|
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<a :class="props.classes" @click="download_and_open($event)">
|
||||
<i class="fa fa-file-pdf-o"></i>
|
||||
Télécharger en pdf
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
import {build_convert_link, download_and_decrypt_doc, download_doc} from "./helpers";
|
||||
import mime from "mime";
|
||||
import {reactive} from "vue";
|
||||
import {StoredObject} from "../../types";
|
||||
|
||||
interface ConvertButtonConfig {
|
||||
storedObject: StoredObject,
|
||||
classes: { [key: string]: boolean},
|
||||
filename?: string,
|
||||
};
|
||||
|
||||
interface DownloadButtonState {
|
||||
content: null|string
|
||||
}
|
||||
|
||||
const props = defineProps<ConvertButtonConfig>();
|
||||
const state: DownloadButtonState = reactive({content: null});
|
||||
|
||||
async function download_and_open(event: Event): Promise<void> {
|
||||
const button = event.target as HTMLAnchorElement;
|
||||
|
||||
if (null === state.content) {
|
||||
event.preventDefault();
|
||||
|
||||
const raw = await download_doc(build_convert_link(props.storedObject.uuid));
|
||||
state.content = window.URL.createObjectURL(raw);
|
||||
|
||||
button.href = window.URL.createObjectURL(raw);
|
||||
button.type = 'application/pdf';
|
||||
|
||||
button.download = (props.filename + '.pdf') || 'document.pdf';
|
||||
}
|
||||
|
||||
button.click();
|
||||
}
|
||||
|
||||
</script>
|
@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<a :class="props.classes" @click="download_and_open($event)">
|
||||
<i class="fa fa-download"></i>
|
||||
Télécharger
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {reactive} from "vue";
|
||||
import {build_download_info_link, download_and_decrypt_doc} from "./helpers";
|
||||
import mime from "mime";
|
||||
import {StoredObject} from "../../types";
|
||||
|
||||
interface DownloadButtonConfig {
|
||||
storedObject: StoredObject,
|
||||
classes: {[k: string]: boolean},
|
||||
filename?: string,
|
||||
}
|
||||
|
||||
interface DownloadButtonState {
|
||||
content: null|string
|
||||
}
|
||||
|
||||
const props = defineProps<DownloadButtonConfig>();
|
||||
const state: DownloadButtonState = reactive({content: null});
|
||||
|
||||
async function download_and_open(event: Event): Promise<void> {
|
||||
const button = event.target as HTMLAnchorElement;
|
||||
|
||||
if (null === state.content) {
|
||||
event.preventDefault();
|
||||
|
||||
const urlInfo = build_download_info_link(props.storedObject.filename);
|
||||
|
||||
const raw = await download_and_decrypt_doc(urlInfo, props.storedObject.keyInfos, new Uint8Array(props.storedObject.iv));
|
||||
state.content = window.URL.createObjectURL(raw);
|
||||
|
||||
button.href = window.URL.createObjectURL(raw);
|
||||
button.type = props.storedObject.type;
|
||||
|
||||
if (props.filename !== undefined) {
|
||||
button.download = props.filename || 'document';
|
||||
|
||||
const ext = mime.getExtension(props.storedObject.type);
|
||||
if (null !== ext) {
|
||||
button.download = button.download + '.' + ext;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button.click();
|
||||
}
|
||||
</script>
|
@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<a :class="Object.assign(props.classes, {'btn': true})" @click="beforeLeave($event)" :href="build_wopi_editor_link(props.storedObject.uuid, props.returnPath)">
|
||||
<i class="fa fa-paragraph"></i>
|
||||
Editer en ligne
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import WopiEditButton from "./WopiEditButton.vue";
|
||||
import {build_wopi_editor_link} from "./helpers";
|
||||
import {StoredObject, WopiEditButtonExecutableBeforeLeaveFunction} from "../../types";
|
||||
|
||||
interface WopiEditButtonConfig {
|
||||
storedObject: StoredObject,
|
||||
returnPath?: string,
|
||||
classes: {[k: string] : boolean},
|
||||
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction,
|
||||
}
|
||||
|
||||
const props = defineProps<WopiEditButtonConfig>();
|
||||
|
||||
let executed = false;
|
||||
|
||||
async function beforeLeave(event: Event): Promise<true> {
|
||||
console.log(executed);
|
||||
if (props.executeBeforeLeave === undefined || executed === true) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
await props.executeBeforeLeave();
|
||||
executed = true;
|
||||
|
||||
const link = event.target as HTMLAnchorElement;
|
||||
link.click();
|
||||
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="sass">
|
||||
</style>
|
||||
|
@ -0,0 +1,193 @@
|
||||
import {StoredObject, StoredObjectStatus, StoredObjectStatusChange} from "../../types";
|
||||
|
||||
const MIMES_EDIT = new Set([
|
||||
'application/vnd.ms-powerpoint',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.oasis.opendocument.text',
|
||||
'application/vnd.oasis.opendocument.text-flat-xml',
|
||||
'application/vnd.oasis.opendocument.spreadsheet',
|
||||
'application/vnd.oasis.opendocument.spreadsheet-flat-xml',
|
||||
'application/vnd.oasis.opendocument.presentation',
|
||||
'application/vnd.oasis.opendocument.presentation-flat-xml',
|
||||
'application/vnd.oasis.opendocument.graphics',
|
||||
'application/vnd.oasis.opendocument.graphics-flat-xml',
|
||||
'application/vnd.oasis.opendocument.chart',
|
||||
'application/msword',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.ms-powerpoint',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-word.document.macroEnabled.12',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
|
||||
'application/vnd.ms-excel.sheet.macroEnabled.12',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
|
||||
'application/x-dif-document',
|
||||
'text/spreadsheet',
|
||||
'text/csv',
|
||||
'application/x-dbase',
|
||||
'text/rtf',
|
||||
'text/plain',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
|
||||
]);
|
||||
|
||||
|
||||
|
||||
const MIMES_VIEW = new Set([
|
||||
...MIMES_EDIT,
|
||||
[
|
||||
'image/svg+xml',
|
||||
'application/vnd.sun.xml.writer',
|
||||
'application/vnd.sun.xml.calc',
|
||||
'application/vnd.sun.xml.impress',
|
||||
'application/vnd.sun.xml.draw',
|
||||
'application/vnd.sun.xml.writer.global',
|
||||
'application/vnd.sun.xml.writer.template',
|
||||
'application/vnd.sun.xml.calc.template',
|
||||
'application/vnd.sun.xml.impress.template',
|
||||
'application/vnd.sun.xml.draw.template',
|
||||
'application/vnd.oasis.opendocument.text-master',
|
||||
'application/vnd.oasis.opendocument.text-template',
|
||||
'application/vnd.oasis.opendocument.text-master-template',
|
||||
'application/vnd.oasis.opendocument.spreadsheet-template',
|
||||
'application/vnd.oasis.opendocument.presentation-template',
|
||||
'application/vnd.oasis.opendocument.graphics-template',
|
||||
'application/vnd.ms-word.template.macroEnabled.12',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
|
||||
'application/vnd.ms-excel.template.macroEnabled.12',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.template',
|
||||
'application/vnd.ms-powerpoint.template.macroEnabled.12',
|
||||
'application/vnd.wordperfect',
|
||||
'application/x-aportisdoc',
|
||||
'application/x-hwp',
|
||||
'application/vnd.ms-works',
|
||||
'application/x-mswrite',
|
||||
'application/vnd.lotus-1-2-3',
|
||||
'image/cgm',
|
||||
'image/vnd.dxf',
|
||||
'image/x-emf',
|
||||
'image/x-wmf',
|
||||
'application/coreldraw',
|
||||
'application/vnd.visio2013',
|
||||
'application/vnd.visio',
|
||||
'application/vnd.ms-visio.drawing',
|
||||
'application/x-mspublisher',
|
||||
'application/x-sony-bbeb',
|
||||
'application/x-gnumeric',
|
||||
'application/macwriteii',
|
||||
'application/x-iwork-numbers-sffnumbers',
|
||||
'application/vnd.oasis.opendocument.text-web',
|
||||
'application/x-pagemaker',
|
||||
'application/x-fictionbook+xml',
|
||||
'application/clarisworks',
|
||||
'image/x-wpg',
|
||||
'application/x-iwork-pages-sffpages',
|
||||
'application/x-iwork-keynote-sffkey',
|
||||
'application/x-abiword',
|
||||
'image/x-freehand',
|
||||
'application/vnd.sun.xml.chart',
|
||||
'application/x-t602',
|
||||
'image/bmp',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/tiff',
|
||||
'image/jpg',
|
||||
'image/jpeg',
|
||||
'application/pdf',
|
||||
]
|
||||
])
|
||||
|
||||
function is_extension_editable(mimeType: string): boolean {
|
||||
return MIMES_EDIT.has(mimeType);
|
||||
}
|
||||
|
||||
function is_extension_viewable(mimeType: string): boolean {
|
||||
return MIMES_VIEW.has(mimeType);
|
||||
}
|
||||
|
||||
function build_convert_link(uuid: string) {
|
||||
return `/chill/wopi/convert/${uuid}`;
|
||||
}
|
||||
|
||||
function build_download_info_link(object_name: string) {
|
||||
return `/asyncupload/temp_url/generate/GET?object_name=${object_name}`;
|
||||
}
|
||||
|
||||
function build_wopi_editor_link(uuid: string, returnPath?: string) {
|
||||
if (returnPath === undefined) {
|
||||
returnPath = window.location.pathname + window.location.search + window.location.hash;
|
||||
}
|
||||
|
||||
return `/chill/wopi/edit/${uuid}?returnPath=` + encodeURIComponent(returnPath);
|
||||
}
|
||||
|
||||
function download_doc(url: string): Promise<Blob> {
|
||||
return window.fetch(url).then(r => {
|
||||
if (r.ok) {
|
||||
return r.blob()
|
||||
}
|
||||
|
||||
throw new Error('Could not download document');
|
||||
});
|
||||
}
|
||||
|
||||
async function download_and_decrypt_doc(urlGenerator: string, keyData: JsonWebKey, iv: Uint8Array): Promise<Blob>
|
||||
{
|
||||
const algo = 'AES-CBC';
|
||||
// get an url to download the object
|
||||
const downloadInfoResponse = await window.fetch(urlGenerator);
|
||||
|
||||
if (!downloadInfoResponse.ok) {
|
||||
throw new Error("error while downloading url " + downloadInfoResponse.status + " " + downloadInfoResponse.statusText);
|
||||
}
|
||||
|
||||
const downloadInfo = await downloadInfoResponse.json() as {url: string};
|
||||
const rawResponse = await window.fetch(downloadInfo.url);
|
||||
|
||||
if (!rawResponse.ok) {
|
||||
throw new Error("error while downloading raw file " + rawResponse.status + " " + rawResponse.statusText);
|
||||
}
|
||||
|
||||
if (iv.length === 0) {
|
||||
return rawResponse.blob();
|
||||
}
|
||||
|
||||
const rawBuffer = await rawResponse.arrayBuffer();
|
||||
|
||||
try {
|
||||
const key = await window.crypto.subtle
|
||||
.importKey('jwk', keyData, { name: algo }, false, ['decrypt']);
|
||||
const decrypted = await window.crypto.subtle
|
||||
.decrypt({ name: algo, iv: iv }, key, rawBuffer);
|
||||
|
||||
return Promise.resolve(new Blob([decrypted]));
|
||||
} catch (e) {
|
||||
console.error('get error while keys and decrypt operations');
|
||||
console.error(e);
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function is_object_ready(storedObject: StoredObject): Promise<StoredObjectStatusChange>
|
||||
{
|
||||
const new_status_response = await window
|
||||
.fetch( `/api/1.0/doc-store/stored-object/${storedObject.uuid}/is-ready`);
|
||||
|
||||
if (!new_status_response.ok) {
|
||||
throw new Error("could not fetch the new status");
|
||||
}
|
||||
|
||||
return await new_status_response.json();
|
||||
}
|
||||
|
||||
export {
|
||||
build_convert_link,
|
||||
build_download_info_link,
|
||||
build_wopi_editor_link,
|
||||
download_and_decrypt_doc,
|
||||
download_doc,
|
||||
is_extension_editable,
|
||||
is_extension_viewable,
|
||||
is_object_ready,
|
||||
};
|
@ -46,21 +46,8 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
{{ m.download_button(document.object, document.title) }}
|
||||
{{ document.object|chill_document_button_group(document.title, not freezed) }}
|
||||
</li>
|
||||
{% if chill_document_is_editable(document.object) %}
|
||||
{% if not freezed %}
|
||||
<li>
|
||||
{{ document.object|chill_document_edit_button({'title': document.title|e('html') }) }}
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<a class="btn btn-wopilink disabled" href="#" title="{{ 'workflow.freezed document'|trans }}">
|
||||
{{ 'Update document'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE', document) and document.course != null %}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': document.course.id, 'id': document.id}) }}" class="btn btn-show"></a>
|
||||
|
@ -9,11 +9,6 @@
|
||||
<dt>{{ 'Title'|trans }}</dt>
|
||||
<dd>{{ document.title }}</dd>
|
||||
|
||||
{% if document.scope is not null %}
|
||||
<dt>{{ 'Scope' | trans }}</dt>
|
||||
<dd>{{ document.scope.name | localize_translatable_string }}</dd>
|
||||
{% endif %}
|
||||
|
||||
<dt>{{ 'Category'|trans }}</dt>
|
||||
<dd>{{ document.category.name|localize_translatable_string }}</dd>
|
||||
|
||||
|
@ -8,16 +8,16 @@
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_async_upload') }}
|
||||
{{ encore_entry_script_tags('mod_docgen_picktemplate') }}
|
||||
{{ encore_entry_script_tags('mod_entity_workflow_pick') }}
|
||||
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_async_upload') }}
|
||||
{{ encore_entry_link_tags('mod_docgen_picktemplate') }}
|
||||
{{ encore_entry_link_tags('mod_entity_workflow_pick') }}
|
||||
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -14,6 +14,7 @@
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_async_upload') }}
|
||||
{{ encore_entry_link_tags('mod_entity_workflow_pick') }}
|
||||
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@ -61,13 +62,8 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
{{ m.download_button(document.object, document.title) }}
|
||||
{{ document.object|chill_document_button_group(document.title) }}
|
||||
</li>
|
||||
{% if chill_document_is_editable(document.object) %}
|
||||
<li>
|
||||
{{ document.object|chill_document_edit_button }}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% set workflows_frame = chill_entity_workflow_list('Chill\\DocStoreBundle\\Entity\\AccompanyingCourseDocument', document.id) %}
|
||||
{% if workflows_frame is not empty %}
|
||||
<li>
|
||||
@ -86,4 +82,5 @@
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_async_upload') }}
|
||||
{{ encore_entry_script_tags('mod_entity_workflow_pick') }}
|
||||
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
@ -0,0 +1,7 @@
|
||||
{%- import "@ChillDocStore/Macro/macro.html.twig" as m -%}
|
||||
<div
|
||||
data-download-buttons
|
||||
data-stored-object="{{ document_json|json_encode|escape('html_attr') }}"
|
||||
data-can-edit="{{ can_edit ? '1' : '0' }}"
|
||||
{% if options['small'] is defined %}data-button-small="{{ options['small'] ? '1' : '0' }}"{% endif %}
|
||||
{% if title|default(document.title)|default(null) is not null %}data-filename="{{ title|default(document.title)|escape('html_attr') }}"{% endif %}></div>
|
@ -5,18 +5,25 @@
|
||||
<div class="item-bloc">
|
||||
<div class="item-row">
|
||||
<div class="item-col" style="width: unset">
|
||||
{% if document.object.isPending %}
|
||||
<div class="badge text-bg-info" data-docgen-is-pending="{{ document.object.id }}">{{ 'docgen.Doc generation is pending'|trans }}</div>
|
||||
{% elseif document.object.isFailure %}
|
||||
<div class="badge text-bg-warning">{{ 'docgen.Doc generation failed'|trans }}</div>
|
||||
{% endif %}
|
||||
<div class="denomination h2">
|
||||
{{ document.title }}
|
||||
</div>
|
||||
<div>
|
||||
{{ mm.mimeIcon(document.object.type) }}
|
||||
</div>
|
||||
{% if document.object.type is not empty %}
|
||||
<div>
|
||||
{{ mm.mimeIcon(document.object.type) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<p>{{ document.category.name|localize_translatable_string }}</p>
|
||||
</div>
|
||||
{% if document.template is not null %}
|
||||
{% if document.object.hasTemplate %}
|
||||
<div>
|
||||
<p>{{ document.template.name.fr }}</p>
|
||||
<p>{{ document.object.template.name|localize_translatable_string }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -53,15 +60,10 @@
|
||||
<li>
|
||||
<a href="{{ path('accompanying_course_document_edit', {'course': accompanyingCourse.id, 'id': document.id }) }}" class="btn btn-update"></a>
|
||||
</li>
|
||||
{% if chill_document_is_editable(document.object) %}
|
||||
<li>
|
||||
{{ document.object|chill_document_edit_button }}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
|
||||
<li>
|
||||
{{ m.download_button(document.object, document.title) }}
|
||||
{{ document.object|chill_document_button_group(document.title, is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document)) }}
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a>
|
||||
@ -80,15 +82,10 @@
|
||||
<li>
|
||||
<a href="{{ path('person_document_edit', {'person': person.id, 'id': document.id}) }}" class="btn btn-update"></a>
|
||||
</li>
|
||||
{% if chill_document_is_editable(document.object) %}
|
||||
<li>
|
||||
{{ document.object|chill_document_edit_button }}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %}
|
||||
<li>
|
||||
{{ m.download_button(document.object, document.title) }}
|
||||
{{ document.object|chill_document_button_group(document.title, is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document)) }}
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ path('person_document_show', {'person': person.id, 'id': document.id}) }}" class="btn btn-show"></a>
|
||||
|
@ -2,13 +2,13 @@
|
||||
{% if storedObject is null %}
|
||||
<!-- No document to download -->
|
||||
{% else %}
|
||||
<a class="btn btn-download"
|
||||
data-label-preparing="{{ ('Preparing'|trans ~ '...')|escape('html_attr') }}"
|
||||
data-label-ready="{{ 'Ready to show'|trans|escape('html_attr') }}"
|
||||
data-download-button
|
||||
data-key="{{ storedObject.keyInfos|json_encode|escape('html_attr') }}"
|
||||
data-iv="{{ storedObject.iv|json_encode|escape('html_attr') }}"
|
||||
data-temp-url-get-generator="{{ storedObject|generate_url|escape('html_attr') }}"
|
||||
<a class="btn btn-download"
|
||||
data-label-preparing="{{ ('Preparing'|trans ~ '...')|escape('html_attr') }}"
|
||||
data-label-ready="{{ 'Ready to show'|trans|escape('html_attr') }}"
|
||||
data-download-button
|
||||
data-key="{{ storedObject.keyInfos|json_encode|escape('html_attr') }}"
|
||||
data-iv="{{ storedObject.iv|json_encode|escape('html_attr') }}"
|
||||
data-temp-url-get-generator="{{ storedObject|generate_url|escape('html_attr') }}"
|
||||
data-mime-type="{{ storedObject.type|escape('html_attr') }}" {% if filename is not null %}data-filename="{{ filename|escape('html_attr') }}"{% endif %}>
|
||||
{{ 'Download'|trans }}</a>
|
||||
{% endif %}
|
||||
@ -28,4 +28,21 @@
|
||||
data-mime-type="{{ storedObject.type|escape('html_attr') }}" {% if filename is not null %}data-filename="{{ filename|escape('html_attr') }}"{% endif %}>
|
||||
{{ 'Download'|trans }}</a>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro download_button_group(storedObject, canEdit = true, filename = null, options = {}) %}
|
||||
{% if storedObject is null %}
|
||||
<!-- No document to download -->
|
||||
{% else %}
|
||||
<div
|
||||
data-download-buttons
|
||||
data-uuid="{{ storedObject.uuid|escape('html_attr') }}"
|
||||
data-key="{{ storedObject.keyInfos|json_encode|escape('html_attr') }}"
|
||||
data-iv="{{ storedObject.iv|json_encode|escape('html_attr') }}"
|
||||
data-temp-url-generator="{{ storedObject|generate_url|escape('html_attr') }}"
|
||||
data-mime-type="{{ storedObject.type|escape('html_attr') }}"
|
||||
data-can-edit="{{ canEdit ? '1' : '0' }}"
|
||||
{% if options['small'] is defined %}data-button-small="{{ options['small'] ? '1' : '0' }}"{% endif %}
|
||||
{% if filename|default(storedObject.title)|default(null) is not null %}data-filename="{{ filename|default(storedObject.title)|escape('html_attr') }}"{% endif %}></div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
@ -27,16 +27,16 @@
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_async_upload') }}
|
||||
{{ encore_entry_script_tags('mod_docgen_picktemplate') }}
|
||||
{{ encore_entry_script_tags('mod_entity_workflow_pick') }}
|
||||
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_async_upload') }}
|
||||
{{ encore_entry_link_tags('mod_docgen_picktemplate') }}
|
||||
{{ encore_entry_link_tags('mod_entity_workflow_pick') }}
|
||||
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -24,7 +24,11 @@
|
||||
{% block title %}{{ 'Detail of document of %name%'|trans({ '%name%': person|chill_entity_render_string } ) }}{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ encore_entry_script_tags('mod_async_upload') }}
|
||||
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@ -70,6 +74,10 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li>
|
||||
{{ document.object|chill_document_button_group(document.title, is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document)) }}
|
||||
</li>
|
||||
|
||||
{% if is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document) %}
|
||||
<li>
|
||||
<a href="{{ path('person_document_edit', {'id': document.id, 'person': person.id}) }}" class="btn btn-edit">
|
||||
@ -77,16 +85,4 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li>
|
||||
{{ m.download_button(document.object, document.title) }}
|
||||
</li>
|
||||
|
||||
{% if chill_document_is_editable(document.object) %}
|
||||
<li>
|
||||
{{ document.object|chill_document_edit_button }}
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{# {{ include('ChillDocStoreBundle:PersonDocument:_delete_form.html.twig') }} #}
|
||||
{% endblock %}
|
||||
|
@ -24,6 +24,10 @@ class WopiEditTwigExtension extends AbstractExtension
|
||||
'needs_environment' => true,
|
||||
'is_safe' => ['html'],
|
||||
]),
|
||||
new TwigFilter('chill_document_button_group', [WopiEditTwigExtensionRuntime::class, 'renderButtonGroup'], [
|
||||
'needs_environment' => true,
|
||||
'is_safe' => ['html'],
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,8 @@ namespace Chill\DocStoreBundle\Templating;
|
||||
|
||||
use ChampsLibres\WopiLib\Contract\Service\Discovery\DiscoveryInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Twig\Environment;
|
||||
use Twig\Extension\RuntimeExtensionInterface;
|
||||
|
||||
@ -112,20 +114,53 @@ final class WopiEditTwigExtensionRuntime implements RuntimeExtensionInterface
|
||||
'application/pdf',
|
||||
];
|
||||
|
||||
private const DEFAULT_OPTIONS_TEMPLATE_BUTTON_GROUP = [
|
||||
'small' => false,
|
||||
];
|
||||
|
||||
private const TEMPLATE = '@ChillDocStore/Button/wopi_edit_document.html.twig';
|
||||
|
||||
private const TEMPLATE_BUTTON_GROUP = '@ChillDocStore/Button/button_group.html.twig';
|
||||
|
||||
private DiscoveryInterface $discovery;
|
||||
|
||||
public function __construct(DiscoveryInterface $discovery)
|
||||
private NormalizerInterface $normalizer;
|
||||
|
||||
public function __construct(DiscoveryInterface $discovery, NormalizerInterface $normalizer)
|
||||
{
|
||||
$this->discovery = $discovery;
|
||||
$this->normalizer = $normalizer;
|
||||
}
|
||||
|
||||
/**
|
||||
* return true if the document is editable.
|
||||
*
|
||||
* **NOTE**: as the Vue button does have similar test, this is not required if in use with
|
||||
* the dedicated Vue component (GroupDownloadButton.vue, WopiEditButton.vue)
|
||||
*/
|
||||
public function isEditable(StoredObject $document): bool
|
||||
{
|
||||
return in_array($document->getType(), self::SUPPORTED_MIMES, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{small: boolean} $options
|
||||
*
|
||||
* @throws \Twig\Error\LoaderError
|
||||
* @throws \Twig\Error\RuntimeError
|
||||
* @throws \Twig\Error\SyntaxError
|
||||
*/
|
||||
public function renderButtonGroup(Environment $environment, StoredObject $document, ?string $title = null, bool $canEdit = true, array $options = []): string
|
||||
{
|
||||
return $environment->render(self::TEMPLATE_BUTTON_GROUP, [
|
||||
'document' => $document,
|
||||
'document_json' => $this->normalizer->normalize($document, 'json', [AbstractNormalizer::GROUPS => ['read']]),
|
||||
'title' => $title,
|
||||
'can_edit' => $canEdit,
|
||||
'options' => array_merge(self::DEFAULT_OPTIONS_TEMPLATE_BUTTON_GROUP, $options),
|
||||
]);
|
||||
}
|
||||
|
||||
public function renderEditButton(Environment $environment, StoredObject $document, ?array $options = null): string
|
||||
{
|
||||
return $environment->render(self::TEMPLATE, [
|
||||
|
@ -4,4 +4,5 @@ module.exports = function(encore)
|
||||
ChillDocStoreAssets: __dirname + '/Resources/public'
|
||||
});
|
||||
encore.addEntry('mod_async_upload', __dirname + '/Resources/public/module/async_upload/index.js');
|
||||
encore.addEntry('mod_document_action_buttons_group', __dirname + '/Resources/public/module/document_action_buttons_group/index');
|
||||
};
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
@ -28,7 +28,7 @@ class LocationController extends CRUDController
|
||||
|
||||
protected function customizeQuery(string $action, Request $request, $query): void
|
||||
{
|
||||
$query->where('e.availableForUsers = true'); //TODO not working
|
||||
$query->where('e.availableForUsers = "TRUE"');
|
||||
}
|
||||
|
||||
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
|
||||
|
@ -83,9 +83,9 @@ interface ExportInterface extends ExportElementInterface
|
||||
*
|
||||
* @param string $key The column key, as added in the query
|
||||
* @param mixed[] $values The values from the result. if there are duplicates, those might be given twice. Example: array('FR', 'BE', 'CZ', 'FR', 'BE', 'FR')
|
||||
* @param mixed $data The data from the export's form (as defined in `buildForm`
|
||||
* @param mixed $data The data from the export's form (as defined in `buildForm`)
|
||||
*
|
||||
* @return Closure where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }`
|
||||
* @return pure-callable(null|string|int|float|'_header' $value):string|int|\DateTimeInterface where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }`
|
||||
*/
|
||||
public function getLabels($key, array $values, $data);
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user