Merge branch '21-save-exports' into '111_exports_suite'

Allow to save exports

See merge request Chill-Projet/chill-bundles!465
This commit is contained in:
Julien Fastré 2022-11-09 09:44:07 +00:00
commit 9dba5965f6
28 changed files with 1516 additions and 81 deletions

View File

@ -11,23 +11,32 @@ declare(strict_types=1);
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Entity\SavedExport;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\ExportManager;
use Chill\MainBundle\Form\SavedExportType;
use Chill\MainBundle\Form\Type\Export\ExportType;
use Chill\MainBundle\Form\Type\Export\FormatterType;
use Chill\MainBundle\Form\Type\Export\PickCenterType;
use Chill\MainBundle\Redis\ChillRedis;
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
use Doctrine\ORM\EntityManagerInterface;
use LogicException;
use Psr\Log\LoggerInterface;
use RedisException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use function count;
use function serialize;
use function unserialize;
@ -38,35 +47,37 @@ use function unserialize;
*/
class ExportController extends AbstractController
{
private EntityManagerInterface $entityManager;
/**
* @var ExportManager
*/
protected $exportManager;
private $exportManager;
/**
* @var FormFactoryInterface
*/
protected $formFactory;
private $formFactory;
/**
* @var LoggerInterface
*/
protected $logger;
private $logger;
/**
* @var ChillRedis
*/
protected $redis;
private $redis;
/**
* @var SessionInterface
*/
protected $session;
private $session;
/**
* @var TranslatorInterface
*/
protected $translator;
private $translator;
public function __construct(
ChillRedis $chillRedis,
@ -74,8 +85,10 @@ class ExportController extends AbstractController
FormFactoryInterface $formFactory,
LoggerInterface $logger,
SessionInterface $session,
TranslatorInterface $translator
TranslatorInterface $translator,
EntityManagerInterface $entityManager
) {
$this->entityManager = $entityManager;
$this->redis = $chillRedis;
$this->exportManager = $exportManager;
$this->formFactory = $formFactory;
@ -142,6 +155,29 @@ class ExportController extends AbstractController
);
}
/**
* @Route("/{_locale}/exports/generate-from-saved/{id}", name="chill_main_export_generate_from_saved")
*
* @throws RedisException
*/
public function generateFromSavedExport(SavedExport $savedExport): RedirectResponse
{
$this->denyAccessUnlessGranted(SavedExportVoter::GENERATE, $savedExport);
$key = md5(uniqid((string) mt_rand(), false));
$this->redis->setEx($key, 3600, serialize($savedExport->getOptions()));
return $this->redirectToRoute(
'chill_main_export_download',
[
'alias' => $savedExport->getExportAlias(),
'key' => $key, 'prevent_save' => true,
'returnPath' => $this->generateUrl('chill_main_export_saved_list_my'),
]
);
}
/**
* Render the list of available exports.
*/
@ -203,6 +239,46 @@ class ExportController extends AbstractController
}
}
/**
* @Route("/{_locale}/export/save-from-key/{alias}/{key}", name="chill_main_export_save_from_key")
*/
public function saveFromKey(string $alias, string $key, Request $request): Response
{
$this->denyAccessUnlessGranted('ROLE_USER');
$user = $this->getUser();
if (!$user instanceof User) {
throw new AccessDeniedHttpException();
}
$data = $this->rebuildRawData($key);
$savedExport = new SavedExport();
$savedExport
->setOptions($data)
->setExportAlias($alias)
->setUser($user);
$form = $this->createForm(SavedExportType::class, $savedExport);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->persist($savedExport);
$this->entityManager->flush();
return $this->redirectToRoute('chill_main_export_index');
}
return $this->render(
'@ChillMain/SavedExport/new.html.twig',
[
'form' => $form->createView(),
'saved_export' => $savedExport,
]
);
}
/**
* create a form to show on different steps.
*
@ -418,28 +494,7 @@ class ExportController extends AbstractController
protected function rebuildData($key)
{
if (null === $key) {
throw $this->createNotFoundException('key does not exists');
}
if ($this->redis->exists($key) !== 1) {
$this->addFlash('error', $this->translator->trans('This report is not available any more'));
throw $this->createNotFoundException('key does not exists');
}
$serialized = $this->redis->get($key);
if (false === $serialized) {
throw new LogicException('the key could not be reached from redis');
}
$rawData = unserialize($serialized);
$this->logger->notice('[export] choices for an export unserialized', [
'key' => $key,
'rawData' => json_encode($rawData),
]);
$rawData = $this->rebuildRawData($key);
$alias = $rawData['alias'];
@ -585,4 +640,32 @@ class ExportController extends AbstractController
throw new LogicException("the step {$step} is not defined.");
}
}
private function rebuildRawData(string $key): array
{
if (null === $key) {
throw $this->createNotFoundException('key does not exists');
}
if ($this->redis->exists($key) !== 1) {
$this->addFlash('error', $this->translator->trans('This report is not available any more'));
throw $this->createNotFoundException('key does not exists');
}
$serialized = $this->redis->get($key);
if (false === $serialized) {
throw new LogicException('the key could not be reached from redis');
}
$rawData = unserialize($serialized);
$this->logger->notice('[export] choices for an export unserialized', [
'key' => $key,
'rawData' => json_encode($rawData),
]);
return $rawData;
}
}

View File

@ -0,0 +1,185 @@
<?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\MainBundle\Controller;
use Chill\MainBundle\Entity\SavedExport;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\ExportManager;
use Chill\MainBundle\Export\GroupedExportInterface;
use Chill\MainBundle\Form\SavedExportType;
use Chill\MainBundle\Repository\SavedExportRepositoryInterface;
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Templating\EngineInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function count;
class SavedExportController
{
private EntityManagerInterface $entityManager;
private ExportManager $exportManager;
private FormFactoryInterface $formFactory;
private SavedExportRepositoryInterface $savedExportRepository;
private Security $security;
private SessionInterface $session;
private EngineInterface $templating;
private TranslatorInterface $translator;
private UrlGeneratorInterface $urlGenerator;
public function __construct(
EngineInterface $templating,
EntityManagerInterface $entityManager,
ExportManager $exportManager,
FormFactoryInterface $formBuilder,
SavedExportRepositoryInterface $savedExportRepository,
Security $security,
SessionInterface $session,
TranslatorInterface $translator,
UrlGeneratorInterface $urlGenerator
) {
$this->exportManager = $exportManager;
$this->entityManager = $entityManager;
$this->formFactory = $formBuilder;
$this->savedExportRepository = $savedExportRepository;
$this->security = $security;
$this->session = $session;
$this->templating = $templating;
$this->translator = $translator;
$this->urlGenerator = $urlGenerator;
}
/**
* @Route("/{_locale}/exports/saved/{id}/delete", name="chill_main_export_saved_delete")
*/
public function delete(SavedExport $savedExport, Request $request): Response
{
if (!$this->security->isGranted(SavedExportVoter::DELETE, $savedExport)) {
throw new AccessDeniedHttpException();
}
$form = $this->formFactory->create();
$form->add('submit', SubmitType::class, ['label' => 'Delete']);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->remove($savedExport);
$this->entityManager->flush();
$this->session->getFlashBag()->add('success', $this->translator->trans('saved_export.Export is deleted'));
return new RedirectResponse(
$this->urlGenerator->generate('chill_main_export_saved_list_my')
);
}
return new Response(
$this->templating->render(
'@ChillMain/SavedExport/delete.html.twig',
[
'saved_export' => $savedExport,
'delete_form' => $form->createView(),
]
)
);
}
/**
* @Route("/{_locale}/exports/saved/{id}/edit", name="chill_main_export_saved_edit")
*/
public function edit(SavedExport $savedExport, Request $request): Response
{
if (!$this->security->isGranted(SavedExportVoter::EDIT, $savedExport)) {
throw new AccessDeniedHttpException();
}
$form = $this->formFactory->create(SavedExportType::class, $savedExport);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->flush();
$this->session->getFlashBag()->add('success', $this->translator->trans('saved_export.Saved export is saved!'));
return new RedirectResponse(
$this->urlGenerator->generate('chill_main_export_saved_list_my')
);
}
return new Response(
$this->templating->render(
'@ChillMain/SavedExport/edit.html.twig',
[
'form' => $form->createView(),
]
)
);
}
/**
* @Route("/{_locale}/exports/saved/my", name="chill_main_export_saved_list_my")
*/
public function list(): Response
{
$user = $this->security->getUser();
if (!$this->security->isGranted('ROLE_USER') || !$user instanceof User) {
throw new AccessDeniedHttpException();
}
$exports = $this->savedExportRepository->findByUser($user, ['title' => 'ASC']);
// group by center
/** @var array<string, array{saved: SavedExport, export: ExportInterface}> $exportsGrouped */
$exportsGrouped = [];
foreach ($exports as $savedExport) {
$export = $this->exportManager->getExport($savedExport->getExportAlias());
$exportsGrouped[
$export instanceof GroupedExportInterface
? $this->translator->trans($export->getGroup()) : '_'
][] = ['saved' => $savedExport, 'export' => $export];
}
ksort($exportsGrouped);
return new Response(
$this->templating->render(
'@ChillMain/SavedExport/index.html.twig',
[
'grouped_exports' => $exportsGrouped,
'total' => count($exports),
]
)
);
}
}

