From f1eefa7ed744f44af7b836e6ac788309488ab97e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 30 Oct 2019 22:54:24 +0100 Subject: [PATCH] add search by phone + format phonenumber --- CHANGELOG.md | 20 +++ DependencyInjection/ChillPersonExtension.php | 12 +- DependencyInjection/Configuration.php | 14 +- Entity/PersonRepository.php | 29 +++- Repository/PersonRepository.php | 100 +++++++++++++ Resources/config/doctrine/Person.orm.yml | 2 +- Resources/config/services.yml | 6 - Resources/config/services/repository.yml | 13 ++ Resources/config/services/search_by_phone.yml | 11 ++ Resources/public/sass/index.js | 5 + Resources/public/sass/mobile-alt-solid.svg | 1 + .../public/sass/person_by_phonenumber.scss | 25 ++++ Resources/public/sass/phone-alt-solid.svg | 1 + Resources/translations/messages.fr.yml | 1 + Resources/views/Person/list.html.twig | 9 ++ .../Person/list_by_phonenumber.html.twig | 108 ++++++++++++++ Resources/views/Person/view.html.twig | 4 +- Search/PersonSearchByPhone.php | 133 ++++++++++++++++++ chill.webpack.config.js | 1 + 19 files changed, 469 insertions(+), 26 deletions(-) create mode 100644 Repository/PersonRepository.php create mode 100644 Resources/config/services/repository.yml create mode 100644 Resources/config/services/search_by_phone.yml create mode 100644 Resources/public/sass/index.js create mode 100644 Resources/public/sass/mobile-alt-solid.svg create mode 100644 Resources/public/sass/person_by_phonenumber.scss create mode 100644 Resources/public/sass/phone-alt-solid.svg create mode 100644 Resources/views/Person/list_by_phonenumber.html.twig create mode 100644 Search/PersonSearchByPhone.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 38a69343d..bab2f2d1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,3 +48,23 @@ Version 1.5.7 - add a link between accompanying person and user - add an icon when the file is opened / closed in result list, and in person rendering macro - improve command to move person and all data: allow to delete some entities during move and add events + +Master branch +============= + +- add search by phonenumber, with a custom SearchInterface + + This can be activated or desactivated by config: + + ``` + chill_person: + enabled: true + search: + enabled: true + + # enable search by phone. 'always' show the result on every result. 'on-domain' will show the result only if the domain is given in the search box. 'never' disable this feature + search_by_phone: on-domain # One of "always"; "on-domain"; "never" + ``` +- format phonenumber using twilio (if available) ; +- add `record_actions` in person search result list: users can click on a little eye to open person page ; + diff --git a/DependencyInjection/ChillPersonExtension.php b/DependencyInjection/ChillPersonExtension.php index 3f78351cc..cdf707f1d 100644 --- a/DependencyInjection/ChillPersonExtension.php +++ b/DependencyInjection/ChillPersonExtension.php @@ -44,10 +44,6 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); - // set configuration for double metaphone - $container->setParameter('cl_chill_person.search.use_double_metaphone', - $config['search']['use_double_metaphone']); - // set configuration for validation $container->setParameter('chill_person.validation.birtdate_not_before', $config['validation']['birthdate_not_after']); @@ -67,6 +63,14 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac $loader->load('services/command.yml'); $loader->load('services/actions.yml'); $loader->load('services/form.yml'); + $loader->load('services/repository.yml'); + + // load service advanced search only if configure + if ($config['search']['search_by_phone'] != 'never') { + $loader->load('services/search_by_phone.yml'); + $container->setParameter('chill_person.search.search_by_phone', + $config['search']['search_by_phone']); + } if ($container->getParameter('chill_person.accompanying_period') !== 'hidden') { $loader->load('services/exports_accompanying_period.yml'); diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index f71344029..471e7d958 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -30,12 +30,14 @@ class Configuration implements ConfigurationInterface ->arrayNode('search') ->canBeDisabled() ->children() - ->booleanNode('use_double_metaphone') - ->defaultFalse() - ->end() // use_double_metaphone, parent = children for 'search' - ->booleanNode('use_trigrams') - ->defaultFalse() - ->end() // use_trigrams, parent = children of 'search' + ->enumNode('search_by_phone') + ->values(['always', 'on-domain', 'never']) + ->defaultValue('on-domain') + ->info('enable search by phone. \'always\' show the result ' + . 'on every result. \'on-domain\' will show the result ' + . 'only if the domain is given in the search box. ' + . '\'never\' disable this feature') + ->end() ->end() //children for 'search', parent = array node 'search' ->end() // array 'search', parent = children of root ->arrayNode('validation') diff --git a/Entity/PersonRepository.php b/Entity/PersonRepository.php index 6d1b86f0b..9619b2264 100644 --- a/Entity/PersonRepository.php +++ b/Entity/PersonRepository.php @@ -1,15 +1,30 @@ + * + * 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\Entity; -use Doctrine\ORM\EntityRepository; +@trigger_error(__CLASS__." is deprecated since 2019-10-30. Use " + .\Chill\PersonBundle\Repository\PersonRepository::class.' instead.', + E_USER_DEPRECATED); /** - * PersonRepository - * - * This class was generated by the Doctrine ORM. Add your own custom - * repository methods below. + * + * @deprecated since 2019-10-30. Use \Chill\PersonBundle\Repository\PersonRepository instead. */ -class PersonRepository extends EntityRepository +class PersonRepository extends \Chill\PersonBundle\Repository\PersonRepository { } diff --git a/Repository/PersonRepository.php b/Repository/PersonRepository.php new file mode 100644 index 000000000..3b61845b8 --- /dev/null +++ b/Repository/PersonRepository.php @@ -0,0 +1,100 @@ + + * + * 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\Repository; + +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\QueryBuilder; + +/** + * PersonRepository + * + */ +class PersonRepository extends EntityRepository +{ + public function findByPhone( + string $phonenumber, + $centers, + $firstResult, + $maxResults, + array $only = ['mobile', 'phone'] + ) { + $qb = $this->createQueryBuilder('p'); + $qb->select('p'); + + $this->addByCenters($qb, $centers); + $this->addPhoneNumber($qb, $phonenumber, $only); + + $qb->setFirstResult($firstResult) + ->setMaxResults($maxResults) + ; + + return $qb->getQuery()->getResult(); + } + + public function countByPhone( + string $phonenumber, + $centers, + array $only = ['mobile', 'phone'] + ): int + { + $qb = $this->createQueryBuilder('p'); + $qb->select('COUNT(p)'); + + $this->addByCenters($qb, $centers); + $this->addPhoneNumber($qb, $phonenumber, $only); + + return $qb->getQuery()->getSingleScalarResult(); + } + + protected function addPhoneNumber(QueryBuilder $qb, string $phonenumber, array $only) + { + if (count($only) === 0) { + throw new \Exception("No array field to search"); + } + + $phonenumber = $this->parsePhoneNumber($phonenumber); + + $orX = $qb->expr()->orX(); + + if (\in_array('mobile', $only)) { + $orX->add($qb->expr()->like("REPLACE(p.mobilenumber, ' ', '')", ':phonenumber')); + } + if (\in_array('phone', $only)) { + $orX->add($qb->expr()->like("REPLACE(p.phonenumber, ' ', '')", ':phonenumber')); + } + + $qb->andWhere($orX); + + $qb->setParameter('phonenumber', '%'.$phonenumber.'%'); + } + + + protected function parsePhoneNumber($phonenumber): string + { + return \str_replace(' ', '', $phonenumber); + } + + protected function addByCenters(QueryBuilder $qb, array $centers) + { + if (count($centers) > 0) { + $qb->andWhere($qb->expr()->in('p.center', ':centers')); + $qb->setParameter('centers', $centers); + } + } +} diff --git a/Resources/config/doctrine/Person.orm.yml b/Resources/config/doctrine/Person.orm.yml index 543757f9a..081a710da 100644 --- a/Resources/config/doctrine/Person.orm.yml +++ b/Resources/config/doctrine/Person.orm.yml @@ -4,7 +4,7 @@ Chill\PersonBundle\Entity\Person: indexes: person_names: columns: [firstName, lastName] - repositoryClass: Chill\PersonBundle\Entity\PersonRepository + repositoryClass: Chill\PersonBundle\Repository\PersonRepository fields: id: type: integer diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 26ffd979c..b17bf6acb 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -47,9 +47,3 @@ services: tags: - { name: form.type, alias: chill_personbundle_person_creation } - - chill.person.repository.person: - class: Chill\PersonBundle\Entity\PersonRepository - factory: ['@doctrine.orm.entity_manager', getRepository] - arguments: - - 'Chill\PersonBundle\Entity\Person' diff --git a/Resources/config/services/repository.yml b/Resources/config/services/repository.yml new file mode 100644 index 000000000..d693ef841 --- /dev/null +++ b/Resources/config/services/repository.yml @@ -0,0 +1,13 @@ +services: + chill.person.repository.person: + class: Chill\PersonBundle\Person\PersonRepository + deprecated: the service '%service_id%' is deprecated since 2019-10-30 and will be removed soon. Use 'Chill\PersonBundle\Repository\PersonRepository' instead + factory: ['@doctrine.orm.entity_manager', getRepository] + arguments: + - 'Chill\PersonBundle\Entity\Person' + + Chill\PersonBundle\Repository\PersonRepository: + class: Chill\PersonBundle\Person\PersonRepository + factory: ['@doctrine.orm.entity_manager', getRepository] + arguments: + - 'Chill\PersonBundle\Entity\Person' diff --git a/Resources/config/services/search_by_phone.yml b/Resources/config/services/search_by_phone.yml new file mode 100644 index 000000000..4a20e898a --- /dev/null +++ b/Resources/config/services/search_by_phone.yml @@ -0,0 +1,11 @@ +services: + Chill\PersonBundle\Search\PersonSearchByPhone: + arguments: + - '@Chill\PersonBundle\Repository\PersonRepository' + - '@security.token_storage' + - '@chill.main.security.authorization.helper' + - '@chill_main.paginator_factory' + - '@Symfony\Component\Templating\EngineInterface' + - '%chill_person.search.search_by_phone%' + tags: + - { name: chill.search, alias: 'person_by_phone' } \ No newline at end of file diff --git a/Resources/public/sass/index.js b/Resources/public/sass/index.js new file mode 100644 index 000000000..35945c7ff --- /dev/null +++ b/Resources/public/sass/index.js @@ -0,0 +1,5 @@ +require('./phone-alt-solid.svg'); +require('./mobile-alt-solid.svg'); +require('./person_by_phonenumber.scss'); + + diff --git a/Resources/public/sass/mobile-alt-solid.svg b/Resources/public/sass/mobile-alt-solid.svg new file mode 100644 index 000000000..ae8b81bb1 --- /dev/null +++ b/Resources/public/sass/mobile-alt-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/public/sass/person_by_phonenumber.scss b/Resources/public/sass/person_by_phonenumber.scss new file mode 100644 index 000000000..a1066c08f --- /dev/null +++ b/Resources/public/sass/person_by_phonenumber.scss @@ -0,0 +1,25 @@ +.person-list__--by-phonenumber { + .person-list__--by-phonenumber__phones { + ul { + list-style: none inside; + padding: 0; + margin: 0; + + li { + margin: 0.80rem; + + img { + vertical-align: baseline; + height: 0.90rem; + margin-right: 0.20rem; + } + pre { + display: inline; + } + } + } + + + } +} +; diff --git a/Resources/public/sass/phone-alt-solid.svg b/Resources/public/sass/phone-alt-solid.svg new file mode 100644 index 000000000..6460d2d9e --- /dev/null +++ b/Resources/public/sass/phone-alt-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/translations/messages.fr.yml b/Resources/translations/messages.fr.yml index 17005b044..85ed089ac 100644 --- a/Resources/translations/messages.fr.yml +++ b/Resources/translations/messages.fr.yml @@ -93,6 +93,7 @@ Reset: 'Remise à zéro' '%nb% person with similar name. Please verify that this is a new person': '{1} Une personne a un nom similaire. Vérifiez qu''il ne s''agit pas d''elle. | ]1, Inf] %nb% personnes ont un nom similaire. Vérifiez qu''il ne s''agit pas de l''une d''elles.' 'The person has been created': 'Le dossier a été créé' 'Person search results': 'Recherche de personnes' +Person search results by phonenumber: Recherche de personnes par numéro de téléphone 'Search within persons': 'Recherche parmi les personnes' '%total% persons matching the search pattern:': '{0} Aucune personne ne correspond aux termes de recherche : | {1} Une personne a été trouvée par la recherche : | ]1,Inf] %total% personnes correspondent aux termes de recherche :' 'Last opening since %last_opening%': 'Dernière ouverture le %last_opening%.' diff --git a/Resources/views/Person/list.html.twig b/Resources/views/Person/list.html.twig index ab1a12a7d..3234d496f 100644 --- a/Resources/views/Person/list.html.twig +++ b/Resources/views/Person/list.html.twig @@ -33,6 +33,7 @@ {% trans %}Name{% endtrans %} {% trans %}Date of birth{% endtrans %} {% trans %}Nationality{% endtrans %} +   @@ -63,6 +64,14 @@ {{ 'Without nationality'|trans }} {% endif %} + +
    +
  • + {% if is_granted('CHILL_PERSON_UPDATE', person) %} +
  • + {% endif %} +
