diff --git a/src/Bundle/ChillPersonBundle/Controller/PersonController.php b/src/Bundle/ChillPersonBundle/Controller/PersonController.php index f33e9cce8..77a564157 100644 --- a/src/Bundle/ChillPersonBundle/Controller/PersonController.php +++ b/src/Bundle/ChillPersonBundle/Controller/PersonController.php @@ -29,11 +29,16 @@ use Chill\PersonBundle\Form\PersonType; use Chill\PersonBundle\Form\CreationPersonType; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Form\Extension\Core\Type\ButtonType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Form; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\Security\Core\Role\Role; use Chill\PersonBundle\Security\Authorization\PersonVoter; use Chill\PersonBundle\Search\SimilarPersonMatcher; +use Symfony\Component\Security\Core\Security; use Symfony\Component\Translation\TranslatorInterface; use Chill\MainBundle\Search\SearchProvider; use Chill\PersonBundle\Repository\PersonRepository; @@ -94,7 +99,8 @@ final class PersonController extends AbstractController ConfigPersonAltNamesHelper $configPersonAltNameHelper, LoggerInterface $logger, ValidatorInterface $validator, - EntityManagerInterface $em + EntityManagerInterface $em, + Security $security ) { $this->similarPersonMatcher = $similarPersonMatcher; $this->translator = $translator; @@ -104,6 +110,7 @@ final class PersonController extends AbstractController $this->logger = $logger; $this->validator = $validator; $this->em = $em; + $this->security = $security; } public function getCFGroup() @@ -209,11 +216,21 @@ final class PersonController extends AbstractController } } - public function newAction() + /** + * Method for creating a new person + * + *The controller register data from a previous post on the form, and + * register it in the session. + * + * The next post compare the data with previous one and, if yes, show a + * review page if there are "alternate persons". + * + * @param Request $request + * @return \Symfony\Component\HttpFoundation\RedirectResponse|Response + */ + public function newAction(Request $request) { - // this is a dummy default center. - $defaultCenter = $this->get('security.token_storage') - ->getToken() + $defaultCenter = $this->security ->getUser() ->getGroupCenters()[0] ->getCenter(); @@ -221,38 +238,103 @@ final class PersonController extends AbstractController $person = (new Person(new \DateTime('now'))) ->setCenter($defaultCenter); - $form = $this->createForm( - CreationPersonType::class, - $person, - array( - 'action' => $this->generateUrl('chill_person_review'), - 'form_status' => CreationPersonType::FORM_NOT_REVIEWED - )); + $form = $this->createForm(CreationPersonType::class, $person, [ + 'validation_groups' => ['create'] + ])->add('editPerson', SubmitType::class, [ + 'label' => 'Add the person' + ])->add('createPeriod', SubmitType::class, [ + 'label' => 'Add the person and create an accompanying period' + ]); - return $this->_renderNewForm($form); + $form->handleRequest($request); + + if ($request->getMethod() === Request::METHOD_GET) { + $this->lastPostDataReset(); + } elseif ($request->getMethod() === Request::METHOD_POST + && $form->isValid()) { + + $alternatePersons = $this->similarPersonMatcher + ->matchPerson($person); + + if ( + FALSE === $this->isLastPostDataChanges($form, $request, true) + || + count($alternatePersons) === 0 + ) { + $this->em->persist($person); + + $this->em->flush(); + $this->lastPostDataReset(); + + if ($form->get('createPeriod')->isClicked()) { + return $this->redirectToRoute('chill_person_accompanying_course_new', [ + 'person_id' => [ $person->getId() ] + ]); + } + + return $this->redirectToRoute('chill_person_general_edit', + ['person_id' => $person->getId()]); + } + } elseif ($request->getMethod() === Request::METHOD_POST) { + $this->addFlash('error', $this->translator->trans('This form contains errors')); + } + + return $this->render('@ChillPerson/Person/create.html.twig', + [ + 'form' => $form->createView(), + 'alternatePersons' => $alternatePersons ?? [] + ] + ); } - private function _renderNewForm($form) + private function isLastPostDataChanges(Form $form, Request $request, bool $replace = false): bool { - return $this->render('ChillPersonBundle:Person:create.html.twig', - array( - 'form' => $form->createView() - )); + /** @var SessionInterface $session */ + $session = $this->get('session'); + if (!$session->has('last_person_data')) { + return true; + } + + $newPost = $this->lastPostDataBuildHash($form, $request); + + $isChanged = $newPost !== $session->get('last_person_data'); + + if ($replace) { + $session->set('last_person_data', $newPost); + } + + return $isChanged ; + } + + private function lastPostDataReset(): void + { + $this->get('session')->set('last_person_data', ""); } /** + * build the hash for posted data * - * @param type $form - * @return \Chill\PersonBundle\Entity\Person + * For privacy reasons, the data are hashed using sha512 + * + * @param Form $form + * @param Request $request + * @return string */ - private function _bindCreationForm($form) + private function lastPostDataBuildHash(Form $form, Request $request): string { - /** - * @var Person - */ - $person = $form->getData(); + $fields = []; + $ignoredFields = ['form_status', '_token']; - return $person; + foreach ($request->request->all()[$form->getName()] as $field => $value) { + if (\in_array($field, $ignoredFields)) { + continue; + } + $fields[$field] = \is_array($value) ? + \implode(",", $value) : $value; + } + ksort($fields); + + return \hash('sha512', \implode("&", $fields)); } /** @@ -281,120 +363,6 @@ final class PersonController extends AbstractController return $errors; } - public function reviewAction(Request $request, PersonNotDuplicateRepository $personNotDuplicateRepository) - { - if ($request->getMethod() !== 'POST') { - $r = new Response("You must send something to review the creation of a new Person"); - $r->setStatusCode(400); - return $r; - } - - $form = $this->createForm( - CreationPersonType::class, - new Person(), - array( - 'action' => $this->generateUrl('chill_person_create'), - 'form_status' => CreationPersonType::FORM_BEING_REVIEWED - )); - - $form->handleRequest($request); - - $person = $this->_bindCreationForm($form); - - $errors = $this->_validatePersonAndAccompanyingPeriod($person); - $this->logger->info(sprintf('Person created with %d errors ', count($errors))); - - if ($errors->count() > 0) { - $this->logger->info('The created person has errors'); - $flashBag = $this->get('session')->getFlashBag(); - $translator = $this->get('translator'); - - $flashBag->add('error', $translator->trans('The person data are not valid')); - - foreach($errors as $error) { - $flashBag->add('info', $error->getMessage()); - } - - $form = $this->createForm( - CreationPersonType::class, - $person, - array( - 'action' => $this->generateUrl('chill_person_review'), - 'form_status' => CreationPersonType::FORM_NOT_REVIEWED - )); - - $form->handleRequest($request); - - return $this->_renderNewForm($form); - } else { - $this->logger->info('Person created without errors'); - } - - $this->em->persist($person); - - $alternatePersons = $this->similarPersonMatcher->matchPerson($person, $personNotDuplicateRepository); - - if (count($alternatePersons) === 0) { - return $this->forward('ChillPersonBundle:Person:create'); - } - - $this->get('session')->getFlashBag()->add('info', - $this->get('translator')->trans( - '%nb% person with similar name. Please verify that this is a new person', - array('%nb%' => count($alternatePersons))) - ); - - return $this->render('ChillPersonBundle:Person:create_review.html.twig', - array( - 'person' => $person, - 'alternatePersons' => $alternatePersons, - 'firstName' => $form['firstName']->getData(), - 'lastName' => $form['lastName']->getData(), - 'birthdate' => $form['birthdate']->getData(), - 'gender' => $form['gender']->getData(), - 'form' => $form->createView())); - } - - public function createAction(Request $request) - { - - if ($request->getMethod() !== 'POST') { - $r = new Response('You must send something to create a person !'); - $r->setStatusCode(400); - return $r; - } - - $form = $this->createForm(CreationPersonType::class, null, array( - 'form_status' => CreationPersonType::FORM_REVIEWED - )); - - $form->handleRequest($request); - - $person = $this->_bindCreationForm($form); - - $errors = $this->_validatePersonAndAccompanyingPeriod($person); - - $this->denyAccessUnlessGranted('CHILL_PERSON_CREATE', $person, - 'You are not allowed to create this person'); - - if ($errors->count() === 0) { - $this->em->persist($person); - - $this->em->flush(); - - return $this->redirect($this->generateUrl('chill_person_general_edit', - array('person_id' => $person->getId()))); - } else { - $text = "this should not happen if you reviewed your submission\n"; - foreach ($errors as $error) { - $text .= $error->getMessage()."\n"; - } - $r = new Response($text); - $r->setStatusCode(400); - return $r; - } - } - /** * easy getting a person by his id * @return \Chill\PersonBundle\Entity\Person diff --git a/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php b/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php index c001185ca..32b7a69f2 100644 --- a/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php +++ b/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php @@ -21,6 +21,7 @@ namespace Chill\PersonBundle\Form; +use Chill\PersonBundle\Entity\Person; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -48,7 +49,7 @@ final class CreationPersonType extends AbstractType * @var CenterTransformer */ private $centerTransformer; - + /** * * @var ConfigPersonAltNamesHelper @@ -69,57 +70,22 @@ final class CreationPersonType extends AbstractType */ public function buildForm(FormBuilderInterface $builder, array $options) { - if ($options['form_status'] === self::FORM_BEING_REVIEWED) { + $builder + ->add('firstName') + ->add('lastName') + ->add('birthdate', ChillDateType::class, [ + 'required' => false, + ]) + ->add('gender', GenderType::class, array( + 'required' => true, 'placeholder' => null + )) + ->add('center', CenterType::class) + ; - $dateToStringTransformer = new DateTimeToStringTransformer( - null, null, 'd-m-Y', false); - - $builder->add('firstName', HiddenType::class) - ->add('lastName', HiddenType::class) - ->add('birthdate', HiddenType::class, array( - 'property_path' => 'birthdate' - )) - ->add('gender', HiddenType::class) - ->add('form_status', HiddenType::class, array( - 'mapped' => false, - 'data' => $options['form_status'] - )) - ->add('center', HiddenType::class) - ; - - if ($this->configPersonAltNamesHelper->hasAltNames()) { - $builder->add('altNames', PersonAltNameType::class, [ - 'by_reference' => false, - 'force_hidden' => true - ]); - } - - $builder->get('birthdate') - ->addModelTransformer($dateToStringTransformer); - $builder->get('center') - ->addModelTransformer($this->centerTransformer); - } else { - $builder - ->add('firstName') - ->add('lastName') - ->add('birthdate', ChillDateType::class, [ - 'required' => false, - ]) - ->add('gender', GenderType::class, array( - 'required' => true, 'placeholder' => null - )) - ->add('form_status', HiddenType::class, array( - 'data' => $options['form_status'], - 'mapped' => false - )) - ->add('center', CenterType::class) - ; - - if ($this->configPersonAltNamesHelper->hasAltNames()) { - $builder->add('altNames', PersonAltNameType::class, [ - 'by_reference' => false - ]); - } + if ($this->configPersonAltNamesHelper->hasAltNames()) { + $builder->add('altNames', PersonAltNameType::class, [ + 'by_reference' => false + ]); } } @@ -129,15 +95,8 @@ final class CreationPersonType extends AbstractType public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( - 'data_class' => 'Chill\PersonBundle\Entity\Person' + 'data_class' => Person::class )); - - $resolver->setRequired('form_status') - ->setAllowedValues('form_status', array( - self::FORM_BEING_REVIEWED, - self::FORM_NOT_REVIEWED, - self::FORM_REVIEWED - )); } /** diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig index 036dc5d01..9718308ed 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig @@ -20,27 +20,70 @@ {% block content %}
+

{{ 'Add a person'|trans }}

+ + {% if alternatePersons is not empty %} +
+ + {% transchoice alternatePersons|length with { '%nb%': alternatePersons|length } %} + %nb% person with similar name. Please verify that this is a new person + {% endtranschoice %} + +
+ + + + + + + + + + + {% for person in alternatePersons %} + + + + + + {% endfor %} + +
{{ 'Name'|trans }}{{ 'Date of birth'|trans }}{{ 'Nationality'|trans }}
+ + {{ person|chill_entity_render_string }}{% apply spaceless %} + {% if person.isOpen == false %} + + {% endif %} + {% endapply %} + + {% if person.birthdate is not null %}{{ person.birthdate|format_date('long') }}{% else %} {% endif %} + {% if person.nationality is not null %}{{ person.nationality.name|localize_translatable_string }}{% else %}{{ 'Without nationality'|trans }}{% endif %} +
+ {% endif %} + {{ form_start(form) }} -

{{ 'Add a person'|trans }}

- {{ form_row(form.firstName, { 'label' : 'First name'|trans }) }} + {{ form_row(form.firstName, { 'label' : 'First name'|trans }) }} - {{ form_row(form.lastName, { 'label' : 'Last name'|trans }) }} + {{ form_row(form.lastName, { 'label' : 'Last name'|trans }) }} - {% if form.altNames is defined %} - {{ form_widget(form.altNames) }} - {% endif %} + {% if form.altNames is defined %} + {{ form_widget(form.altNames) }} + {% endif %} - {{ form_row(form.birthdate, { 'label' : 'Date of birth'|trans }) }} + {{ form_row(form.birthdate, { 'label' : 'Date of birth'|trans }) }} - {{ form_row(form.gender, { 'label' : 'Gender'|trans }) }} + {{ form_row(form.gender, { 'label' : 'Gender'|trans }) }} - {{ form_rest(form) }} - - + {{ form_end(form) }} diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Person/create_review.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Person/create_review.html.twig deleted file mode 100644 index cf3e37beb..000000000 --- a/src/Bundle/ChillPersonBundle/Resources/views/Person/create_review.html.twig +++ /dev/null @@ -1,97 +0,0 @@ -{# - * 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 . -#} -{% extends "@ChillMain/layout.html.twig" %} - -{% block title %}{{ 'Alreay existing person'|trans }}{% endblock title %} - -{% block content %} -
- -
- - {% transchoice alternatePersons|length with { '%nb%': alternatePersons|length } %} - %nb% person with similar name. Please verify that this is a new person - {% endtranschoice %} - -
- - {% if alternatePersons is not empty %} - - - - - - - - - - {% for person in alternatePersons %} - - - - - - {% endfor %} - -
{{ 'Name'|trans }}{{ 'Date of birth'|trans }}{{ 'Nationality'|trans }}
- - {{ person|chill_entity_render_string }}{% apply spaceless %} - {% if person.isOpen == false %} - - {% endif %} - {% endapply %} - - {% if person.birthdate is not null %}{{ person.birthdate|format_date('long') }}{% else %} {% endif %} - {% if person.nationality is not null %}{{ person.nationality.name|localize_translatable_string }}{% else %}{{ 'Without nationality'|trans }}{% endif %} -
- {% endif %} - -
- {{ form_start(form) }} -

{{ 'You will create this person'|trans }}

-
-
{{ 'Name'|trans }}
-
{{ person|chill_entity_render_string }}
- -
{{ 'Date of birth'|trans }}
- {% if birthdate is empty %} -
{{ 'Unknown date of birth'|trans }}
- {% else %} -
{{ birthdate|format_date('long') }}
- {% endif %} - -
{{ 'Gender'|trans }}
-
{{ gender|trans }}
- - {% if form.altNames is defined %} - {# mark as rendered #} - {{ form_widget(form.altNames) }} - {% endif %} -
- - {{ form_rest(form) }} -
    -
  • - -
  • -
- {{ form_end(form) }} - -
- -
-{% endblock content %} diff --git a/src/Bundle/ChillPersonBundle/Search/SimilarPersonMatcher.php b/src/Bundle/ChillPersonBundle/Search/SimilarPersonMatcher.php index 1c564aa11..273367711 100644 --- a/src/Bundle/ChillPersonBundle/Search/SimilarPersonMatcher.php +++ b/src/Bundle/ChillPersonBundle/Search/SimilarPersonMatcher.php @@ -19,6 +19,7 @@ namespace Chill\PersonBundle\Search; use Chill\PersonBundle\Entity\PersonNotDuplicate; +use Chill\PersonBundle\Templating\Entity\PersonRender; use Doctrine\ORM\EntityManagerInterface; use Chill\PersonBundle\Entity\Person; use Chill\MainBundle\Security\Authorization\AuthorizationHelper; @@ -53,35 +54,54 @@ class SimilarPersonMatcher */ protected $tokenStorage; + protected PersonNotDuplicateRepository $personNotDuplicateRepository; + + protected PersonRender $personRender; + public function __construct( EntityManagerInterface $em, AuthorizationHelper $authorizationHelper, - TokenStorageInterface $tokenStorage + TokenStorageInterface $tokenStorage, + PersonNotDuplicateRepository $personNotDuplicateRepository, + PersonRender $personRender ) { $this->em = $em; $this->authorizationHelper = $authorizationHelper; $this->tokenStorage = $tokenStorage; + $this->personNotDuplicateRepository = $personNotDuplicateRepository; + $this->personRender = $personRender; } - public function matchPerson(Person $person, PersonNotDuplicateRepository $personNotDuplicateRepository, $precision = 0.15, $orderBy = self::SIMILAR_SEARCH_ORDER_BY_SIMILARITY) - { + public function matchPerson( + Person $person, + float $precision = 0.15, + string $orderBy = self::SIMILAR_SEARCH_ORDER_BY_SIMILARITY, + bool $addYearComparison = false + ) { $centers = $this->authorizationHelper->getReachableCenters( $this->tokenStorage->getToken()->getUser(), new Role(PersonVoter::SEE) ); + $query = $this->em->createQuery(); $dql = 'SELECT p from ChillPersonBundle:Person p ' . ' WHERE (' . ' SIMILARITY(p.fullnameCanonical, UNACCENT(LOWER(:fullName))) >= :precision ' . ' ) ' . ' AND p.center IN (:centers)' - . ' AND p.id != :personId ' + ; - $notDuplicatePersons = $personNotDuplicateRepository->findNotDuplicatePerson($person); + if ($person->getId() !== NULL) { + $dql .= ' AND p.id != :personId '; + $notDuplicatePersons = $this->personNotDuplicateRepository->findNotDuplicatePerson($person); - if (count($notDuplicatePersons)) { - $dql .= ' AND p.id not in (:notDuplicatePersons)'; + $query->setParameter('personId', $person->getId()); + + if (count($notDuplicatePersons)) { + $dql .= ' AND p.id not in (:notDuplicatePersons)'; + $query->setParameter('notDuplicatePersons', $notDuplicatePersons); + } } switch ($orderBy) { @@ -93,18 +113,13 @@ class SimilarPersonMatcher $dql .= ' ORDER BY SIMILARITY(p.fullnameCanonical, UNACCENT(LOWER(:fullName))) DESC '; } - $query = $this->em - ->createQuery($dql) - ->setParameter('fullName', $person->getFirstName() . ' ' . $person->getLastName()) + $query = $query + ->setDQL($dql) + ->setParameter('fullName', $this->personRender->renderString($person, [])) ->setParameter('centers', $centers) - ->setParameter('personId', $person->getId()) ->setParameter('precision', $precision) ; - if (count($notDuplicatePersons)) { - $query->setParameter('notDuplicatePersons', $notDuplicatePersons); - } - return $query->getResult(); } } diff --git a/src/Bundle/ChillPersonBundle/config/services/controller.yaml b/src/Bundle/ChillPersonBundle/config/services/controller.yaml index bad7e1745..6efdb4640 100644 --- a/src/Bundle/ChillPersonBundle/config/services/controller.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/controller.yaml @@ -1,14 +1,6 @@ services: Chill\PersonBundle\Controller\PersonController: - arguments: - $similarPersonMatcher: '@Chill\PersonBundle\Search\SimilarPersonMatcher' - $translator: '@Symfony\Component\Translation\TranslatorInterface' - $eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface' - $personRepository: '@Chill\PersonBundle\Repository\PersonRepository' - $configPersonAltNameHelper: '@Chill\PersonBundle\Config\ConfigPersonAltNamesHelper' - $logger: '@Psr\Log\LoggerInterface' - $validator: '@Symfony\Component\Validator\Validator\ValidatorInterface' - $em: '@Doctrine\ORM\EntityManagerInterface' + autowire: true tags: ['controller.service_arguments'] Chill\PersonBundle\Controller\TimelinePersonController: diff --git a/src/Bundle/ChillPersonBundle/config/services/search.yaml b/src/Bundle/ChillPersonBundle/config/services/search.yaml index ca6591246..284773196 100644 --- a/src/Bundle/ChillPersonBundle/config/services/search.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/search.yaml @@ -10,7 +10,7 @@ services: - ['setContainer', ["@service_container"]] tags: - { name: chill.search, alias: 'person_regular' } - + Chill\PersonBundle\Search\SimilarityPersonSearch: arguments: - "@doctrine.orm.entity_manager" @@ -24,10 +24,8 @@ services: - { name: chill.search, alias: 'person_similarity' } Chill\PersonBundle\Search\SimilarPersonMatcher: - arguments: - $em: '@Doctrine\ORM\EntityManagerInterface' - $tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface' - $authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper' + autowire: true + autoconfigure: true Chill\PersonBundle\Search\SearchPersonApiProvider: autowire: true diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 61a3cd035..e3a6ff485 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -119,6 +119,7 @@ address_country_code: Code pays 'Alreay existing person': 'Dossiers déjà encodés' 'Add the person': 'Ajouter la personne' +'Add the person and create an accompanying period': "Ajouter la personne et créer une période d'accompagnement" Show person: Voir le dossier de la personne 'Confirm the creation': 'Confirmer la création' 'You will create this person': 'Vous allez créer le dossier suivant'