View File

@ -0,0 +1,133 @@
<?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\MainBundle\Entity;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Doctrine\ORM\Mapping as ORM;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
/**
* @ORM\Entity
* @ORM\Table(name="chill_main_saved_export")
*/
class SavedExport implements TrackCreationInterface, TrackUpdateInterface
{
use TrackCreationTrait;
use TrackUpdateTrait;
/**
* @ORM\Column(type="text", nullable=false, options={"default": ""})
*/
private string $description = '';
/**
* @ORM\Column(type="text", nullable=false, options={"default": ""})
*/
private string $exportAlias;
/**
* @ORM\Id
* @ORM\Column(name="id", type="uuid", unique="true")
* @ORM\GeneratedValue(strategy="NONE")
*/
private UuidInterface $id;
/**
* @ORM\Column(type="json", nullable=false, options={"default": "[]"})
*/
private array $options = [];
/**
* @ORM\Column(type="text", nullable=false, options={"default": ""})
*/
private string $title = '';
/**
* @ORM\ManyToOne(targetEntity=User::class)
*/
private User $user;
public function __construct()
{
$this->id = Uuid::uuid4();
}
public function getDescription(): string
{
return $this->description;
}
public function getExportAlias(): string
{
return $this->exportAlias;
}
public function getId(): UuidInterface
{
return $this->id;
}
public function getOptions(): array
{
return $this->options;
}
public function getTitle(): string
{
return $this->title;
}
public function getUser(): User
{
return $this->user;
}
public function setDescription(string $description): SavedExport
{
$this->description = $description;
return $this;
}
public function setExportAlias(string $exportAlias): SavedExport
{
$this->exportAlias = $exportAlias;
return $this;
}
public function setOptions(array $options): SavedExport
{
$this->options = $options;
return $this;
}
public function setTitle(string $title): SavedExport
{
$this->title = $title;
return $this;
}
public function setUser(User $user): SavedExport
{
$this->user = $user;
return $this;
}
}

