diff --git a/Controller/PostalCodeController.php b/Controller/PostalCodeController.php new file mode 100644 index 000000000..cd7dd9ace --- /dev/null +++ b/Controller/PostalCodeController.php @@ -0,0 +1,89 @@ + + * + * 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\MainBundle\Controller; + +use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Component\HttpFoundation\Request; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; +use Chill\MainBundle\Entity\PostalCode; +use Symfony\Component\HttpFoundation\JsonResponse; +use Chill\MainBundle\Templating\TranslatableStringHelper; +use Doctrine\ORM\Query; + +/** + * + * + * @author Julien Fastré + */ +class PostalCodeController extends Controller +{ + /** + * + * @var TranslatableStringHelper + */ + protected $translatableStringHelper; + + public function __construct(TranslatableStringHelper $translatableStringHelper) + { + $this->translatableStringHelper = $translatableStringHelper; + } + + + /** + * + * @Route( + * "{_locale}/postalcode/search" + * ) + * @param Request $request + * @return JsonResponse + */ + public function searchAction(Request $request) + { + $pattern = $request->query->getAlnum('q', ''); + + if (empty($pattern)) { + return new JsonResponse(["results" => [], "pagination" => [ "more" => false]]); + } + + $query = $this->getDoctrine()->getManager() + ->createQuery(sprintf( + "SELECT p.id AS id, p.name AS name, p.code AS code, " + . "country.name AS country_name, " + . "country.countryCode AS country_code " + . "FROM %s p " + . "JOIN p.country country " + . "WHERE LOWER(p.name) LIKE LOWER(:pattern) OR LOWER(p.code) LIKE LOWER(:pattern) " + . "ORDER BY code" + , PostalCode::class) + ) + ->setParameter('pattern', '%'.$pattern.'%') + ->setMaxResults(30) + ; + + $results = \array_map(function($row) { + $row['country_name'] = $this->translatableStringHelper->localize($row['country_name']); + $row['text'] = $row['code']." ".$row["name"]." (".$row['country_name'].")"; + + return $row; + }, $query->getResult(Query::HYDRATE_ARRAY)); + + return new JsonResponse([ 'results' => $results, "pagination" => [ "more" => false ] ]); + } +} diff --git a/Form/ChoiceLoader/PostalCodeChoiceLoader.php b/Form/ChoiceLoader/PostalCodeChoiceLoader.php new file mode 100644 index 000000000..71ebbd919 --- /dev/null +++ b/Form/ChoiceLoader/PostalCodeChoiceLoader.php @@ -0,0 +1,85 @@ + + * + * 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\MainBundle\Form\ChoiceLoader; + +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Chill\MainBundle\Repository\PostalCodeRepository; +use Symfony\Component\Form\ChoiceList\LazyChoiceList; +use Chill\MainBundle\Entity\PostalCode; + +/** + * + * + * @author Julien Fastré + */ +class PostalCodeChoiceLoader implements ChoiceLoaderInterface +{ + /** + * + * @var PostalCodeRepository + */ + protected $postalCodeRepository; + + protected $lazyLoadedPostalCodes = []; + + public function __construct(PostalCodeRepository $postalCodeRepository) + { + $this->postalCodeRepository = $postalCodeRepository; + } + + public function loadChoiceList($value = null): ChoiceListInterface + { + $list = new \Symfony\Component\Form\ChoiceList\ArrayChoiceList( + $this->lazyLoadedPostalCodes, + function(PostalCode $pc) use ($value) { + return \call_user_func($value, $pc); + }); + + return $list; + } + + public function loadChoicesForValues(array $values, $value = null) + { + $choices = []; + + foreach($values as $value) { + $choices[] = $this->postalCodeRepository->find($value); + } + + 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->lazyLoadedPostalCodes[$id] = $choice; + } + + return $values; + } +} diff --git a/Form/Type/PostalCodeType.php b/Form/Type/PostalCodeType.php index bdaae6bc9..1cbbc549d 100644 --- a/Form/Type/PostalCodeType.php +++ b/Form/Type/PostalCodeType.php @@ -21,10 +21,14 @@ namespace Chill\MainBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Form\FormBuilderInterface; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\MainBundle\Entity\PostalCode; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Chill\MainBundle\Form\ChoiceLoader\PostalCodeChoiceLoader; +use Symfony\Component\Translation\TranslatorInterface; /** * A form to pick between PostalCode @@ -39,10 +43,35 @@ class PostalCodeType extends AbstractType * @var TranslatableStringHelper */ protected $translatableStringHelper; + + /** + * + * @var UrlGeneratorInterface + */ + protected $urlGenerator; + + /** + * + * @var PostalCodeChoiceLoader + */ + protected $choiceLoader; + + /** + * + * @var TranslatorInterface + */ + protected $translator; - public function __construct(TranslatableStringHelper $helper) - { + public function __construct( + TranslatableStringHelper $helper, + UrlGeneratorInterface $urlGenerator, + PostalCodeChoiceLoader $choiceLoader, + TranslatorInterface $translator + ) { $this->translatableStringHelper = $helper; + $this->urlGenerator = $urlGenerator; + $this->choiceLoader = $choiceLoader; + $this->translator = $translator; } @@ -55,11 +84,26 @@ class PostalCodeType extends AbstractType { // create a local copy for usage in Closure $helper = $this->translatableStringHelper; - $resolver->setDefault('class', PostalCode::class) + $resolver + ->setDefault('class', PostalCode::class) ->setDefault('choice_label', function(PostalCode $code) use ($helper) { return $code->getCode().' '.$code->getName().' ['. $helper->localize($code->getCountry()->getName()).']'; - } - ); + }) + ->setDefault('choice_loader', $this->choiceLoader) + ->setDefault('placeholder', 'Select a postal code') + ; } + + public function buildView(FormView $view, FormInterface $form, array $options) + { + $view->vars['attr']['data-postal-code'] = 'data-postal-code'; + $view->vars['attr']['data-search-url'] = $this->urlGenerator + ->generate('chill_main_postal_code_search'); + $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/Repository/PostalCodeRepository.php b/Repository/PostalCodeRepository.php new file mode 100644 index 000000000..5b8ab8423 --- /dev/null +++ b/Repository/PostalCodeRepository.php @@ -0,0 +1,28 @@ + + * + * 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\MainBundle\Repository; + +/** + * + * + * @author Julien Fastré + */ +class PostalCodeRepository extends \Doctrine\ORM\EntityRepository +{ + +} diff --git a/Resources/config/doctrine/PostalCode.orm.yml b/Resources/config/doctrine/PostalCode.orm.yml index 09381d465..59d74ddf3 100644 --- a/Resources/config/doctrine/PostalCode.orm.yml +++ b/Resources/config/doctrine/PostalCode.orm.yml @@ -1,6 +1,9 @@ Chill\MainBundle\Entity\PostalCode: type: entity table: chill_main_postal_code + repositoryClass: Chill\MainBundle\Repository\PostalCodeRepository + indexes: + - { name: search_name_code, columns: [ "code", "label" ] } id: id: type: integer diff --git a/Resources/config/routing.yml b/Resources/config/routing.yml index 989a5e8f3..231580c49 100644 --- a/Resources/config/routing.yml +++ b/Resources/config/routing.yml @@ -17,6 +17,10 @@ chill_main_admin: chill_main_exports: resource: "@ChillMainBundle/Resources/config/routing/exports.yml" prefix: "{_locale}/exports" + +chill_postal_code: + resource: "@ChillMainBundle/Resources/config/routing/postal-code.yml" + prefix: "{_locale}/postal-code" root: path: / diff --git a/Resources/config/routing/postal-code.yml b/Resources/config/routing/postal-code.yml new file mode 100644 index 000000000..d8892c098 --- /dev/null +++ b/Resources/config/routing/postal-code.yml @@ -0,0 +1,4 @@ +chill_main_postal_code_search: + path: /search + defaults: { _controller: ChillMainBundle:PostalCode:search } + \ No newline at end of file diff --git a/Resources/config/services/controller.yml b/Resources/config/services/controller.yml index 6f5bbd751..7ead910ec 100644 --- a/Resources/config/services/controller.yml +++ b/Resources/config/services/controller.yml @@ -1,4 +1,6 @@ services: + Chill\MainBundle\Controller\: + autowire: true resource: '../../../Controller' tags: ['controller.service_arguments'] diff --git a/Resources/config/services/form.yml b/Resources/config/services/form.yml index 30502a399..78bfa0280 100644 --- a/Resources/config/services/form.yml +++ b/Resources/config/services/form.yml @@ -54,9 +54,17 @@ services: class: Chill\MainBundle\Form\Type\PostalCodeType arguments: - "@chill.main.helper.translatable_string" + - '@Symfony\Component\Routing\Generator\UrlGeneratorInterface' + - '@chill.main.form.choice_loader.postal_code' + - '@Symfony\Component\Translation\TranslatorInterface' tags: - { name: form.type } + chill.main.form.choice_loader.postal_code: + class: Chill\MainBundle\Form\ChoiceLoader\PostalCodeChoiceLoader + arguments: + - '@Chill\MainBundle\Repository\PostalCodeRepository' + chill.main.form.type.export: class: Chill\MainBundle\Form\Type\Export\ExportType arguments: diff --git a/Resources/config/services/repositories.yml b/Resources/config/services/repositories.yml index 3f40e82dc..56ea84656 100644 --- a/Resources/config/services/repositories.yml +++ b/Resources/config/services/repositories.yml @@ -15,4 +15,13 @@ services: class: Doctrine\ORM\EntityRepository factory: ["@doctrine.orm.entity_manager", getRepository] arguments: - - "Chill\\MainBundle\\Entity\\Scope" \ No newline at end of file + - "Chill\\MainBundle\\Entity\\Scope" + + chill.main.postalcode_repository: + class: Doctrine\ORM\EntityRepository + factory: ["@doctrine.orm.entity_manager", getRepository] + arguments: + - "Chill\\MainBundle\\Entity\\PostalCode" + + Chill\MainBundle\Repository\PostalCodeRepository: '@chill.main.postalcode_repository' + \ No newline at end of file diff --git a/Resources/migrations/Version20180703191509.php b/Resources/migrations/Version20180703191509.php new file mode 100644 index 000000000..82117de9d --- /dev/null +++ b/Resources/migrations/Version20180703191509.php @@ -0,0 +1,33 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + try { + $this->addSql('CREATE EXTENSION IF NOT EXISTS pg_trgm'); + $this->addSql('CREATE INDEX search_name_code ON chill_main_postal_code USING GIN (LOWER(code) gin_trgm_ops, LOWER(label) gin_trgm_ops)'); + } catch (\Exception $e) { + $this->skipIf(true, "Could not create extension pg_trgm"); + } + + } + + public function down(Schema $schema) : void + { + $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('DROP INDEX search_name_code'); + + } +} diff --git a/Resources/public/modules/postal-code/index.js b/Resources/public/modules/postal-code/index.js new file mode 100644 index 000000000..ba81fba90 --- /dev/null +++ b/Resources/public/modules/postal-code/index.js @@ -0,0 +1,36 @@ + +window.addEventListener('load', function (e) { + var + postalCodes = document.querySelectorAll('[data-postal-code]') + ; + + for (let i = 0; i < postalCodes.length; i++) { + let + searchUrl = postalCodes[i].dataset.searchUrl, + noResultsLabel = postalCodes[i].dataset.noResultsLabel, + errorLoadLabel = postalCodes[i].dataset.errorLoadLabel, + searchingLabel = postalCodes[i].dataset.searchingLabel + ; + + + $(postalCodes[i]).select2({ + allowClear: true, + language: { + errorLoading: function () { + return errorLoadLabel; + }, + noResults: function () { + return noResultsLabel; + }, + searching: function () { + return searchingLabel; + } + }, + ajax: { + url: searchUrl, + dataType: 'json', + delay: 250 + } + }); + } +}); diff --git a/Resources/public/sass/custom/modules/_forms.scss b/Resources/public/sass/custom/modules/_forms.scss index 6676680cc..0e5099ef0 100644 --- a/Resources/public/sass/custom/modules/_forms.scss +++ b/Resources/public/sass/custom/modules/_forms.scss @@ -18,4 +18,11 @@ span.force-inline-label label { align-self: center; margin-left: 1em; } +} + + +.chill-form__errors { + .chill-form_errors__entry.chill-form__errors__entry--warning { + color: var(--chill-green-dark); + } } \ No newline at end of file diff --git a/Resources/translations/messages.fr.yml b/Resources/translations/messages.fr.yml index e8e0edcaf..24c4eb710 100644 --- a/Resources/translations/messages.fr.yml +++ b/Resources/translations/messages.fr.yml @@ -182,4 +182,9 @@ column: colonne Comma separated values (CSV): Valeurs séparées par des virgules (CSV - tableur) # spreadsheet formatter -Choose the format: Choisir le format \ No newline at end of file +Choose the format: Choisir le format + +# select2 +'select2.no_results': Aucun résultat +'select2.error_loading': Erreur de chargement des résultats +'select2.searching': Recherche en cours... \ No newline at end of file diff --git a/Resources/views/Form/fields.html.twig b/Resources/views/Form/fields.html.twig index 33b42c6e8..841dd72e2 100644 --- a/Resources/views/Form/fields.html.twig +++ b/Resources/views/Form/fields.html.twig @@ -141,9 +141,9 @@ {% block form_errors %} {% spaceless %} {% if errors|length > 0 %} -
    +
      {% for error in errors %} -
    • {{ error.message }}
    • +
    • {{ error.message }}
    • {% endfor %}
    {% endif %} diff --git a/chill.webpack.config.js b/chill.webpack.config.js index 13bb15998..bf4ebf8b4 100644 --- a/chill.webpack.config.js +++ b/chill.webpack.config.js @@ -27,5 +27,6 @@ require('./Resources/public/modules/download-report/index.js'); //require('./Resources/public/css/scratch.css'); //require('./Resources/public/css/select2/select2.css'); require('select2/dist/css/select2.css'); +require('./Resources/public/modules/postal-code/index.js'); // img