+ {% endfor %} diff --git a/Resources/views/Person/list_by_phonenumber.html.twig b/Resources/views/Person/list_by_phonenumber.html.twig new file mode 100644 index 000000000..78195f165 --- /dev/null +++ b/Resources/views/Person/list_by_phonenumber.html.twig @@ -0,0 +1,108 @@ +{# + * Copyright (C) 2014, Champs Libres Cooperative SCRLFS, + * + * 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 . +#} +

{{ title|default('Person search results by phonenumber')|trans }}

+ +

+ {{ '%total% persons matching the search pattern:'|transchoice( total, { '%total%' : total}) }} + + {{ pattern }} + +

+ +

{{ 'Results %start%-%end% of %total%'|trans({ '%start%' : start, '%end%': start + persons|length, '%total%' : total } ) }}

+ +{% if persons|length > 0 %} + + + + + + + + + + + + {% for person in persons %} + + + + + + + {% endfor %} + +
{% trans %}Name{% endtrans %}{% trans %}Date of birth{% endtrans %}{% trans %}Phonenumber{% endtrans %} 
+ {% set is_open = person.isOpen() %} + + {{person.firstName}} {{person.lastName}} + {% spaceless %} + {% if chill_person.fields.accompanying_period == 'visible' %} + {% if is_open == false %} + + {% else %} + + {% endif %} + {% endif %} + {% endspaceless %} + + + {% if person.birthdate is not null %}{{person.birthdate|localizeddate('long', 'none', app.request.locale) }}{% else %}{{ 'Unknown date of birth'|trans }}{% endif %} + + + +
    +
  • + {% if is_granted('CHILL_PERSON_UPDATE', person) %} +
  • + {% endif %} +
+
+ + +{% endif %} + +{% if preview == false %} +{{ chill_pagination(paginator) }} +{% endif %} + diff --git a/Resources/views/Person/view.html.twig b/Resources/views/Person/view.html.twig index 61be78b6e..c6db21ef5 100644 --- a/Resources/views/Person/view.html.twig +++ b/Resources/views/Person/view.html.twig @@ -206,13 +206,13 @@ This view should receive those arguments: {%- if chill_person.fields.phonenumber == 'visible' -%}
{{ 'Phonenumber'|trans }} :
-
{% if person.phonenumber is not empty %}
{{ person.phonenumber}}
{% else %}{{ 'No data given'|trans }}{% endif %}
+
{% if person.phonenumber is not empty %}
{{ person.phonenumber|chill_format_phonenumber }}
{% else %}{{ 'No data given'|trans }}{% endif %}
{% endif %} {%- if chill_person.fields.mobilenumber == 'visible' -%}
{{ 'Mobilenumber'|trans }} :
-
{% if person.mobilenumber is not empty %}
{{ person.mobilenumber}}
{% else %}{{ 'No data given'|trans }}{% endif %}
+
{% if person.mobilenumber is not empty %}
{{ person.mobilenumber|chill_format_phonenumber }}
{% else %}{{ 'No data given'|trans }}{% endif %}
{% endif %} {%- if chill_person.fields.contact_info == 'visible' -%} diff --git a/Search/PersonSearchByPhone.php b/Search/PersonSearchByPhone.php new file mode 100644 index 000000000..08152144b --- /dev/null +++ b/Search/PersonSearchByPhone.php @@ -0,0 +1,133 @@ + + * + * 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\Search; + +use Chill\MainBundle\Search\AbstractSearch; +use Chill\PersonBundle\Repository\PersonRepository; +use Chill\PersonBundle\Entity\Person; +use Chill\MainBundle\Search\SearchInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Chill\PersonBundle\Security\Authorization\PersonVoter; +use Chill\MainBundle\Security\Authorization\AuthorizationHelper; +use Chill\MainBundle\Pagination\PaginatorFactory; +use Symfony\Component\Security\Core\Role\Role; +use Symfony\Component\Templating\EngineInterface; + +/** + * + * + */ +class PersonSearchByPhone extends AbstractSearch +{ + + /** + * + * @var PersonRepository + */ + private $personRepository; + + /** + * + * @var TokenStorageInterface + */ + private $tokenStorage; + + /** + * + * @var AuthorizationHelper + */ + private $helper; + + /** + * + * @var PaginatorFactory + */ + protected $paginatorFactory; + + /** + * + * @var bool + */ + protected $activeByDefault; + + /** + * + * @var Templating + */ + protected $engine; + + const NAME = 'phone'; + + public function __construct( + PersonRepository $personRepository, + TokenStorageInterface $tokenStorage, + AuthorizationHelper $helper, + PaginatorFactory $paginatorFactory, + EngineInterface $engine, + $activeByDefault) + { + $this->personRepository = $personRepository; + $this->tokenStorage = $tokenStorage; + $this->helper = $helper; + $this->paginatorFactory = $paginatorFactory; + $this->engine = $engine; + $this->activeByDefault = $activeByDefault === 'always'; + } + + public function getOrder(): int + { + return 110; + } + + public function isActiveByDefault(): bool + { + return $this->activeByDefault; + } + + public function renderResult(array $terms, $start = 0, $limit = 50, $options = array(), $format = 'html') + { + $phonenumber = $terms['_default']; + $centers = $this->helper->getReachableCenters($this->tokenStorage + ->getToken()->getUser(), new Role(PersonVoter::SEE)); + $total = $this->personRepository + ->countByPhone($phonenumber, $centers); + $persons = $this->personRepository + ->findByPhone($phonenumber, $centers, $start, $limit) + ; + $paginator = $this->paginatorFactory + ->create($total); + + return $this->engine->render('ChillPersonBundle:Person:list_by_phonenumber.html.twig', + array( + 'persons' => $persons, + 'pattern' => $this->recomposePattern($terms, array(), $terms['_domain'] ?? self::NAME), + 'phonenumber' => $phonenumber, + 'total' => $total, + 'start' => $start, + 'search_name' => self::NAME, + 'preview' => $options[SearchInterface::SEARCH_PREVIEW_OPTION], + 'paginator' => $paginator + )); + } + + public function supports($domain, $format): bool + { + return $domain === 'phone' && $format === 'html'; + } +} diff --git a/chill.webpack.config.js b/chill.webpack.config.js index 97222b223..cfc6b7daa 100644 --- a/chill.webpack.config.js +++ b/chill.webpack.config.js @@ -1,4 +1,5 @@ // this file loads all assets from the Chill person bundle require('./Resources/public/css/person.css'); +require('./Resources/public/sass/index.js'); //require('./Resources/public/sass/person.scss');