Merge branch '111_exports_suite' into calendar/finalization

This commit is contained in:
2022-10-24 11:10:44 +02:00
158 changed files with 3064 additions and 1522 deletions

View File

@@ -22,6 +22,7 @@ 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\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
@@ -191,18 +192,12 @@ class ExportController extends AbstractController
case 'export':
return $this->exportFormStep($request, $export, $alias);
break;
case 'formatter':
return $this->formatterFormStep($request, $export, $alias);
break;
case 'generate':
return $this->forwardToGenerate($request, $export, $alias);
break;
default:
throw $this->createNotFoundException("The given step '{$step}' is invalid");
}
@@ -214,10 +209,8 @@ class ExportController extends AbstractController
* @param string $alias
* @param array $data the data from previous step. Required for steps 'formatter' and 'generate_formatter'
* @param mixed $step
*
* @return \Symfony\Component\Form\Form
*/
protected function createCreateFormExport($alias, $step, $data = [])
protected function createCreateFormExport($alias, $step, $data = []): FormInterface
{
/** @var \Chill\MainBundle\Export\ExportManager $exportManager */
$exportManager = $this->exportManager;
@@ -475,8 +468,6 @@ class ExportController extends AbstractController
* @param \Chill\MainBundle\Export\DirectExportInterface|\Chill\MainBundle\Export\ExportInterface $export
* @param string $alias
*
* @throws type
*
* @return Response
*/
protected function selectCentersStep(Request $request, $export, $alias)
@@ -543,6 +534,8 @@ class ExportController extends AbstractController
}
}
}
return '';
}
/**

View File

@@ -22,6 +22,7 @@ use Chill\MainBundle\Controller\UserController;
use Chill\MainBundle\Controller\UserJobApiController;
use Chill\MainBundle\Controller\UserJobController;
use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface;
use Chill\MainBundle\Doctrine\DQL\Age;
use Chill\MainBundle\Doctrine\DQL\Extract;
use Chill\MainBundle\Doctrine\DQL\GetJsonFieldByKey;
use Chill\MainBundle\Doctrine\DQL\JsonAggregate;
@@ -248,6 +249,7 @@ class ChillMainExtension extends Extension implements
'datetime_functions' => [
'EXTRACT' => Extract::class,
'TO_CHAR' => ToChar::class,
'AGE' => Age::class,
],
],
'hydrators' => [

View File

@@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\DependencyInjection\CompilerPass;
use Chill\MainBundle\Export\ExportManager;
use LogicException;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -30,53 +31,19 @@ class ExportsCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->has('Chill\MainBundle\Export\ExportManager')) {
throw new LogicException('service Chill\MainBundle\Export\ExportManager '
if (!$container->has(ExportManager::class)) {
throw new LogicException('service ' . ExportManager::class . ' '
. 'is not defined. It is required by ExportsCompilerPass');
}
$chillManagerDefinition = $container->findDefinition(
'Chill\MainBundle\Export\ExportManager'
ExportManager::class
);
$this->compileExports($chillManagerDefinition, $container);
$this->compileFilters($chillManagerDefinition, $container);
$this->compileAggregators($chillManagerDefinition, $container);
$this->compileFormatters($chillManagerDefinition, $container);
$this->compileExportElementsProvider($chillManagerDefinition, $container);
}
private function compileAggregators(
Definition $chillManagerDefinition,
ContainerBuilder $container
) {
$taggedServices = $container->findTaggedServiceIds(
'chill.export_aggregator'
);
$knownAliases = [];
foreach ($taggedServices as $id => $tagAttributes) {
foreach ($tagAttributes as $attributes) {
if (!isset($attributes['alias'])) {
throw new LogicException("the 'alias' attribute is missing in your " .
"service '{$id}' definition");
}
if (array_search($attributes['alias'], $knownAliases, true)) {
throw new LogicException('There is already a chill.export_aggregator service with alias '
. $attributes['alias'] . '. Choose another alias.');
}
$knownAliases[] = $attributes['alias'];
$chillManagerDefinition->addMethodCall(
'addAggregator',
[new Reference($id), $attributes['alias']]
);
}
}
}
private function compileExportElementsProvider(
Definition $chillManagerDefinition,
ContainerBuilder $container
@@ -108,68 +75,6 @@ class ExportsCompilerPass implements CompilerPassInterface
}
}
private function compileExports(
Definition $chillManagerDefinition,
ContainerBuilder $container
) {
$taggedServices = $container->findTaggedServiceIds(
'chill.export'
);
$knownAliases = [];
foreach ($taggedServices as $id => $tagAttributes) {
foreach ($tagAttributes as $attributes) {
if (!isset($attributes['alias'])) {
throw new LogicException("the 'alias' attribute is missing in your " .
"service '{$id}' definition");
}
if (array_search($attributes['alias'], $knownAliases, true)) {
throw new LogicException('There is already a chill.export service with alias '
. $attributes['alias'] . '. Choose another alias.');
}
$knownAliases[] = $attributes['alias'];
$chillManagerDefinition->addMethodCall(
'addExport',
[new Reference($id), $attributes['alias']]
);
}
}
}
private function compileFilters(
Definition $chillManagerDefinition,
ContainerBuilder $container
) {
$taggedServices = $container->findTaggedServiceIds(
'chill.export_filter'
);
$knownAliases = [];
foreach ($taggedServices as $id => $tagAttributes) {
foreach ($tagAttributes as $attributes) {
if (!isset($attributes['alias'])) {
throw new LogicException("the 'alias' attribute is missing in your " .
"service '{$id}' definition");
}
if (array_search($attributes['alias'], $knownAliases, true)) {
throw new LogicException('There is already a chill.export_filter service with alias '
. $attributes['alias'] . '. Choose another alias.');
}
$knownAliases[] = $attributes['alias'];
$chillManagerDefinition->addMethodCall(
'addFilter',
[new Reference($id), $attributes['alias']]
);
}
}
}
private function compileFormatters(
Definition $chillManagerDefinition,
ContainerBuilder $container

View File

@@ -0,0 +1,54 @@
<?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\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker;
class Age extends FunctionNode
{
private $value1;
private $value2;
public function getSql(SqlWalker $sqlWalker)
{
if (null !== $this->value2) {
return sprintf(
'AGE(%s, %s)',
$this->value1->dispatch($sqlWalker),
$this->value2->dispatch($sqlWalker)
);
}
return sprintf(
'AGE(%s)',
$this->value1->dispatch($sqlWalker),
);
}
public function parse(Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->value1 = $parser->SimpleArithmeticExpression();
$parser->match(Lexer::T_COMMA);
$this->value2 = $parser->SimpleArithmeticExpression();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
}

View File

@@ -27,7 +27,7 @@ class JsonbExistsInArray extends FunctionNode
return sprintf(
'%s ?? %s',
$this->expr1->dispatch($sqlWalker),
$sqlWalker->walkInputParameter($this->expr2)
$this->expr2->dispatch($sqlWalker)
);
}

View File

@@ -17,6 +17,7 @@ use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\DateIntervalType;
use Exception;
use LogicException;
use function count;
use function current;
use function preg_match;
@@ -40,7 +41,7 @@ class NativeDateIntervalType extends DateIntervalType
return $value->format(self::FORMAT);
}
throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'DateInterval']);
throw ConversionException::conversionFailedInvalidType($value, 'string', ['null', 'DateInterval']);
}
public function convertToPHPValue($value, AbstractPlatform $platform)
@@ -80,7 +81,7 @@ class NativeDateIntervalType extends DateIntervalType
protected function createConversionException($value, $exception = null)
{
return ConversionException::conversionFailedFormat($value, $this->getName(), 'xx year xx mons xx days 01:02:03', $exception);
return ConversionException::conversionFailedFormat($value, 'string', 'xx year xx mons xx days 01:02:03', $exception);
}
private function convertEntry(&$strings)
@@ -125,5 +126,7 @@ class NativeDateIntervalType extends DateIntervalType
return $intervalSpec;
}
throw new LogicException();
}
}

View File

@@ -28,6 +28,11 @@ use Symfony\Component\Serializer\Annotation\Groups;
*/
class Scope
{
/**
* @ORM\Column(type="boolean", nullable=false, options={"default": true})
*/
private bool $active = true;
/**
* @ORM\Id
* @ORM\Column(name="id", type="integer")
@@ -88,6 +93,18 @@ class Scope
return $this->roleScopes;
}
public function isActive(): bool
{
return $this->active;
}
public function setActive(bool $active): Scope
{
$this->active = $active;
return $this;
}
/**
* @param $name
*

View File

@@ -11,28 +11,22 @@ declare(strict_types=1);
namespace Chill\MainBundle\Export;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\HttpFoundation\Response;
interface DirectExportInterface extends ExportElementInterface
{
/**
* Generate the export.
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function generate(array $acl, array $data = []);
public function generate(array $acl, array $data = []): Response;
/**
* get a description, which will be used in UI (and translated).
*
* @return string
*/
public function getDescription();
public function getDescription(): string;
/**
* authorized role.
*
* @return \Symfony\Component\Security\Core\Role\Role
*/
public function requiredRole();
public function requiredRole(): string;
}