View File

@ -0,0 +1,45 @@
<?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\MainBundle\Form\DataMapper;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\Exception;
class RollingDateDataMapper implements DataMapperInterface
{
public function mapDataToForms($viewData, $forms)
{
if (null === $viewData) {
return;
}
if (!$viewData instanceof RollingDate) {
throw new Exception\UnexpectedTypeException($viewData, RollingDate::class);
}
$forms = iterator_to_array($forms);
$forms['roll']->setData($viewData->getRoll());
$forms['fixedDate']->setData($viewData->getFixedDate());
}
public function mapFormsToData($forms, &$viewData): void
{
$forms = iterator_to_array($forms);
$viewData = new RollingDate(
$forms['roll']->getData(),
$forms['fixedDate']->getData()
);
}
}

View File

@ -0,0 +1,40 @@
<?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\MainBundle\Form;
use Chill\MainBundle\Entity\SavedExport;
use Chill\MainBundle\Form\Type\ChillTextareaType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class SavedExportType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', TextType::class, [
'required' => true,
])
->add('description', ChillTextareaType::class, [
'required' => false,
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'class' => SavedExport::class,
]);
}
}

View File

@ -0,0 +1,66 @@
<?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\MainBundle\Form\Type;
use Chill\MainBundle\Form\DataMapper\RollingDateDataMapper;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
class PickRollingDateType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('roll', ChoiceType::class, [
'choices' => array_combine(
array_map(static fn (string $item) => 'rolling_date.' . $item, RollingDate::ALL_T),
RollingDate::ALL_T
),
'multiple' => false,
'expanded' => false,
'label' => 'rolling_date.roll_movement',
])
->add('fixedDate', ChillDateType::class, [
'input' => 'datetime_immutable',
'label' => 'rolling_date.fixed_date_date',
]);
$builder->setDataMapper(new RollingDateDataMapper());
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'class' => RollingDate::class,
'empty_data' => new RollingDate(RollingDate::T_TODAY),
'constraints' => [
new Callback([$this, 'validate']),
],
]);
}
public function validate($data, ExecutionContextInterface $context, $payload): void
{
/** @var RollingDate $data */
if (RollingDate::T_FIXED_DATE === $data->getRoll() && null === $data->getFixedDate()) {
$context
->buildViolation('rolling_date.When fixed date is selected, you must provide a date')
->atPath('roll')
->addViolation();
}
}
}

View File

@ -0,0 +1,82 @@
<?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\MainBundle\Repository;
use Chill\MainBundle\Entity\SavedExport;
use Chill\MainBundle\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
/**
* @implements ObjectRepository<SavedExport>
*/
class SavedExportRepository implements SavedExportRepositoryInterface
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository($this->getClassName());
}
public function find($id): ?SavedExport
{
return $this->repository->find($id);
}
/**
* @return array|SavedExport[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findByUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array
{
$qb = $this->repository->createQueryBuilder('se');
$qb
->where($qb->expr()->eq('se.user', ':user'))
->setParameter('user', $user);
if (null !== $limit) {
$qb->setMaxResults($limit);
}
if (null !== $offset) {
$qb->setFirstResult($offset);
}
foreach ($orderBy as $field => $order) {
$qb->addOrderBy('se.' . $field, $order);
}
return $qb->getQuery()->getResult();
}
public function findOneBy(array $criteria): ?SavedExport
{
return $this->repository->findOneBy($criteria);
}
public function getClassName(): string
{
return SavedExport::class;
}
}

View File

@ -0,0 +1,40 @@
<?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\MainBundle\Repository;
use Chill\MainBundle\Entity\SavedExport;
use Chill\MainBundle\Entity\User;
use Doctrine\Persistence\ObjectRepository;
/**
* @implements ObjectRepository<SavedExport>
*/
interface SavedExportRepositoryInterface extends ObjectRepository
{
public function find($id): ?SavedExport;
/**
* @return array|SavedExport[]
*/
public function findAll(): array;
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;
/**
* @return array|SavedExport[]
*/
public function findByUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array;
public function findOneBy(array $criteria): ?SavedExport;
public function getClassName(): string;
}

View File

@ -0,0 +1,12 @@
<ul class="nav nav-pills justify-content-center">
<li class="nav-item">
<a href="{{ chill_path_forward_return_path('chill_main_export_index') }}" class="nav-link {% if current == 'common' %}active{% endif %}">
{{ 'Exports list'|trans }}
</a>
</li>
<li class="nav-item">
<a href="{{ chill_path_forward_return_path('chill_main_export_saved_list_my') }}" class="nav-link {% if current == 'my' %}active{% endif %}">
{{ 'saved_export.My saved exports'|trans }}
</a>
</li>
</ul>

View File

