Partage d'export enregistré et génération asynchrone des exports

This commit is contained in:
2025-07-08 13:53:25 +00:00
parent c4cc0baa8e
commit 8bc16dadb0
447 changed files with 14134 additions and 3854 deletions

View File

@@ -1,56 +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\MainBundle\Form\DataMapper;
use Chill\MainBundle\Entity\Regroupment;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\FormInterface;
final readonly class ExportPickCenterDataMapper implements DataMapperInterface
{
public function mapDataToForms($viewData, \Traversable $forms): void
{
if (null === $viewData) {
return;
}
/** @var array<string, FormInterface> $form */
$form = iterator_to_array($forms);
$form['center']->setData($viewData);
// NOTE: we do not map back the regroupments
}
public function mapFormsToData(\Traversable $forms, &$viewData): void
{
/** @var array<string, FormInterface> $forms */
$forms = iterator_to_array($forms);
$centers = [];
foreach ($forms['center']->getData() as $center) {
$centers[spl_object_hash($center)] = $center;
}
if (\array_key_exists('regroupment', $forms)) {
/** @var Regroupment $regroupment */
foreach ($forms['regroupment']->getData() as $regroupment) {
foreach ($regroupment->getCenters() as $center) {
$centers[spl_object_hash($center)] = $center;
}
}
}
$viewData = array_values($centers);
}
}

View File

@@ -13,15 +13,22 @@ namespace Chill\MainBundle\Form;
use Chill\MainBundle\Entity\SavedExport;
use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Form\Type\PickUserGroupOrUserDynamicType;
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Security;
class SavedExportType extends AbstractType
{
public function __construct(private readonly Security $security) {}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$savedExport = $options['data'];
$builder
->add('title', TextType::class, [
'required' => true,
@@ -29,6 +36,14 @@ class SavedExportType extends AbstractType
->add('description', ChillTextareaType::class, [
'required' => false,
]);
if ($this->security->isGranted(SavedExportVoter::SHARE, $savedExport)) {
$builder->add('share', PickUserGroupOrUserDynamicType::class, [
'multiple' => true,
'required' => false,
'label' => 'saved_export.Share',
]);
}
}
public function configureOptions(OptionsResolver $resolver)

View File

@@ -66,8 +66,11 @@ class EntityToJsonTransformer implements DataTransformerInterface
]);
}
private function denormalizeOne(array $item)
private function denormalizeOne(array|string $item)
{
if ('me' === $item) {
return $item;
}
if (!\array_key_exists('type', $item)) {
throw new TransformationFailedException('the key "type" is missing on element');
}
@@ -98,5 +101,6 @@ class EntityToJsonTransformer implements DataTransformerInterface
'json',
$context,
);
}
}

View File

