Duplicate module

Signed-off-by: Mathieu Jaumotte <mathieu.jaumotte@champs-libres.coop>
This commit is contained in:
Mathieu Jaumotte 2021-03-21 14:06:37 +01:00
parent 728ea73bdf
commit 0149457fba
12 changed files with 292 additions and 111 deletions

View File

@ -94,7 +94,6 @@ class PersonMove
foreach ($metadata->getAssociationMappings() as $field => $mapping) {
if ($mapping['targetEntity'] === Person::class) {
if (\in_array($metadata->getName(), $toDelete)) {
$sql = $this->createDeleteSQL($metadata, $from, $field);
$event = new ActionEvent($from->getId(), $metadata->getName(), $sql,

View File

@ -2,13 +2,17 @@
namespace Chill\PersonBundle\Controller;
use Chill\PersonBundle\Actions\Remove\PersonMove;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\PersonNotDuplicate;
use Chill\PersonBundle\Form\PersonConfimDuplicateType;
use Chill\PersonBundle\Privacy\PrivacyEvent;
use Chill\PersonBundle\Repository\PersonRepository;
use Chill\PersonBundle\Search\SimilarPersonMatcher;
use http\Exception\InvalidArgumentException;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Translation\TranslatorInterface;
@ -29,14 +33,28 @@ class PersonDuplicateController extends Controller
*/
private $personRepository;
/**
* @var \Chill\PersonBundle\Actions\Remove\PersonMove
*/
private $personMove;
/**
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
private $eventDispatcher;
public function __construct(
SimilarPersonMatcher $similarPersonMatcher,
TranslatorInterface $translator,
PersonRepository $personRepository
PersonRepository $personRepository,
PersonMove $personMove,
EventDispatcherInterface $eventDispatcher
) {
$this->similarPersonMatcher = $similarPersonMatcher;
$this->translator = $translator;
$this->personRepository = $personRepository;
$this->personMove = $personMove;
$this->eventDispatcher = $eventDispatcher;
}
public function viewAction($person_id)
@ -47,88 +65,88 @@ class PersonDuplicateController extends Controller
. " found on this server");
}
$duplicatePersons = $this->similarPersonMatcher->matchPerson($person, 0.5);
$duplicatePersons = $this->similarPersonMatcher->
matchPerson($person, 0.5, SimilarPersonMatcher::SIMILAR_SEARCH_ORDER_BY_ALPHABETICAL);
$notDuplicatePersons = $this->getDoctrine()->getRepository(PersonNotDuplicate::class)
->findNotDuplicatePerson($person);
return $this->render('ChillPersonBundle:PersonDuplicate:view.html.twig', [
"person" => $person,
'duplicatePersons' => $duplicatePersons
'person' => $person,
'duplicatePersons' => $duplicatePersons,
'notDuplicatePersons' => $notDuplicatePersons,
]);
}
public function confirmAction($person_id, $person2_id, Request $request)
public function confirmAction($person1_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");
}
[$person1, $person2] = $this->_getPersonsByPriority($person1_id, $person2_id);
$form = $this->createForm(PersonConfimDuplicateType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
dd('todo');
$event = new PrivacyEvent($person1, array(
'element_class' => Person::class,
'action' => 'move'
));
$event->addPerson($person2);
$this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event);
$sqls = $this->personMove->getSQL($person2, $person1);
$connection = $this->getDoctrine()->getConnection();
$connection->beginTransaction();
foreach($sqls as $sql) {
$connection->executeQuery($sql);
}
$connection->commit();
return $this->redirectToRoute('chill_person_duplicate_view', ['person_id' => $person1->getId()]);
}
return $this->render('ChillPersonBundle:PersonDuplicate:confirm.html.twig', [
'person' => $person,
'person' => $person1,
'person2' => $person2,
'form' => $form->createView(),
]);
}
public function notDuplicateAction($person_id, $person2_id)
public function notDuplicateAction($person1_id, $person2_id)
{
if ($person_id === $person2_id) {
throw new InvalidArgumentException('Can not merge same person');
}
[$person1, $person2] = $this->_getPersonsByPriority($person1_id, $person2_id);
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 = $this->getDoctrine()->getRepository(PersonNotDuplicate::class)
->findOneBy(['person1' => $person1, 'person2' => $person2]);
if (!$personNotDuplicate instanceof PersonNotDuplicate) {
$personNotDuplicate = new PersonNotDuplicate();
$personNotDuplicate->setPerson1($person);
$personNotDuplicate->setPerson1($person1);
$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()]);
return $this->redirectToRoute('chill_person_duplicate_view', ['person_id' => $person1->getId()]);
}
public function removeNotDuplicateAction($person1_id, $person2_id)
{
[$person1, $person2] = $this->_getPersonsByPriority($person1_id, $person2_id);
$personNotDuplicate = $this->getDoctrine()->getRepository(PersonNotDuplicate::class)
->findOneBy(['person1' => $person1, 'person2' => $person2]);
if ($personNotDuplicate instanceof PersonNotDuplicate) {
$this->getDoctrine()->getManager()->remove($personNotDuplicate);
$this->getDoctrine()->getManager()->flush();
}
return $this->redirectToRoute('chill_person_duplicate_view', ['person_id' => $person1->getId()]);
}
/**
@ -138,4 +156,31 @@ class PersonDuplicateController extends Controller
{
return $this->personRepository->find($id);
}
private function _getPersonsByPriority($person1_id, $person2_id)
{
if ($person1_id === $person2_id) {
throw new InvalidArgumentException('Can not merge same person');
}
if ($person1_id > $person2_id) {
$person1 = $this->_getPerson($person2_id);
$person2 = $this->_getPerson($person1_id);
} else {
$person1 = $this->_getPerson($person1_id);
$person2 = $this->_getPerson($person2_id);
}
if ($person1 === null) {
throw $this->createNotFoundException("Person with id $person1_id not"
. " found on this server");
}
if ($person2 === null) {
throw $this->createNotFoundException("Person with id $person2_id not"
. " found on this server");
}
return [$person1, $person2];
}
}

View File

@ -9,7 +9,7 @@ use Chill\MainBundle\Entity\User;
* PersonNotDuplicate
*
* @ORM\Table(name="chill_person_not_duplicate")
* @ORM\Entity()
* @ORM\Entity(repositoryClass="Chill\PersonBundle\Repository\PersonNotDuplicateRepository")
*/
class PersonNotDuplicate
{

View File

@ -16,7 +16,7 @@ class PersonConfimDuplicateType extends AbstractType
{
$builder
->add('confirm', CheckboxType::class, [
'label' => 'Je confirme la fusion de ces 2 personnes',
'label' => 'I confirm the merger of these 2 people',
'mapped' => false,
]);
}

View File

@ -0,0 +1,41 @@
<?php
namespace Chill\PersonBundle\Repository;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\PersonNotDuplicate;
use Doctrine\ORM\EntityRepository;
/**
* Class PersonNotDuplicateRepository
*
* @package Chill\PersonBundle\Repository
*/
class PersonNotDuplicateRepository extends EntityRepository
{
/**
* @param \Chill\PersonBundle\Entity\Person $person
*
* @return array
*/
public function findNotDuplicatePerson(Person $person)
{
$qb = $this->createQueryBuilder('pnd');
$qb->select('pnd')
->where('pnd.person1 = :person OR pnd.person2 = :person')
;
$qb->setParameter('person', $person);
$result = $qb->getQuery()->getResult();
$persons = [];
foreach ($result as $row) {
if ($row->getPerson1() === $person) {
$persons[] = $row->getPerson2();
} elseif ($row->getPerson2() === $person) {
$persons[] = $row->getPerson1();
}
}
return $persons;
}
}

View File

@ -6,12 +6,12 @@
' ' ~ person.lastName }}{% endblock %}
{% block personcontent %}
<h2>Ancien dossier</h2>
<h2>{{ 'Old person'|trans }}</h2>
{{ person2 }}
<a class="sc-button bt-show" target="_blank" href="{{ path('chill_person_view', { person_id : person2.id }) }}"></a>
<h2>Nouveau Dossier</h2>
<h2>{{ 'New person'|trans }}</h2>
{{ person }}
<a class="sc-button bt-show" target="_blank" href="{{ path('chill_person_view', { person_id : person.id }) }}"></a>

View File

@ -8,6 +8,9 @@
{% block personcontent %}
{% if duplicatePersons|length > 0 %}
<h2>{{ title|default('Person duplicate')|trans }}</h2>
<table>
@ -54,12 +57,70 @@
<td>
<ul class="record_actions">
<li><a class="sc-button bt-show" target="_blank" href="{{ path('chill_person_view', { person_id : duplicatePerson.id }) }}"></a></li>
<li><a class="sc-button bt-duplicate" href="{{ path('chill_person_duplicate_confirm', { person_id : person.id, person2_id : duplicatePerson.id }) }}"></a></li>
<li><a class="sc-button bt-not-duplicate" href="{{ path('chill_person_duplicate_not_duplicate', {person_id : person.id, person2_id : duplicatePerson.id}) }}"></a></li>
<li><a class="sc-button bt-duplicate" href="{{ path('chill_person_duplicate_confirm', { person1_id : person.id, person2_id : duplicatePerson.id }) }}"></a></li>
<li><a class="sc-button bt-not-duplicate" href="{{ path('chill_person_duplicate_not_duplicate', {person1_id : person.id, person2_id : duplicatePerson.id}) }}"></a></li>
</ul>
</td>
</tr>
{% endfor %}
{% endif %}
{% if notDuplicatePersons|length > 0 %}
<h2>{{ 'Person flaged as duplicate' | trans }}</h2>
<table>
<thead>
<tr>
<th class="chill-red">{% trans %}Name{% endtrans %}</th>
<th class="chill-green">{% trans %}Date of birth{% endtrans %}</th>
<th class="chill-orange">{% trans %}Nationality{% endtrans %}</th>
<th>&nbsp;</th>
</tr>
</thead>
{% for notDuplicatePerson in notDuplicatePersons %}
<tr>
<td>
{% set is_open = notDuplicatePerson.isOpen() %}
<a href="{{ path('chill_person_view', { person_id : notDuplicatePerson.getId }) }}" {% if chill_person.fields.accompanying_period == 'visible' %}{% if is_open %} alt="{{ 'An accompanying period is open'|trans|e('html_attr') }}"{% else %} alt="{{ 'Any accompanying periods are open'|trans|e('html_attr') }}" {% endif %}{% endif %}>
{{ notDuplicatePerson|chill_entity_render_box }}
{% apply spaceless %}
{% if chill_person.fields.accompanying_period == 'visible' %}
{% if is_open == false %}
<i class="fa fa-lock" ></i>
{% else %}
<i class="fa fa-unlock" ></i>
{% endif %}
{% endif %}
{% endapply %}
</a>
</td>
<td>
{% if notDuplicatePerson.birthdate is not null %}
{{ notDuplicatePerson.birthdate|format_date('long') }}
{% else %}
{{ 'Unknown date of birth'|trans }}
{% endif %}
</td>
<td>
{% if notDuplicatePerson.nationality is not null %}
{{ notDuplicatePerson.nationality.name|localize_translatable_string }}
{% else %}
{{ 'Without nationality'|trans }}
{% endif %}
</td>
<td>
<ul class="record_actions">
<li><a class="sc-button bt-show" target="_blank" href="{{ path('chill_person_view', { person_id : notDuplicatePerson.id }) }}"></a></li>
<li><a class="sc-button bt-not-duplicate" href="{{ path('chill_person_remove_duplicate_not_duplicate', {person1_id : person.id, person2_id : notDuplicatePerson.id}) }}"></a></li>
</ul>
</td>
</tr>
{% endfor %}
</table>
{% endif %}
</table>
{% endblock %}

View File

@ -18,6 +18,7 @@
*/
namespace Chill\PersonBundle\Search;
use Chill\PersonBundle\Entity\PersonNotDuplicate;
use Doctrine\ORM\EntityManagerInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
@ -32,6 +33,10 @@ use Chill\PersonBundle\Security\Authorization\PersonVoter;
*/
class SimilarPersonMatcher
{
CONST SIMILAR_SEARCH_ORDER_BY_ALPHABETICAL = 'alphabetical';
CONST SIMILAR_SEARCH_ORDER_BY_SIMILARITY = 'similarity';
/**
*
* @var EntityManagerInterface
@ -61,7 +66,7 @@ class SimilarPersonMatcher
}
public function matchPerson(Person $person, $precision = 0.15)
public function matchPerson(Person $person, $precision = 0.15, $orderBy = self::SIMILAR_SEARCH_ORDER_BY_SIMILARITY)
{
$centers = $this->authorizationHelper->getReachableCenters(
$this->tokenStorage->getToken()->getUser(),
@ -71,22 +76,40 @@ class SimilarPersonMatcher
$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 '
;
$notDuplicatePersons = $this->em->getRepository(PersonNotDuplicate::class)
->findNotDuplicatePerson($person);
if (count($notDuplicatePersons)) {
$dql .= ' AND p.id not in (:notDuplicatePersons)';
}
switch ($orderBy) {
case self::SIMILAR_SEARCH_ORDER_BY_ALPHABETICAL:
$dql .= ' ORDER BY p.fullnameCanonical ASC ';
break;
case self::SIMILAR_SEARCH_ORDER_BY_SIMILARITY:
default :
$dql .= ' ORDER BY SIMILARITY(p.fullnameCanonical, UNACCENT(LOWER(:fullName))) DESC ';
}
$query = $this->em
->createQuery($dql)
->setParameter('fullName', $person->getFirstName() . ' ' . $person->getLastName())
->setParameter('fullNameInverted', $person->getLastName() . ' ' . $person->getFirstName())
->setParameter('centers', $centers)
->setParameter('personId', $person->getId())
->setParameter('precision', $precision)
;
if (count($notDuplicatePersons)) {
$query->setParameter('notDuplicatePersons', $notDuplicatePersons);
}
return $query->getResult();
}
}

View File

@ -112,7 +112,7 @@ chill_person_closingmotive_admin:
label: 'person_admin.closing motives'
chill_person_duplicate_confirm:
path: /{_locale}/person/{person_id}/duplicate/{person2_id}/confirm
path: /{_locale}/person/{person1_id}/duplicate/{person2_id}/confirm
controller: Chill\PersonBundle\Controller\PersonDuplicateController::confirmAction
chill_person_maritalstatus_admin:
@ -125,5 +125,9 @@ chill_person_maritalstatus_admin:
label: 'person_admin.marital status'
chill_person_duplicate_not_duplicate:
path: /{_locale}/person/{person_id}/duplicate/{person2_id}/not-duplicate
path: /{_locale}/person/{person1_id}/duplicate/{person2_id}/not-duplicate
controller: Chill\PersonBundle\Controller\PersonDuplicateController::notDuplicateAction
chill_person_remove_duplicate_not_duplicate:
path: /{_locale}/person/{person1_id}/duplicate/{person2_id}/remove-not-duplicate
controller: Chill\PersonBundle\Controller\PersonDuplicateController::removeNotDuplicateAction

View File

@ -28,4 +28,6 @@ services:
$similarPersonMatcher: '@Chill\PersonBundle\Search\SimilarPersonMatcher'
$translator: '@Symfony\Component\Translation\TranslatorInterface'
$personRepository: '@Chill\PersonBundle\Repository\PersonRepository'
$personMove: '@Chill\PersonBundle\Actions\Remove\PersonMove'
$eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface'
tags: ['controller.service_arguments']

View File

@ -67,6 +67,8 @@ Reset: 'Remise à zéro'
'Closing motive': 'Motif de clôture'
'Person details': 'Détails de la personne'
'Person duplicate': 'Find duplicate'
Old person: Old person
New person: New person
Create an accompanying period: Create an accompanying period
'Create': Create

View File

@ -66,6 +66,10 @@ Married: Marié(e)
'Contact information': 'Informations de contact'
'Administrative information': Administratif
File number: Dossier n°
Old person: Ancien dossier
New person: Nouveau dossier
I confirm the merger of these 2 people : Je confime la fusion de ces 2 dossiers
Person flaged as duplicate: Dossiers marqués comme faux-positif
# addresses part
address_street_address_1: Adresse ligne 1