From c54fb17fc23d2b499998fb6d3689810ad3a99605 Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Thu, 8 Dec 2022 17:11:58 +0100
Subject: [PATCH 6/7] toulouse-maelis: add search on referentials (#70982)

---
 .../0003_referential_item_unaccent_text.py    |  18 ++
 passerelle/contrib/toulouse_maelis/models.py  | 186 ++++++++++++++----
 tests/test_toulouse_maelis.py                 |  36 ++++
 3 files changed, 203 insertions(+), 37 deletions(-)
 create mode 100644 passerelle/contrib/toulouse_maelis/migrations/0003_referential_item_unaccent_text.py

diff --git a/passerelle/contrib/toulouse_maelis/migrations/0003_referential_item_unaccent_text.py b/passerelle/contrib/toulouse_maelis/migrations/0003_referential_item_unaccent_text.py
new file mode 100644
index 00000000..77f4e895
--- /dev/null
+++ b/passerelle/contrib/toulouse_maelis/migrations/0003_referential_item_unaccent_text.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.26 on 2022-12-08 16:11
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('toulouse_maelis', '0002_referential'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='referential',
+            name='item_unaccent_text',
+            field=models.CharField(max_length=64, null=True, verbose_name='Text'),
+        ),
+    ]
diff --git a/passerelle/contrib/toulouse_maelis/models.py b/passerelle/contrib/toulouse_maelis/models.py
index 3ad5038e..b0d1a775 100644
--- a/passerelle/contrib/toulouse_maelis/models.py
+++ b/passerelle/contrib/toulouse_maelis/models.py
@@ -18,16 +18,17 @@ from urllib.parse import urljoin
 import zeep
 from django.contrib.postgres.fields import JSONField
 from django.db import models
 from zeep.helpers import serialize_object
 from zeep.wsse.username import UsernameToken
 
 from passerelle.base.models import BaseResource, HTTPResource
 from passerelle.utils.api import endpoint
+from passerelle.utils.conversion import simplify
 from passerelle.utils.jsonresponse import APIError
 
 from . import schemas
 
 
 class UpdateError(Exception):
     pass
 
@@ -115,16 +116,17 @@ class ToulouseMaelis(BaseResource, HTTPResource):
         for item in response:
             text = item[text_key].strip()
             self.referential.update_or_create(
                 resource_id=self.id,
                 referential_name=referential_name,
                 item_id=item[id_key],
                 defaults={
                     'item_text': text,
+                    'item_unaccent_text': simplify(text),
                     'item_data': dict({'id': item[id_key], 'text': text}, **item),
                 },
             )
 
         # clean item removed from referential
         if latest_update is not None:
             self.referential.filter(referential_name=referential_name, updated=latest_update).delete()
 
@@ -153,23 +155,36 @@ class ToulouseMaelis(BaseResource, HTTPResource):
     def daily(self):
         try:
             self.update_referentials()
         except UpdateError as e:
             self.logger.warning('Erreur sur la mise à jour: %s' % e)
         else:
             self.logger.info('Réferentiels mis à jour.')
 
-    def get_referential(self, referential_name):
-        return [
-            x.item_data
-            for x in self.referential.distinct('referential_name', 'item_text').filter(
-                referential_name=referential_name
-            )
-        ]
+    def get_referential(self, referential_name, id=None, q=None, limit=None, distinct=True):
+        if id is not None:
+            queryset = self.referential.filter(referential_name=referential_name, item_id=id)
+        else:
+            queryset = self.referential.filter(referential_name=referential_name).all()
+            if q:
+                queryset = queryset.filter(item_unaccent_text__icontains=simplify(q))
+
+        if distinct:
+            queryset = queryset.distinct('referential_name', 'item_text')
+
+        if limit:
+            try:
+                limit = int(limit)
+            except ValueError:
+                pass
+            else:
+                queryset = queryset[:limit]
+
+        return [x.item_data for x in queryset]
 
     def get_referential_value(self, referential_name, key):
         try:
             return self.referential.get(referential_name=referential_name, item_id=key).item_text
         except Referential.DoesNotExist:
             self.logger.warning("No '%s' key into Maelis '%s' referential", key, referential_name)
             return key
 