@@ -21,15 +21,18 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class AggregatorType extends AbstractType
{
public const ENABLED_FIELD = 'enabled';
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$exportManager = $options['export_manager'];
$aggregator = $exportManager->getAggregator($options['aggregator_alias']);
$builder
->add('enabled', CheckboxType::class, [
->add(self::ENABLED_FIELD, CheckboxType::class, [
'value' => true,
'required' => false,
'disabled' => $options['disable_enable_field'],
]);
$aggregatorFormBuilder = $builder->create('form', FormType::class, [
@@ -53,6 +56,7 @@ class AggregatorType extends AbstractType
{
$resolver->setRequired('aggregator_alias')
->setRequired('export_manager')
->setDefault('disable_enable_field', false)
->setDefault('compound', true)
->setDefault('error_bubbling', false);
}

View File

@@ -35,7 +35,7 @@ class ExportType extends AbstractType
public function __construct(
private readonly ExportManager $exportManager,
private readonly SortExportElement $sortExportElement,
protected ParameterBagInterface $parameterBag,
ParameterBagInterface $parameterBag,
) {
$this->personFieldsConfig = $parameterBag->get('chill_person.person_fields');
}
@@ -43,6 +43,8 @@ class ExportType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$export = $this->exportManager->getExport($options['export_alias']);
/** @var bool $canEditFull */
$canEditFull = $options['can_edit_full'];
$exportOptions = [
'compound' => true,
@@ -59,8 +61,18 @@ class ExportType extends AbstractType
if ($export instanceof \Chill\MainBundle\Export\ExportInterface) {
// add filters
$filters = $this->exportManager->getFiltersApplyingOn($export, $options['picked_centers']);
$filterAliases = $options['allowed_filters'];
$filters = [];
if (is_iterable($filterAliases)) {
foreach ($filterAliases as $alias => $filter) {
$filters[$alias] = $filter;
}
} else {
$filters = $this->exportManager->getFiltersApplyingOn($export, $options['picked_centers']);
}
$this->sortExportElement->sortFilters($filters);
$filterBuilder = $builder->create(self::FILTER_KEY, FormType::class, ['compound' => true]);
foreach ($filters as $alias => $filter) {
@@ -70,15 +82,26 @@ class ExportType extends AbstractType
'constraints' => [
new ExportElementConstraint(['element' => $filter]),
],
'disable_enable_field' => !$canEditFull,
]);
}
$builder->add($filterBuilder);
// add aggregators
$aggregators = $this->exportManager
->getAggregatorsApplyingOn($export, $options['picked_centers']);
$aggregatorsAliases = $options['allowed_aggregators'];
$aggregators = [];
if (is_iterable($aggregatorsAliases)) {
foreach ($aggregatorsAliases as $alias => $aggregator) {
$aggregators[$alias] = $aggregator;
}
} else {
$aggregators = $this->exportManager
->getAggregatorsApplyingOn($export, $options['picked_centers']);
}
$this->sortExportElement->sortAggregators($aggregators);
$aggregatorBuilder = $builder->create(
self::AGGREGATOR_KEY,
FormType::class,
@@ -96,11 +119,11 @@ class ExportType extends AbstractType
}
}
$aggregatorBuilder->add($alias, AggregatorType::class, [
'aggregator_alias' => $alias,
'export_manager' => $this->exportManager,
'label' => $aggregator->getTitle(),
'disable_enable_field' => !$canEditFull,
'constraints' => [
new ExportElementConstraint(['element' => $aggregator]),
],
@@ -125,8 +148,13 @@ class ExportType extends AbstractType
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setRequired(['export_alias', 'picked_centers'])
$resolver->setRequired(['export_alias', 'picked_centers', 'can_edit_full'])
->setAllowedTypes('export_alias', ['string'])
->setAllowedValues('can_edit_full', [true, false])
->setDefault('allowed_filters', null)
->setAllowedTypes('allowed_filters', ['iterable', 'null'])
->setDefault('allowed_aggregators', null)
->setAllowedTypes('allowed_aggregators', ['iterable', 'null'])
->setDefault('compound', true)
->setDefault('constraints', [
// new \Chill\MainBundle\Validator\Constraints\Export\ExportElementConstraint()

View File

@@ -34,6 +34,7 @@ class FilterType extends AbstractType
->add(self::ENABLED_FIELD, CheckboxType::class, [
'value' => true,
'required' => false,
'disabled' => $options['disable_enable_field'],
]);
$filterFormBuilder = $builder->create('form', FormType::class, [
@@ -58,6 +59,7 @@ class FilterType extends AbstractType
$resolver
->setRequired('filter')
->setAllowedTypes('filter', [FilterInterface::class])
->setDefault('disable_enable_field', false)
->setDefault('compound', true)
->setDefault('error_bubbling', false);
}

View File

@@ -14,9 +14,9 @@ namespace Chill\MainBundle\Form\Type\Export;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Regroupment;
use Chill\MainBundle\Export\ExportManager;
use Chill\MainBundle\Form\DataMapper\ExportPickCenterDataMapper;
use Chill\MainBundle\Repository\RegroupmentRepository;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
use Chill\MainBundle\Service\Regroupement\RegroupementFiltering;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
@@ -27,27 +27,26 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
*/
final class PickCenterType extends AbstractType
{
public const CENTERS_IDENTIFIERS = 'c';
public function __construct(
private readonly ExportManager $exportManager,
private readonly RegroupmentRepository $regroupmentRepository,
private readonly AuthorizationHelperForCurrentUserInterface $authorizationHelper,
private readonly RegroupementFiltering $regroupementFiltering,
) {}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$export = $this->exportManager->getExport($options['export_alias']);
$centers = $this->authorizationHelper->getReachableCenters(
$export->requiredRole()
$export->requiredRole(),
);
$centersActive = array_filter($centers, fn (Center $c) => $c->getIsActive());
// order alphabetically
usort($centersActive, fn (Center $a, Center $b) => $a->getCenter() <=> $b->getName());
usort($centersActive, fn (Center $a, Center $b) => $a->getName() <=> $b->getName());
$builder->add('center', EntityType::class, [
$builder->add('centers', EntityType::class, [
'class' => Center::class,
'choices' => $centersActive,
'label' => 'center',
@@ -56,18 +55,22 @@ final class PickCenterType extends AbstractType
'choice_label' => static fn (Center $c) => $c->getName(),
]);
if (\count($this->regroupmentRepository->findAllActive()) > 0) {
$builder->add('regroupment', EntityType::class, [
$groups = $this->regroupementFiltering
->filterContainsAtLeastOneCenter($this->regroupmentRepository->findAllActive(), $centersActive);
// order alphabetically
usort($groups, fn (Regroupment $a, Regroupment $b) => $a->getName() <=> $b->getName());
if (\count($groups) > 0) {
$builder->add('regroupments', EntityType::class, [
'class' => Regroupment::class,
'label' => 'regroupment',
'multiple' => true,
'expanded' => true,
'choices' => $this->regroupmentRepository->findAllActive(),
'choices' => $groups,
'choice_label' => static fn (Regroupment $r) => $r->getName(),
]);
}
$builder->setDataMapper(new ExportPickCenterDataMapper());
}
public function configureOptions(OptionsResolver $resolver)

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\Form\Type;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\Type\DataTransformer\EntityToJsonTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Pick user dymically, using vuejs module "AddPerson".
*
* Possible options:
*
* - `multiple`: pick one or more users
* - `suggested`: a list of suggested users
* - `suggest_myself`: append the current user to the list of suggested
* - `as_id`: only the id will be set in the returned data
* - `submit_on_adding_new_entity`: the browser will immediately submit the form when new users are checked
*/
class PickUserOrMeDynamicType extends AbstractType
{
public function __construct(
private readonly DenormalizerInterface $denormalizer,
private readonly SerializerInterface $serializer,
private readonly NormalizerInterface $normalizer,
) {}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer(new EntityToJsonTransformer($this->denormalizer, $this->serializer, $options['multiple'], 'user'));
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['multiple'] = $options['multiple'];
$view->vars['types'] = ['user'];
$view->vars['uniqid'] = uniqid('pick_user_or_me_dyn');
$view->vars['suggested'] = [];
$view->vars['as_id'] = true === $options['as_id'] ? '1' : '0';
$view->vars['submit_on_adding_new_entity'] = true === $options['submit_on_adding_new_entity'] ? '1' : '0';
foreach ($options['suggested'] as $user) {
$view->vars['suggested'][] = $this->normalizer->normalize($user, 'json', ['groups' => 'read']);
}
// $user = /* should come from context */ $options['context'];
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefault('multiple', false)
->setAllowedTypes('multiple', ['bool'])
->setDefault('compound', false)
->setDefault('suggested', [])
// if set to true, only the id will be set inside the content. The denormalization will not work.
->setDefault('as_id', false)
->setAllowedTypes('as_id', ['bool'])
->setDefault('submit_on_adding_new_entity', false)
->setAllowedTypes('submit_on_adding_new_entity', ['bool']);
}
public function getBlockPrefix()
{
return 'pick_entity_dynamic';
}
}

View File

@@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form;
use Chill\MainBundle\Entity\UserGroup;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
use Symfony\Component\Form\AbstractType;
@@ -23,6 +24,9 @@ class UserGroupType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
/** @var UserGroup $userGroup */
$userGroup = $options['data'];
$builder
->add('label', TranslatableStringFormType::class, [
'label' => 'user_group.Label',
@@ -46,20 +50,25 @@ class UserGroupType extends AbstractType
'help' => 'user_group.ExcludeKeyHelp',
'required' => false,
'empty_data' => '',
])
->add('users', PickUserDynamicType::class, [
'label' => 'user_group.Users',
'multiple' => true,
'required' => false,
'empty_data' => [],
])
->add('adminUsers', PickUserDynamicType::class, [
'label' => 'user_group.adminUsers',
'multiple' => true,
'required' => false,
'empty_data' => [],
'help' => 'user_group.adminUsersHelp',
])
;
]);
if (!$userGroup->hasUserJob()) {
$builder
->add('users', PickUserDynamicType::class, [
'label' => 'user_group.Users',
'multiple' => true,
'required' => false,
'empty_data' => [],
])
->add('adminUsers', PickUserDynamicType::class, [
'label' => 'user_group.adminUsers',
'multiple' => true,
'required' => false,
'empty_data' => [],
'help' => 'user_group.adminUsersHelp',
])
;
}
}
}