View File

@@ -14,10 +14,8 @@ namespace Chill\MainBundle\Export;
use Chill\MainBundle\Form\Type\Export\ExportType;
use Chill\MainBundle\Form\Type\Export\PickCenterType;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Generator;
use InvalidArgumentException;
use LogicException;
use Psr\Log\LoggerInterface;
use RuntimeException;
@@ -50,8 +48,6 @@ class ExportManager
private AuthorizationHelperInterface $authorizationHelper;
private EntityManagerInterface $em;
/**
* Collected Exports, injected by DI.
*
@@ -82,16 +78,28 @@ class ExportManager
public function __construct(
LoggerInterface $logger,
EntityManagerInterface $em,
AuthorizationCheckerInterface $authorizationChecker,
AuthorizationHelperInterface $authorizationHelper,
TokenStorageInterface $tokenStorage
TokenStorageInterface $tokenStorage,
iterable $exports,
iterable $aggregators,
iterable $filters
//iterable $formatters,
//iterable $exportElementProvider
) {
$this->logger = $logger;
$this->em = $em;
$this->authorizationChecker = $authorizationChecker;
$this->authorizationHelper = $authorizationHelper;
$this->user = $tokenStorage->getToken()->getUser();
$this->exports = iterator_to_array($exports);
$this->aggregators = iterator_to_array($aggregators);
$this->filters = iterator_to_array($filters);
// NOTE: PHP crashes on the next line (exit error code 11). This is desactivated until further investigation
//$this->formatters = iterator_to_array($formatters);
//foreach ($exportElementProvider as $prefix => $provider) {
// $this->addExportElementsProvider($provider, $prefix);
//}
}
/**
@@ -141,52 +149,17 @@ class ExportManager
}
}
/**
* add an aggregator.
*
* @internal used by DI
*
* @param string $alias
*/
public function addAggregator(AggregatorInterface $aggregator, $alias)
{
$this->aggregators[$alias] = $aggregator;
}
/**
* add an export.
*
* @internal used by DI
*
* @param DirectExportInterface|ExportInterface $export
* @param type $alias
*/
public function addExport($export, $alias)
{
if ($export instanceof ExportInterface || $export instanceof DirectExportInterface) {
$this->exports[$alias] = $export;
} else {
throw new InvalidArgumentException(sprintf(
'The export with alias %s '
. 'does not implements %s or %s.',
$alias,
ExportInterface::class,
DirectExportInterface::class
));
}
}
public function addExportElementsProvider(ExportElementsProviderInterface $provider, $prefix)
{
foreach ($provider->getExportElements() as $suffix => $element) {
$alias = $prefix . '_' . $suffix;
if ($element instanceof ExportInterface) {
$this->addExport($element, $alias);
$this->exports[$alias] = $element;
} elseif ($element instanceof FilterInterface) {
$this->addFilter($element, $alias);
$this->filters[$alias] = $element;
} elseif ($element instanceof AggregatorInterface) {
$this->addAggregator($element, $alias);
$this->aggregators[$alias] = $element;
} elseif ($element instanceof FormatterInterface) {
$this->addFormatter($element, $alias);
} else {
@@ -196,24 +169,12 @@ class ExportManager
}
}
/**
* add a Filter.
*
* @internal Normally used by the dependency injection
*
* @param string $alias
*/
public function addFilter(FilterInterface $filter, $alias)
{
$this->filters[$alias] = $filter;
}
/**
* add a formatter.
*
* @internal used by DI
*
* @param type $alias
* @param string $alias
*/
public function addFormatter(FormatterInterface $formatter, $alias)
{
@@ -231,7 +192,6 @@ class ExportManager
public function generate($exportAlias, array $pickedCentersData, array $data, array $formatterData)
{
$export = $this->getExport($exportAlias);
//$qb = $this->em->createQueryBuilder();
$centers = $this->getPickedCenters($pickedCentersData);
if ($export instanceof DirectExportInterface) {

View File

@@ -0,0 +1,50 @@
<?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\LocationType;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PickLocationTypeType extends AbstractType
{
private TranslatableStringHelper $translatableStringHelper;
public function __construct(TranslatableStringHelper $translatableStringHelper)
{
$this->translatableStringHelper = $translatableStringHelper;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefaults([
'class' => LocationType::class,
'choice_label' => function (LocationType $type) {
return $this->translatableStringHelper->localize($type->getTitle());
},
'placeholder' => 'Pick a location type',
'required' => false,
'attr' => ['class' => 'select2'],
'label' => 'Location type',
'multiple' => false,
])
->setAllowedTypes('multiple', ['bool']);
}
public function getParent(): string
{
return EntityType::class;
}
}

View File

@@ -0,0 +1,57 @@
<?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\Location;
use Chill\MainBundle\Repository\LocationRepository;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PickUserLocationType extends AbstractType
{
private LocationRepository $locationRepository;
private TranslatableStringHelper $translatableStringHelper;
public function __construct(TranslatableStringHelper $translatableStringHelper, LocationRepository $locationRepository)
{
$this->translatableStringHelper = $translatableStringHelper;
$this->locationRepository = $locationRepository;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefaults([
'class' => Location::class,
'choices' => $this->locationRepository->findByPublicLocations(),
'choice_label' => function (Location $entity) {
return $entity->getName() ?
$entity->getName() . ' (' . $this->translatableStringHelper->localize($entity->getLocationType()->getTitle()) . ')' :
$this->translatableStringHelper->localize($entity->getLocationType()->getTitle());
},
'placeholder' => 'Pick a location',
'required' => false,
'attr' => ['class' => 'select2'],
'label' => 'Current location',
'multiple' => false,
])
->setAllowedTypes('multiple', ['bool']);
}
public function getParent(): string
{
return EntityType::class;
}
}

View File

@@ -15,9 +15,9 @@ use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\DataMapper\ScopePickerDataMapper;
use Chill\MainBundle\Repository\ScopeRepository;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use RuntimeException;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
@@ -26,11 +26,9 @@ use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Security\Core\Security;
use function array_map;
use Symfony\Component\Security\Core\Security;
use function count;
/**
@@ -44,47 +42,37 @@ use function count;
*/
class ScopePickerType extends AbstractType
{
protected AuthorizationHelperInterface $authorizationHelper;
private AuthorizationHelperInterface $authorizationHelper;
/**
* @var ScopeRepository
*/
protected $scopeRepository;
private Security $security;
protected Security $security;
/**
* @var TokenStorageInterface
*/
protected $tokenStorage;
/**
* @var TranslatableStringHelper
*/
protected $translatableStringHelper;
private TranslatableStringHelperInterface $translatableStringHelper;
public function __construct(
AuthorizationHelperInterface $authorizationHelper,
TokenStorageInterface $tokenStorage,
ScopeRepository $scopeRepository,
Security $security,
TranslatableStringHelper $translatableStringHelper
TranslatableStringHelperInterface $translatableStringHelper
) {
$this->authorizationHelper = $authorizationHelper;
$this->tokenStorage = $tokenStorage;
$this->scopeRepository = $scopeRepository;
$this->security = $security;
$this->translatableStringHelper = $translatableStringHelper;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$items = $this->authorizationHelper->getReachableScopes(
$this->security->getUser(),
$options['role'] instanceof Role ? $options['role']->getRole() : $options['role'],
$options['center']
$items = array_filter(
$this->authorizationHelper->getReachableScopes(
$this->security->getUser(),
$options['role'] instanceof Role ? $options['role']->getRole() : $options['role'],
$options['center']
),
static function (Scope $s) { return $s->isActive(); }
);
if (0 === count($items)) {
throw new RuntimeException('no scopes are reachable. This form should not be shown to user');
}
if (1 !== count($items)) {
$builder->add('scope', EntityType::class, [
'class' => Scope::class,
@@ -123,35 +111,4 @@ class ScopePickerType extends AbstractType
->setRequired('role')
->setAllowedTypes('role', ['string', Role::class]);
}
/**
* @param array|Center|Center[] $center
* @param string $role
*
* @return \Doctrine\ORM\QueryBuilder
*/
protected function buildAccessibleScopeQuery($center, $role)
{
$roles = $this->authorizationHelper->getParentRoles($role);
$roles[] = $role;
$centers = $center instanceof Center ? [$center] : $center;
$qb = $this->scopeRepository->createQueryBuilder('s');
$qb
// jointure to center
->join('s.roleScopes', 'rs')
->join('rs.permissionsGroups', 'pg')
->join('pg.groupCenters', 'gc')
// add center constraint
->where($qb->expr()->in('IDENTITY(gc.center)', ':centers'))
->setParameter('centers', array_map(static fn (Center $c) => $c->getId(), $centers))
// role constraints
->andWhere($qb->expr()->in('rs.role', ':roles'))
->setParameter('roles', $roles)
// user contraint
->andWhere(':user MEMBER OF gc.users')
->setParameter('user', $this->tokenStorage->getToken()->getUser());
return $qb;
}
}

View File

@@ -11,39 +11,14 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form;
use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Repository\LocationRepository;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Chill\MainBundle\Form\Type\PickUserLocationType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class UserCurrentLocationType extends AbstractType
{
private LocationRepository $locationRepository;
private TranslatableStringHelper $translatableStringHelper;
public function __construct(TranslatableStringHelper $translatableStringHelper, LocationRepository $locationRepository)
{
$this->translatableStringHelper = $translatableStringHelper;
$this->locationRepository = $locationRepository;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('currentLocation', EntityType::class, [
'class' => Location::class,
'choices' => $this->locationRepository->findByPublicLocations(),
'choice_label' => function (Location $entity) {
return $entity->getName() ?
$entity->getName() . ' (' . $this->translatableStringHelper->localize($entity->getLocationType()->getTitle()) . ')' :
$this->translatableStringHelper->localize($entity->getLocationType()->getTitle());
},
'placeholder' => 'Pick a location',
'required' => false,
'attr' => ['class' => 'select2'],
]);
$builder->add('currentLocation', PickUserLocationType::class);
}
}

View File

@@ -43,6 +43,15 @@ final class ScopeRepository implements ScopeRepositoryInterface
return $this->repository->findAll();
}
public function findAllActive(): array
{
$qb = $this->repository->createQueryBuilder('s');
$qb->where('s.active = \'TRUE\'');
return $qb->getQuery()->getResult();
}
/**
* @param mixed|null $limit
* @param mixed|null $offset

View File

@@ -22,10 +22,15 @@ interface ScopeRepositoryInterface extends ObjectRepository
public function find($id, $lockMode = null, $lockVersion = null): ?Scope;
/**
* @return Scope[]
* @return array|Scope[]
*/
public function findAll(): array;
/**
* @return array|Scope[]
*/
public function findAllActive(): array;
/**
* @param null|mixed $limit
* @param null|mixed $offset

View File

@@ -14,9 +14,8 @@ namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\UserJob;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
class UserJobRepository implements ObjectRepository
class UserJobRepository implements UserJobRepositoryInterface
{
private EntityRepository $repository;
@@ -38,6 +37,11 @@ class UserJobRepository implements ObjectRepository
return $this->repository->findAll();
}
public function findAllActive(): array
{
return $this->repository->findBy(['active' => true]);
}
/**
* @param mixed|null $limit
* @param mixed|null $offset
@@ -49,12 +53,12 @@ class UserJobRepository implements ObjectRepository
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria)
public function findOneBy(array $criteria): ?UserJob
{
return $this->repository->findOneBy($criteria);
}
public function getClassName()
public function getClassName(): string
{
return UserJob::class;
}

View File

@@ -0,0 +1,42 @@
<?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\UserJob;
use Doctrine\Persistence\ObjectRepository;
interface UserJobRepositoryInterface extends ObjectRepository
{
public function find($id): ?UserJob;
/**
* @return array|UserJob[]
*/
public function findAll(): array;
/**
* @return array|UserJob[]
*/
public function findAllActive(): array;
/**
* @param mixed|null $limit
* @param mixed|null $offset
*
* @return array|object[]|UserJob[]
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null);
public function findOneBy(array $criteria): ?UserJob;
public function getClassName(): string;
}

View File

@@ -1,12 +1,5 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
/*

View File

@@ -119,7 +119,3 @@ document.addEventListener('DOMContentLoaded', function(e) {
loadDynamicPicker(document)
})

View File

@@ -20,7 +20,12 @@
{% block title %}{{ export.title|trans }}{% endblock %}
{% block css %}
{{ encore_entry_link_tags('mod_pickentity_type') }}
{% endblock %}
{% block js %}
{{ encore_entry_script_tags('mod_pickentity_type') }}
{{ encore_entry_script_tags('page_export') }}
{% if export_alias == 'count_social_work_actions' %}
{{ encore_entry_script_tags('vue_export_action_goal_result') }}

View File

@@ -29,6 +29,18 @@ use function is_string;
*/
abstract class AbstractAggregatorTest extends KernelTestCase
{
/**
* provide data for `testAliasDidNotDisappears`.
*/
public function dataProviderAliasDidNotDisappears()
{
foreach ($this->getQueryBuilders() as $qb) {
foreach ($this->getFormData() as $data) {
yield [clone $qb, $data];
}
}
}
/**
* provide data for `testAlterQuery`.
*/
@@ -95,6 +107,28 @@ abstract class AbstractAggregatorTest extends KernelTestCase
*/
abstract public function getQueryBuilders();
/**
* Compare aliases array before and after that aggregator alter query.
*
* @dataProvider dataProviderAliasDidNotDisappears
*
* @return void
*/
public function testAliasDidNotDisappears(QueryBuilder $qb, array $data)
{
$aliases = $qb->getAllAliases();
$this->getAggregator()->alterQuery($qb, $data);
$alteredQuery = $qb->getAllAliases();
$this->assertGreaterThanOrEqual(count($aliases), count($alteredQuery));
foreach ($aliases as $alias) {
$this->assertContains($alias, $alteredQuery);
}
}
/**
* test the alteration of query by the filter.
*

View File

@@ -40,6 +40,18 @@ abstract class AbstractFilterTest extends KernelTestCase
$this->prophet = $this->getProphet();
}
/**
* provide data for `testAliasDidNotDisappears`.
*/
public function dataProviderAliasDidNotDisappears()
{
foreach ($this->getQueryBuilders() as $qb) {
foreach ($this->getFormData() as $data) {
yield [clone $qb, $data];
}
}
}
public function dataProviderAlterQuery()
{
foreach ($this->getQueryBuilders() as $qb) {
@@ -87,6 +99,28 @@ abstract class AbstractFilterTest extends KernelTestCase
*/
abstract public function getQueryBuilders();
/**
* Compare aliases array before and after that filter alter query.
*
* @dataProvider dataProviderAliasDidNotDisappears
*
* @return void
*/
public function testAliasDidNotDisappears(QueryBuilder $qb, array $data)
{
$aliases = $qb->getAllAliases();
$this->getFilter()->alterQuery($qb, $data);
$alteredQuery = $qb->getAllAliases();
$this->assertGreaterThanOrEqual(count($aliases), count($alteredQuery));
foreach ($aliases as $alias) {
$this->assertContains($alias, $alteredQuery);
}
}
/**
* test the alteration of query by the filter.
*

View File

@@ -0,0 +1,79 @@
<?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\Tests\Doctrine\DQL;
use Chill\MainBundle\Entity\Address;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @internal
* @coversNothing
*/
final class AgeTest extends KernelTestCase
{
private EntityManagerInterface $entityManager;
protected function setUp(): void
{
self::bootKernel();
$this->entityManager = self::$container->get(EntityManagerInterface::class);
}
public function generateQueries(): iterable
{
yield [
'SELECT AGE(a.validFrom, a.validTo) FROM ' . Address::class . ' a',
[],
];
yield [
'SELECT AGE(:date0, :date1) FROM ' . Address::class . ' a',
[
'date0' => new DateTimeImmutable('now'),
'date1' => new DateTimeImmutable('2020-01-01'),
],
];
yield [
'SELECT AGE(a.validFrom, :date1) FROM ' . Address::class . ' a',
[
'date1' => new DateTimeImmutable('now'),
],
];
yield [
'SELECT AGE(:date0, a.validFrom) FROM ' . Address::class . ' a',
[
'date0' => new DateTimeImmutable('now'),
],
];
}
/**
* @dataProvider generateQueries
*/
public function testWorking(string $dql, array $args)
{
$dql = $this->entityManager->createQuery($dql)->setMaxResults(3);
foreach ($args as $key => $value) {
$dql->setParameter($key, $value);
}
$results = $dql->getResult();
$this->assertIsArray($results);
}
}

View File

@@ -0,0 +1,41 @@
<?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 Doctrine\DQL;
use Chill\MainBundle\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @internal
* @coversNothing
*/
final class JsonbExistsInArrayTest extends KernelTestCase
{
private EntityManagerInterface $em;
protected function setUp(): void
{
self::bootKernel();
$this->em = self::$container->get(EntityManagerInterface::class);
}
public function testDQLFunctionWorks()
{
$result = $this->em
->createQuery('SELECT JSONB_EXISTS_IN_ARRAY(u.attributes, :param) FROM ' . User::class . ' u')
->setParameter('param', 'fr')
->getResult();
$this->assertIsArray($result);
}
}

View File

@@ -0,0 +1,127 @@
<?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\Entity\Center;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\Type\ScopePickerType;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ManagerRegistry;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Security\Core\Security;
/**
* @internal
* @coversNothing
*/
final class ScopePickerTypeTest extends TypeTestCase
{
use ProphecyTrait;
public function testBuildOneScopeIsSuccessful()
{
$form = $this->factory->create(ScopePickerType::class, null, [
'center' => new Center(),
'role' => 'ONE_SCOPE',
]);
$view = $form->createView();
$this->assertContains('hidden', $view['scope']->vars['block_prefixes']);
}
public function testBuildThreeScopesIsSuccessful()
{
$form = $this->factory->create(ScopePickerType::class, null, [
'center' => new Center(),
'role' => 'THREE_SCOPE',
]);
$view = $form->createView();
$this->assertContains('entity', $view['scope']->vars['block_prefixes']);
}
public function testBuildTwoScopesIsSuccessful()
{
$form = $this->factory->create(ScopePickerType::class, null, [
'center' => new Center(),
'role' => 'TWO_SCOPE',
]);
$view = $form->createView();
$this->assertContains('entity', $view['scope']->vars['block_prefixes']);
}
protected function getExtensions()
{
$user = new User();
$role1Scope = 'ONE_SCOPE';
$role2Scope = 'TWO_SCOPE';
$role3Scope = 'THREE_SCOPE';
$scopeA = (new Scope())->setName(['fr' => 'scope a']);
$scopeB = (new Scope())->setName(['fr' => 'scope b']);
$scopeC = (new Scope())->setName(['fr' => 'scope b'])->setActive(false);
$authorizationHelper = $this->prophesize(AuthorizationHelperInterface::class);
$authorizationHelper->getReachableScopes($user, $role1Scope, Argument::any())
->willReturn([$scopeA]);
$authorizationHelper->getReachableScopes($user, $role2Scope, Argument::any())
->willReturn([$scopeA, $scopeB]);
$authorizationHelper->getReachableScopes($user, $role3Scope, Argument::any())
->willReturn([$scopeA, $scopeB, $scopeC]);
$security = $this->prophesize(Security::class);
$security->getUser()->willReturn($user);
$translatableStringHelper = $this->prophesize(TranslatableStringHelperInterface::class);
$translatableStringHelper->localize(Argument::type('array'))->will(
static function ($args) { return $args[0]['fr']; }
);
$type = new ScopePickerType(
$authorizationHelper->reveal(),
$security->reveal(),
$translatableStringHelper->reveal()
);
// add the mocks for creating EntityType
$entityManager = DoctrineTestHelper::createTestEntityManager();
$em = $this->prophesize(EntityManagerInterface::class);
$em->getClassMetadata(Scope::class)->willReturn($entityManager->getClassMetadata(Scope::class));
$em->contains(Argument::type(Scope::class))->willReturn(true);
$em->initializeObject(Argument::type(Scope::class))->will(static fn ($o) => $o);
$emRevealed = $em->reveal();
$managerRegistry = $this->prophesize(ManagerRegistry::class);
$managerRegistry->getManager(Argument::any())->willReturn($emRevealed);
$managerRegistry->getManagerForClass(Scope::class)->willReturn($emRevealed);
$entityType = $this->prophesize(EntityType::class);
$entityType->getParent()->willReturn(ChoiceType::class);
return [
new PreloadedExtension([$type], []),
new DoctrineOrmExtension($managerRegistry->reveal()),
];
}
}

View File

@@ -91,6 +91,14 @@ services:
Chill\MainBundle\Export\ExportManager:
autoconfigure: true
autowire: true
arguments:
$exports: !tagged_iterator { tag: chill.export, index_by: alias }
$aggregators: !tagged_iterator { tag: chill.export_aggregator, index_by: alias }
$filters: !tagged_iterator { tag: chill.export_filter, index_by: alias }
# for an unknown reason, iterator_to_array($formatter) cause a segmentation fault error (php-fpm code 11). removed temporarily
# $formatters: !tagged_iterator { tag: chill.export_formatter, index_by: alias }
# remove until we can properly test it
# $exportElementProvider: !tagged_iterator { tag: chill.export_elements_provider, index_by: prefix }
Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface: '@Chill\MainBundle\Security\Resolver\CenterResolverDispatcher'

View File

@@ -122,8 +122,6 @@ services:
Chill\MainBundle\Form\Type\PickAddressType: ~
Chill\MainBundle\Form\DataTransform\AddressToIdDataTransformer: ~
Chill\MainBundle\Form\DataTransform\AddressToIdDataTransformer:
autoconfigure: true
autowire: true
@@ -132,10 +130,6 @@ services:
autowire: true
autoconfigure: true
Chill\MainBundle\Form\UserCurrentLocationType:
autowire: true
autoconfigure: true
Chill\MainBundle\Form\Type\LocationFormType: ~
Chill\MainBundle\Form\WorkflowStepType: ~

View File

@@ -0,0 +1,33 @@
<?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 Version20221010142417 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE scopes DROP active');
}
public function getDescription(): string
{
return 'Allow a scope to be desactivated';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE scopes ADD active BOOLEAN DEFAULT true NOT NULL');
}
}

View File

@@ -189,6 +189,7 @@ Main scope: Cercle
Main center: Centre
user job: Métier de l'utilisateur
Job: Métier
Jobs: Métiers
Choose a main center: Choisir un centre
Choose a main scope: Choisir un cercle
choose a job: Choisir un métier
@@ -227,6 +228,7 @@ never: jamais
Create a new location: Créer une nouvelle localisation
Location list: Liste des localisations
Location type: Type de localisation
Pick a location type: Choisir un type de localisation
Phonenumber1: Numéro de téléphone
Phonenumber2: Autre numéro de téléphone
Location configuration: Configuration des localisations