@@ -406,154 +421,250 @@ class ToulouseMaelis(BaseResource, HTTPResource):
             if value is None:
                 dico[key] = ''
 
     @endpoint(
         display_category='Famille',
         description='Liste des catégories',
         name='read-category-list',
         perm='can_access',
+        parameters={
+            'id': {'description': 'Identifiant de l’enregistrement'},
+            'q': {'description': 'Recherche en texte intégral'},
+            'limit': {'description': 'Nombre maximal de résultats; doit être inférieur à 20.'},
+            'distinct': {'description': 'Supression des doublons'},
+        },
     )
-    def read_category_list(self, request):
-        return {'data': self.get_referential('Category')}
+    def read_category_list(self, request, id=None, q=None, limit=None, distinct=True):
+        return {'data': self.get_referential('Category', id, q, limit, distinct)}
 
     @endpoint(
         display_category='Famille',
         description='Liste des indicateurs sur le enfants',
         name='read-child-indicator-list',
         perm='can_access',
+        parameters={
+            'id': {'description': 'Identifiant de l’enregistrement'},
+            'q': {'description': 'Recherche en texte intégral'},
+            'limit': {'description': 'Nombre maximal de résultats; doit être inférieur à 20.'},
+            'distinct': {'description': 'Supression des doublons'},
+        },
     )
-    def read_child_indicator_list(self, request):
-        return {'data': self.get_referential('ChildIndicator')}
+    def read_child_indicator_list(self, request, id=None, q=None, limit=None, distinct=True):
+        return {'data': self.get_referential('ChildIndicator', id, q, limit, distinct)}
 
     @endpoint(
         display_category='Famille',
         description='Liste des civilités',
         name='read-civility-list',
         perm='can_access',
+        parameters={
+            'id': {'description': 'Identifiant de l’enregistrement'},
+            'q': {'description': 'Recherche en texte intégral'},
+            'limit': {'description': 'Nombre maximal de résultats; doit être inférieur à 20.'},
+            'distinct': {'description': 'Supression des doublons'},
+        },
     )
-    def read_civility_list(self, request):
-        return {'data': self.get_referential('Civility')}
+    def read_civility_list(self, request, id=None, q=None, limit=None, distinct=True):
+        return {'data': self.get_referential('Civility', id, q, limit, distinct)}
 
     @endpoint(
         display_category='Famille',
         description='Liste des compléments du numéro de voie',
         name='read-complement-list',
         perm='can_access',
+        parameters={
+            'id': {'description': 'Identifiant de l’enregistrement'},
+            'q': {'description': 'Recherche en texte intégral'},
+            'limit': {'description': 'Nombre maximal de résultats; doit être inférieur à 20.'},
+            'distinct': {'description': 'Supression des doublons'},
+        },
     )
-    def read_complement_list(self, request):
-        return {'data': self.get_referential('Complement')}
+    def read_complement_list(self, request, id=None, q=None, limit=None, distinct=True):
+        return {'data': self.get_referential('Complement', id, q, limit, distinct)}
 
     @endpoint(
         display_category='Famille',
         description='Liste des pays',
         name='read-country-list',
         perm='can_access',
+        parameters={
+            'id': {'description': 'Identifiant de l’enregistrement'},
+            'q': {'description': 'Recherche en texte intégral'},
+            'limit': {'description': 'Nombre maximal de résultats; doit être inférieur à 20.'},
+            'distinct': {'description': 'Supression des doublons'},
+        },
     )
-    def read_country_list(self, request):
-        return {'data': self.get_referential('Country')}
+    def read_country_list(self, request, id=None, q=None, limit=None, distinct=True):
+        return {'data': self.get_referential('Country', id, q, limit, distinct)}
 
     @endpoint(
         display_category='Famille',
         description='liste des catégories socio-professionnelles',
         name='read-csp-list',
         perm='can_access',
+        parameters={
+            'id': {'description': 'Identifiant de l’enregistrement'},
+            'q': {'description': 'Recherche en texte intégral'},
+            'limit': {'description': 'Nombre maximal de résultats'},
+            'distinct': {'description': 'Supression des doublons'},
+        },
     )
-    def read_csp_list(self, request):
-        return {'data': self.get_referential('CSP')}
+    def read_csp_list(self, request, id=None, q=None, limit=None, distinct=True):
+        return {'data': self.get_referential('CSP', id, q, limit, distinct)}
 
     @endpoint(
         display_category='Famille',
         description='Liste des régimes alimentaires',
         name='read-dietcode-list',
         perm='can_access',
+        parameters={
+            'id': {'description': 'Identifiant de l’enregistrement'},
+            'q': {'description': 'Recherche en texte intégral'},
+            'limit': {'description': 'Nombre maximal de résultats; doit être inférieur à 20.'},
+            'distinct': {'description': 'Supression des doublons'},
+        },
     )