@ -49,5 +49,14 @@ window.addEventListener("DOMContentLoaded", function(e) {
data-download-text="{{ "Download your report"|trans|escape('html_attr') }}"
><span id="waiting_text">{{ "Waiting for your report"|trans ~ '...' }}</span></div>
</div>
<ul class="record_actions sticky-form-buttons">
<li class="cancel"><a href="{{ chill_return_path_or('chill_main_export_index') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a></li>
{% if not app.request.query.has('prevent_save') %}
<li>
<a href="{{ chill_path_add_return_path('chill_main_export_save_from_key', { alias: alias, key: app.request.query.get('key')}) }}" class="btn btn-save">{{ 'Save'|trans }}</a>
</li>
{% endif %}
</ul>
</div>
{% endblock content %}

View File

@ -22,9 +22,11 @@
{% block content %}
{{ include('@ChillMain/Export/_navbar.html.twig', {'current' : 'common'}) }}
<div class="col-md-10">
<h1>{{ 'Exports list'|trans }}</h1>
<div class="container mt-4">
{% for group, exports in grouped_exports %}{% if group != '_' %}
@ -32,13 +34,17 @@
<div class="row grouped">
{% for export_alias, export in exports %}
<div class="col-6 col-md-4 mb-3">
<h2>{{ export.title|trans }}</h2>
<p>{{ export.description|trans }}</p>
<p>
<a class="btn btn-action" href="{{ path('chill_main_export_new', { 'alias': export_alias } ) }}">
{{ 'Create an export'|trans }}
</a>
</p>
<div class="card">
<div class="card-body">
<h2 class="card-title">{{ export.title|trans }}</h2>
<p class="card-text">{{ export.description|trans }}</p>
<p>
<a class="btn btn-action" href="{{ path('chill_main_export_new', { 'alias': export_alias } ) }}">
{{ 'Create an export'|trans }}
</a>
</p>
</div>
</div>
</div>
{% endfor %}
</div>
@ -52,13 +58,17 @@
{% for export_alias,export in grouped_exports['_'] %}
<div class="col-6 col-md-4 mb-3">
<h2>{{ export.title|trans }}</h2>
<p>{{ export.description|trans }}</p>
<p>
<a class="btn btn-action" href="{{ path('chill_main_export_new', { 'alias': export_alias } ) }}">
{{ 'Create an export'|trans }}
</a>
</p>
<div class="card">
<div class="card-body">
<h2 class="card-title">{{ export.title|trans }}</h2>
<p class="card-text">{{ export.description|trans }}</p>
<p>
<a class="btn btn-action" href="{{ path('chill_main_export_new', { 'alias': export_alias } ) }}">
{{ 'Create an export'|trans }}
</a>
</p>
</div>
</div>
</div>
{% endfor %}

View File

@ -0,0 +1,25 @@
{% extends '@ChillMain/layout.html.twig' %}
{% block title 'saved_export.Delete saved ?'|trans %}
{% block display_content %}
<div class="col-10">
<h3>{{ saved_export.title }}</h3>
<p>{{ saved_export.description|chill_markdown_to_html }}</p>
</div>
{% endblock %}
{% block content %}
<div class="container chill-md-10">
{{ include('@ChillMain/Util/confirmation_template.html.twig',
{
'title' : 'saved_export.Delete saved ?'|trans,
'confirm_question' : 'saved_export.Are you sure you want to delete this saved ?'|trans,
'display_content' : block('display_content'),
'cancel_route' : 'chill_main_export_saved_list_my',
'cancel_parameters' : {},
'form' : delete_form
} ) }}
</div>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends "@ChillMain/layout.html.twig" %}
{% block title %}{{ 'saved_export.Edit'|trans }}{% endblock %}
{% block content %}
<h1>{{ block('title') }}</h1>
{{ form_start(form) }}
{{ form_row(form.title) }}
{{ form_row(form.description) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ chill_return_path_or('chill_main_homepage') }}" class="btn btn-cancel">{{ 'Cancel'|trans }}</a>
</li>
<li>
<button type="submit" class="btn btn-save">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(form) }}
{% endblock %}

View File

@ -0,0 +1,82 @@
{% extends "@ChillMain/layout.html.twig" %}
{% block title %}{{ 'saved_export.My saved exports'|trans }}{% endblock %}
{% block content %}
<div class="col-md-10">
{{ include('@ChillMain/Export/_navbar.html.twig', {'current' : 'my'}) }}
<div class="container mt-4">
{% if total == 0 %}
<p class="chill-no-data-statement" >{{ 'saved_export.Any saved export'|trans }}</p>
{% endif %}
{% for group, saveds in grouped_exports %}
{% if group != '_' %}
<h2 class="display-6">{{ group }}</h2>
<div class="row grouped">
{% for s in saveds %}
<div class="col-6 col-md-4 mb-3">
<div class="card">
<div class="card-body">
<h2 class="card-title">{{ s.saved.title }}</h2>
<p class="card-subtitle"><strong>{{ s.export.title|trans }}</strong></p>
<div class="card-text">
{{ s.saved.description|chill_markdown_to_html }}
</div>
<div class="createdBy">{{ 'saved_export.Created on %date%'|trans({'%date%': s.saved.createdAt|format_datetime('long', 'short')}) }}</div>
<ul class="record_actions">
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_delete', {'id': s.saved.id }) }}" class="btn btn-delete"></a></li>
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_edit', {'id': s.saved.id }) }}" class="btn btn-edit"></a></li>
<li><a href="{{ path('chill_main_export_generate_from_saved', { id: s.saved.id }) }}" class="btn btn-action"><i class="fa fa-cog"></i></a></li>
</ul>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endfor %}
{% if grouped_exports|keys|length > 1 and grouped_exports['_']|length > 0 %}
<h2 class="display-6">{{ 'Ungrouped exports'|trans }}</h2>
{% endif %}
<div class="row ungrouped">
{% for saveds in grouped_exports['_']|default([]) %}
{% for s in saveds %}
<div class="col-6 col-md-4 mb-3">
<div class="col-6 col-md-4 mb-3">
<div class="card">
<div class="card-body">
<h2 class="card-title">{{ s.saved.title }}</h2>
<p class="card-subtitle"><strong>{{ s.export.title|trans }}</strong></p>
<div class="card-text">
{{ s.saved.description|chill_markdown_to_html }}
</div>
<div class="createdBy">{{ 'saved_export.Created on %date%'|trans({'%date%': s.saved.createdAt|format_datetime('long', 'short')}) }}</div>
<ul class="record_actions">
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_delete', {'id': s.saved.id }) }}" class="btn btn-delete"></a></li>
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_edit', {'id': s.saved.id }) }}" class="btn btn-edit"></a></li>
<li><a href="{{ path('chill_main_export_generate_from_saved', { id: s.saved.id }) }}" class="btn btn-action"><i class="fa fa-cog"></i></a></li>
</ul>
</div>
</div>
</div>
</div>
{% endfor %}
{% endfor %}
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends "@ChillMain/layout.html.twig" %}
{% block title %}{{ 'saved_export.New'|trans }}{% endblock %}
{% block content %}
<h1>{{ block('title') }}</h1>
{{ form_start(form) }}
{{ form_row(form.title) }}
{{ form_row(form.description) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ chill_return_path_or('chill_main_homepage') }}" class="btn btn-cancel">{{ 'Cancel'|trans }}</a>
</li>
<li>
<button type="submit" class="btn btn-save">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(form) }}
{% endblock %}

