Merge branch '106_tasks_to_parcours'

This commit is contained in:
2021-11-03 11:45:40 +01:00
66 changed files with 2211 additions and 1247 deletions

View File

@@ -23,6 +23,7 @@ use Chill\MainBundle\Controller\AddressApiController;
use Chill\MainBundle\Controller\LocationController;
use Chill\MainBundle\Controller\LocationTypeController;
use Chill\MainBundle\Controller\UserController;
use Chill\MainBundle\Doctrine\DQL\JsonbArrayLength;
use Chill\MainBundle\Doctrine\DQL\STContains;
use Chill\MainBundle\Doctrine\DQL\StrictWordSimilarityOPS;
use Chill\MainBundle\Entity\User;
@@ -96,6 +97,10 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
$configuration = $this->getConfiguration($configs, $container);
$config = $this->processConfiguration($configuration, $configs);
// replace all config with a main key:
$container->setParameter('chill_main', $config);
// legacy config
$container->setParameter('chill_main.installation_name',
$config['installation_name']);
@@ -199,6 +204,7 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
'OVERLAPSI' => OverlapsI::class,
'STRICT_WORD_SIMILARITY_OPS' => StrictWordSimilarityOPS::class,
'ST_CONTAINS' => STContains::class,
'JSONB_ARRAY_LENGTH' => JsonbArrayLength::class,
],
],
'hydrators' => [

View File

@@ -16,23 +16,23 @@ use Symfony\Component\HttpFoundation\Request;
*/
class Configuration implements ConfigurationInterface
{
use AddWidgetConfigurationTrait;
/**
*
* @var ContainerBuilder
*/
private $containerBuilder;
public function __construct(array $widgetFactories = array(),
public function __construct(array $widgetFactories = array(),
ContainerBuilder $containerBuilder)
{
$this->setWidgetFactories($widgetFactories);
$this->containerBuilder = $containerBuilder;
}
/**
* {@inheritDoc}
*/
@@ -97,6 +97,14 @@ class Configuration implements ConfigurationInterface
->end()
->end()
->end()
->arrayNode('acl')
->addDefaultsIfNotSet()
->children()
->booleanNode('form_show_scopes')
->defaultTrue()
->end()
->end()
->end()
->arrayNode('redis')
->children()
->scalarNode('host')
@@ -247,7 +255,7 @@ class Configuration implements ConfigurationInterface
->end() // end of root
;
return $treeBuilder;
}
}

View File

@@ -0,0 +1,35 @@
<?php
/*
*
*
*/
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;
/**
* Return the length of an array
*/
class JsonbArrayLength extends FunctionNode
{
private $expr1;
public function getSql(SqlWalker $sqlWalker): string
{
return sprintf(
'jsonb_array_length(%s)',
$this->expr1->dispatch($sqlWalker),
);
}
public function parse(Parser $parser): void
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->expr1 = $parser->StringPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
}

View File

@@ -1,7 +1,7 @@
<?php
/*
*
*
*
*
*/
namespace Chill\MainBundle\Doctrine\DQL;
@@ -11,19 +11,18 @@ use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
@author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class JsonbExistsInArray extends FunctionNode
{
private $expr1;
private $expr2;
public function getSql(SqlWalker $sqlWalker): string
{
return sprintf(
'jsonb_exists(%s, %s)',
'%s ?? %s',
$this->expr1->dispatch($sqlWalker),
$sqlWalker->walkInputParameter($this->expr2)
);

View File

@@ -3,9 +3,12 @@
namespace Chill\MainBundle\Form\Type\Listing;
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\SearchType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\HttpFoundation\RequestStack;
final class FilterOrderType extends \Symfony\Component\Form\AbstractType
@@ -29,9 +32,32 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType
]);
}
$checkboxesBuilder = $builder->create('checkboxes', null, [ 'compound' => true ]);
foreach ($helper->getCheckboxes() as $name => $c) {
$choices = \array_combine(
\array_map(function($c, $t) {
if ($t !== NULL) { return $t; }
else { return $c; }
}, $c['choices'], $c['trans']),
$c['choices']
);
$checkboxesBuilder->add($name, ChoiceType::class, [
'choices' => $choices,
'expanded' => true,
'multiple' => true,
]);
}
if (0 < count($helper->getCheckboxes())) {
$builder->add($checkboxesBuilder);
}
foreach ($this->requestStack->getCurrentRequest()->query->getIterator() as $key => $value) {
switch($key) {
case 'q':
case 'checkboxes'.$key:
break;
case 'page':
$builder->add($key, HiddenType::class, [
@@ -47,6 +73,17 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType
}
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
/** @var FilterOrderHelper $helper */
$helper = $options['helper'];
$view->vars['has_search_box'] = $helper->hasSearchBox();
$view->vars['checkboxes'] = [];
foreach ($helper->getCheckboxes() as $name => $c) {
$view->vars['checkboxes'][$name] = [];
}
}
public function configureOptions(\Symfony\Component\OptionsResolver\OptionsResolver $resolver)
{
$resolver->setRequired('helper')

View File

@@ -23,7 +23,9 @@ use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\DataMapper\ScopePickerDataMapper;
use Chill\MainBundle\Repository\ScopeRepository;
use Chill\MainBundle\Repository\UserACLAwareRepositoryInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
@@ -36,6 +38,7 @@ 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;
/**
* Allow to pick amongst available scope for the current
@@ -46,14 +49,10 @@ use Symfony\Component\Security\Core\Role\Role;
* - `center`: the center of the entity
* - `role` : the role of the user
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class ScopePickerType extends AbstractType
{
/**
* @var AuthorizationHelper
*/
protected $authorizationHelper;
protected AuthorizationHelperInterface $authorizationHelper;
/**
* @var TokenStorageInterface
@@ -70,22 +69,26 @@ class ScopePickerType extends AbstractType
*/
protected $translatableStringHelper;
protected Security $security;
public function __construct(
AuthorizationHelper $authorizationHelper,
AuthorizationHelperInterface $authorizationHelper,
TokenStorageInterface $tokenStorage,
ScopeRepository $scopeRepository,
Security $security,
TranslatableStringHelper $translatableStringHelper
) {
$this->authorizationHelper = $authorizationHelper;
$this->tokenStorage = $tokenStorage;
$this->scopeRepository = $scopeRepository;
$this->security = $security;
$this->translatableStringHelper = $translatableStringHelper;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$query = $this->buildAccessibleScopeQuery($options['center'], $options['role']);
$items = $query->getQuery()->execute();
$items = $this->authorizationHelper->getReachableScopes($this->security->getUser(),
$options['role'], $options['center']);
if (1 !== count($items)) {
$builder->add('scope', EntityType::class, [
@@ -94,9 +97,7 @@ class ScopePickerType extends AbstractType
'choice_label' => function (Scope $c) {
return $this->translatableStringHelper->localize($c->getName());
},
'query_builder' => function () use ($options) {
return $this->buildAccessibleScopeQuery($options['center'], $options['role']);
},
'choices' => $items,
]);
$builder->setDataMapper(new ScopePickerDataMapper());
} else {
@@ -121,19 +122,22 @@ class ScopePickerType extends AbstractType
$resolver
// create `center` option
->setRequired('center')
->setAllowedTypes('center', [Center::class])
->setAllowedTypes('center', [Center::class, 'array', 'null'])
// create ``role` option
->setRequired('role')
->setAllowedTypes('role', ['string', Role::class]);
}
/**
* @param Center|array|Center[] $center
* @param string $role
* @return \Doctrine\ORM\QueryBuilder
*/
protected function buildAccessibleScopeQuery(Center $center, Role $role)
protected function buildAccessibleScopeQuery($center, $role)
{
$roles = $this->authorizationHelper->getParentRoles($role);
$roles[] = $role;
$centers = $center instanceof Center ? [$center]: $center;
$qb = $this->scopeRepository->createQueryBuilder('s');
$qb
@@ -142,8 +146,8 @@ class ScopePickerType extends AbstractType
->join('rs.permissionsGroups', 'pg')
->join('pg.groupCenters', 'gc')
// add center constraint
->where($qb->expr()->eq('IDENTITY(gc.center)', ':center'))
->setParameter('center', $center->getId())
->where($qb->expr()->in('IDENTITY(gc.center)', ':centers'))
->setParameter('centers', \array_map(fn(Center $c) => $c->getId(), $centers))
// role constraints
->andWhere($qb->expr()->in('rs.role', ':roles'))
->setParameter('roles', $roles)

View File

@@ -17,6 +17,8 @@
*/
namespace Chill\MainBundle\Form\Type;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Repository\UserACLAwareRepositoryInterface;
use Symfony\Component\Form\AbstractType;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Doctrine\ORM\EntityRepository;
@@ -56,14 +58,18 @@ class UserPickerType extends AbstractType
protected UserRepository $userRepository;
protected UserACLAwareRepositoryInterface $userACLAwareRepository;
public function __construct(
AuthorizationHelper $authorizationHelper,
TokenStorageInterface $tokenStorage,
UserRepository $userRepository
UserRepository $userRepository,
UserACLAwareRepositoryInterface $userACLAwareRepository
) {
$this->authorizationHelper = $authorizationHelper;
$this->tokenStorage = $tokenStorage;
$this->userRepository = $userRepository;
$this->userACLAwareRepository = $userACLAwareRepository;
}
@@ -72,7 +78,7 @@ class UserPickerType extends AbstractType
$resolver
// create `center` option
->setRequired('center')
->setAllowedTypes('center', [\Chill\MainBundle\Entity\Center::class ])
->setAllowedTypes('center', [\Chill\MainBundle\Entity\Center::class, 'null', 'array' ])
// create ``role` option
->setRequired('role')
->setAllowedTypes('role', ['string', \Symfony\Component\Security\Core\Role\Role::class ])
@@ -86,17 +92,19 @@ class UserPickerType extends AbstractType
->setDefault('choice_label', function(User $u) {
return $u->getUsername();
})
->setDefault('scope', null)
->setAllowedTypes('scope', [Scope::class, 'array', 'null'])
->setNormalizer('choices', function(Options $options) {
$users = $this->authorizationHelper
->findUsersReaching($options['role'], $options['center']);
$users = $this->userACLAwareRepository
->findUsersByReachedACL($options['role'], $options['center'], $options['scope'], true);
if (NULL !== $options['having_permissions_group_flag']) {
return $this->userRepository
->findUsersHavingFlags($options['having_permissions_group_flag'], $users)
;
}
return $users;
})
;

View File

@@ -0,0 +1,54 @@
<?php
namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
class UserACLAwareRepository implements UserACLAwareRepositoryInterface
{
private AuthorizationHelper $authorizationHelper;
public function findUsersByReachedACL(string $role, $center, $scope = null, bool $onlyEnabled = true): array
{
$parents = $this->authorizationHelper->getParentRoles($role);
$parents[] = $role;
$qb = $this->em->createQueryBuilder();
$qb
->select('u')
->from(User::class, 'u')
->join('u.groupCenters', 'gc')
->join('gc.permissionsGroup', 'pg')
->join('pg.roleScopes', 'rs')
->where($qb->expr()->in('rs.role', $parents))
;
if ($onlyEnabled) {
$qb->andWhere($qb->expr()->eq('u.enabled', "'TRUE'"));
}
if (NULL !== $center) {
$centers = $center instanceof Center ? [$center] : $center;
$qb
->andWhere($qb->expr()->in('gc.center', ':centers'))
->setParameter('centers', $centers)
;
}
if (NULL !== $scope) {
$scopes = $scope instanceof Scope ? [$scope] : $scope;
$qb
->andWhere($qb->expr()->in('rs.scope', ':scopes'))
->setParameter('scopes', $scopes)
;
}
return $qb->getQuery()->getResult();
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
interface UserACLAwareRepositoryInterface
{
/**
* Find the users reaching the given center and scope, for the given role
*
* @param string $role
* @param Center|Center[]|array $center
* @param Scope|Scope[]|array|null $scope
* @param bool $onlyActive true if get only active users
*
* @return User[]
*/
public function findUsersByReachedACL(string $role, $center, $scope = null, bool $onlyActive = true): array;
}

View File

@@ -1,12 +1,39 @@
{{ form_start(form) }}
<div class="chill_filter_order container">
<div class="row">
<div class="col-md-12">
<div class="input-group mb-3">
{{ form_widget(form.q)}}
<button type="submit" class="btn btn-chill-l-gray"><i class="fa fa-search"></i></button>
{% if form.vars.has_search_box %}
<div class="col-md-12">
<div class="input-group mb-3">
{{ form_widget(form.q)}}
<button type="submit" class="btn btn-chill-l-gray"><i class="fa fa-search"></i></button>
</div>
</div>
</div>
{% endif %}
</div>
{% if form.checkboxes|length > 0 %}
{% for checkbox_name, options in form.checkboxes %}
<div class="row gx-0">
<div class="col-md-12">
{% for c in form['checkboxes'][checkbox_name].children %}
<div class="form-check form-check-inline">
{{ form_widget(c) }}
{{ form_label(c) }}
</div>
{% endfor %}
</div>
</div>
{% if loop.last %}
<div class="row gx-0">
<div class="col-md-12">
<ul class="record_actions">
<li>
<button type="submit" class="btn btn-misc"><i class="fa fa-filter"></i></button>
</li>
</ul>
</div>
</div>
{% endif %}
{% endfor %}
{% endif %}
</div>
{{ form_end(form) }}

View File

@@ -8,7 +8,7 @@
{{ form_start(form) }}
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path(cancel_route, cancel_parameters|default( { } ) ) }}" class="btn btn-cancel">
{{ 'Cancel'|trans }}
@@ -18,5 +18,5 @@
{{ form_widget(form.submit, { 'attr' : { 'class' : "btn btn-delete" } } ) }}
</li>
</ul>
{{ form_end(form) }}
{{ form_end(form) }}

View File

@@ -23,6 +23,8 @@ use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\HasCenterInterface;
use Chill\MainBundle\Entity\HasScopeInterface;
use Chill\MainBundle\Repository\UserACLAwareRepository;
use Chill\MainBundle\Repository\UserACLAwareRepositoryInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher;
use Chill\MainBundle\Security\Resolver\ScopeResolverInterface;
@@ -62,6 +64,8 @@ class AuthorizationHelper implements AuthorizationHelperInterface
protected LoggerInterface $logger;
private UserACLAwareRepositoryInterface $userACLAwareRepository;
public function __construct(
RoleHierarchyInterface $roleHierarchy,
ParameterBagInterface $parameterBag,
@@ -320,33 +324,15 @@ class AuthorizationHelper implements AuthorizationHelperInterface
/**
*
* @deprecated use UserACLAwareRepositoryInterface::findUsersByReachedACL instead
* @param Center|Center[]|array $center
* @param Scope|Scope[]|array|null $scope
* @param bool $onlyActive true if get only active users
* @return User[]
*/
public function findUsersReaching(string $role, Center $center, Scope $circle = null): array
public function findUsersReaching(string $role, $center, $scope = null, bool $onlyEnabled = true): array
{
$parents = $this->getParentRoles($role);
$parents[] = $role;
$qb = $this->em->createQueryBuilder();
$qb
->select('u')
->from(User::class, 'u')
->join('u.groupCenters', 'gc')
->join('gc.permissionsGroup', 'pg')
->join('pg.roleScopes', 'rs')
->where('gc.center = :center')
->andWhere($qb->expr()->in('rs.role', $parents))
;
$qb->setParameter('center', $center);
if ($circle !== null) {
$qb->andWhere('rs.scope = :circle')
->setParameter('circle', $circle)
;
}
return $qb->getQuery()->getResult();
return $this->userACLAwareRepository->findUsersByReachedACL($role, $center, $scope, $onlyEnabled);
}
/**

View File

@@ -12,6 +12,12 @@ class FilterOrderHelper
private FormFactoryInterface $formFactory;
private RequestStack $requestStack;
private ?array $searchBoxFields = null;
private array $checkboxes = [];
private ?array $submitted = null;
private ?string $formName = 'filter';
private string $formType = FilterOrderType::class;
private array $formOptions = [];
public function __construct(
FormFactoryInterface $formFactory,
@@ -28,6 +34,32 @@ class FilterOrderHelper
return $this;
}
public function addCheckbox(string $name, array $choices, ?array $default = [], ?array $trans = []): self
{
$missing = count($choices) - count($trans) - 1;
$this->checkboxes[$name] = [
'choices' => $choices, 'default' => $default,
'trans' =>
\array_merge(
$trans,
0 < $missing ?
array_fill(0, $missing, null) : []
)
];
return $this;
}
public function getCheckboxData(string $name): array
{
return $this->getFormData()['checkboxes'][$name];
}
public function getCheckboxes(): array
{
return $this->checkboxes;
}
public function hasSearchBox(): bool
{
return $this->searchBoxFields !== null;
@@ -35,26 +67,41 @@ class FilterOrderHelper
private function getFormData(): array
{
return [
'q' => $this->getQueryString()
];
if (NULL === $this->submitted) {
$this->submitted = $this->buildForm()
->getData();
}
return $this->submitted;
}
private function getDefaultData(): array
{
$r = [];
if ($this->hasSearchBox()) {
$r['q'] = '';
}
foreach ($this->checkboxes as $name => $c) {
$r['checkboxes'][$name] = $c['default'];
}
return $r;
}
public function getQueryString(): ?string
{
$q = $this->requestStack->getCurrentRequest()
->query->get('q', null);
return empty($q) ? NULL : $q;
return $this->getFormData()['q'];
}
public function buildForm($name = null, string $type = FilterOrderType::class, array $options = []): FormInterface
public function buildForm(): FormInterface
{
return $this->formFactory
->createNamed($name, $type, $this->getFormData(), \array_merge([
->createNamed($this->formName, $this->formType, $this->getDefaultData(), \array_merge([
'helper' => $this,
'method' => 'GET',
'csrf_protection' => false,
], $options));
], $this->formOptions))
->handleRequest($this->requestStack->getCurrentRequest());
}
}

View File

@@ -8,6 +8,7 @@ use Symfony\Component\HttpFoundation\RequestStack;
class FilterOrderHelperBuilder
{
private ?array $searchBoxFields = null;
private array $checkboxes = [];
private FormFactoryInterface $formFactory;
private RequestStack $requestStack;
@@ -26,6 +27,13 @@ class FilterOrderHelperBuilder
return $this;
}
public function addCheckbox(string $name, array $choices, ?array $default = [], ?array $trans = []): self
{
$this->checkboxes[$name] = [ 'choices' => $choices, 'default' => $default, 'trans' => $trans];
return $this;
}
public function build(): FilterOrderHelper
{
$helper = new FilterOrderHelper(
@@ -34,6 +42,13 @@ class FilterOrderHelperBuilder
);
$helper->setSearchBox($this->searchBoxFields);
foreach ($this->checkboxes as $name => [
'choices' => $choices,
'default' => $default,
'trans' => $trans
]) {
$helper->addCheckbox($name, $choices, $default, $trans);
}
return $helper;
}

View File

@@ -11,6 +11,8 @@ services:
autowire: true
autoconfigure: true
Chill\MainBundle\Repository\UserACLAwareRepositoryInterface: '@Chill\MainBundle\Repository\UserACLAwareRepository'
Chill\MainBundle\Serializer\Normalizer\:
resource: '../Serializer/Normalizer'
autoconfigure: true