-    def read_dietcode_list(self, request):
-        return {'data': self.get_referential('DietCode')}
+    def read_dietcode_list(self, request, id=None, q=None, limit=None, distinct=True):
+        return {'data': self.get_referential('DietCode', id, q, limit, distinct)}
 
     @endpoint(
         display_category='Famille',
         description='Liste des organismes (CAF)',
         name='read-organ-list',
         perm='can_access',
+        parameters={
+            'id': {'description': 'Identifiant de l’enregistrement'},
+            'q': {'description': 'Recherche en texte intégral'},
+            'limit': {'description': 'Nombre maximal de résultats; doit être inférieur à 20.'},
+            'distinct': {'description': 'Supression des doublons'},
+        },
     )
-    def read_organ_list(self, request):
+    def read_organ_list(self, request, id=None, q=None, limit=None, distinct=True):
         return {'data': self.get_referential('Organ')}
 
     @endpoint(
         display_category='Famille',
         description="Liste des projet d'accueil individualisés",
         name='read-pai-list',
         perm='can_access',
+        parameters={
+            'id': {'description': 'Identifiant de l’enregistrement'},
+            'q': {'description': 'Recherche en texte intégral'},
+            'limit': {'description': 'Nombre maximal de résultats; doit être inférieur à 20.'},
+            'distinct': {'description': 'Supression des doublons'},
+        },
     )
-    def read_pai_list(self, request):
-        return {'data': self.get_referential('PAI')}
+    def read_pai_list(self, request, id=None, q=None, limit=None, distinct=True):
+        return {'data': self.get_referential('PAI', id, q, limit, distinct)}
 
     @endpoint(
         display_category='Famille',
         description='liste des qualités du référenciel',
         name='read-quality-list',
         perm='can_access',
+        parameters={
+            'id': {'description': 'Identifiant de l’enregistrement'},
+            'q': {'description': 'Recherche en texte intégral'},
+            'limit': {'description': 'Nombre maximal de résultats; doit être inférieur à 20.'},
+            'distinct': {'description': 'Supression des doublons'},
+        },
     )
-    def read_quality_list(self, request):
-        return {'data': self.get_referential('Quality')}
+    def read_quality_list(self, request, id=None, q=None, limit=None, distinct=True):
+        return {'data': self.get_referential('Quality', id, q, limit, distinct)}
 
     @endpoint(
         display_category='Famille',
         description='Liste des quotients',
         name='read-quotient-list',
         perm='can_access',
+        parameters={
+            'id': {'description': 'Identifiant de l’enregistrement'},
+            'q': {'description': 'Recherche en texte intégral'},
+            'limit': {'description': 'Nombre maximal de résultats; doit être inférieur à 20.'},
+            'distinct': {'description': 'Supression des doublons'},
+        },
     )
-    def read_quotient_list(self, request):
-        return {'data': self.get_referential('Quotient')}
+    def read_quotient_list(self, request, id=None, q=None, limit=None, distinct=True):
+        return {'data': self.get_referential('Quotient', id, q, limit, distinct)}
 
     @endpoint(
         display_category='Famille',
         description='Liste des indicateurs sur les responsables légaux',
         name='read-rl-indicator-list',
         perm='can_access',
+        parameters={
+            'id': {'description': 'Identifiant de l’enregistrement'},
+            'q': {'description': 'Recherche en texte intégral'},
+            'limit': {'description': 'Nombre maximal de résultats; doit être inférieur à 20.'},
+            'distinct': {'description': 'Supression des doublons'},
+        },
     )
-    def read_rl_indicator_list(self, request):
+    def read_rl_indicator_list(self, request, id=None, q=None, limit=None, distinct=True):
         return {'data': self.get_referential('RLIndicator')}
 
     @endpoint(
         display_category='Famille',
         description='Liste des sexes',
         name='read-sex-list',
         perm='can_access',
+        parameters={
+            'id': {'description': 'Identifiant de l’enregistrement'},
+            'q': {'description': 'Recherche en texte intégral'},
+            'limit': {'description': 'Nombre maximal de résultats; doit être inférieur à 20.'},
+            'distinct': {'description': 'Supression des doublons'},
+        },
     )
