diff --git a/CHANGELOG.md b/CHANGELOG.md index 34047e541..2734e50de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ and this project adheres to ## Unreleased - +* [tasks] improve tasks with filter order +* [tasks] refactor singleControllerTasks: limit the number of conditions from the context ## Test releases diff --git a/src/Bundle/ChillCalendarBundle/Menu/UserMenuBuilder.php b/src/Bundle/ChillCalendarBundle/Menu/UserMenuBuilder.php index 14e775234..7eb161a50 100644 --- a/src/Bundle/ChillCalendarBundle/Menu/UserMenuBuilder.php +++ b/src/Bundle/ChillCalendarBundle/Menu/UserMenuBuilder.php @@ -17,6 +17,7 @@ */ namespace Chill\CalendarBundle\Menu; +use Chill\MainBundle\Entity\User; use Chill\MainBundle\Routing\LocalMenuBuilderInterface; use Knp\Menu\MenuItem; use Chill\TaskBundle\Templating\UI\CountNotificationTask; @@ -72,17 +73,19 @@ class UserMenuBuilder implements LocalMenuBuilderInterface { $user = $this->tokenStorage->getToken()->getUser(); - if ($this->authorizationChecker->isGranted('ROLE_USER')){ - $menu->addChild("My calendar list", [ - 'route' => 'chill_calendar_calendar_list', - 'routeParameters' => [ - 'user_id' => $user->getId(), - ] - ]) - ->setExtras([ - 'order' => 9, - 'icon' => 'tasks' - ]); + if ($this->authorizationChecker->isGranted('ROLE_USER') + && $user instanceof User + ) { + $menu->addChild("My calendar list", [ + 'route' => 'chill_calendar_calendar_list', + 'routeParameters' => [ + 'user_id' => $user->getId(), + ] + ]) + ->setExtras([ + 'order' => 9, + 'icon' => 'tasks' + ]); } } @@ -90,5 +93,5 @@ class UserMenuBuilder implements LocalMenuBuilderInterface { return [ 'user' ]; } - + } diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index 972c046d9..7b2f0a3a1 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -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' => [ diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php b/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php index c711f911f..b7cff5821 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php @@ -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; } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonbArrayLength.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonbArrayLength.php new file mode 100644 index 000000000..39049748c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonbArrayLength.php @@ -0,0 +1,35 @@ +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); + } +} diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonbExistsInArray.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonbExistsInArray.php index 6c4eaa5c3..94e85048c 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonbExistsInArray.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonbExistsInArray.php @@ -1,7 +1,7 @@ + @author Julien Fastré */ 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) ); diff --git a/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php b/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php index 3976625a1..564b08596 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php @@ -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') diff --git a/src/Bundle/ChillMainBundle/Form/Type/ScopePickerType.php b/src/Bundle/ChillMainBundle/Form/Type/ScopePickerType.php index ebf474657..2785365cf 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/ScopePickerType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/ScopePickerType.php @@ -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é */ 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) diff --git a/src/Bundle/ChillMainBundle/Form/Type/UserPickerType.php b/src/Bundle/ChillMainBundle/Form/Type/UserPickerType.php index fece2d6d4..2558f4a4f 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/UserPickerType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/UserPickerType.php @@ -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; }) ; diff --git a/src/Bundle/ChillMainBundle/Repository/UserACLAwareRepository.php b/src/Bundle/ChillMainBundle/Repository/UserACLAwareRepository.php new file mode 100644 index 000000000..44399000b --- /dev/null +++ b/src/Bundle/ChillMainBundle/Repository/UserACLAwareRepository.php @@ -0,0 +1,54 @@ +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(); + } + +} diff --git a/src/Bundle/ChillMainBundle/Repository/UserACLAwareRepositoryInterface.php b/src/Bundle/ChillMainBundle/Repository/UserACLAwareRepositoryInterface.php new file mode 100644 index 000000000..eb249911b --- /dev/null +++ b/src/Bundle/ChillMainBundle/Repository/UserACLAwareRepositoryInterface.php @@ -0,0 +1,22 @@ +
-
-
- {{ form_widget(form.q)}} - + {% if form.vars.has_search_box %} +
+
+ {{ form_widget(form.q)}} + +
-
+ {% endif %}
+ {% if form.checkboxes|length > 0 %} + {% for checkbox_name, options in form.checkboxes %} +
+
+ {% for c in form['checkboxes'][checkbox_name].children %} +
+ {{ form_widget(c) }} + {{ form_label(c) }} +
+ {% endfor %} +
+
+ {% if loop.last %} +
+
+
    +
  • + +
  • +
+
+
+ {% endif %} + {% endfor %} + {% endif %}
{{ form_end(form) }} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Util/confirmation_template.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Util/confirmation_template.html.twig index 7ffec0bbd..6bec0975a 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Util/confirmation_template.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Util/confirmation_template.html.twig @@ -8,7 +8,7 @@ {{ form_start(form) }} -