View File

@ -0,0 +1,52 @@
<?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\MainBundle\Security\Authorization;
use Chill\MainBundle\Entity\SavedExport;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use UnexpectedValueException;
use function in_array;
class SavedExportVoter extends Voter
{
public const DELETE = 'CHLL_MAIN_EXPORT_SAVED_DELETE';
public const EDIT = 'CHLL_MAIN_EXPORT_SAVED_EDIT';
public const GENERATE = 'CHLL_MAIN_EXPORT_SAVED_GENERATE';
private const ALL = [
self::DELETE,
self::EDIT,
self::GENERATE,
];
protected function supports($attribute, $subject): bool
{
return $subject instanceof SavedExport && in_array($attribute, self::ALL, true);
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{
/** @var SavedExport $subject */
switch ($attribute) {
case self::DELETE:
case self::EDIT:
case self::GENERATE:
return $subject->getUser() === $token->getUser();
default:
throw new UnexpectedValueException('attribute not supported: ' . $attribute);
}
}
}

View File

@ -0,0 +1,101 @@
<?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\MainBundle\Service\RollingDate;
use DateTimeImmutable;
class RollingDate
{
public const ALL_T = [
self::T_YEAR_PREVIOUS_START,
self::T_QUARTER_PREVIOUS_START,
self::T_MONTH_PREVIOUS_START,
self::T_WEEK_PREVIOUS_START,
self::T_YEAR_CURRENT_START,
self::T_QUARTER_CURRENT_START,
self::T_MONTH_CURRENT_START,
self::T_WEEK_CURRENT_START,
self::T_TODAY,
self::T_WEEK_NEXT_START,
self::T_MONTH_NEXT_START,
self::T_QUARTER_NEXT_START,
self::T_YEAR_NEXT_START,
self::T_FIXED_DATE,
];
/**
* A given fixed date.
*/
public const T_FIXED_DATE = 'fixed_date';
public const T_MONTH_CURRENT_START = 'month_current_start';
public const T_MONTH_NEXT_START = 'month_next_start';
public const T_MONTH_PREVIOUS_START = 'month_previous_start';
public const T_QUARTER_CURRENT_START = 'quarter_current_start';
public const T_QUARTER_NEXT_START = 'quarter_next_start';
public const T_QUARTER_PREVIOUS_START = 'quarter_previous_start';
public const T_TODAY = 'today';
public const T_WEEK_CURRENT_START = 'week_current_start';
public const T_WEEK_NEXT_START = 'week_next_start';
public const T_WEEK_PREVIOUS_START = 'week_previous_start';
public const T_YEAR_CURRENT_START = 'year_current_start';
public const T_YEAR_NEXT_START = 'year_next_start';
public const T_YEAR_PREVIOUS_START = 'year_previous_start';
private ?DateTimeImmutable $fixedDate;
/**
* The pivot date is the date from the rolling is computed. By default, it is "now".
*/
private DateTimeImmutable $pivotDate;
private string $roll;
/**
* @param string|self::T_* $roll
* @param DateTimeImmutable|null $pivotDate Will be "now" if null is given
* @param DateTimeImmutable|null $fixedDate Only to insert if $roll equals @see{self::T_FIXED_DATE}
*/
public function __construct(string $roll, ?DateTimeImmutable $fixedDate = null, ?DateTimeImmutable $pivotDate = null)
{
$this->roll = $roll;
$this->pivotDate = $pivotDate ?? new DateTimeImmutable('now');
$this->fixedDate = $fixedDate;
}
public function getFixedDate(): ?DateTimeImmutable
{
return $this->fixedDate;
}
public function getPivotDate(): DateTimeImmutable
{
return $this->pivotDate;
}
public function getRoll(): string
{
return $this->roll;
}
}

View File

@ -0,0 +1,142 @@
<?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\MainBundle\Service\RollingDate;
use DateInterval;
use DateTimeImmutable;
use LogicException;
use UnexpectedValueException;
class RollingDateConverter implements RollingDateConverterInterface
{
public function convert(RollingDate $rollingDate): DateTimeImmutable
{
switch ($rollingDate->getRoll()) {
case RollingDate::T_MONTH_CURRENT_START:
return $this->toBeginOfMonth($rollingDate->getPivotDate());
case RollingDate::T_MONTH_NEXT_START:
return $this->toBeginOfMonth($rollingDate->getPivotDate()->add(new DateInterval('P1M')));
case RollingDate::T_MONTH_PREVIOUS_START:
return $this->toBeginOfMonth($rollingDate->getPivotDate()->sub(new DateInterval('P1M')));
case RollingDate::T_QUARTER_CURRENT_START:
return $this->toBeginOfQuarter($rollingDate->getPivotDate());
case RollingDate::T_QUARTER_NEXT_START:
return $this->toBeginOfQuarter($rollingDate->getPivotDate()->add(new DateInterval('P3M')));
case RollingDate::T_QUARTER_PREVIOUS_START:
return $this->toBeginOfQuarter($rollingDate->getPivotDate()->sub(new DateInterval('P3M')));
case RollingDate::T_WEEK_CURRENT_START:
return $this->toBeginOfWeek($rollingDate->getPivotDate());
case RollingDate::T_WEEK_NEXT_START:
return $this->toBeginOfWeek($rollingDate->getPivotDate()->add(new DateInterval('P1W')));
case RollingDate::T_WEEK_PREVIOUS_START:
return $this->toBeginOfWeek($rollingDate->getPivotDate()->sub(new DateInterval('P1W')));
case RollingDate::T_YEAR_CURRENT_START:
return $this->toBeginOfYear($rollingDate->getPivotDate());
case RollingDate::T_YEAR_PREVIOUS_START:
return $this->toBeginOfYear($rollingDate->getPivotDate()->sub(new DateInterval('P1Y')));
case RollingDate::T_YEAR_NEXT_START:
return $this->toBeginOfYear($rollingDate->getPivotDate()->add(new DateInterval('P1Y')));
case RollingDate::T_TODAY:
return $rollingDate->getPivotDate();
case RollingDate::T_FIXED_DATE:
if (null === $rollingDate->getFixedDate()) {
throw new LogicException('You must provide a fixed date when selecting a fixed date');
}
return $rollingDate->getFixedDate();
default:
throw new UnexpectedValueException(sprintf('%s rolling operation not supported', $rollingDate->getRoll()));
}
}
private function toBeginOfMonth(DateTimeImmutable $date): DateTimeImmutable
{
return DateTimeImmutable::createFromFormat(
'Y-m-d His',
sprintf('%s-%s-01 000000', $date->format('Y'), $date->format('m'))
);
}
private function toBeginOfQuarter(DateTimeImmutable $date): DateTimeImmutable
{
switch ((int) $date->format('n')) {
case 1:
case 2:
case 3:
$month = '01';
break;
case 4:
case 5:
case 6:
$month = '04';
break;
case 7:
case 8:
case 9:
$month = '07';
break;
case 10:
case 11:
case 12:
$month = '10';
break;
default:
throw new LogicException('this month is not valid: ' . $date->format('n'));
}
return DateTimeImmutable::createFromFormat(
'Y-m-d His',
sprintf('%s-%s-01 000000', $date->format('Y'), $month)
);
}
private function toBeginOfWeek(DateTimeImmutable $date): DateTimeImmutable
{
if (1 === $dayOfWeek = (int) $date->format('N')) {
return $date->setTime(0, 0, 0);
}
return $date
->sub(new DateInterval('P' . ($dayOfWeek - 1) . 'D'))
->setTime(0, 0, 0);
}
private function toBeginOfYear(DateTimeImmutable $date): DateTimeImmutable
{
return DateTimeImmutable::createFromFormat(
'Y-m-d His',
sprintf('%s-01-01 000000', $date->format('Y'))
);
}
}

View File

@ -0,0 +1,19 @@
<?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\MainBundle\Service\RollingDate;
use DateTimeImmutable;
interface RollingDateConverterInterface
{
public function convert(RollingDate $rollingDate): DateTimeImmutable;
}

View File

@ -0,0 +1,53 @@
<?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 Form\Type;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase;
/**
* @internal
* @coversNothing
*/
final class PickRollingDateTypeTest extends TypeTestCase
{
public function testSubmitValidData()
{
$formData = [
'roll' => 'year_previous_start',
'fixedDate' => null,
];
$form = $this->factory->create(PickRollingDateType::class);
$form->submit($formData);
$this->assertTrue($form->isSynchronized());
/** @var RollingDate $rollingDate */
$rollingDate = $form->getData();
$this->assertInstanceOf(RollingDate::class, $rollingDate);
$this->assertEquals(RollingDate::T_YEAR_PREVIOUS_START, $rollingDate->getRoll());
}
protected function getExtensions(): array
{
$type = new PickRollingDateType();
return [
new PreloadedExtension([$type], []),
];
}
}

View File

@ -0,0 +1,101 @@
<?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 Services\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverter;
use DateTime;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
final class RollingDateConverterTest extends TestCase
{
private RollingDateConverter $converter;
protected function setUp(): void
{
$this->converter = new RollingDateConverter();
}
public function generateDataConversionDate(): iterable
{
$format = 'Y-m-d His';
yield [RollingDate::T_MONTH_CURRENT_START, '2022-11-01 000000', $format];
yield [RollingDate::T_MONTH_NEXT_START, '2022-12-01 000000', $format];
yield [RollingDate::T_MONTH_PREVIOUS_START, '2022-10-01 000000', $format];
yield [RollingDate::T_QUARTER_CURRENT_START, '2022-10-01 000000', $format];
yield [RollingDate::T_QUARTER_NEXT_START, '2023-01-01 000000', $format];
yield [RollingDate::T_QUARTER_PREVIOUS_START, '2022-07-01 000000', $format];
yield [RollingDate::T_TODAY, '2022-11-07 000000', $format];
yield [RollingDate::T_WEEK_CURRENT_START, '2022-11-07 000000', $format];
yield [RollingDate::T_WEEK_NEXT_START, '2022-11-14 000000', $format];
yield [RollingDate::T_WEEK_PREVIOUS_START, '2022-10-31 000000', $format];
yield [RollingDate::T_YEAR_CURRENT_START, '2022-01-01 000000', $format];
yield [RollingDate::T_YEAR_NEXT_START, '2023-01-01 000000', $format];
yield [RollingDate::T_YEAR_PREVIOUS_START, '2021-01-01 000000', $format];
}
public function testConversionFixedDate()
{
$rollingDate = new RollingDate(RollingDate::T_FIXED_DATE, new DateTimeImmutable('2022-01-01'));
$this->assertEquals(
'2022-01-01',
$this->converter->convert($rollingDate)->format('Y-m-d')
);
}
public function testConvertOnDateNow()
{
$rollingDate = new RollingDate(RollingDate::T_YEAR_PREVIOUS_START);
$actual = $this->converter->convert($rollingDate);
$this->assertEquals(
(int) (new DateTimeImmutable('now'))->format('Y') - 1,
(int) $actual->format('Y')
);
$this->assertEquals(1, (int) $actual->format('m'));
$this->assertEquals(1, (int) $actual->format('d'));
}
/**
* @dataProvider generateDataConversionDate
*/
public function testConvertOnPivotDate(string $roll, string $expectedDateTime, string $format)
{
$pivot = DateTimeImmutable::createFromFormat('Y-m-d His', '2022-11-07 000000');
$rollingDate = new RollingDate($roll, null, $pivot);
$this->assertEquals(
DateTime::createFromFormat($format, $expectedDateTime),
$this->converter->convert($rollingDate)
);
}
}

View File

@ -105,3 +105,8 @@ services:
resource: '../Service/Import/'
autowire: true
autoconfigure: true
Chill\MainBundle\Service\RollingDate\:
resource: '../Service/RollingDate/'
autowire: true
autoconfigure: true

View File

@ -50,6 +50,8 @@ services:
tags:
- { name: security.voter }
Chill\MainBundle\Security\Authorization\SavedExportVoter: ~
Chill\MainBundle\Security\PasswordRecover\TokenManager:
arguments:
$secret: '%kernel.secret%'

View File

@ -0,0 +1,43 @@
<?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\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20221107212201 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('DROP TABLE chill_main_saved_export');
}
public function getDescription(): string
{
return 'Create table for saved exports';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE chill_main_saved_export (id UUID NOT NULL, user_id INT DEFAULT NULL, description TEXT DEFAULT \'\' NOT NULL, exportAlias TEXT DEFAULT \'\' NOT NULL, options JSONB DEFAULT \'[]\' NOT NULL, title TEXT DEFAULT \'\' NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_C2029B22A76ED395 ON chill_main_saved_export (user_id)');
$this->addSql('CREATE INDEX IDX_C2029B223174800F ON chill_main_saved_export (createdBy_id)');
$this->addSql('CREATE INDEX IDX_C2029B2265FF1AEC ON chill_main_saved_export (updatedBy_id)');
$this->addSql('COMMENT ON COLUMN chill_main_saved_export.id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN chill_main_saved_export.options IS \'(DC2Type:json)\'');
$this->addSql('COMMENT ON COLUMN chill_main_saved_export.createdAt IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN chill_main_saved_export.updatedAt IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE chill_main_saved_export ADD CONSTRAINT FK_C2029B22A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_main_saved_export ADD CONSTRAINT FK_C2029B223174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_main_saved_export ADD CONSTRAINT FK_C2029B2265FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
}

View File

@ -538,3 +538,32 @@ export:
isNoAddress: Adresse incomplète ?
_lat: Latitude
_lon: Longitude
rolling_date:
year_previous_start: Début de l'année précédente
quarter_previous_start: Début du trimestre précédent
month_previous_start: Début du mois précédent
week_previous_start: Début de la semaine précédente
year_current_start: Début de l'année courante
quarter_current_start: Début du trimestre courant
month_current_start: Début du mois courant
week_current_start: Début de la semaine courante
today: Aujourd'hui (aucune modification de la date courante)
year_next_start: Début de l'année suivante
quarter_next_start: Début du trimestre suivante
month_next_start: Début du mois suivant
week_next_start: Début de la semaine suivante
fixed_date: Date fixe
roll_movement: Modification par rapport à aujourd'hui
fixed_date_date: Date fixe
saved_export:
Any saved export: Aucun rapport enregistré
New: Nouveau rapport enregistré
Edit: Modifier un rapport enregistré
Delete saved ?: Supprimer un rapport enregistré ?
Are you sure you want to delete this saved ?: Êtes-vous sûr·e de vouloir supprimer ce rapport ?
My saved exports: Mes rapports enregistrés
Export is deleted: Le rapport est supprimé
Saved export is saved!: Le rapport est enregistré
Created on %date%: Créé le %date%

View File

@ -12,10 +12,11 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Export\Declarations;
use DateTime;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@ -27,11 +28,15 @@ final class StepAggregator implements AggregatorInterface
private const P = 'acp_step_agg_date';
private RollingDateConverterInterface $rollingDateConverter;
private TranslatorInterface $translator;
public function __construct(
RollingDateConverterInterface $rollingDateConverter,
TranslatorInterface $translator
) {
$this->rollingDateConverter = $rollingDateConverter;
$this->translator = $translator;
}
@ -60,7 +65,7 @@ final class StepAggregator implements AggregatorInterface
)
)
)
->setParameter(self::P, $data['on_date'])
->setParameter(self::P, $this->rollingDateConverter->convert($data['on_date']))
->addGroupBy('step_aggregator');
}
@ -71,8 +76,8 @@ final class StepAggregator implements AggregatorInterface
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('on_date', ChillDateType::class, [
'data' => new DateTime(),
$builder->add('on_date', PickRollingDateType::class, [
'data' => new RollingDate(RollingDate::T_TODAY),
]);
}

View File

@ -12,15 +12,23 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\PersonBundle\Export\Declarations;
use DateTime;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class OpenBetweenDatesFilter implements FilterInterface
{
private RollingDateConverterInterface $rollingDateConverter;
public function __construct(RollingDateConverterInterface $rollingDateConverter)
{
$this->rollingDateConverter = $rollingDateConverter;
}
public function addRole(): ?string
{
return null;
@ -34,8 +42,8 @@ class OpenBetweenDatesFilter implements FilterInterface
);
$qb->andWhere($clause);
$qb->setParameter('datefrom', $data['date_from'], Types::DATE_MUTABLE);
$qb->setParameter('dateto', $data['date_to'], Types::DATE_MUTABLE);
$qb->setParameter('datefrom', $this->rollingDateConverter->convert($data['date_from']), Types::DATE_IMMUTABLE);
$qb->setParameter('dateto', $this->rollingDateConverter->convert($data['date_to']), Types::DATE_IMMUTABLE);
}
public function applyOn(): string
@ -46,19 +54,19 @@ class OpenBetweenDatesFilter implements FilterInterface
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('date_from', ChillDateType::class, [
'data' => new DateTime(),
->add('date_from', PickRollingDateType::class, [
'data' => new RollingDate(RollingDate::T_MONTH_PREVIOUS_START),
])
->add('date_to', ChillDateType::class, [
'data' => new DateTime(),
->add('date_to', PickRollingDateType::class, [
'data' => new RollingDate(RollingDate::T_TODAY),
]);
}
public function describeAction($data, $format = 'string'): array
{
return ['Filtered by opening dates: between %datefrom% and %dateto%', [
'%datefrom%' => $data['date_from']->format('d-m-Y'),
'%dateto%' => $data['date_to']->format('d-m-Y'),
'%datefrom%' => $this->rollingDateConverter->convert($data['date_from'])->format('d-m-Y'),
'%dateto%' => $this->rollingDateConverter->convert($data['date_to'])->format('d-m-Y'),
]];
}

View File

@ -12,31 +12,40 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\Query\Expr\Andx;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function in_array;
class StepFilter implements FilterInterface
{
private const A = 'acp_filter_bystep_stephistories';
private const DEFAULT_CHOICE = AccompanyingPeriod::STEP_CONFIRMED;
private const P = 'acp_step_filter_date';
private const STEPS = [
'Draft' => AccompanyingPeriod::STEP_DRAFT,
'Confirmed' => AccompanyingPeriod::STEP_CONFIRMED,
'Closed' => AccompanyingPeriod::STEP_CLOSED,
];
private RollingDateConverterInterface $rollingDateConverter;
/**
* @var TranslatorInterface
*/
protected $translator;
private $translator;
public function __construct(TranslatorInterface $translator)
public function __construct(RollingDateConverterInterface $rollingDateConverter, TranslatorInterface $translator)
{
$this->rollingDateConverter = $rollingDateConverter;
$this->translator = $translator;
}
@ -47,17 +56,25 @@ class StepFilter implements FilterInterface
public function alterQuery(QueryBuilder $qb, $data)
{
$where = $qb->getDQLPart('where');
$clause = $qb->expr()->eq('acp.step', ':step');
if ($where instanceof Andx) {
$where->add($clause);
} else {
$where = $qb->expr()->andX($clause);
if (!in_array(self::A, $qb->getAllAliases(), true)) {
$qb->leftJoin('acp.stepHistories', self::A);
}
$qb->add('where', $where);
$qb->setParameter('step', $data['accepted_steps']);
$qb
->andWhere(
$qb->expr()->andX(
$qb->expr()->lte(self::A . '.startDate', ':' . self::P),
$qb->expr()->orX(
$qb->expr()->isNull(self::A . '.endDate'),
$qb->expr()->lt(self::A . '.endDate', ':' . self::P)
)
)
)
->andWhere(
$qb->expr()->in(self::A . '.step', ':acp_filter_by_step_steps')
)
->setParameter(self::P, $this->rollingDateConverter->convert($data['calc_date']))
->setParameter('acp_filter_by_step_steps', $data['accepted_steps']);
}
public function applyOn()
@ -67,13 +84,17 @@ class StepFilter implements FilterInterface
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('accepted_steps', ChoiceType::class, [
'choices' => self::STEPS,
'multiple' => false,
'expanded' => true,
'empty_data' => self::DEFAULT_CHOICE,
'data' => self::DEFAULT_CHOICE,
]);
$builder
->add('accepted_steps', ChoiceType::class, [
'choices' => self::STEPS,
'multiple' => false,
'expanded' => true,
'empty_data' => self::DEFAULT_CHOICE,
'data' => self::DEFAULT_CHOICE,
])
->add('calc_date', PickRollingDateType::class, [
'label' => 'export.acp.filter.by_step.date_calc',
]);
}
public function describeAction($data, $format = 'string')