-    def read_sex_list(self, request):
-        return {'data': self.get_referential('Sex')}
+    def read_sex_list(self, request, id=None, q=None, limit=None, distinct=True):
+        return {'data': self.get_referential('Sex', id, q, limit, distinct)}
 
     @endpoint(
         display_category='Famille',
         description='liste des situations',
         name='read-situation-list',
         perm='can_access',
+        parameters={
+            'id': {'description': 'Identifiant de l’enregistrement'},
+            'q': {'description': 'Recherche en texte intégral'},
+            'limit': {'description': 'Nombre maximal de résultats'},
+            'distinct': {'description': 'Supression des doublons'},
+        },
     )
-    def read_situation_list(self, request):
-        return {'data': self.get_referential('Situation')}
+    def read_situation_list(self, request, id=None, q=None, limit=None, distinct=True):
+        return {'data': self.get_referential('Situation', id, q, limit, distinct)}
 
     @endpoint(
         display_category='Famille',
         description='liste des voies',
         name='read-street-list',
         perm='can_access',
+        parameters={
+            'id': {'description': 'Identifiant de l’enregistrement'},
+            'q': {'description': 'Recherche en texte intégral'},
+            'limit': {'description': 'Nombre maximal de résultats; doit être inférieur à 20.'},
+            'distinct': {'description': 'Supression des doublons'},
+        },
     )
-    def read_street_list(self, request):
-        return {'data': self.get_referential('Street')}
+    def read_street_list(self, request, id=None, q=None, limit=None, distinct=True):
+        return {'data': self.get_referential('Street', id, q, limit, distinct)}
 
     @endpoint(
         display_category='Famille',
         description='Liste des vaccins',
         name='read-vaccin-list',
         perm='can_access',
+        parameters={
+            'id': {'description': 'Identifiant de l’enregistrement'},
+            'q': {'description': 'Recherche en texte intégral'},
+            'limit': {'description': 'Nombre maximal de résultats; doit être inférieur à 20.'},
+            'distinct': {'description': 'Supression des doublons'},
+        },
     )
-    def read_vaccin_list(self, request):
-        return {'data': self.get_referential('Vaccin')}
+    def read_vaccin_list(self, request, id=None, q=None, limit=None, distinct=True):
+        return {'data': self.get_referential('Vaccin', id, q, limit, distinct)}
 
     @endpoint(
         display_category='Famille',
         description='Lier un compte usager à une famille',
         perm='can_access',
         parameters={'NameID': {'description': 'Publik NameID'}},
         post={'request_body': {'schema': {'application/json': schemas.LINK_SCHEMA}}},
     )
@@ -1217,16 +1328,17 @@ class Referential(models.Model):
         verbose_name='Resource',
         to=ToulouseMaelis,
         on_delete=models.CASCADE,
         related_name='referential',
     )
     referential_name = models.CharField('Name', max_length=64)
     item_id = models.CharField('Key', max_length=64)
     item_text = models.CharField('Text', max_length=128)
+    item_unaccent_text = models.CharField('Text', max_length=64, null=True)
     item_data = JSONField('Data')
     created = models.DateTimeField('Created', auto_now_add=True)
     updated = models.DateTimeField('Updated', auto_now=True)
 
     def __repr__(self):
         return '<Referential "%s/%s">' % (self.referential_name, self.item_id)
 
     class Meta:
diff --git a/tests/test_toulouse_maelis.py b/tests/test_toulouse_maelis.py
index 6db8fb2c..cb11094c 100644
--- a/tests/test_toulouse_maelis.py
+++ b/tests/test_toulouse_maelis.py
@@ -414,24 +414,36 @@ def test_read_complement_list(con, app):
         {'id': 'T', 'text': 'ter'},
     ]
 
 
 def test_read_csp_list(con, app):
     url = get_endpoint('read-csp-list')
     resp = app.get(url)
     assert resp.json['err'] == 0
+    assert len(resp.json['data']) == 27
     assert resp.json['data'][:5] == [
         {'code': '14', 'id': '14', 'libelle': 'AGENT DE MAITRISE', 'text': 'AGENT DE MAITRISE'},
         {'code': '1', 'id': '1', 'libelle': 'AGRICULTEUR', 'text': 'AGRICULTEUR'},
         {'code': 'ART', 'id': 'ART', 'libelle': 'ARTISAN', 'text': 'ARTISAN'},
         {'code': '2', 'id': '2', 'libelle': 'ARTISAN-COMMERCANT', 'text': 'ARTISAN-COMMERCANT'},
         {'code': '15', 'id': '15', 'libelle': 'AUTRES', 'text': 'AUTRES'},
     ]
 
