From dc30675865679ef3380f5fd4a116ed98d28c1b54 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Tue, 17 Dec 2019 15:39:17 +0100
Subject: [PATCH 1/3] toulouse_axel: partial update (#38543)

---
 functests/toulouse_axel/test_toulouse_axel.py |  20 +-
 passerelle/contrib/toulouse_axel/models.py    | 145 ++++++-
 .../flat_update_family_info.json              |  56 ++-
 .../toulouse_axel/update_family_info.json     |  56 ++-
 tests/test_toulouse_axel.py                   | 367 ++++++++++++++++++
 5 files changed, 630 insertions(+), 14 deletions(-)

diff --git a/functests/toulouse_axel/test_toulouse_axel.py b/functests/toulouse_axel/test_toulouse_axel.py
index 09a3f92e..f18a996f 100644
--- a/functests/toulouse_axel/test_toulouse_axel.py
+++ b/functests/toulouse_axel/test_toulouse_axel.py
@@ -35,12 +35,30 @@ def test_link(conn, user):
     payload['N0TELEDOSSIER'] = '42'
     payload['DROITALIMAGE'] = 'NON'
     for key in ['MONTANTTOTAL', 'DATEVALIDITE', 'SFI', 'IREVENUS', 'RNF']:
-        payload['REVENUS'].pop(key)
+        payload['REVENUS'].pop(key, None)
     payload['REVENUS']['CHOIXREVENU'] = ''
     for enfant in payload['ENFANT']:
         for key in ['NOM', 'DATENAISSANCE', 'SEXE', 'PRENOMPERE', 'PRENOMMERE', 'NOMPERE', 'NOMMERE', 'RATTACHEAUTREDUI', 'PRENOM']:
             enfant.pop(key)
         enfant['AUTORISATIONURGENCEMEDICALE'] = 'OUI'
+    flags = [
+        'maj:adresse',
+        'maj:rl1', 'maj:rl1_adresse_employeur',
+        'maj:rl2', 'maj:rl2_adresse_employeur',
+        'maj:revenus',
+    ]
+    for i in range(0, 6):
+        flags += [
+            'maj:enfant_%s' % i,
+            'maj:enfant_%s_sanitaire' % i,
+            'maj:enfant_%s_sanitaire_medecin' % i,
+            'maj:enfant_%s_sanitaire_vaccin' % i,
+            'maj:enfant_%s_sanitaire_allergie' % i,
+            'maj:enfant_%s_assurance' % i,
+            'maj:enfant_%s_contact' % i,
+        ]
+    for key in flags:
+        payload[key] = True
 
     print("Update family info with the following payload:")
     pprint.pprint(payload)
diff --git a/passerelle/contrib/toulouse_axel/models.py b/passerelle/contrib/toulouse_axel/models.py
index 3f41bab6..c12904ab 100644
--- a/passerelle/contrib/toulouse_axel/models.py
+++ b/passerelle/contrib/toulouse_axel/models.py
@@ -78,6 +78,14 @@ def indent(tree, space="  ", level=0):
     _indent_children(tree, 0)
 
 
+def encode_bool(obj):
+    if obj is True or str(obj).lower() in ['true', 'oui', '1']:
+        return 'OUI'
+    if obj is False or str(obj).lower() in ['false', 'non', '0']:
+        return 'NON'
+    return obj
+
+
 class AxelSchema(JSONSchemaFromXMLSchema):
     type_map = {
         '{urn:AllAxelTypes}DATEREQUIREDType': 'date',
@@ -126,11 +134,7 @@ class AxelSchema(JSONSchemaFromXMLSchema):
         }
 
     def encode_bool(self, obj):
-        if obj is True or str(obj).lower() in ['true', 'oui', '1']:
-            return 'OUI'
-        if obj is False or str(obj).lower() in ['false', 'non', '0']:
-            return 'NON'
-        return obj
+        return encode_bool(obj)
 
     def decode_bool(self, data):
         value = False
@@ -261,6 +265,31 @@ class ToulouseAxel(BaseResource):
     LINK_SCHEMA['properties'].pop('IDPERSONNE')
     LINK_SCHEMA['required'].remove('IDPERSONNE')
 
+    UPDATE_FAMILY_FLAGS = {
+        'maj:adresse': 'ADRESSE',
+        'maj:rl1': 'RL1',
+        'maj:rl1_adresse_employeur': 'RL1/ADREMPLOYEUR',
+        'maj:rl2': 'RL2',
+        'maj:rl2_adresse_employeur': 'RL2/ADREMPLOYEUR',
+        'maj:revenus': 'REVENUS',
+    }
+    UPDATE_FAMILY_REQUIRED_FLAGS = [
+        'maj:adresse',
+        'maj:rl1',
+        'maj:rl2',
+        'maj:revenus',
+    ]
+    for i in range(0, 6):
+        UPDATE_FAMILY_FLAGS.update({
+            'maj:enfant_%s' % i: 'ENFANT/%s' % i,
+            'maj:enfant_%s_sanitaire' % i: 'ENFANT/%s/SANITAIRE' % i,
+            'maj:enfant_%s_sanitaire_medecin' % i: 'ENFANT/%s/SANITAIRE/MEDECIN' % i,
+            'maj:enfant_%s_sanitaire_vaccin' % i: 'ENFANT/%s/SANITAIRE/VACCIN' % i,
+            'maj:enfant_%s_sanitaire_allergie' % i: 'ENFANT/%s/SANITAIRE/ALLERGIE' % i,
+            'maj:enfant_%s_assurance' % i: 'ENFANT/%s/ASSURANCE' % i,
+            'maj:enfant_%s_contact' % i: 'ENFANT/%s/CONTACT' % i,
+        })
+        UPDATE_FAMILY_REQUIRED_FLAGS.append('maj:enfant_%s' % i)
     UPDATE_FAMILY_SCHEMA = copy.deepcopy(
         form_maj_famille_dui.request_schema['properties']['PORTAIL']['properties']['DUI'])
     UPDATE_FAMILY_SCHEMA['properties'].pop('IDDUI')
@@ -269,6 +298,20 @@ class ToulouseAxel(BaseResource):
     UPDATE_FAMILY_SCHEMA['required'].remove('IDDUI')
     UPDATE_FAMILY_SCHEMA['required'].remove('DATEDEMANDE')
     UPDATE_FAMILY_SCHEMA['required'].remove('QUIACTUALISEDUI')
+    for flag in sorted(UPDATE_FAMILY_FLAGS.keys()):
+        flag_type = {
+            'oneOf': [
+                {'type': 'boolean'},
+                {
+                    'type': 'string',
+                    'pattern': '[Oo][Uu][Ii]|[Nn][Oo][Nn]|[Tt][Rr][Uu][Ee]|[Ff][Aa][Ll][Ss][Ee]|1|0',
+                }
+            ]
+        }
+        if flag not in UPDATE_FAMILY_REQUIRED_FLAGS:
+            flag_type['oneOf'].append({'type': 'null'})
+        UPDATE_FAMILY_SCHEMA['properties'][flag] = flag_type
+        UPDATE_FAMILY_SCHEMA['required'].append(flag)
     UPDATE_FAMILY_SCHEMA['unflatten'] = True
 
     @endpoint(
@@ -417,6 +460,93 @@ class ToulouseAxel(BaseResource):
 
         raise APIError('Child not found', err_code='not-found')
 
+    def sanitize_update_family_data(self, name_id, post_data):
+        family_data = None
+
+        # transform ENFANT list to dict, where the key is the field IDPERSONNE
+        # because children in post_date are maybe not in the same order than on Axel side
+        children = {}
+        children_by_index = {}
+        for i, child in enumerate(post_data.get('ENFANT', [])):
+            # check if IDPERSONNE is filled
+            if child['IDPERSONNE']:
+                children[child['IDPERSONNE']] = child
+                children_by_index[str(i)] = child['IDPERSONNE']
+        post_data['ENFANT'] = children
+
+        # sanitize post_data
+        flags = sorted(self.UPDATE_FAMILY_FLAGS.keys())
+        for flag in flags:
+            flag_value = post_data.pop(flag)
+            flag_value = encode_bool(flag_value)
+
+            # no update for the related block
+            if flag_value != 'NON':
+                continue
+
+            # build the xml elements to cross
+            key = self.UPDATE_FAMILY_FLAGS[flag]
+            # special case for ENFANT flags
+            if key.startswith('ENFANT/'):
+                # replace the index by IDPERSONNE value
+                index = key.split('/')[1]
+                if index not in children_by_index:
+                    # no child with IDPERSONNE found in post_data
+                    continue
+                key = key.replace('ENFANT/%s' % index, 'ENFANT/%s' % children_by_index[index])
+            elements = key.split('/')
+
+            schema = self.UPDATE_FAMILY_SCHEMA
+            data = post_data
+            # find the structure in schema and data containing the element to remove
+            not_found = False
+            for element in elements[:-1]:
+                if schema.get('type') == 'array':
+                    schema = schema['items']
+                else:
+                    schema = schema['properties'][element]
+                try:
+                    data = data[element]
+                except (IndexError, KeyError):
+                    not_found = True
+                    break
+            if not_found:
+                continue
+
+            element_to_remove = elements[-1]
+            if element_to_remove == 'ADRESSE':
+                # empty all subelements
+                for k in data[element_to_remove].keys():
+                    data[element_to_remove][k] = None
+            elif element_to_remove == 'CONTACT':
+                # get the correct child
+                child_data = post_data['ENFANT'][elements[1]]
+                child_id = child_data['IDPERSONNE']
+                # and reset the contacts
+                if family_data is None:
+                    family_data = self.get_family_data(name_id)
+                for child in family_data['ENFANT']:
+                    if child['IDPERSONNE'] == child_id:
+                        if child.get('CONTACT'):
+                            child_data['CONTACT'] = child['CONTACT']
+                            # XXX set LIENPARENTE because if empty the contact is deleted
+                            for contact in child_data['CONTACT']:
+                                if not contact['LIENPARENTE']:
+                                    contact['LIENPARENTE'] = 'BABY'
+                        elif 'CONTACT' in child_data:
+                            child_data.pop('CONTACT')
+                        break
+            elif element_to_remove in data:
+                # remove block
+                data.pop(element_to_remove)
+
+        # transform ENFANT dict to a list back
+        post_data['ENFANT'] = post_data['ENFANT'].values()
+
+        # if ENFANT block is empty, remove it
+        if not post_data['ENFANT']:
+            post_data.pop('ENFANT')
+
     @endpoint(
         description=_("Update information about user's family"),
         perm='can_access',
@@ -432,12 +562,17 @@ class ToulouseAxel(BaseResource):
         })
     def update_family_info(self, request, NameID, post_data):
         link = self.get_link(NameID)
+
+        # prepare data
         post_data['IDDUI'] = link.dui
         post_data['DATEDEMANDE'] = datetime.date.today().strftime('%Y-%m-%d')
         if 'RL2' in post_data and post_data['RL2'].get('IDPERSONNE') == link.person_id:
             post_data['QUIACTUALISEDUI'] = '2'
         else:
             post_data['QUIACTUALISEDUI'] = '1'
+
+        self.sanitize_update_family_data(name_id=NameID, post_data=post_data)
+
         try:
             result = form_maj_famille_dui(self, {'PORTAIL': {'DUI': post_data}})
         except AxelError as e:
diff --git a/tests/data/toulouse_axel/flat_update_family_info.json b/tests/data/toulouse_axel/flat_update_family_info.json
index 6ceacb0a..4c5e0c19 100644
--- a/tests/data/toulouse_axel/flat_update_family_info.json
+++ b/tests/data/toulouse_axel/flat_update_family_info.json
@@ -16,21 +16,21 @@
   "ENFANT/0/AUTORISATIONURGENCEMEDICALE": "OUI",
   "ENFANT/0/CONTACT/0/CHERCHERLENFANT": "OUI",
   "ENFANT/0/CONTACT/0/ENCASURGENCE": "OUI",
-  "ENFANT/0/CONTACT/0/LIENPARENTE": "",
+  "ENFANT/0/CONTACT/0/LIENPARENTE": "VOI",
   "ENFANT/0/CONTACT/0/NOM": "foo",
   "ENFANT/0/CONTACT/0/PRENOM": "foo",
   "ENFANT/0/CONTACT/0/TELFIXE": "0505050505",
   "ENFANT/0/CONTACT/0/TELPORTABLE": "0606060606",
   "ENFANT/0/CONTACT/1/CHERCHERLENFANT": "OUI",
   "ENFANT/0/CONTACT/1/ENCASURGENCE": "OUI",
-  "ENFANT/0/CONTACT/1/LIENPARENTE": "",
+  "ENFANT/0/CONTACT/1/LIENPARENTE": "VOI",
   "ENFANT/0/CONTACT/1/NOM": "foo",
   "ENFANT/0/CONTACT/1/PRENOM": "foo",
   "ENFANT/0/CONTACT/1/TELFIXE": "0505050505",
   "ENFANT/0/CONTACT/1/TELPORTABLE": "0606060606",
   "ENFANT/0/CONTACT/2/CHERCHERLENFANT": "OUI",
   "ENFANT/0/CONTACT/2/ENCASURGENCE": "OUI",
-  "ENFANT/0/CONTACT/2/LIENPARENTE": "",
+  "ENFANT/0/CONTACT/2/LIENPARENTE": "VOI",
   "ENFANT/0/CONTACT/2/NOM": "foo",
   "ENFANT/0/CONTACT/2/PRENOM": "foo",
   "ENFANT/0/CONTACT/2/TELFIXE": "0505050505",
@@ -152,5 +152,53 @@
   "RL2/PRENOM": "foo",
   "RL2/PROFESSION": "foo",
   "RL2/TELPORTABLE": "0606060606",
-  "TELFIXE": ""
+  "TELFIXE": "",
+  "maj:adresse": "OUI",
+  "maj:enfant_0": "OUI",
+  "maj:enfant_0_assurance": "OUI",
+  "maj:enfant_0_contact": "OUI",
+  "maj:enfant_0_sanitaire": "OUI",
+  "maj:enfant_0_sanitaire_allergie": "OUI",
+  "maj:enfant_0_sanitaire_medecin": "OUI",
+  "maj:enfant_0_sanitaire_vaccin": "OUI",
+  "maj:enfant_1": "OUI",
+  "maj:enfant_1_assurance": "OUI",
+  "maj:enfant_1_contact": "OUI",
+  "maj:enfant_1_sanitaire": "OUI",
+  "maj:enfant_1_sanitaire_allergie": "OUI",
+  "maj:enfant_1_sanitaire_medecin": "OUI",
+  "maj:enfant_1_sanitaire_vaccin": "OUI",
+  "maj:enfant_2": "OUI",
+  "maj:enfant_2_assurance": "OUI",
+  "maj:enfant_2_contact": "OUI",
+  "maj:enfant_2_sanitaire": "OUI",
+  "maj:enfant_2_sanitaire_allergie": "OUI",
+  "maj:enfant_2_sanitaire_medecin": "OUI",
+  "maj:enfant_2_sanitaire_vaccin": "OUI",
+  "maj:enfant_3": "OUI",
+  "maj:enfant_3_assurance": "OUI",
+  "maj:enfant_3_contact": "OUI",
+  "maj:enfant_3_sanitaire": "OUI",
+  "maj:enfant_3_sanitaire_allergie": "OUI",
+  "maj:enfant_3_sanitaire_medecin": "OUI",
+  "maj:enfant_3_sanitaire_vaccin": "OUI",
+  "maj:enfant_4": "OUI",
+  "maj:enfant_4_assurance": "OUI",
+  "maj:enfant_4_contact": "OUI",
+  "maj:enfant_4_sanitaire": "OUI",
+  "maj:enfant_4_sanitaire_allergie": "OUI",
+  "maj:enfant_4_sanitaire_medecin": "OUI",
+  "maj:enfant_4_sanitaire_vaccin": "OUI",
+  "maj:enfant_5": "OUI",
+  "maj:enfant_5_assurance": "OUI",
+  "maj:enfant_5_contact": "OUI",
+  "maj:enfant_5_sanitaire": "OUI",
+  "maj:enfant_5_sanitaire_allergie": "OUI",
+  "maj:enfant_5_sanitaire_medecin": "OUI",
+  "maj:enfant_5_sanitaire_vaccin": "OUI",
+  "maj:revenus": "OUI",
+  "maj:rl1": "OUI",
+  "maj:rl1_adresse_employeur": "OUI",
+  "maj:rl2": "OUI",
+  "maj:rl2_adresse_employeur": "OUI"
 }
