From 728ea73bdf23ba66ea341375cb53534595ec693c Mon Sep 17 00:00:00 2001 From: Mathieu Jaumotte Date: Sun, 21 Mar 2021 13:58:19 +0100 Subject: [PATCH] List duplicate persons Signed-off-by: Mathieu Jaumotte Signed-off-by: Mathieu Jaumotte --- .../Controller/PersonDuplicateController.php | 141 ++++++++++++++++++ .../Entity/PersonNotDuplicate.php | 107 +++++++++++++ .../Form/PersonConfimDuplicateType.php | 31 ++++ .../Menu/PersonMenuBuilder.php | 10 ++ .../views/PersonDuplicate/confirm.html.twig | 36 +++++ .../views/PersonDuplicate/view.html.twig | 65 ++++++++ .../Search/SimilarPersonMatcher.php | 37 +++-- .../ChillPersonBundle/config/routes.yaml | 14 +- .../config/services/controller.yaml | 9 +- .../migrations/Version20210128152747.php | 39 +++++ .../translations/messages.en.yml | 1 + .../translations/messages.fr.yml | 1 + 12 files changed, 469 insertions(+), 22 deletions(-) create mode 100644 src/Bundle/ChillPersonBundle/Controller/PersonDuplicateController.php create mode 100644 src/Bundle/ChillPersonBundle/Entity/PersonNotDuplicate.php create mode 100644 src/Bundle/ChillPersonBundle/Form/PersonConfimDuplicateType.php create mode 100644 src/Bundle/ChillPersonBundle/Resources/views/PersonDuplicate/confirm.html.twig create mode 100644 src/Bundle/ChillPersonBundle/Resources/views/PersonDuplicate/view.html.twig create mode 100644 src/Bundle/ChillPersonBundle/migrations/Version20210128152747.php diff --git a/src/Bundle/ChillPersonBundle/Controller/PersonDuplicateController.php b/src/Bundle/ChillPersonBundle/Controller/PersonDuplicateController.php new file mode 100644 index 000000000..4438bde8d --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Controller/PersonDuplicateController.php @@ -0,0 +1,141 @@ +similarPersonMatcher = $similarPersonMatcher; + $this->translator = $translator; + $this->personRepository = $personRepository; + } + + public function viewAction($person_id) + { + $person = $this->_getPerson($person_id); + if ($person === null) { + throw $this->createNotFoundException("Person with id $person_id not" + . " found on this server"); + } + + $duplicatePersons = $this->similarPersonMatcher->matchPerson($person, 0.5); + + return $this->render('ChillPersonBundle:PersonDuplicate:view.html.twig', [ + "person" => $person, + 'duplicatePersons' => $duplicatePersons + ]); + } + + public function confirmAction($person_id, $person2_id, Request $request) + { + if ($person_id === $person2_id) { + throw new InvalidArgumentException('Can not merge same person'); + } + + if ($person_id > $person2_id) { + $tmpId = $person2_id; + $person2_id = $person_id; + $person_id = $tmpId; + unset($tmpId); + } + + $person = $this->_getPerson($person_id); + if ($person === null) { + throw $this->createNotFoundException("Person with id $person_id not" + . " found on this server"); + } + + $person2 = $this->_getPerson($person2_id); + if ($person2 === null) { + throw $this->createNotFoundException("Person with id $person2_id not" + . " found on this server"); + } + + $form = $this->createForm(PersonConfimDuplicateType::class); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + dd('todo'); + } + + return $this->render('ChillPersonBundle:PersonDuplicate:confirm.html.twig', [ + 'person' => $person, + 'person2' => $person2, + 'form' => $form->createView(), + ]); + } + + public function notDuplicateAction($person_id, $person2_id) + { + if ($person_id === $person2_id) { + throw new InvalidArgumentException('Can not merge same person'); + } + + if ($person_id > $person2_id) { + $tmpId = $person2_id; + $person2_id = $person_id; + $person_id = $tmpId; + unset($tmpId); + } + + $person = $this->_getPerson($person_id); + if ($person === null) { + throw $this->createNotFoundException("Person with id $person_id not" + . " found on this server"); + } + + $person2 = $this->_getPerson($person2_id); + if ($person2 === null) { + throw $this->createNotFoundException("Person with id $person2_id not" + . " found on this server"); + } + + $personNotDuplicate = new PersonNotDuplicate(); + $personNotDuplicate->setPerson1($person); + $personNotDuplicate->setPerson2($person2); + $personNotDuplicate->setUser($this->getUser()); + + $this->getDoctrine()->getManager()->persist($personNotDuplicate); + $this->getDoctrine()->getManager()->flush(); + + return $this->redirectToRoute('chill_person_duplicate_view', ['person_id' => $person->getId()]); + } + + /** + * easy getting a person by his id + */ + private function _getPerson($id): ?Person + { + return $this->personRepository->find($id); + } +} diff --git a/src/Bundle/ChillPersonBundle/Entity/PersonNotDuplicate.php b/src/Bundle/ChillPersonBundle/Entity/PersonNotDuplicate.php new file mode 100644 index 000000000..e8b79698f --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Entity/PersonNotDuplicate.php @@ -0,0 +1,107 @@ +date = new \DateTime(); + } + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + $this->id = $id; + } + + public function getPerson1() + { + return $this->person1; + } + + public function setPerson1(Person $person1) + { + $this->person1 = $person1; + } + + public function getPerson2() + { + return $this->person2; + } + + public function setPerson2(Person $person2) + { + $this->person2 = $person2; + } + + public function getDate() + { + return $this->date; + } + + public function setDate(\DateTime $date) + { + $this->date = $date; + } + + public function getUser() + { + return $this->user; + } + + public function setUser(User $user) + { + $this->user = $user; + } +} diff --git a/src/Bundle/ChillPersonBundle/Form/PersonConfimDuplicateType.php b/src/Bundle/ChillPersonBundle/Form/PersonConfimDuplicateType.php new file mode 100644 index 000000000..acc385f43 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Form/PersonConfimDuplicateType.php @@ -0,0 +1,31 @@ +add('confirm', CheckboxType::class, [ + 'label' => 'Je confirme la fusion de ces 2 personnes', + 'mapped' => false, + ]); + } + + /** + * @return string + */ + public function getBlockPrefix() + { + return 'chill_personbundle_person_confirm_duplicate'; + } +} diff --git a/src/Bundle/ChillPersonBundle/Menu/PersonMenuBuilder.php b/src/Bundle/ChillPersonBundle/Menu/PersonMenuBuilder.php index 3a757d898..b39b14254 100644 --- a/src/Bundle/ChillPersonBundle/Menu/PersonMenuBuilder.php +++ b/src/Bundle/ChillPersonBundle/Menu/PersonMenuBuilder.php @@ -63,6 +63,16 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface ->setExtras([ 'order' => 50 ]); + + $menu->addChild($this->translator->trans('Person duplicate'), [ + 'route' => 'chill_person_duplicate_view', + 'routeParameters' => [ + 'person_id' => $parameters['person']->getId() + ] + ]) + ->setExtras([ + 'order' => 51 + ]); if ($this->showAccompanyingPeriod === 'visible') { $menu->addChild($this->translator->trans('Accompanying period list'), [ diff --git a/src/Bundle/ChillPersonBundle/Resources/views/PersonDuplicate/confirm.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/PersonDuplicate/confirm.html.twig new file mode 100644 index 000000000..fcd314a62 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/views/PersonDuplicate/confirm.html.twig @@ -0,0 +1,36 @@ +{% extends "@ChillPerson/layout.html.twig" %} + +{% set activeRouteKey = 'chill_person_duplicate' %} + +{% block title %}{{ 'Person duplicate'|trans|capitalize ~ ' ' ~ person.firstName|capitalize ~ +' ' ~ person.lastName }}{% endblock %} + +{% block personcontent %} +

Ancien dossier

+ {{ person2 }} + + + +

Nouveau Dossier

+ {{ person }} + + + {{ form_start(form) }} + + {{ form_rest(form) }} + + + + {{ form_end(form) }} + +{% endblock %} diff --git a/src/Bundle/ChillPersonBundle/Resources/views/PersonDuplicate/view.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/PersonDuplicate/view.html.twig new file mode 100644 index 000000000..691f55e1c --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/views/PersonDuplicate/view.html.twig @@ -0,0 +1,65 @@ +{% extends "@ChillPerson/layout.html.twig" %} + +{% set activeRouteKey = 'chill_person_duplicate' %} + +{% block title %}{{ 'Person duplicate'|trans|capitalize ~ ' ' ~ person.firstName|capitalize ~ + ' ' ~ person.lastName }}{% endblock %} + + +{% block personcontent %} + +

{{ title|default('Person duplicate')|trans }}

+ + + + + + + + + + + +{% for duplicatePerson in duplicatePersons %} + + + + + + +{% endfor %} + +
{% trans %}Name{% endtrans %}{% trans %}Date of birth{% endtrans %}{% trans %}Nationality{% endtrans %} 
+ {% set is_open = duplicatePerson.isOpen() %} + + {{ duplicatePerson|chill_entity_render_box }} + {% apply spaceless %} + {% if chill_person.fields.accompanying_period == 'visible' %} + {% if is_open == false %} + + {% else %} + + {% endif %} + {% endif %} + {% endapply %} + + + {% if duplicatePerson.birthdate is not null %} + {{ duplicatePerson.birthdate|format_date('long') }} + {% else %} + {{ 'Unknown date of birth'|trans }} + {% endif %} + + {% if duplicatePerson.nationality is not null %} + {{ duplicatePerson.nationality.name|localize_translatable_string }} + {% else %} + {{ 'Without nationality'|trans }} + {% endif %} + +
    +
  • +
  • +
  • +
+
+{% endblock %} diff --git a/src/Bundle/ChillPersonBundle/Search/SimilarPersonMatcher.php b/src/Bundle/ChillPersonBundle/Search/SimilarPersonMatcher.php index ab13e607a..4b22bd656 100644 --- a/src/Bundle/ChillPersonBundle/Search/SimilarPersonMatcher.php +++ b/src/Bundle/ChillPersonBundle/Search/SimilarPersonMatcher.php @@ -61,35 +61,32 @@ class SimilarPersonMatcher } - public function matchPerson(Person $person) + public function matchPerson(Person $person, $precision = 0.15) { - $centers = $this->authorizationHelper - ->getReachableCenters( - $this->tokenStorage->getToken()->getUser(), - new Role(PersonVoter::SEE) - ); + $centers = $this->authorizationHelper->getReachableCenters( + $this->tokenStorage->getToken()->getUser(), + new Role(PersonVoter::SEE) + ); - $dql = 'SELECT p from ChillPersonBundle:Person p WHERE' - . ' (' - . ' UNACCENT(LOWER(p.firstName)) LIKE UNACCENT(LOWER(:firstName)) ' - . ' OR UNACCENT(LOWER(p.lastName)) LIKE UNACCENT(LOWER(:lastName)) ' - . ' OR UNACCENT(LOWER(p.firstName)) LIKE UNACCENT(LOWER(:lastName)) ' - . ' OR UNACCENT(LOWER(p.lastName)) LIKE UNACCENT(LOWER(:firstName)) ' - . ' OR SIMILARITY(p.fullnameCanonical, UNACCENT(LOWER(:fullName))) >= 0.15 ' + $dql = 'SELECT p from ChillPersonBundle:Person p ' + . ' WHERE (' + . ' SIMILARITY(p.fullnameCanonical, UNACCENT(LOWER(:fullName))) >= :precision ' + . ' OR SIMILARITY(p.fullnameCanonical, UNACCENT(LOWER(:fullNameInverted))) >= :precision ' . ' ) ' . ' AND p.center IN (:centers)' + . ' AND p.id != :personId' . ' ORDER BY SIMILARITY(p.fullnameCanonical, UNACCENT(LOWER(:fullName))) DESC ' - ; + ; - $query = - $this->em + $query = $this->em ->createQuery($dql) - ->setParameter('firstName', $person->getFirstName()) - ->setParameter('lastName', $person->getLastName()) ->setParameter('fullName', $person->getFirstName() . ' ' . $person->getLastName()) + ->setParameter('fullNameInverted', $person->getLastName() . ' ' . $person->getFirstName()) ->setParameter('centers', $centers) - ; - + ->setParameter('personId', $person->getId()) + ->setParameter('precision', $precision) + ; + return $query->getResult(); } } diff --git a/src/Bundle/ChillPersonBundle/config/routes.yaml b/src/Bundle/ChillPersonBundle/config/routes.yaml index a9049840d..9db6ec12f 100644 --- a/src/Bundle/ChillPersonBundle/config/routes.yaml +++ b/src/Bundle/ChillPersonBundle/config/routes.yaml @@ -98,6 +98,10 @@ chill_person_admin_redirect_to_admin_index: order: 0 label: Main admin menu +chill_person_duplicate_view: + path: /{_locale}/person/{person_id}/duplicate/view + controller: Chill\PersonBundle\Controller\PersonDuplicateController::viewAction + chill_person_closingmotive_admin: path: /{_locale}/admin/closing-motive controller: cscrud_closing_motive_controller:index @@ -107,6 +111,10 @@ chill_person_closingmotive_admin: order: 90 label: 'person_admin.closing motives' +chill_person_duplicate_confirm: + path: /{_locale}/person/{person_id}/duplicate/{person2_id}/confirm + controller: Chill\PersonBundle\Controller\PersonDuplicateController::confirmAction + chill_person_maritalstatus_admin: path: /{_locale}/admin/marital-status controller: cscrud_marital_status_controller:index @@ -114,4 +122,8 @@ chill_person_maritalstatus_admin: menus: admin_person: order: 120 - label: 'person_admin.marital status' \ No newline at end of file + label: 'person_admin.marital status' + +chill_person_duplicate_not_duplicate: + path: /{_locale}/person/{person_id}/duplicate/{person2_id}/not-duplicate + controller: Chill\PersonBundle\Controller\PersonDuplicateController::notDuplicateAction diff --git a/src/Bundle/ChillPersonBundle/config/services/controller.yaml b/src/Bundle/ChillPersonBundle/config/services/controller.yaml index 112d609b5..a6e446ebf 100644 --- a/src/Bundle/ChillPersonBundle/config/services/controller.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/controller.yaml @@ -20,5 +20,12 @@ services: arguments: $eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface' tags: ['controller.service_arguments'] - + Chill\PersonBundle\Controller\AdminController: ~ + + Chill\PersonBundle\Controller\PersonDuplicateController: + arguments: + $similarPersonMatcher: '@Chill\PersonBundle\Search\SimilarPersonMatcher' + $translator: '@Symfony\Component\Translation\TranslatorInterface' + $personRepository: '@Chill\PersonBundle\Repository\PersonRepository' + tags: ['controller.service_arguments'] diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20210128152747.php b/src/Bundle/ChillPersonBundle/migrations/Version20210128152747.php new file mode 100644 index 000000000..8dbcb91da --- /dev/null +++ b/src/Bundle/ChillPersonBundle/migrations/Version20210128152747.php @@ -0,0 +1,39 @@ +addSql('CREATE SEQUENCE chill_person_not_duplicate_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE chill_person_not_duplicate (id INT NOT NULL, person1_id INT DEFAULT NULL, person2_id INT DEFAULT NULL, user_id INT DEFAULT NULL, date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_BD211EE23EF5821B ON chill_person_not_duplicate (person1_id)'); + $this->addSql('CREATE INDEX IDX_BD211EE22C402DF5 ON chill_person_not_duplicate (person2_id)'); + $this->addSql('CREATE INDEX IDX_BD211EE2A76ED395 ON chill_person_not_duplicate (user_id)'); + $this->addSql('ALTER TABLE chill_person_not_duplicate ADD CONSTRAINT FK_BD211EE23EF5821B FOREIGN KEY (person1_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_person_not_duplicate ADD CONSTRAINT FK_BD211EE22C402DF5 FOREIGN KEY (person2_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_person_not_duplicate ADD CONSTRAINT FK_BD211EE2A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema) : void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP SEQUENCE chill_person_not_duplicate_id_seq CASCADE'); + $this->addSql('DROP TABLE chill_person_not_duplicate'); + } +} diff --git a/src/Bundle/ChillPersonBundle/translations/messages.en.yml b/src/Bundle/ChillPersonBundle/translations/messages.en.yml index 4c0c13d83..cd9212d5d 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.en.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.en.yml @@ -66,6 +66,7 @@ Reset: 'Remise à zéro' 'Create accompanying period': 'Create accompanying period' 'Closing motive': 'Motif de clôture' 'Person details': 'Détails de la personne' +'Person duplicate': 'Find duplicate' Create an accompanying period: Create an accompanying period 'Create': Create diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 08e1219e6..354625f05 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -119,6 +119,7 @@ Update accompanying period: Mettre à jour une période d'accompagnement "Period not opened : form is invalid": "La période n'a pas été ouverte: le formulaire est invalide." 'Closing motive': 'Motif de clôture' 'Person details': 'Détails de la personne' +'Person duplicate': 'Trouver des doublons' 'Update details for %name%': 'Modifier détails de %name%' Any accompanying periods are open: Aucune période d'accompagnement ouverte An accompanying period is open: Une période d'accompagnement est ouverte