+    resp = app.get(url + '?distinct=False')
+    assert resp.json['err'] == 0
+    assert len(resp.json['data']) == 43
+    assert resp.json['data'][:5] == [
+        {'id': '14', 'code': '14', 'text': 'AGENT DE MAITRISE', 'libelle': 'AGENT DE MAITRISE'},
+        {'id': '1', 'code': '1', 'text': 'AGRICULTEUR', 'libelle': 'AGRICULTEUR'},
+        {'id': 'AGR', 'code': 'AGR', 'text': 'AGRICULTEUR', 'libelle': 'AGRICULTEUR'},
+        {'id': 'ART', 'code': 'ART', 'text': 'ARTISAN', 'libelle': 'ARTISAN'},
+        {'id': 'ARTI', 'code': 'ARTI', 'text': 'ARTISAN', 'libelle': 'ARTISAN'},
+    ]
+
 
 def test_read_dietcode_list(con, app):
     url = get_endpoint('read-dietcode-list')
     resp = app.get(url)
     assert resp.json['err'] == 0
     assert resp.json['data'] == [
         {'id': 'STD', 'code': 'STD', 'text': '1- REPAS STANDARD', 'libelle': '1- REPAS STANDARD'},
         {'id': 'RSP', 'code': 'RSP', 'text': '2- RÉGIME SANS PORC', 'libelle': '2- RÉGIME SANS PORC'},
@@ -542,24 +554,48 @@ def test_read_sex_list(con, app):
         {'id': 'M', 'text': 'Masculin'},
     ]
 
 
 def test_read_situation_list(con, app):
     url = get_endpoint('read-situation-list')
     resp = app.get(url)
     assert resp.json['err'] == 0
+    assert len(resp.json['data']) == 9
     assert resp.json['data'][:5] == [
         {'id': 'C', 'code': 'C', 'text': 'Célibataire', 'libelle': 'Célibataire'},
         {'id': 'D', 'code': 'D', 'text': 'Divorcé (e)', 'libelle': 'Divorcé (e)'},
         {'id': 'CS', 'code': 'CS', 'text': 'EN COURS DE SEPARATION', 'libelle': 'EN COURS DE SEPARATION'},
         {'id': 'M', 'code': 'M', 'text': 'Marié (e)', 'libelle': 'Marié (e)'},
         {'id': 'P', 'code': 'P', 'text': 'Pacsé (e)', 'libelle': 'Pacsé (e)'},
     ]
 
+    resp = app.get(url + '?q= ')
+    assert resp.json['err'] == 0
+    assert len(resp.json['data']) == 9
+
+    resp = app.get(url + '?id=V')
+    assert resp.json['err'] == 0
+    assert resp.json['data'] == [{'id': 'V', 'code': 'V', 'text': 'Veuf (ve)', 'libelle': 'Veuf (ve)'}]
+
+    resp = app.get(url + '?q= sÉ ')
+    assert resp.json['err'] == 0
+    assert resp.json['data'] == [
+        {'id': 'CS', 'code': 'CS', 'text': 'EN COURS DE SEPARATION', 'libelle': 'EN COURS DE SEPARATION'},
+        {'id': 'P', 'code': 'P', 'text': 'Pacsé (e)', 'libelle': 'Pacsé (e)'},
+        {'id': 'S', 'code': 'S', 'text': 'Séparé (e)', 'libelle': 'Séparé (e)'},
+    ]
+
+    resp = app.get(url + '?q= sÉ &limit=2')
+    assert resp.json['err'] == 0
+    assert resp.json['data'] == [
+        {'id': 'CS', 'code': 'CS', 'text': 'EN COURS DE SEPARATION', 'libelle': 'EN COURS DE SEPARATION'},
+        {'id': 'P', 'code': 'P', 'text': 'Pacsé (e)', 'libelle': 'Pacsé (e)'},
+    ]
+
 
 def test_read_street_list(con, app):
     url = get_endpoint('read-street-list')
     resp = app.get(url)
     assert resp.json['err'] == 0
     assert len(resp.json['data']) == 3
     assert resp.json['data'] == [
         {'id': 'AP0594', 'text': 'AVENUE PAULIANI', 'idStreet': 'AP0594', 'libelleStreet': 'AVENUE PAULIANI'},
-- 
2.30.2