diff --git a/tests/data/toulouse_axel/update_family_info.json b/tests/data/toulouse_axel/update_family_info.json
index 8ee1e3bd..596f0b7a 100644
--- a/tests/data/toulouse_axel/update_family_info.json
+++ b/tests/data/toulouse_axel/update_family_info.json
@@ -24,7 +24,7 @@
                 {
                     "CHERCHERLENFANT": "OUI",
                     "ENCASURGENCE": "OUI",
-                    "LIENPARENTE": null,
+                    "LIENPARENTE": "VOI",
                     "NOM": "foo",
                     "PRENOM": "foo",
                     "TELFIXE": "0505050505",
@@ -33,7 +33,7 @@
                 {
                     "CHERCHERLENFANT": "OUI",
                     "ENCASURGENCE": "OUI",
-                    "LIENPARENTE": null,
+                    "LIENPARENTE": "VOI",
                     "NOM": "foo",
                     "PRENOM": "foo",
                     "TELFIXE": "0505050505",
@@ -42,7 +42,7 @@
                 {
                     "CHERCHERLENFANT": "OUI",
                     "ENCASURGENCE": "OUI",
-                    "LIENPARENTE": null,
+                    "LIENPARENTE": "VOI",
                     "NOM": "foo",
                     "PRENOM": "foo",
                     "TELFIXE": "0505050505",
@@ -210,5 +210,53 @@
         "PROFESSION": "foo",
         "TELPORTABLE": "0606060606"
     },
-    "TELFIXE": null
+    "TELFIXE": null,
+    "maj:adresse": "OUI",
+    "maj:enfant_0": "OUI",
+    "maj:enfant_0_assurance": "OUI",
+    "maj:enfant_0_contact": "OUI",
+    "maj:enfant_0_sanitaire": "OUI",
+    "maj:enfant_0_sanitaire_allergie": "OUI",
+    "maj:enfant_0_sanitaire_medecin": "OUI",
+    "maj:enfant_0_sanitaire_vaccin": "OUI",
+    "maj:enfant_1": "OUI",
+    "maj:enfant_1_assurance": "OUI",
+    "maj:enfant_1_contact": "OUI",
+    "maj:enfant_1_sanitaire": "OUI",
+    "maj:enfant_1_sanitaire_allergie": "OUI",
+    "maj:enfant_1_sanitaire_medecin": "OUI",
+    "maj:enfant_1_sanitaire_vaccin": "OUI",
+    "maj:enfant_2": "OUI",
+    "maj:enfant_2_assurance": "OUI",
+    "maj:enfant_2_contact": "OUI",
+    "maj:enfant_2_sanitaire": "OUI",
+    "maj:enfant_2_sanitaire_allergie": "OUI",
+    "maj:enfant_2_sanitaire_medecin": "OUI",
+    "maj:enfant_2_sanitaire_vaccin": "OUI",
+    "maj:enfant_3": "OUI",
+    "maj:enfant_3_assurance": "OUI",
+    "maj:enfant_3_contact": "OUI",
+    "maj:enfant_3_sanitaire": "OUI",
+    "maj:enfant_3_sanitaire_allergie": "OUI",
+    "maj:enfant_3_sanitaire_medecin": "OUI",
+    "maj:enfant_3_sanitaire_vaccin": "OUI",
+    "maj:enfant_4": "OUI",
+    "maj:enfant_4_assurance": "OUI",
+    "maj:enfant_4_contact": "OUI",
+    "maj:enfant_4_sanitaire": "OUI",
+    "maj:enfant_4_sanitaire_allergie": "OUI",
+    "maj:enfant_4_sanitaire_medecin": "OUI",
+    "maj:enfant_4_sanitaire_vaccin": "OUI",
+    "maj:enfant_5": "OUI",
+    "maj:enfant_5_assurance": "OUI",
+    "maj:enfant_5_contact": "OUI",
+    "maj:enfant_5_sanitaire": "OUI",
+    "maj:enfant_5_sanitaire_allergie": "OUI",
+    "maj:enfant_5_sanitaire_medecin": "OUI",
+    "maj:enfant_5_sanitaire_vaccin": "OUI",
+    "maj:revenus": "OUI",
+    "maj:rl1": "OUI",
+    "maj:rl1_adresse_employeur": "OUI",
+    "maj:rl2": "OUI",
+    "maj:rl2_adresse_employeur": "OUI"
 }
diff --git a/tests/test_toulouse_axel.py b/tests/test_toulouse_axel.py
index e3fc3f10..6b267cf3 100644
--- a/tests/test_toulouse_axel.py
+++ b/tests/test_toulouse_axel.py
@@ -15,6 +15,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from contextlib import contextmanager
+import copy
 import datetime
 import json
 import mock
@@ -35,6 +36,7 @@ from passerelle.contrib.toulouse_axel.models import (
     ref_facture_pdf,
     ref_verif_dui,
 )
+from passerelle.utils.jsonresponse import APIError
 import utils
 
 
@@ -620,6 +622,371 @@ def test_update_family_info_flat_endpoint(app, resource, flat_update_params):
     assert 'xml_response' in resp.json['data']
 
 
+def test_sanitize_update_family_data_adresse(app, resource, update_params):
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+
+    # test maj:adresse
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:adresse'] = False
+    for key in partial_update_params['ADRESSE'].keys():
+        partial_update_params['ADRESSE'][key] = None  # reset fields
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    for value in partial_update_params['ADRESSE'].values():
+        assert value is None
+
+
+def test_sanitize_update_family_data_revenus(app, resource, update_params):
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+
+    # test maj:revenus
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:revenus'] = False
+    for key in partial_update_params['REVENUS'].keys():
+        partial_update_params['REVENUS'][key] = None  # reset fields
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert 'REVENUS' not in partial_update_params
+
+
+def test_sanitize_update_family_data_rln(app, resource, update_params):
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+
+    # test maj:rln
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:rl1'] = False
+    for key in partial_update_params['RL1'].keys():
+        partial_update_params['RL1'][key] = None  # reset fields
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert 'RL1' not in partial_update_params
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:rl2'] = False
+    for key in partial_update_params['RL2'].keys():
+        partial_update_params['RL2'][key] = None  # reset fields
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert 'RL2' not in partial_update_params
+
+
+def test_sanitize_update_family_data_rln_adresse_employeur(app, resource, update_params):
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+
+    # test maj:rln_adresse_employeur
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:rl1_adresse_employeur'] = False
+    for key in partial_update_params['RL1']['ADREMPLOYEUR'].keys():
+        partial_update_params['RL1']['ADREMPLOYEUR'][key] = None  # reset fields
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert 'ADREMPLOYEUR' not in partial_update_params['RL1']
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:rl2_adresse_employeur'] = False
+    for key in partial_update_params['RL2']['ADREMPLOYEUR'].keys():
+        partial_update_params['RL2']['ADREMPLOYEUR'][key] = None  # reset fields
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert 'ADREMPLOYEUR' not in partial_update_params['RL2']
+    # combine with maj:rln
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:rl1'] = False
+    partial_update_params['maj:rl1_adresse_employeur'] = False
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert 'RL1' not in partial_update_params
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:rl2'] = False
+    partial_update_params['maj:rl2_adresse_employeur'] = False
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert 'RL2' not in partial_update_params
+
+    # test maj:rln_adresse_employeur not set
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:rl1_adresse_employeur'] = None
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert 'ADREMPLOYEUR' in partial_update_params['RL1']
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:rl2_adresse_employeur'] = None
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert 'ADREMPLOYEUR' in partial_update_params['RL2']
+
+
+def test_sanitize_update_family_data_enfant_n(app, resource, update_params):
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+
+    # test maj:enfant_n
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_0'] = False
+    for key in partial_update_params['ENFANT'][0].keys():
+        if key == 'IDPERSONNE':
+            continue
+        partial_update_params['ENFANT'][0][key] = None  # reset fields
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert len(partial_update_params['ENFANT']) == 1
+    assert partial_update_params['ENFANT'][0]['IDPERSONNE'] == "3535"
+
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_1'] = False
+    for key in partial_update_params['ENFANT'][1].keys():
+        if key == 'IDPERSONNE':
+            continue
+        partial_update_params['ENFANT'][1][key] = None  # reset fields
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert len(partial_update_params['ENFANT']) == 1
+    assert partial_update_params['ENFANT'][0]['IDPERSONNE'] == "4242"
+
+    # do not fill IDPERSONNE for the removed child
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_1'] = False
+    for key in partial_update_params['ENFANT'][1].keys():
+        partial_update_params['ENFANT'][1][key] = None  # reset fields
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert len(partial_update_params['ENFANT']) == 1
+    assert partial_update_params['ENFANT'][0]['IDPERSONNE'] == "4242"
+
+    # remove all children
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_0'] = False
+    partial_update_params['maj:enfant_1'] = False
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert 'ENFANT' not in partial_update_params
+
+    # unknown child
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_5'] = False
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert len(partial_update_params['ENFANT']) == 2
+
+
+def test_sanitize_update_family_data_enfant_n_assurance(app, resource, update_params):
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+
+    # test maj:enfant_n_assurance
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_1_assurance'] = False
+    for key in partial_update_params['ENFANT'][1]['ASSURANCE'].keys():
+        partial_update_params['ENFANT'][1]['ASSURANCE'][key] = None  # reset fields
+    partial_update_params['maj:enfant_0'] = False  # check that ordering is not a problem
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert len(partial_update_params['ENFANT']) == 1
+    assert partial_update_params['ENFANT'][0]['IDPERSONNE'] == "3535"
+    assert 'ASSURANCE' not in partial_update_params['ENFANT'][0]
+    # combine with maj:enfant_n
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_1'] = False
+    partial_update_params['maj:enfant_1_assurance'] = False
+    partial_update_params['maj:enfant_0'] = False
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert 'ENFANT' not in partial_update_params
+
+    # test maj:enfant_n_assurance not set
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_1_assurance'] = None
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert 'ASSURANCE' in partial_update_params['ENFANT'][0]
+
+
+def test_sanitize_update_family_data_enfant_n_contact(app, resource, update_params):
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+
+    # test maj:enfant_n_contact
+    filepath = os.path.join(os.path.dirname(__file__), 'data/toulouse_axel/family_info.xml')
+    with open(filepath) as xml:
+        content = xml.read()
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_0_contact'] = False
+    for i, contact in enumerate(partial_update_params['ENFANT'][0]['CONTACT']):
+        for key in contact.keys():
+            partial_update_params['ENFANT'][0]['CONTACT'][i][key] = None  # reset fields
+    partial_update_params['maj:enfant_1'] = False
+    with mock_getdata(content, 'RefFamilleDui'):
+        resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+        original_contacts = resource.get_family_data(name_id='yyy')['ENFANT'][0]['CONTACT']
+    assert len(partial_update_params['ENFANT']) == 1
+    assert partial_update_params['ENFANT'][0]['IDPERSONNE'] == "4242"
+    # CONTACT was set with origin values found in Axel
+    for i, contact in enumerate(partial_update_params['ENFANT'][0]['CONTACT']):
+        for key, value in contact.items():
+            if key == 'LIENPARENTE':
+                assert original_contacts[i][key] is None
+                assert 'BABY' == value  # XXX this is a hack
+                continue
+            assert original_contacts[i][key] == value
+    # combine with maj:enfant_n
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_1'] = False
+    partial_update_params['maj:enfant_1_contact'] = False
+    partial_update_params['maj:enfant_0'] = False
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert 'ENFANT' not in partial_update_params
+
+    # test maj:enfant_n_contact not set
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_0_contact'] = None
+    with mock.patch('passerelle.contrib.toulouse_axel.models.ref_famille_dui') as operation:
+        resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert operation.call_args_list == []
+
+
+def test_sanitize_update_family_data_enfant_n_sanitaire(app, resource, update_params):
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+
+    # test maj:enfant_n_sanitaire
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_1_sanitaire'] = False
+    for key in partial_update_params['ENFANT'][1]['SANITAIRE'].keys():
+        partial_update_params['ENFANT'][1]['SANITAIRE'][key] = None  # reset fields
+    partial_update_params['maj:enfant_0'] = False
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert len(partial_update_params['ENFANT']) == 1
+    assert partial_update_params['ENFANT'][0]['IDPERSONNE'] == "3535"
+    assert 'SANITAIRE' not in partial_update_params['ENFANT'][0]
+    # combine with maj:enfant_n
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_1'] = False
+    partial_update_params['maj:enfant_1_sanitaire'] = False
+    partial_update_params['maj:enfant_0'] = False
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert 'ENFANT' not in partial_update_params
+
+    # test maj:enfant_n_sanitaire not set
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_0_sanitaire'] = None
+    partial_update_params['maj:enfant_1_sanitaire'] = None
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert 'SANITAIRE' in partial_update_params['ENFANT'][0]
+    assert 'SANITAIRE' in partial_update_params['ENFANT'][1]
+
+
+def test_sanitize_update_family_data_enfant_n_sanitaire_allergie(app, resource, update_params):
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+
+    # test maj:enfant_n_sanitaire_allergie
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_1_sanitaire_allergie'] = False
+    for i, allergie in enumerate(partial_update_params['ENFANT'][0]['SANITAIRE']['ALLERGIE']):
+        for key in allergie.keys():
+            partial_update_params['ENFANT'][0]['SANITAIRE']['ALLERGIE'][i][key] = None  # reset fields
+    partial_update_params['maj:enfant_0'] = False
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert len(partial_update_params['ENFANT']) == 1
+    assert partial_update_params['ENFANT'][0]['IDPERSONNE'] == "3535"
+    assert 'ALLERGIE' not in partial_update_params['ENFANT'][0]['SANITAIRE']
+    # combine with maj:enfant_n_sanitaire
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_1_sanitaire'] = False
+    partial_update_params['maj:enfant_1_sanitaire_allergie'] = False
+    partial_update_params['maj:enfant_0'] = False
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert len(partial_update_params['ENFANT']) == 1
+    assert partial_update_params['ENFANT'][0]['IDPERSONNE'] == "3535"
+    assert 'SANITAIRE' not in partial_update_params['ENFANT'][0]
+    # combine with maj:enfant_n
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_1'] = False
+    partial_update_params['maj:enfant_1_sanitaire_allergie'] = False
+    partial_update_params['maj:enfant_0'] = False
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert 'ENFANT' not in partial_update_params
+
+    # test maj:enfant_n_sanitaire_allergie not set
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_0_sanitaire_allergie'] = None
+    partial_update_params['maj:enfant_1_sanitaire_allergie'] = None
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert 'ALLERGIE' in partial_update_params['ENFANT'][0]['SANITAIRE']
+    assert 'ALLERGIE' in partial_update_params['ENFANT'][1]['SANITAIRE']
+
+
+def test_sanitize_update_family_data_enfant_n_sanitaire_medecin(app, resource, update_params):
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+
+    # test maj:enfant_n_sanitaire_medecin
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_1_sanitaire_medecin'] = False
+    for key in partial_update_params['ENFANT'][1]['SANITAIRE']['MEDECIN'].keys():
+        partial_update_params['ENFANT'][1]['SANITAIRE']['MEDECIN'][key] = None  # reset fields
+    partial_update_params['maj:enfant_0'] = False
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert len(partial_update_params['ENFANT']) == 1
+    assert partial_update_params['ENFANT'][0]['IDPERSONNE'] == "3535"
+    assert 'MEDECIN' not in partial_update_params['ENFANT'][0]['SANITAIRE']
+    # combine with maj:enfant_n_sanitaire
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_1_sanitaire'] = False
+    partial_update_params['maj:enfant_1_sanitaire_medecin'] = False
+    partial_update_params['maj:enfant_0'] = False
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert len(partial_update_params['ENFANT']) == 1
+    assert partial_update_params['ENFANT'][0]['IDPERSONNE'] == "3535"
+    assert 'SANITAIRE' not in partial_update_params['ENFANT'][0]
+    # combine with maj:enfant_n
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_1'] = False
+    partial_update_params['maj:enfant_1_sanitaire_medecin'] = False
+    partial_update_params['maj:enfant_0'] = False
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert 'ENFANT' not in partial_update_params
+
+    # test maj:enfant_n_sanitaire_medecin not set
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_0_sanitaire_medecin'] = None
+    partial_update_params['maj:enfant_1_sanitaire_medecin'] = None
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert 'MEDECIN' in partial_update_params['ENFANT'][0]['SANITAIRE']
+    assert 'MEDECIN' in partial_update_params['ENFANT'][1]['SANITAIRE']
+
+
+def test_sanitize_update_family_data_enfant_n_sanitaire_vaccin(app, resource, update_params):
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+
+    # test maj:enfant_n_sanitaire_vaccin
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_1_sanitaire_vaccin'] = False
+    for key in partial_update_params['ENFANT'][1]['SANITAIRE']['VACCIN'].keys():
+        partial_update_params['ENFANT'][1]['SANITAIRE']['VACCIN'][key] = None  # reset fields
+    partial_update_params['maj:enfant_0'] = False
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert len(partial_update_params['ENFANT']) == 1
+    assert partial_update_params['ENFANT'][0]['IDPERSONNE'] == "3535"
+    assert 'VACCIN' not in partial_update_params['ENFANT'][0]['SANITAIRE']
+    # combine with maj:enfant_n_sanitaire
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_1_sanitaire'] = False
+    partial_update_params['maj:enfant_1_sanitaire_vaccin'] = False
+    partial_update_params['maj:enfant_0'] = False
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert len(partial_update_params['ENFANT']) == 1
+    assert partial_update_params['ENFANT'][0]['IDPERSONNE'] == "3535"
+    assert 'SANITAIRE' not in partial_update_params['ENFANT'][0]
+    # combine with maj:enfant_n
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_1'] = False
+    partial_update_params['maj:enfant_1_sanitaire_vaccin'] = False
+    partial_update_params['maj:enfant_0'] = False
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert 'ENFANT' not in partial_update_params
+
+    # test maj:enfant_n_sanitaire_vaccin not set
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_0_sanitaire_vaccin'] = None
+    partial_update_params['maj:enfant_1_sanitaire_vaccin'] = None
+    resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+    assert 'VACCIN' in partial_update_params['ENFANT'][0]['SANITAIRE']
+    assert 'VACCIN' in partial_update_params['ENFANT'][1]['SANITAIRE']
+
+
+def test_sanitize_update_family_data_axel_error(app, resource, update_params):
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+    partial_update_params = copy.deepcopy(update_params)
+    partial_update_params['maj:enfant_0_contact'] = False
+    with mock.patch('passerelle.contrib.toulouse_axel.models.ref_famille_dui') as operation:
+        operation.side_effect = AxelError('FooBar')
+        with pytest.raises(APIError, match='Axel error: FooBar'):
+            resource.sanitize_update_family_data(name_id='yyy', post_data=partial_update_params)
+
+
+def test_update_family_info_endpoint_sanitize_axel_error(app, resource, update_params):
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+    with mock.patch('passerelle.contrib.toulouse_axel.models.ToulouseAxel.sanitize_update_family_data') as sanitize:
+        sanitize.side_effect = APIError('Axel error: FooBar')
+        resp = app.post_json('/toulouse-axel/test/update_family_info?NameID=yyy', params=update_params)
+        assert resp.json['err_desc'] == "Axel error: FooBar"
+
+
 def test_invoices_endpoint_axel_error(app, resource):
     Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
     with mock.patch('passerelle.contrib.toulouse_axel.models.ref_facture_a_payer') as operation:
-- 
2.20.1

