From f2ed32e458344972271c3c1dc221b5a4baffa17b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 27 Aug 2018 13:16:54 +0200 Subject: [PATCH] allow to load person interactively - return results as json in PersonSearch - update PickPersonType to load peoples with ChoiceLoader --- Controller/PersonController.php | 3 +- Form/ChoiceLoader/PersonChoiceLoader.php | 104 +++++++++++++++++++++++ Form/Type/PickPersonType.php | 60 +++++++++---- Resources/config/services.yml | 2 + Search/PersonSearch.php | 70 ++++++++++----- 5 files changed, 201 insertions(+), 38 deletions(-) create mode 100644 Form/ChoiceLoader/PersonChoiceLoader.php diff --git a/Controller/PersonController.php b/Controller/PersonController.php index abec3629f..1ab1f5fa1 100644 --- a/Controller/PersonController.php +++ b/Controller/PersonController.php @@ -32,6 +32,7 @@ use Symfony\Component\Security\Core\Role\Role; use Chill\PersonBundle\Security\Authorization\PersonVoter; use Chill\PersonBundle\Search\SimilarPersonMatcher; use Symfony\Component\Translation\TranslatorInterface; +use Chill\MainBundle\Search\SearchProvider; class PersonController extends Controller { @@ -46,7 +47,7 @@ class PersonController extends Controller * @var TranslatorInterface */ protected $translator; - + public function __construct( SimilarPersonMatcher $similarPersonMatcher, TranslatorInterface $translator diff --git a/Form/ChoiceLoader/PersonChoiceLoader.php b/Form/ChoiceLoader/PersonChoiceLoader.php new file mode 100644 index 000000000..1c2ea9897 --- /dev/null +++ b/Form/ChoiceLoader/PersonChoiceLoader.php @@ -0,0 +1,104 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Form\ChoiceLoader; + +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Doctrine\ORM\EntityRepository; +use Symfony\Component\Form\ChoiceList\LazyChoiceList; +use Chill\PersonBundle\Entity\Person; + +/** + * + * + * @author Julien Fastré + */ +class PersonChoiceLoader implements ChoiceLoaderInterface +{ + /** + * + * @var EntityRepository + */ + protected $personRepository; + + protected $lazyLoadedPersons = []; + + protected $centers = []; + + public function __construct( + EntityRepository $personRepository, + array $centers = null + ) { + $this->personRepository = $personRepository; + if (NULL !== $centers) { + $this->centers = $centers; + } + } + + protected function hasCenterFilter() + { + return count($this->centers) > 0; + } + + public function loadChoiceList($value = null): ChoiceListInterface + { + $list = new \Symfony\Component\Form\ChoiceList\ArrayChoiceList( + $this->lazyLoadedPersons, + function(Person $p) use ($value) { + return \call_user_func($value, $p); + }); + + return $list; + } + + public function loadChoicesForValues(array $values, $value = null) + { + $choices = []; + + foreach($values as $value) { + $person = $this->personRepository->find($value); + + if ($this->hasCenterFilter() && + !\in_array($person->getCenter(), $this->centers)) { + throw new \RuntimeException("chosen a person not in correct center"); + } + + $choices[] = $person; + } + + return $choices; + } + + public function loadValuesForChoices(array $choices, $value = null) + { + $values = []; + + foreach ($choices as $choice) { + if (NULL === $choice) { + $values[] = null; + continue; + } + + $id = \call_user_func($value, $choice); + $values[] = $id; + $this->lazyLoadedPersons[$id] = $choice; + } + + return $values; + } +} diff --git a/Form/Type/PickPersonType.php b/Form/Type/PickPersonType.php index 7ee2f3fb4..cd45f7dc7 100644 --- a/Form/Type/PickPersonType.php +++ b/Form/Type/PickPersonType.php @@ -26,12 +26,16 @@ use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInt use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Role\Role; use Symfony\Bridge\Doctrine\Form\Type\EntityType; - +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Chill\MainBundle\Entity\GroupCenter; use Chill\PersonBundle\Entity\Person; use Chill\MainBundle\Security\Authorization\AuthorizationHelper; use Chill\MainBundle\Entity\Center; use Chill\PersonBundle\Entity\PersonRepository; +use Chill\PersonBundle\Search\PersonSearch; +use Symfony\Component\Translation\TranslatorInterface; +use Chill\PersonBundle\Form\ChoiceLoader\PersonChoiceLoader; +use Symfony\Component\OptionsResolver\Options; /** * This type allow to pick a person. @@ -67,22 +71,36 @@ class PickPersonType extends AbstractType * @var AuthorizationHelper */ protected $authorizationHelper; + + /** + * + * @var UrlGeneratorInterface + */ + protected $urlGenerator; + + /** + * + * @var TranslatorInterface + */ + protected $translator; public function __construct( PersonRepository $personRepository, TokenStorageInterface $tokenStorage, - AuthorizationHelper $authorizationHelper + AuthorizationHelper $authorizationHelper, + UrlGeneratorInterface $urlGenerator, + TranslatorInterface $translator ) { $this->personRepository = $personRepository; $this->user = $tokenStorage->getToken()->getUser(); $this->authorizationHelper = $authorizationHelper; + $this->urlGenerator = $urlGenerator; + $this->translator = $translator; } - public function buildForm(FormBuilderInterface $builder, array $options) + protected function filterCentersfom(Options $options) { - $qb = $options['query_builder']; - if ($options['role'] === NULL) { $centers = array_map(function (GroupCenter $g) { @@ -98,10 +116,10 @@ class PickPersonType extends AbstractType $selectedCenters = $centers; } else { $selectedCenters = array(); - $options['centers'] = is_array($options['centers']) ? + $optionsCenters = is_array($options['centers']) ? $options['centers'] : array($options['centers']); - foreach ($options['centers'] as $c) { + foreach ($optionsCenters as $c) { // check that every member of the array is a center if (!$c instanceof Center) { throw new \RuntimeException('Every member of the "centers" ' @@ -115,14 +133,8 @@ class PickPersonType extends AbstractType $selectedCenters[] = $c; } } - - - $qb - ->orderBy('p.firstName', 'ASC') - ->orderBy('p.lastName', 'ASC') - ->where($qb->expr()->in('p.center', ':centers')) - ->setParameter('centers', $selectedCenters) - ; + + return $selectedCenters; } public function configureOptions(OptionsResolver $resolver) @@ -151,7 +163,11 @@ class PickPersonType extends AbstractType ); }, 'attr' => array('class' => 'select2 '), - 'query_builder' => $this->personRepository->createQueryBuilder('p') + 'choice_loader' => function(Options $options) { + $centers = $this->filterCentersfom($options); + + return new PersonChoiceLoader($this->personRepository, $centers); + } )); } @@ -159,5 +175,17 @@ class PickPersonType extends AbstractType { return EntityType::class; } + + public function buildView(\Symfony\Component\Form\FormView $view, \Symfony\Component\Form\FormInterface $form, array $options) + { + $view->vars['attr']['data-person-picker'] = true; + $view->vars['attr']['data-select-interactive-loading'] = true; + $view->vars['attr']['data-search-url'] = $this->urlGenerator + ->generate('chill_main_search', [ 'name' => PersonSearch::NAME, '_format' => 'json' ]); + $view->vars['attr']['data-placeholder'] = $this->translator->trans($options['placeholder']); + $view->vars['attr']['data-no-results-label'] = $this->translator->trans('select2.no_results'); + $view->vars['attr']['data-error-load-label'] = $this->translator->trans('select2.error_loading'); + $view->vars['attr']['data-searching-label'] = $this->translator->trans('select2.searching'); + } } diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 5a61e8378..ceeeeeaed 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -65,6 +65,8 @@ services: - "@chill.person.repository.person" - "@security.token_storage" - "@chill.main.security.authorization.helper" + - '@Symfony\Component\Routing\Generator\UrlGeneratorInterface' + - '@Symfony\Component\Translation\TranslatorInterface' tags: - { name: form.type } diff --git a/Search/PersonSearch.php b/Search/PersonSearch.php index 7e349ba27..0c9d0eab4 100644 --- a/Search/PersonSearch.php +++ b/Search/PersonSearch.php @@ -35,6 +35,7 @@ use Chill\MainBundle\Form\Type\ChillDateType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; use Chill\MainBundle\Search\HasAdvancedSearchFormInterface; +use Doctrine\ORM\Query; class PersonSearch extends AbstractSearch implements ContainerAwareInterface, HasAdvancedSearchFormInterface @@ -104,7 +105,7 @@ class PersonSearch extends AbstractSearch implements ContainerAwareInterface, return true; } - public function supports($domain) + public function supports($domain, $format) { return 'person' === $domain; } @@ -113,23 +114,32 @@ class PersonSearch extends AbstractSearch implements ContainerAwareInterface, * (non-PHPdoc) * @see \Chill\MainBundle\Search\SearchInterface::renderResult() */ - public function renderResult(array $terms, $start = 0, $limit = 50, array $options = array()) + public function renderResult(array $terms, $start = 0, $limit = 50, array $options = array(), $format = 'html') { $total = $this->count($terms); $paginator = $this->paginatorFactory->create($total); - return $this->container->get('templating')->render('ChillPersonBundle:Person:list.html.twig', - array( - 'persons' => $this->search($terms, $start, $limit, $options), - 'pattern' => $this->recomposePattern($terms, array('nationality', - 'firstname', 'lastname', 'birthdate', 'gender', - 'birthdate-before','birthdate-after'), $terms['_domain']), - 'total' => $total, - 'start' => $start, - 'search_name' => self::NAME, - 'preview' => $options[SearchInterface::SEARCH_PREVIEW_OPTION], - 'paginator' => $paginator - )); + if ($format === 'html') { + return $this->container->get('templating')->render('ChillPersonBundle:Person:list.html.twig', + array( + 'persons' => $this->search($terms, $start, $limit, $options), + 'pattern' => $this->recomposePattern($terms, array('nationality', + 'firstname', 'lastname', 'birthdate', 'gender', + 'birthdate-before','birthdate-after'), $terms['_domain']), + 'total' => $total, + 'start' => $start, + 'search_name' => self::NAME, + 'preview' => $options[SearchInterface::SEARCH_PREVIEW_OPTION], + 'paginator' => $paginator + )); + } elseif ($format === 'json') { + return [ + 'results' => $this->search($terms, $start, $limit, \array_merge($options, [ 'simplify' => true ])), + 'pagination' => [ + 'more' => $paginator->hasNextPage() + ] + ]; + } } /** @@ -143,17 +153,35 @@ class PersonSearch extends AbstractSearch implements ContainerAwareInterface, protected function search(array $terms, $start, $limit, array $options = array()) { $qb = $this->createQuery($terms, 'search'); + + if ($options['simplify'] ?? false) { + $qb->select( + 'p.id', + $qb->expr()->concat( + 'p.firstName', + $qb->expr()->literal(' '), + 'p.lastName' + ).'AS text' + ); + } else { + $qb->select('p'); + } - $qb->select('p') - ->setMaxResults($limit) - ->setFirstResult($start); + $qb + ->setMaxResults($limit) + ->setFirstResult($start); //order by firstname, lastname - $qb->orderBy('p.firstName') - ->addOrderBy('p.lastName'); - - return $qb->getQuery()->getResult(); + $qb + ->orderBy('p.firstName') + ->addOrderBy('p.lastName'); + + if ($options['simplify'] ?? false) { + return $qb->getQuery()->getResult(Query::HYDRATE_ARRAY); + } else { + return $qb->getQuery()->getResult(); + } } protected function count(array $terms)