# Copyright (C) 2022 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import base64
import datetime
from operator import itemgetter
from urllib.parse import urljoin

import zeep
from dateutil import rrule
from django.contrib.postgres.fields import JSONField
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django.utils import dateformat
from django.utils.timezone import now
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 passerelle.utils.templates import render_to_string

from . import activity_schemas, family_schemas, invoice_schemas, schemas, utils


class UpdateError(Exception):
    pass


class ToulouseMaelis(BaseResource, HTTPResource):
    # noqa pylint: disable=too-many-public-methods

    base_wsdl_url = models.CharField(
        max_length=128,
        blank=False,
        verbose_name='URL de base des WSDL',
        default='https://demo-toulouse.sigec.fr/maelisws-toulouse/services/',
    )
    zeep_wsse_username = models.CharField(
        max_length=64, blank=True, default='', verbose_name='Identifiant utilisateur WSSE'
    )
    zeep_wsse_password = models.CharField(
        max_length=64, blank=True, default='', verbose_name='Mot de passe WSSE'
    )

    category = 'Connecteurs métiers'
    _category_ordering = ['Famille', 'Activités']

    class Meta:
        verbose_name = 'Toulouse Maelis'

    def get_client(self, wsdl_short_name):
        wsse = UsernameToken(self.zeep_wsse_username, self.zeep_wsse_password)
        wsdl_name = wsdl_short_name + 'Service?wsdl'
        wsdl_url = urljoin(self.base_wsdl_url, wsdl_name)
        settings = zeep.Settings(strict=False, xsd_ignore_sequence_order=True)
        return self.soap_client(wsdl_url=wsdl_url, wsse=wsse, settings=settings)

    def call(self, wsdl_short_name, service, **kwargs):
        client = self.get_client(wsdl_short_name)
        method = getattr(client.service, service)
        try:
            return method(**kwargs)
        except zeep.exceptions.Fault as e:
            raise APIError(e.message, err_code='%s-%s-%s' % (wsdl_short_name, service, e.code))

    def check_status(self):
        assert self.call('Family', 'isWSRunning')
        assert self.call('Activity', 'isWSRunning')
        assert self.call('Invoice', 'isWSRunning')
        assert self.call('Site', 'isWSRunning')

    def update_referential(self, referential_name, data, id_key, text_key):
        last_update = now()
        for item in data:
            text = item[text_key] or ''
            if isinstance(text, int):
                text = str(text)
            text = text.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),
                    'updated': last_update,
                },
            )
        self.referential.filter(referential_name=referential_name, updated__lt=last_update).delete()

    def get_referential_data(self, service_name, referential_name):
        try:
            response = self.call(service_name, 'read' + referential_name + 'List')
            return serialize_object(response)
        except Exception as e:
            raise UpdateError('Service indisponible : %s' % str(e))

    def update_family_referentials(self):

        # local referentials
        complement_data = [
            {'id': 'B', 'text': 'bis'},
            {'id': 'Q', 'text': 'quater'},
            {'id': 'T', 'text': 'ter'},
        ]
        sex_data = [
            {'id': 'F', 'text': 'Féminin'},
            {'id': 'M', 'text': 'Masculin'},
        ]
        self.update_referential('Complement', complement_data, 'id', 'text')
        self.update_referential('Sex', sex_data, 'id', 'text')

        # remote referentials
        for referential_name in (
            'Category',
            'ChildIndicator',
            'Civility',
            'Country',
            'CSP',
            'DietCode',
            'Document',
            'Organ',
            'PAI',
            'Quality',
            'Quotient',
            'RLIndicator',
            'Situation',
            'Street',
            'Vaccin',
        ):
            id_key, text_key = 'code', 'libelle'
            data = self.get_referential_data('Family', referential_name)
            if referential_name == 'Organ':
                id_key, text_key = 'id', 'code'
            elif referential_name == 'Street':
                id_key, text_key = 'idStreet', 'libelleStreet'

            self.update_referential(referential_name, data, id_key, text_key)

    def update_site_referentials(self):
        for referential_name in ('YearSchool', 'Level', 'DerogReason'):
            id_key, text_key = 'code', 'libelle'
            data = self.get_referential_data('Site', referential_name)
            if referential_name == 'YearSchool':
                id_key, text_key = 'schoolYear', 'schoolYear'
            self.update_referential(referential_name, data, id_key, text_key)

    def daily(self):
        try:
            self.update_family_referentials()
            self.update_site_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, 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('resource', '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

    def get_link(self, NameID):
        try:
            return self.link_set.get(name_id=NameID)
        except Link.DoesNotExist:
            raise APIError('User not linked to family', err_code='not-linked')

    def get_family_raw(self, family_id, **kwargs):
        response = self.call('Family', 'readFamily', dossierNumber=family_id, **kwargs)
        data = serialize_object(response)
        return data

    def get_rl_raw(self, family_id, rl_id, **kwargs):
        data = self.get_family_raw(family_id, **kwargs)
        if data['RL1']['num'] == rl_id:
            return data['RL1']
        elif data['RL2'] and data['RL2']['num'] == rl_id:
            return data['RL2']
        raise APIError("no '%s' RL on '%s' family" % (rl_id, family_id), err_code='not-found')

    def get_person_raw(self, family_id, person_id):
        data = self.get_family_raw(family_id)
        for person in data['emergencyPersonList']:
            if str(person['numPerson']) == person_id:
                return person
        raise APIError(
            "no '%s' emergency person on '%s' family" % (person_id, family_id), err_code='not-found'
        )

    def get_child_raw(self, family_id, child_id):
        data = self.get_family_raw(family_id)
        for child in data['childList']:
            if child['num'] == child_id:
                return child
        raise APIError("no '%s' child on '%s' family" % (child_id, family_id), err_code='not-found')

    def get_child_person_raw(self, family_id, child_id, person_id):
        data = self.get_child_raw(family_id, child_id)
        for person in data['authorizedPersonList']:
            if str(person['personInfo']['num']) == person_id:
                return person
        raise APIError(
            "no '%s' authorized person on '%s' child" % (person_id, child_id), err_code='not-found'
        )

    def add_text_value(self, referential_name, data, keys):
        '''add text from referentials'''
        last_key = keys.pop()
        for key in keys:
            if not isinstance(data, dict) or not key in data:
                return
            data = data[key]
        if isinstance(data, dict) and last_key in data and data[last_key] is not None:
            data[last_key + '_text'] = self.get_referential_value(referential_name, data[last_key])

    def add_indicators_field(self, referential_name, data):
        active_indicators = {x['code']: x for x in data['indicatorList']}
        indicators = self.get_referential(referential_name)
        result = {}
        for item in indicators:
            active_indicator = active_indicators.get(item['id']) or {}
            item['isActive'] = bool(active_indicator)
            if item['typeDesc'] == 'NOTE':
                item['note'] = active_indicator.get('note')
            del item['choiceList']  # no list based indicator on parsifal project
            result[item['id']] = item
        data['indicators'] = result

    def add_text_value_to_rl_indicator(self, data):
        self.add_text_value('RLIndicator', data, ['code'])

    def add_text_value_to_child_indicator(self, data):
        self.add_text_value('ChildIndicator', data, ['code'])

    def add_text_value_to_child_person(self, data):
        self.add_text_value('Civility', data, ['personInfo', 'civility'])
        self.add_text_value('Quality', data, ['personQuality', 'code'])
        self.add_text_value('Sex', data, ['personInfo', 'sexe'])

    def add_text_value_to_child(self, data):
        self.add_text_value('Sex', data, ['sexe'])
        self.add_text_value('DietCode', data, ['dietcode'])
        self.add_text_value('PAI', data, ['paiInfoBean', 'code'])
        for person in data['authorizedPersonList']:
            self.add_text_value_to_child_person(person)
        for indicator in data['indicatorList']:
            self.add_text_value_to_child_indicator(indicator)
        self.add_indicators_field('ChildIndicator', data)

    def add_text_value_to_person(self, data):
        self.add_text_value('Civility', data, ['civility'])
        self.add_text_value('Quality', data, ['quality'])
        self.add_text_value('Sex', data, ['sexe'])

    def add_text_value_to_rl(self, data):
        self.add_text_value('Civility', data, ['civility'])
        self.add_text_value('Quality', data, ['quality'])
        self.add_text_value('Complement', data, ['adresse', 'numComp'])
        self.add_text_value('Street', data, ['adresse', 'idStreet'])
        self.add_text_value('CSP', data, ['profession', 'codeCSP'])
        self.add_text_value('Organ', data, ['CAFInfo', 'organ'])
        for indicator in data['indicatorList']:
            self.add_text_value_to_rl_indicator(indicator)
        for quotient in data['quotientList']:
            self.add_text_value('Quotient', quotient, ['cdquo'])
        self.add_indicators_field('RLIndicator', data)

    def add_text_value_to_family(self, data):
        self.add_text_value('Category', data, ['category'])
        self.add_text_value('Situation', data, ['situation'])
        for rlg in 'RL1', 'RL2':
            if data.get(rlg):
                self.add_text_value_to_rl(data[rlg])
        for child in data['childList']:
            self.add_text_value_to_child(child)
            for person in data['emergencyPersonList']:
                self.add_text_value_to_person(person)

    def get_child_person(self, family_id, child_id, person_id):
        data = self.get_child_person_raw(family_id, child_id, person_id)
        self.add_text_value_to_child_person(data)
        return data

    def get_child(self, family_id, child_id):
        data = self.get_child_raw(family_id, child_id)
        self.add_text_value_to_child(data)
        return data

    def get_person(self, family_id, person_id):
        data = self.get_person_raw(family_id, person_id)
        self.add_text_value_to_person(data)
        return data

    def get_rl(self, family_id, rl_id, **kwargs):
        data = self.get_rl_raw(family_id, rl_id, **kwargs)
        self.add_text_value_to_rl(data)
        return data

    def get_family(self, family_id, **kwargs):
        data = self.get_family_raw(family_id, **kwargs)
        self.add_text_value_to_family(data)
        return data

    def assert_key_in_referential(self, referential_name, key_value, keys_text, required=True):
        if not key_value:
            if required:
                raise APIError("%s is required and could not be None" % keys_text, err_code='field-required')
            return
        try:
            self.referential.get(referential_name=referential_name, item_id=key_value)
        except Referential.DoesNotExist:
            ref_text = "required " if required else ""
            ref_text = ref_text + "referential"
            raise APIError(
                "%s key value '%s' do not belong to '%s' %s"
                % (keys_text, key_value, referential_name, ref_text),
                err_code='wrong-key',
            )

    def assert_post_data_in_referential(self, referential_name, data, keys, required=True):
        key_value = None
        for key in keys:
            if not (isinstance(data, list) and isinstance(key, int)) and not (
                isinstance(data, dict) and key in data
            ):
                break
            data = data[key]
        else:
            key_value = data
        self.assert_key_in_referential(referential_name, key_value, '/'.join(str(x) for x in keys), required)

    def encode_bool(self, obj):
        if obj is True or str(obj).lower() in ['true', 'oui', '1']:
            return True
        if obj is False or str(obj).lower() in ['false', 'non', '0']:
            return False

    def assert_update_indicator_payload_in_referential(self, referential, post_data, parent_keys=None):
        keys = parent_keys or []
        data = post_data
        for key in keys:
            data = data[key]

        for i, item in enumerate(data.get('indicatorList', [])):
            self.assert_post_data_in_referential(
                referential, post_data, keys + ['indicatorList', i, 'code'], required=True
            )
            item['isActive'] = self.encode_bool(item['isActive'])

    def assert_child_medical_record_payload_in_referential(self, post_data, parent_keys=None):
        keys = parent_keys or []
        data = post_data
        for key in keys:
            data = data[key]
        if 'isAuthHospital' in data:
            data['isAuthHospital'] = self.encode_bool(data['isAuthHospital'])

        for i in range(0, len(data.get('vaccinList', []))):
            self.assert_post_data_in_referential(
                'Vaccin', post_data, keys + ['vaccinList', i, 'code'], required=False
            )

    def assert_child_pai_payoad_in_referential(self, post_data, parent_keys=None):
        keys = parent_keys or []
        self.assert_post_data_in_referential('PAI', post_data, keys + ['code'])

    def assert_child_person_payload_in_referential(self, post_data, parent_keys=None):
        keys = parent_keys or []
        self.assert_post_data_in_referential(
            'Civility', post_data, keys + ['personInfo', 'civility'], required=False
        )
        self.assert_post_data_in_referential('Sex', post_data, keys + ['personInfo', 'sexe'], required=False)
        self.assert_post_data_in_referential('Quality', post_data, keys + ['personQuality', 'code'])

    def assert_child_payload_in_referential(self, post_data, parent_keys=None):
        keys = parent_keys or []
        self.assert_post_data_in_referential('Sex', post_data, keys + ['sexe'])

        data = post_data
        for key in keys:
            data = data[key]
        for key in ('bPhoto', 'bLeaveAlone'):
            if key in data:
                data[key] = self.encode_bool(data[key])

        if 'dietcode' in data:
            self.assert_post_data_in_referential('DietCode', post_data, keys + ['dietcode'], required=False)
        if 'paiInfoBean' in data:
            self.assert_child_pai_payoad_in_referential(post_data, keys + ['paiInfoBean'])
        if 'medicalRecord' in data:
            # dead code as updateFamily seems not to modify medicalRecord
            self.assert_child_medical_record_payload_in_referential(post_data, keys + ['medicalRecord'])
        self.assert_update_indicator_payload_in_referential('ChildIndicator', post_data, keys)

    def assert_person_payload_in_referential(self, post_data, parent_keys=None):
        keys = parent_keys or []
        self.assert_post_data_in_referential('Civility', post_data, keys + ['civility'], required=False)
        self.assert_post_data_in_referential('Sex', post_data, keys + ['sexe'], required=False)
        self.assert_post_data_in_referential('Quality', post_data, keys + ['quality'])

    def assert_update_coordinate_payload_in_referential(self, post_data, parent_keys=None):
        keys = parent_keys or []
        self.assert_post_data_in_referential(
            'Street', post_data, keys + ['adresse', 'idStreet'], required=False
        )
        self.assert_post_data_in_referential(
            'Complement', post_data, keys + ['adresse', 'numComp'], required=False
        )
        self.assert_post_data_in_referential(
            'CSP', post_data, keys + ['profession', 'codeCSP'], required=False
        )
        self.assert_post_data_in_referential('Organ', post_data, keys + ['CAFInfo', 'organ'], required=False)

        data = post_data
        for key in keys:
            data = data[key]
        if 'contact' in data:
            data = data['contact']
            for key in ('isContactMail', 'isContactSms', 'isInvoicePdf'):
                if key in data:
                    data[key] = self.encode_bool(data[key])

    def assert_rl_payload_in_referential(self, post_data, parent_keys=None):
        keys = parent_keys or []
        self.assert_post_data_in_referential('Civility', post_data, keys + ['civility'])
        self.assert_post_data_in_referential('Quality', post_data, keys + ['quality'])
        self.assert_update_coordinate_payload_in_referential(post_data, keys)
        self.assert_update_indicator_payload_in_referential('RLIndicator', post_data, keys)

    def assert_create_rl1_payload_in_referential(self, post_data):
        self.assert_post_data_in_referential('Category', post_data, ['category'])
        self.assert_post_data_in_referential('Situation', post_data, ['situation'])
        self.assert_rl_payload_in_referential(post_data, ['rl1'])

    def assert_family_payload_in_referential(self, post_data):
        self.assert_post_data_in_referential('Category', post_data, ['category'])
        self.assert_post_data_in_referential('Situation', post_data, ['situation'])
        for rlg in 'rl1', 'rl2':
            if rlg in post_data:
                self.assert_rl_payload_in_referential(post_data, [rlg])
        for i, person in enumerate(post_data.get('emergencyPersonList') or []):
            for j in range(0, len(person.get('personList') or [])):
                self.assert_person_payload_in_referential(
                    post_data, ['emergencyPersonList', i, 'personList', j]
                )
        for i in range(0, len(post_data.get('childList') or [])):
            self.assert_child_payload_in_referential(post_data, ['childList', i])
        if 'flagCom' in post_data:
            post_data['flagCom'] = self.encode_bool(post_data['flagCom'])

    def replace_null_values(self, dico):
        '''send null fields as empty SOAP tag to tell maelis to empty the value'''
        for key, value in dico.items():
            if isinstance(value, dict):
                self.replace_null_values(value)
            if value is None:
                dico[key] = ''

    def get_person_activity_list_raw(
        self, family_id, person_id, nature_id=None, reference_year=None, start_date=None, end_date=None
    ):
        params = {
            'numDossier': family_id,
            'numPerson': person_id,
            'codeNatureActivity': nature_id,
            'yearSchool': reference_year,
            'dateStartActivity': start_date,
            'dateEndActivity': end_date,
        }
        response = self.call(
            'Activity', 'getPersonCatalogueActivity', getPersonCatalogueActivityRequestBean=params
        )
        data = serialize_object(response)
        return data

    @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, 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, 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, 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, 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, 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, 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, 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 pièces jointes',
        name='read-document-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_document_list(self, request, id=None, q=None, limit=None, distinct=True):
        return {'data': self.get_referential('Document', 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, 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, 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, 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, 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, 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, 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, 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, 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, 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}}},
    )
    def link(self, request, NameID, post_data):
        family_id = post_data['family_id']
        response = self.call('Family', 'readFamily', dossierNumber=family_id)
        if not (
            response['RL1']['firstname'] == post_data['firstname'].upper()
            and response['RL1']['lastname'] == post_data['lastname'].upper()
            and response['RL1']['birth']['dateBirth'].strftime('%Y-%m-%d') == post_data['dateBirth']
        ):
            raise APIError("RL1 does not match '%s' family" % family_id, err_code='not-found')
        Link.objects.update_or_create(resource=self, name_id=NameID, defaults={'family_id': family_id})
        return {'data': 'ok'}

    @endpoint(
        display_category='Famille',
        description='Supprimer une liaison entre un compte usager et une famille',
        methods=['post'],
        perm='can_access',
        parameters={
            'NameID': {'description': 'Publik NameID'},
        },
    )
    def unlink(self, request, NameID):
        link = self.get_link(NameID)
        link.delete()
        return {'data': 'ok'}

    @endpoint(
        display_category='Famille',
        description='Informations sur la famille',
        perm='can_access',
        name='read-family',
        parameters={
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
            'income_year': {'description': 'Année de revenu pour filtrer les quotients'},
        },
    )
    def read_family(self, request, NameID=None, family_id=None, income_year=None):
        family_id = family_id or self.get_link(NameID).family_id
        data = self.get_family(family_id, incomeYear=income_year)
        data['family_id'] = family_id
        return {'data': data}

    @endpoint(
        display_category='Famille',
        description="Liste des responsables légaux",
        perm='can_access',
        name='read-rl-list',
        parameters={
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
            'text_template': {
                'description': 'template utilisée pour la valeur text',
                'example_value': '{{ lastname }} {{ firstname }}',
            },
            'income_year': {'description': 'Année de revenu pour filtrer les quotients'},
        },
    )
    def read_rl_list(self, request, NameID=None, family_id=None, text_template=None, income_year=None):
        family_id = family_id or self.get_link(NameID).family_id
        result = self.get_family_raw(family_id, incomeYear=income_year)
        if not text_template:
            text_template = '{{ lastname }} {{ firstname }}'

        data = []
        for rlg in 'RL1', 'RL2':
            item = result.get(rlg)
            self.add_text_value_to_rl(item)
            if not item:
                break
            item['id'] = item['num']
            item['text'] = render_to_string(text_template, item).strip()
            item['family_id'] = family_id
            data.append(item)
        return {'data': data}

    @endpoint(
        display_category='Famille',
        description="Liste des personnes à prévenir en cas d'urgence",
        perm='can_access',
        name='read-person-list',
        parameters={
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
            'text_template': {
                'description': 'template utilisée pour la valeur text',
                'example_value': '{{ lastname }} {{ firstname }}',
            },
        },
    )
    def read_person_list(self, request, NameID=None, family_id=None, text_template=None):
        family_id = family_id or self.get_link(NameID).family_id
        result = self.get_family_raw(family_id)
        if not text_template:
            text_template = '{{ lastname }} {{ firstname }}'

        data = []
        for item in result['emergencyPersonList']:
            self.add_text_value_to_person(item)
            item['id'] = item['numPerson']
            item['text'] = render_to_string(text_template, item).strip()
            item['family_id'] = family_id
            data.append(item)
        return {'data': data}

    @endpoint(
        display_category='Famille',
        description="Liste des enfants",
        perm='can_access',
        name='read-child-list',
        parameters={
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
            'text_template': {
                'description': 'template utilisée pour la valeur text',
                'example_value': '{{ lastname }} {{ firstname }}',
            },
        },
    )
    def read_child_list(self, request, NameID=None, family_id=None, text_template=None):
        family_id = family_id or self.get_link(NameID).family_id
        result = self.get_family_raw(family_id)
        if not text_template:
            text_template = '{{ lastname }} {{ firstname }}'

        data = []
        for item in result['childList']:
            self.add_text_value_to_child(item)
            item['id'] = item['num']
            item['text'] = render_to_string(text_template, item).strip()
            item['family_id'] = family_id
            data.append(item)
        return {'data': data}

    @endpoint(
        display_category='Famille',
        description="Liste des personnes autorisées à récupérer l'enfant",
        perm='can_access',
        name='read-child-person-list',
        parameters={
            'child_id': {'description': "Numéro de l'enfant"},
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
            'text_template': {
                'description': 'template utilisée pour la valeur text',
                'example_value': '{{ personInfo.lastname }} {{ personInfo.firstname }}',
            },
        },
    )
    def read_child_person_list(self, request, child_id, NameID=None, family_id=None, text_template=None):
        family_id = family_id or self.get_link(NameID).family_id
        result = self.get_child_raw(family_id, child_id)
        if not text_template:
            text_template = '{{ personInfo.lastname }} {{ personInfo.firstname }}'

        data = []
        for item in result['authorizedPersonList']:
            self.add_text_value_to_child_person(item)
            item['id'] = item['personInfo']['num']
            item['text'] = render_to_string(text_template, item).strip()
            item['family_id'] = family_id
            data.append(item)
        return {'data': data}

    @endpoint(
        display_category='Famille',
        description="Informations sur un responsable légal",
        perm='can_access',
        name='read-rl',
        parameters={
            'rl_id': {'description': 'Numéro du représentant légal'},
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
            'income_year': {'description': 'Année de revenu pour filtrer les quotients'},
        },
    )
    def read_rl(
        self,
        request,
        rl_id,
        NameID=None,
        family_id=None,
        income_year=None,
    ):
        family_id = family_id or self.get_link(NameID).family_id
        data = self.get_rl(family_id, rl_id, incomeYear=income_year)
        data['family_id'] = family_id
        return {'data': data}

    @endpoint(
        display_category='Famille',
        description="Informations sur une personne autorisée à récupérer les enfants",
        perm='can_access',
        name='read-person',
        parameters={
            'person_id': {'description': 'Numéro de la personne'},
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
    )
    def read_person(self, request, person_id, NameID=None, family_id=None):
        family_id = family_id or self.get_link(NameID).family_id
        data = self.get_person(family_id, person_id)
        data['family_id'] = family_id
        return {'data': data}

    @endpoint(
        display_category='Famille',
        description="Informations sur un enfant",
        perm='can_access',
        name='read-child',
        parameters={
            'child_id': {'description': "Numéro de l'enfant"},
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
    )
    def read_child(self, request, child_id, NameID=None, family_id=None):
        family_id = family_id or self.get_link(NameID).family_id
        data = self.get_child(family_id, child_id)
        data['family_id'] = family_id
        return {'data': data}

    @endpoint(
        display_category='Famille',
        description="Informations sur une personne autorisée à récupérer l'enfant",
        perm='can_access',
        name='read-child-person',
        parameters={
            'child_id': {'description': "Numéro de l'enfant"},
            'person_id': {'description': 'Numéro de la personne'},
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
    )
    def read_child_person(self, request, child_id, person_id, NameID=None, family_id=None):
        family_id = family_id or self.get_link(NameID).family_id
        data = self.get_child_person(family_id, child_id, person_id)
        data['family_id'] = family_id
        return {'data': data}

    @endpoint(
        display_category='Famille',
        description="Vérifier qu'un responsable légal existe en base",
        perm='can_access',
        name='is-rl-exists',
        post={'request_body': {'schema': {'application/json': family_schemas.ISEXISTS_SCHEMA}}},
    )
    def is_rl_exists(self, request, post_data):
        response = self.call('Family', 'isRLExists', **post_data)
        return {'data': response}

    @endpoint(
        display_category='Famille',
        description="Vérifier qu'un responsable légal existe en base",
        perm='can_access',
        name='is-child-exists',
        post={'request_body': {'schema': {'application/json': family_schemas.ISEXISTS_SCHEMA}}},
    )
    def is_child_exists(self, request, post_data):
        response = self.call('Family', 'isChildExists', **post_data)
        return {'data': response}

    @endpoint(
        display_category='Famille',
        description='Création de la famille',
        name='create-family',
        perm='can_access',
        parameters={'NameID': {'description': 'Publik NameID'}},
        post={'request_body': {'schema': {'application/json': family_schemas.CREATE_FAMILY_SCHEMA}}},
    )
    def create_family(self, request, post_data, NameID=None):
        if self.link_set.filter(name_id=NameID).exists():
            raise APIError('User already linked to family', err_code='already-linked')
        self.assert_family_payload_in_referential(post_data)

        response = self.call('Family', 'createFamily', **post_data)
        data = serialize_object(response)
        family_id = data.get('number')
        if not family_id:
            errors = data.get('rl1ErrorList') + data.get('childErrorList')
            err_codes = [x.split(':')[0][:4] for x in errors]
            raise APIError(' ; '.join(errors), err_code=', '.join(err_codes))

        if NameID:
            Link.objects.create(resource=self, name_id=NameID, family_id=family_id)
        return {'data': data}

    @endpoint(
        display_category='Famille',
        description='Modification de la famille',
        name='update-family',
        perm='can_access',
        parameters={
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
        post={'request_body': {'schema': {'application/json': family_schemas.UPDATE_FAMILY_SCHEMA}}},
    )
    def update_family(self, request, post_data, NameID=None, family_id=None):
        family_id = family_id or self.get_link(NameID).family_id
        self.assert_family_payload_in_referential(post_data)
        self.replace_null_values(post_data)

        # adapt payload to use same input as create_family
        if len(post_data.get('emergencyPersonList', [])):
            persons = post_data.pop('emergencyPersonList')
            post_data['emergencyPersonList'] = [{'personList': persons}]

        response = self.call('Family', 'updateFamily', dossierNumber=family_id, **post_data)
        data = serialize_object(response)
        family_id = data.get('number')
        errors = data.get('childErrorList')
        if errors:
            err_codes = [x.split(':')[0][:4] for x in errors]
            raise APIError(' ; '.join(errors), err_code=', '.join(err_codes))
        return {'data': data}

    @endpoint(
        display_category='Famille',
        description='Création du RL1',
        name='create-rl1',
        perm='can_access',
        parameters={'NameID': {'description': 'Publik NameID'}},
        post={'request_body': {'schema': {'application/json': family_schemas.CREATE_RL1_SCHEMA}}},
    )
    def create_rl1(self, request, post_data, NameID=None):
        if self.link_set.filter(name_id=NameID).exists():
            raise APIError('User already linked to family', err_code='already-linked')
        self.assert_create_rl1_payload_in_referential(post_data)

        response = self.call('Family', 'createFamily', **post_data)
        data = serialize_object(response)
        family_id = data.get('number')
        if not family_id:
            errors = data.get('rl1ErrorList') or []
            raise APIError(' ; '.join(errors), err_code='already-rl1')

        if NameID:
            Link.objects.create(resource=self, name_id=NameID, family_id=family_id)
        return {'data': {'family_id': family_id}}

    @endpoint(
        display_category='Famille',
        description='Modification du RL1',
        name='update-rl1',
        perm='can_access',
        parameters={
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
        post={'request_body': {'schema': {'application/json': family_schemas.UPDATE_RL1_SCHEMA}}},
    )
    def update_rl1(self, request, post_data, NameID=None, family_id=None):
        family_id = family_id or self.get_link(NameID).family_id
        self.assert_rl_payload_in_referential(post_data)
        self.replace_null_values(post_data)
        family = self.get_family_raw(family_id)

        rl1 = post_data
        rl1['adresse'] = family['RL1']['adresse']
        payload = {
            'dossierNumber': family_id,
            'category': family['category'],
            'situation': family['situation'],
            'flagCom': family['flagCom'],
            'nbChild': family['nbChild'],
            'nbTotalChild': family['nbTotalChild'],
            'nbAES': family['nbAES'],
            'rl1': rl1,
        }
        self.call('Family', 'updateFamily', **payload)
        return {'data': 'ok'}

    @endpoint(
        display_category='Famille',
        description='Création du RL2',
        name='create-rl2',
        perm='can_access',
        parameters={
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
        post={'request_body': {'schema': {'application/json': family_schemas.CREATE_RL2_SCHEMA}}},
    )
    def create_rl2(self, request, post_data, NameID=None, family_id=None):
        family_id = family_id or self.get_link(NameID).family_id
        self.assert_rl_payload_in_referential(post_data)
        family = self.get_family_raw(family_id)
        if family['RL2']:
            raise APIError('RL2 already defined on family', err_code='already-rl2')

        payload = {
            'dossierNumber': family_id,
            'category': family['category'],
            'situation': family['situation'],
            'flagCom': family['flagCom'],
            'nbChild': family['nbChild'],
            'nbTotalChild': family['nbTotalChild'],
            'nbAES': family['nbAES'],
            'rl2': post_data,
        }
        response = self.call('Family', 'updateFamily', **payload)
        return {'data': {'id': response['RL2']['num']}}

    @endpoint(
        display_category='Famille',
        description='Modification du RL2',
        name='update-rl2',
        perm='can_access',
        parameters={
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
        post={'request_body': {'schema': {'application/json': family_schemas.UPDATE_RL2_SCHEMA}}},
    )
    def update_rl2(self, request, post_data, NameID=None, family_id=None):
        family_id = family_id or self.get_link(NameID).family_id
        self.assert_rl_payload_in_referential(post_data)
        self.replace_null_values(post_data)
        family = self.get_family_raw(family_id)
        if not family['RL2']:
            raise APIError('No RL2 to update on family', err_code='no-rl2')

        rl2 = post_data
        rl2['adresse'] = family['RL2']['adresse']
        payload = {
            'dossierNumber': family_id,
            'category': family['category'],
            'situation': family['situation'],
            'flagCom': family['flagCom'],
            'nbChild': family['nbChild'],
            'nbTotalChild': family['nbTotalChild'],
            'nbAES': family['nbAES'],
            'rl2': rl2,
        }
        self.call('Family', 'updateFamily', **payload)
        return {'data': 'ok'}

    @endpoint(
        display_category='Famille',
        description="Ajout d'un enfant",
        name='create-child',
        perm='can_access',
        parameters={
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
            'force': {
                'description': 'boolean to bypass doublon error',
                'type': 'bool',
                'example_value': 'false',
            },
        },
        post={'request_body': {'schema': {'application/json': family_schemas.CREATE_CHILD_SCHEMA}}},
    )
    def create_child(self, request, post_data, NameID=None, family_id=None, force=False):
        family_id = family_id or self.get_link(NameID).family_id
        self.assert_child_payload_in_referential(post_data)

        payload = {
            'numDossier': family_id,
            'isForceCreateChild': force,
            'child': post_data,
        }
        response = self.call('Family', 'createChild', **payload)
        data = serialize_object(response)
        child_id = data.get('number')
        if not child_id:
            errors = data.get('childErrorList') or []
            raise APIError(' ; '.join(errors), err_code='already-child')
        return {'data': {'child_id': child_id}}

    @endpoint(
        display_category='Famille',
        description="Modification d'un enfant",
        name='update-child',
        perm='can_access',
        parameters={
            'child_id': {'description': "Numéro de l'enfant"},
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
        post={'request_body': {'schema': {'application/json': family_schemas.UPDATE_CHILD_SCHEMA}}},
    )
    def update_child(self, request, post_data, child_id, NameID=None, family_id=None):
        family_id = family_id or self.get_link(NameID).family_id
        self.assert_child_payload_in_referential(post_data)
        self.replace_null_values(post_data)
        family = self.get_family_raw(family_id)

        child = post_data
        child['num'] = child_id
        for known_child in family['childList']:
            if str(known_child['num']) == child_id:
                child['authorizedPersonList'] = known_child['authorizedPersonList']
                break
        else:
            raise APIError('No child %s to update on family' % child_id, err_code='no-child')
        payload = {
            'dossierNumber': family_id,
            'category': family['category'],
            'situation': family['situation'],
            'flagCom': family['flagCom'],
            'nbChild': family['nbChild'],
            'nbTotalChild': family['nbTotalChild'],
            'nbAES': family['nbAES'],
            'childList': [child],
        }
        self.call('Family', 'updateFamily', **payload)
        return {'data': 'ok'}

    @endpoint(
        display_category='Famille',
        description="Mise à jour des coordonnées d'un responsable légal",
        name='update-coordinate',
        perm='can_access',
        parameters={
            'rl_id': {'description': 'Numéro du représentant légal'},
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
        post={'request_body': {'schema': {'application/json': family_schemas.UPDATE_COORDINATE_SCHEMA}}},
    )
    def update_coordinate(self, request, post_data, rl_id, NameID=None, family_id=None):
        family_id = family_id or self.get_link(NameID).family_id
        self.assert_update_coordinate_payload_in_referential(post_data)
        self.replace_null_values(post_data)

        self.call('Family', 'updateCoordinate', numDossier=family_id, numPerson=rl_id, **post_data)
        return {'data': 'ok'}

    @endpoint(
        display_category='Famille',
        description="Mise à jour des indicateurs d'un responsable légal",
        name='update-rl-indicator',
        perm='can_access',
        parameters={
            'rl_id': {'description': 'Numéro du représentant légal'},
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
        post={'request_body': {'schema': {'application/json': family_schemas.UPDATE_INDICATOR_SCHEMA}}},
    )
    def update_rl_indicator(self, request, post_data, rl_id, NameID=None, family_id=None):
        assert family_id or self.get_link(NameID)
        self.assert_update_indicator_payload_in_referential('RLIndicator', post_data)

        self.call('Family', 'updatePersonIndicatorList', numPerson=rl_id, **post_data)
        return {'data': 'ok'}

    @endpoint(
        display_category='Famille',
        description="Créer ou mettre à jour un quotient d'un responsable légal",
        name='update-quotient',
        perm='can_access',
        parameters={
            'rl_id': {'description': "Numéro du responsable légal"},
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
        post={'request_body': {'schema': {'application/json': family_schemas.UPDATE_QUOTIENT_SCHEMA}}},
    )
    def update_quotient(self, request, post_data, rl_id, NameID=None, family_id=None):
        family_id = family_id or self.get_link(NameID).family_id
        self.assert_post_data_in_referential('Quotient', post_data, ['cdquo'])

        payload = {
            'dossierNumber': family_id,
            'personNumber': rl_id,
            'quotient': post_data,
        }
        self.call('Family', 'createUpdateQuotient', **payload)
        return {'data': 'ok'}

    @endpoint(
        display_category='Famille',
        description="Création d'une personne à prévenir en cas d'urgence",
        name='create-person',
        perm='can_access',
        parameters={
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
        post={'request_body': {'schema': {'application/json': family_schemas.EMERGENCY_PERSON_SCHEMA}}},
    )
    def create_person(self, request, post_data, NameID=None, family_id=None):
        family_id = family_id or self.get_link(NameID).family_id
        self.assert_person_payload_in_referential(post_data)
        family = self.get_family_raw(family_id)

        personList = family['emergencyPersonList']
        personList.append(post_data)
        payload = {
            'dossierNumber': family_id,
            'category': family['category'],
            'situation': family['situation'],
            'flagCom': family['flagCom'],
            'nbChild': family['nbChild'],
            'nbTotalChild': family['nbTotalChild'],
            'nbAES': family['nbAES'],
            'emergencyPersonList': [{'personList': personList}],
        }
        self.call('Family', 'updateFamily', **payload)
        return {'data': 'ok'}

    @endpoint(
        display_category='Famille',
        description="Mise à jour d'une personne à prévenir en cas d'urgence",
        name='update-person',
        perm='can_access',
        parameters={
            'person_id': {'description': 'Numéro de la personne'},
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
        post={'request_body': {'schema': {'application/json': family_schemas.EMERGENCY_PERSON_SCHEMA}}},
    )
    def update_person(self, request, post_data, person_id, NameID=None, family_id=None):
        family_id = family_id or self.get_link(NameID).family_id
        self.assert_person_payload_in_referential(post_data)
        family = self.get_family_raw(family_id)

        personList = family['emergencyPersonList']
        for i, person in enumerate(personList):
            if str(person['numPerson']) == person_id:
                personList[i] = post_data
                personList[i]['numPerson'] = person_id
                break
        else:
            raise APIError(
                "no '%s' authorized person on '%s' family" % (person_id, family_id), err_code='not-found'
            )
        payload = {
            'dossierNumber': family_id,
            'category': family['category'],
            'situation': family['situation'],
            'flagCom': family['flagCom'],
            'nbChild': family['nbChild'],
            'nbTotalChild': family['nbTotalChild'],
            'nbAES': family['nbAES'],
            'emergencyPersonList': [{'personList': personList}],
        }
        self.call('Family', 'updateFamily', **payload)
        return {'data': 'ok'}

    @endpoint(
        display_category='Famille',
        description="Suppression d'une personne à prévenir en cas d'urgence",
        name='delete-person',
        perm='can_access',
        parameters={
            'person_id': {'description': 'Numéro de la personne'},
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
        methods=['post'],
    )
    def delete_person(self, request, person_id, NameID=None, family_id=None):
        family_id = family_id or self.get_link(NameID).family_id
        family = self.get_family_raw(family_id)

        personList = family['emergencyPersonList']
        for i, person in enumerate(personList):
            if str(person['numPerson']) == person_id:
                del personList[i]
                break
        else:
            raise APIError(
                "no '%s' authorized person on '%s' family" % (person_id, family_id), err_code='not-found'
            )
        payload = {
            'dossierNumber': family_id,
            'category': family['category'],
            'situation': family['situation'],
            'flagCom': family['flagCom'],
            'nbChild': family['nbChild'],
            'nbTotalChild': family['nbTotalChild'],
            'nbAES': family['nbAES'],
            'emergencyPersonList': [{'personList': personList}],
        }
        self.call('Family', 'updateFamily', **payload)
        return {'data': 'ok'}

    @endpoint(
        display_category='Famille',
        description="Création d'une personne autorisée à récupérer l'enfant",
        name='create-child-person',
        perm='can_access',
        parameters={
            'child_id': {'description': "Numéro de l'enfant"},
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
        post={'request_body': {'schema': {'application/json': family_schemas.AUTHORIZED_PERSON_SCHEMA}}},
    )
    def create_child_person(self, request, post_data, child_id, NameID=None, family_id=None):
        family_id = family_id or self.get_link(NameID).family_id
        self.assert_child_person_payload_in_referential(post_data)
        child = self.get_child_raw(family_id, child_id)

        personList = child['authorizedPersonList']
        personList.append(post_data)
        req = {
            'numFamily': family_id,
            'numPerson': child_id,
            'bLeaveAlone': child['bLeaveAlone'],
            'bPhoto': child['bPhoto'],
            'personList': personList,
        }
        self.call('Family', 'updateChildAutorization', updateChildAutorizationRequest=req)
        return {'data': 'ok'}

    @endpoint(
        display_category='Famille',
        description="Mise à jour d'une personne autorisée à récupérer l'enfant",
        name='update-child-person',
        perm='can_access',
        parameters={
            'child_id': {'description': "Numéro de l'enfant"},
            'person_id': {'description': 'Numéro de la personne'},
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
        post={'request_body': {'schema': {'application/json': family_schemas.AUTHORIZED_PERSON_SCHEMA}}},
    )
    def update_child_person(self, request, post_data, child_id, person_id, NameID=None, family_id=None):
        family_id = family_id or self.get_link(NameID).family_id
        self.assert_child_person_payload_in_referential(post_data)
        child = self.get_child_raw(family_id, child_id)

        personList = child['authorizedPersonList']
        for i, person in enumerate(personList):
            if str(person['personInfo']['num']) == person_id:
                personList[i] = post_data
                personList[i]['personInfo']['num'] = person_id
                break
        else:
            raise APIError(
                "No '%s' authorized person on '%s' child" % (person_id, child_id), err_code='not-found'
            )
        req = {
            'numFamily': family_id,
            'numPerson': child_id,
            'bLeaveAlone': child['bLeaveAlone'],
            'bPhoto': child['bPhoto'],
            'personList': personList,
        }
        self.call('Family', 'updateChildAutorization', updateChildAutorizationRequest=req)
        return {'data': 'ok'}

    @endpoint(
        display_category='Famille',
        description="Suppression d'une personne autorisée à récupérer l'enfant",
        name='delete-child-person',
        perm='can_access',
        parameters={
            'child_id': {'description': "Numéro de l'enfant"},
            'person_id': {'description': 'Numéro de la personne'},
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
        methods=['post'],
    )
    def delete_child_person(self, request, child_id, person_id, NameID=None, family_id=None):
        family_id = family_id or self.get_link(NameID).family_id
        child = self.get_child_raw(family_id, child_id)

        personList = child['authorizedPersonList']
        for i, person in enumerate(personList):
            if str(person['personInfo']['num']) == person_id:
                del personList[i]
                break
        else:
            raise APIError(
                "No '%s' authorized person on '%s' child" % (person_id, child_id), err_code='not-found'
            )
        req = {
            'numFamily': family_id,
            'numPerson': child_id,
            'bLeaveAlone': child['bLeaveAlone'],
            'bPhoto': child['bPhoto'],
            'personList': personList,
        }
        self.call('Family', 'updateChildAutorization', updateChildAutorizationRequest=req)
        return {'data': 'ok'}

    @endpoint(
        display_category='Famille',
        description="Créer ou mettre à jour le régime alimentaire d'un enfant",
        name='update-child-dietcode',
        perm='can_access',
        parameters={
            'child_id': {'description': "Numéro de l'enfant"},
            'dietcode': {'description': 'code du régime alimentaire'},
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
        methods=['post'],
    )
    def update_child_dietcode(self, request, child_id, dietcode, NameID=None, family_id=None):
        assert family_id or self.get_link(NameID)
        self.assert_key_in_referential('DietCode', dietcode, 'dietcode parameter', required=False)

        self.call('Family', 'createOrUpdateChildDiet', personNumber=child_id, code=dietcode)
        return {'data': 'ok'}

    @endpoint(
        display_category='Famille',
        description="Créer ou mettre à jour les informations relatives au PAI d'un enfant",
        name='update-child-pai',
        perm='can_access',
        parameters={
            'child_id': {'description': "Numéro de l'enfant"},
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
        post={'request_body': {'schema': {'application/json': family_schemas.PAIINFO_SCHEMA}}},
    )
    def update_child_pai(self, request, post_data, child_id, NameID=None, family_id=None):
        assert family_id or self.get_link(NameID)
        self.assert_child_pai_payoad_in_referential(post_data)

        # use None to empty date passed as an empty string by date filter
        for key in ('dateDeb', 'dateFin'):
            if post_data[key] == '':
                post_data[key] = None

        self.call('Family', 'updateChildPAI', personNumber=child_id, **post_data)
        return {'data': 'ok'}

    @endpoint(
        display_category='Famille',
        description="Créer ou mettre à jour les données médicales d'un enfant",
        name='update-child-medical-record',
        perm='can_access',
        parameters={
            'child_id': {'description': "Numéro de l'enfant"},
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
        post={'request_body': {'schema': {'application/json': family_schemas.MEDICALRECORD_SCHEMA}}},
    )
    def update_child_medical_record(self, request, post_data, child_id, NameID=None, family_id=None):
        assert family_id or self.get_link(NameID)
        self.assert_child_medical_record_payload_in_referential(post_data)
        self.replace_null_values(post_data)

        payload = {
            'numPerson': child_id,
            'medicalRecord': post_data,
        }
        self.call('Family', 'updateChildMedicalRecord', updateChildMedicalRecordRequest=payload)
        return {'data': 'ok'}

    @endpoint(
        display_category='Famille',
        description="Mise à jour des indicateurs d'un enfant",
        name='update-child-indicator',
        perm='can_access',
        parameters={
            'child_id': {'description': "Numéro de l'enfant"},
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
        post={'request_body': {'schema': {'application/json': family_schemas.UPDATE_INDICATOR_SCHEMA}}},
    )
    def update_child_indicator(self, request, post_data, child_id, NameID=None, family_id=None):
        assert family_id or self.get_link(NameID)
        self.assert_update_indicator_payload_in_referential('ChildIndicator', post_data)

        self.call('Family', 'updatePersonIndicatorList', numPerson=child_id, **post_data)
        return {'data': 'ok'}

    @endpoint(
        display_category='Famille',
        description='Ajoute un document pour une famille, un responsable légal ou un enfant',
        name='add-supplied-document',
        perm='can_access',
        parameters={
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
        post={'request_body': {'schema': {'application/json': family_schemas.SUPPLIED_DOCUMENTS_SCHEMA}}},
    )
    def add_supplied_document(self, request, post_data, NameID=None, family_id=None):
        family_id = family_id or self.get_link(NameID).family_id
        for i in range(0, len(post_data.get('documentList', []))):
            self.assert_post_data_in_referential('Document', post_data, ['documentList', i, 'code'])

        for item in post_data['documentList']:
            file = item.pop('file')
            item['filename'] = file['filename']
            item['fileSupplied'] = {
                'dataHandler': base64.b64decode(file['content']),
                'fileType': file['content_type'],
                'name': file['filename'],
            }
        payload = {'addSuppliedDocumentRequestBean': post_data}
        payload['addSuppliedDocumentRequestBean']['numDossier'] = family_id
        response = self.call('Family', 'addSuppliedDocument', **payload)
        data = serialize_object(response)
        if data != 'OK':
            raise APIError('maelis fails to add the supplied document')
        return {'data': 'ok'}

    def get_start_and_end_dates(self, start_date, end_date):
        try:
            start_date = datetime.datetime.strptime(start_date, utils.json_date_format).date()
            end_date = datetime.datetime.strptime(end_date, utils.json_date_format).date()
        except ValueError:
            raise APIError('bad date format, should be YYYY-MM-DD', err_code='bad-request', http_status=400)

        if start_date > end_date:
            raise APIError(
                'start_date should be before end_date',
                err_code='bad-request',
                http_status=400,
            )
        reference_year = utils.get_reference_year_from_date(start_date)
        end_reference_year = utils.get_reference_year_from_date(end_date)
        if reference_year != end_reference_year:
            raise APIError(
                'start_date and end_date are in different reference year (%s != %s)'
                % (reference_year, end_reference_year),
                err_code='bad-request',
                http_status=400,
            )
        return start_date, end_date, reference_year

    def get_bookings(self, family_id, child_id, start_date, end_date):
        bookings = []
        for booking_date in rrule.rrule(rrule.MONTHLY, dtstart=start_date.replace(day=1), until=end_date):
            payload = {
                'requestBean': {
                    'numDossier': family_id,
                    'numPerson': child_id,
                    'year': booking_date.year,
                    'month': booking_date.month,
                }
            }
            response = self.call('Activity', 'getPersonScheduleList', **payload)
            result = serialize_object(response)
            for result_data in result or []:
                for schedule in result_data['activityScheduleList']:
                    activity = schedule['activity']
                    if not activity['activityType']['natureSpec']:
                        continue
                    if activity['activityType']['natureSpec']['code'] not in ['A', 'R']:
                        continue
                    activity_id = activity['idAct']
                    many_units = len(schedule['unitScheduleList']) > 1
                    for unit in schedule['unitScheduleList']:
                        days = unit['dayInfoList']
                        for day in days:
                            if day['status'] in ['NO_READ', 'NO_CUSTODY']:
                                continue
                            booking = {
                                'id': '%s:%s:%s'
                                % (child_id, activity_id, day['day'].strftime(utils.json_date_format)),
                                'text': dateformat.format(day['day'], 'l j F Y'),
                                'prefill': day['scheduledPresence'] > 0 or day['realPresence'] > 1,
                                'disabled': day['status'] != 'WRITABLE',
                                'details': day,
                            }
                            color = 'white'
                            if booking['prefill']:
                                color = 'green'
                            booking['details']['status_color'] = color
                            booking['details']['activity_id'] = activity_id
                            booking['details']['activity_type'] = activity['activityType']['code']
                            booking['details']['activity_label'] = activity['activityType']['libelle']
                            if many_units:
                                booking['details']['activity_label'] += ' (%s)' % unit['unit']['libelle']
                            booking['details']['child_id'] = child_id
                            booking['details']['day_str'] = day['day'].strftime(utils.json_date_format)
                            booking['details']['unit_id'] = unit['unit']['idUnit']
                            bookings.append(booking)

        # sort bookings
        activity_types = ['ACCMAT', 'RESTSCOL', 'ACCPERI', 'ACCSOIR']
        bookings = [
            (
                b['details']['day'],
                activity_types.index(b['details']['activity_type'])
                if b['details']['activity_type'] in activity_types
                else 0,
                b['details']['activity_label'],
                b,
            )
            for b in bookings
        ]
        bookings = sorted(bookings, key=itemgetter(0, 1, 2))
        bookings = [b for d, a, l, b in bookings]
        return bookings

    @endpoint(
        display_category='Réservation',
        description="Agenda d'un enfant",
        name='read-child-agenda',
        perm='can_access',
        parameters={
            'child_id': {'description': "Numéro de l'enfant"},
            'start_date': {'description': 'Début de la période'},
            'end_date': {'description': 'Fin de la période'},
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
    )
    def read_child_agenda(self, request, child_id, start_date, end_date, NameID=None, family_id=None):
        family_id = family_id or self.get_link(NameID).family_id
        start_date, end_date, reference_year = self.get_start_and_end_dates(start_date, end_date)
        bookings = self.get_bookings(family_id, child_id, start_date, end_date)
        return {
            'data': bookings,
            'extra_data': {
                'start_date': start_date,
                'end_date': end_date,
                'school_year': '%s/%s' % (reference_year, reference_year + 1),
            },
        }

    @endpoint(
        display_category='Réservation',
        description="Modifier l'agenda d'un enfant",
        name='update-child-agenda',
        perm='can_access',
        parameters={
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
        post={
            'request_body': {
                'schema': {
                    'application/json': activity_schemas.BOOKING_SCHEMA,
                }
            }
        },
    )
    def update_child_agenda(self, request, post_data, NameID=None, family_id=None):
        family_id = family_id or self.get_link(NameID).family_id
        child_id = post_data['child_id']
        start_date, end_date, dummy = self.get_start_and_end_dates(
            post_data['start_date'], post_data['end_date']
        )
        requested_bookings = post_data['booking_list']

        # build list of existing booked days
        bookings = self.get_bookings(family_id, child_id, start_date, end_date)
        legacy_bookings = [b['id'] for b in bookings if b['prefill'] is True]
        available_bookings = [b['id'] for b in bookings if b['disabled'] is False]

        bookings_to_update = []
        updated = []
        for booking_info in bookings:
            day_id = booking_info['id']
            booked = None
            action = booking_info['details']['action']
            if day_id not in available_bookings:
                # disabled or not available: not bookable
                booked = None
            elif (
                day_id not in legacy_bookings
                and day_id in requested_bookings
                and action in ['ADD_PRES_PREVI', 'ADD_PRES_REAL', 'DEL_ABSENCE']
            ):
                booked = action
            elif (
                day_id in legacy_bookings
                and day_id not in requested_bookings
                and action in ['DEL_PRES_PREVI', 'DEL_PRES_REAL', 'ADD_ABSENCE']
            ):
                booked = action
            if booked is not None:
                # no changes, don't send the day
                bookings_to_update.append(
                    {
                        'numPerson': child_id,
                        'idAct': booking_info['details']['activity_id'],
                        'idUni': booking_info['details']['unit_id'],
                        'date': booking_info['details']['day_str'],
                        'action': booked,
                    }
                )
                updated.append(
                    {
                        'activity_id': booking_info['details']['activity_id'],
                        'activity_type': booking_info['details']['activity_type'],
                        'activity_label': booking_info['details']['activity_label'],
                        'day': booking_info['details']['day_str'],
                        'booked': booked in ['ADD_PRES_PREVI', 'ADD_PRES_REAL', 'DEL_ABSENCE'],
                    }
                )
        if not bookings_to_update:
            # don't call maelis if no changes
            return updated

        payload = {
            'requestBean': {
                'numDossier': family_id,
                'unitPersonDayInfoList': bookings_to_update,
            }
        }
        response = self.call('Activity', 'updatePersonSchedule', **payload)
        errors = serialize_object(response)
        if errors:
            raise APIError(' ; '.join(errors), err_code='agenda-child-error')

        # sort changes
        activity_types = ['ACCMAT', 'RESTSCOL']
        updated = [
            (
                not u['booked'],
                activity_types.index(u['activity_type']) if u['activity_type'] in activity_types else 0,
                u['activity_label'],
                u['day'],
                u,
            )
            for u in updated
        ]
        updated = sorted(updated, key=itemgetter(0, 1, 2, 3))
        updated = [u for b, a, l, d, u in updated]
        updated = [
            {
                'booked': u['booked'],
                'activity_id': u['activity_id'],
                'activity_label': u['activity_label'],
                'day': u['day'],
            }
            for u in updated
        ]

        return {
            'updated': True,
            'count': len(updated),
            'changes': updated,
        }

    @endpoint(
        display_category='Facture',
        description="Ajout d'autorisation de prélèvement",
        name='add-rl1-direct-debit-order',
        perm='can_access',
        parameters={
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
        post={
            'request_body': {'schema': {'application/json': invoice_schemas.ADD_DIRECT_DEBIT_ORDER_SCHEMA}}
        },
    )
    def add_rl1_direct_debit_order(self, request, post_data, NameID=None, family_id=None):
        family_id = family_id or self.get_link(NameID).family_id
        family = self.get_family_raw(family_id)

        post_data['numPerson'] = family['RL1']['num']
        self.call('Invoice', 'addDirectDebitOrder', numDossier=family_id, **post_data)
        return {'data': 'ok'}

    @endpoint(
        display_category='Facture',
        description="Lecture des informations relatives à l'autorisation de prélèvement en cours à la date de référence",
        name='get-rl1-direct-debit-order',
        perm='can_access',
        parameters={
            'codeRegie': {'description': 'Code de la régie'},
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
            'dateRef': {
                'description': 'Date de référence',
                'type': 'date',
            },
        },
    )
    def get_rl1_direct_debit_order(self, request, codeRegie, dateRef, NameID=None, family_id=None):
        family_id = family_id or self.get_link(NameID).family_id
        family = self.get_family_raw(family_id)

        payload = {
            'numDossier': family_id,
            'numPerson': family['RL1']['num'],
            'codeRegie': codeRegie,
            'dateRef': dateRef,
        }
        response = self.call('Invoice', 'getDirectDebitOrder', **payload)
        data = serialize_object(response)
        return {'data': data}

    @endpoint(
        display_category='Inscriptions',
        description="Liste des années scolaires",
        name='read-school-years-list',
        perm='can_access',
    )
    def read_school_years_list(self, request):
        return {'data': self.get_referential('YearSchool')}

    @endpoint(
        display_category='Inscriptions',
        description="Liste des niveaux scolaires",
        name='read-school-levels-list',
        perm='can_access',
        parameters={
            'age': {'description': 'Age de l\'enfant', 'example_value': '6'},
        },
    )
    def read_school_levels_list(self, request, age=None):
        data = self.get_referential('Level')
        if age and age.isnumeric():
            return {'data': [item for item in data if item.get('age') == int(age)]}
        return {'data': data}

    @endpoint(
        display_category='Inscriptions',
        description="Liste des motifs de dérogation",
        name='read-exemption-reasons-list',
        perm='can_access',
    )
    def read_exemption_reasons_list(self, request):
        return {'data': self.get_referential('DerogReason')}

    @endpoint(
        display_category='Inscriptions',
        description="Liste les écoles pour une adresse et niveau scolaire",
        name='read-schools-for-address-and-level',
        perm='can_access',
        parameters={
            'year': {'description': 'Année', 'example_value': '2022'},
            'id_street': {'description': 'Identifiant de la voie', 'example_value': '2317'},
            'num': {'description': 'Numero dans la voie', 'example_value': '4'},
            'comp': {'description': 'Complément d\'adresse (bis, ...)'},
            'level': {'description': 'Niveau scolaire'},
        },
    )
    def read_schools_for_address_and_level(self, request, id_street, year, num, comp=None, level=None):
        data = {'schoolYear': year, 'adresse': {'idStreet': id_street, 'num': num}}
        if level:
            data['levelCode'] = level
        if comp:
            data['adresse']['numComp'] = comp
        response = self.call(
            'Site', 'readSchoolForAdressAndLevel', readSchoolForAdressAndLevelRequestBean=data
        )
        data = []
        for item in serialize_object(response):
            item['id'] = item['idSchool']
            item['text'] = item['schoolName']
            data.append(item)
        return {'data': serialize_object(data)}

    @endpoint(
        display_category='Inscriptions',
        description="Liste les écoles pour un enfant et niveau scolaire",
        name='read-schools-for-child-and-level',
        perm='can_access',
        parameters={
            'year': {'description': 'Année', 'example_value': '2023'},
            'child_id': {'description': 'Identifiant de l\'enfant', 'example_value': '190115'},
            'level': {'description': 'Niveau scolaire'},
        },
    )
    def read_schools_for_child_and_level(self, request, child_id, year, level=None):
        data = {
            'numPerson': child_id,
            'schoolYear': year,
        }
        if level:
            data['levelCode'] = level

        response = self.call('Family', 'readSchoolForChildAndLevel', **data)
        data = []
        for item in serialize_object(response):
            item['id'] = item['idSchool']
            item['text'] = item['schoolName']
            data.append(item)
        return {'data': serialize_object(data)}

    @endpoint(
        display_category='Inscriptions',
        description="Remontée des informations scolaires d'un enfant",
        name='read-child-school-informations',
        perm='can_access',
        parameters={
            'child_id': {'description': 'Identifiant de l\'enfant', 'example_value': '190115'},
            'level': {'description': 'Niveau scolaire', 'example_value': 'CP'},
            'year': {'description': 'Année scolaire', 'example_value': '2023'},
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
        },
    )
    def read_child_school_informations(self, request, child_id, level, year, NameID=None, family_id=None):
        family_id = family_id or self.get_link(NameID).family_id
        data = {'numDossier': family_id, 'numPerson': child_id, 'schoolYear': year, 'level': level}

        response = self.call(
            'Family', 'getChildSubscribeSchoolInformation', getFamilySubscribeSchoolInfoRequestBean=data
        )
        return {'data': serialize_object(response)}

    @endpoint(
        display_category='Inscriptions',
        description="Création d'une pré-inscription scolaire pour un enfant",
        name='create-child-school-pre-registration',
        perm='can_access',
        post={
            'request_body': {'schema': {'application/json': family_schemas.SCHOOL_PRE_REGISTRATION_SCHEMA}}
        },
    )
    def create_child_school_pre_registration(self, request, post_data):
        response = self.call('Family', 'preSubscribeSchoolPerim', **post_data)
        return {'data': serialize_object(response)}

    @endpoint(
        display_category='Inscriptions',
        description="Création d'une pré-inscription scolaire avec demande de dérogation",
        name='create-child-school-pre-registration-with-exemption',
        perm='can_access',
        post={
            'request_body': {
                'schema': {'application/json': family_schemas.SCHOOL_PRE_REGISTRATION_WITH_EXEMPTION_SCHEMA}
            }
        },
    )
    def create_child_school_pre_registration_with_exemption(self, request, post_data):
        response = self.call('Family', 'presubscribeSchoolDerog', **post_data)
        return {'data': serialize_object(response)}

    @endpoint(
        display_category='Inscriptions',
        description="Création d'une pré-inscription scolaire avec rapprochement de fratrie",
        name='create-child-school-pre-registration-with-sibling',
        perm='can_access',
        post={
            'request_body': {
                'schema': {'application/json': family_schemas.SCHOOL_PRE_REGISTRATION_WITH_SIBLING_SCHEMA}
            }
        },
    )
    def create_child_school_pre_registration_with_sibling(self, request, post_data):
        response = self.call('Family', 'presubscribeSchoolSibling', **post_data)
        return {'data': serialize_object(response)}

    @endpoint(
        display_category='Inscriptions',
        description="Catalogue des activités d'une personne",
        name='get-person-activity-list',
        perm='can_access',
        parameters={
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
            'person_id': {'description': "Numéro du responsale légal ou de l'enfant"},
            'nature_id': {'description': "Numéro de la nature des activités"},
            'start_date': {'description': 'Début de la période'},
            'end_date': {'description': 'Fin de la période'},
            'text_template': {
                'description': "template utilisé pour la valeur text (URL encoding)",
                'example_value': '{{ activity.libelle2 }}',
            },
        },
    )
    def get_person_activity_list(
        self,
        request,
        person_id,
        NameID=None,
        family_id=None,
        nature_id=None,
        start_date=None,
        end_date=None,
        text_template=None,
    ):
        family_id = family_id or self.get_link(NameID).family_id
        reference_year = None
        if start_date and end_date:
            start_date, end_date, reference_year = self.get_start_and_end_dates(start_date, end_date)
        if not text_template:
            text_template = '{{ activity.libelle2|default:activity.libelle1 }}'

        response = self.get_person_activity_list_raw(
            family_id,
            person_id,
            nature_id=nature_id,
            reference_year=reference_year,
            start_date=start_date and start_date.strftime(utils.json_date_format),
            end_date=start_date and end_date.strftime(utils.json_date_format),
        )
        for item in response['catalogueActivityList']:
            item['id'] = item['activity']['idActivity']
            item['text'] = render_to_string(text_template, item).strip()
        return {'data': response['catalogueActivityList'], 'meta': {'person': response['person']}}

    @endpoint(
        display_category='Inscriptions',
        description="Liste des unités d'une activité pour une personne",
        name='get-person-unit-list',
        perm='can_access',
        parameters={
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
            'person_id': {'description': "Numéro du responsale légal ou de l'enfant"},
            'activity_id': {'description': "Numéro de l'activités"},
            'start_date': {'description': 'Début de la période'},
            'end_date': {'description': 'Fin de la période'},
            'text_template': {
                'description': 'template utilisée pour la valeur text (URL encoding)',
                'example_value': '{{ libelle }}',
            },
        },
    )
    def get_person_unit_list(
        self,
        request,
        person_id,
        activity_id,
        NameID=None,
        family_id=None,
        start_date=None,
        end_date=None,
        text_template=None,
    ):
        family_id = family_id or self.get_link(NameID).family_id
        reference_year = None
        if start_date and end_date:
            start_date, end_date, reference_year = self.get_start_and_end_dates(start_date, end_date)
        if not text_template:
            text_template = '{{ libelle }}'

        response = self.get_person_activity_list_raw(
            family_id,
            person_id,
            reference_year=reference_year,
            start_date=start_date and start_date.strftime(utils.json_date_format),
            end_date=start_date and end_date.strftime(utils.json_date_format),
        )
        for activity in response['catalogueActivityList']:
            if activity['activity']['idActivity'] == activity_id:
                break
        else:
            raise APIError('No activity %s for person' % activity_id, err_code='no-activity')
        data = activity.pop('unitInfoList')
        meta = {'person': response['person'], 'activity': activity}
        for item in data:
            item['id'] = item['idUnit']
            context = dict(item)
            context['meta'] = meta
            item['text'] = render_to_string(text_template, context).strip()
        return {'data': data, 'meta': meta}

    @endpoint(
        display_category='Inscriptions',
        description="Liste des lieux d'une unité pour une personne",
        name='get-person-place-list',
        perm='can_access',
        parameters={
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
            'person_id': {'description': "Numéro du responsale légal ou de l'enfant"},
            'activity_id': {'description': "Numéro de l'activités"},
            'unit_id': {'description': "Numéro de l'unité"},
            'start_date': {'description': 'Début de la période'},
            'end_date': {'description': 'Fin de la période'},
            'text_template': {
                'description': 'template utilisée pour la valeur text (URL encoding)',
                'example_value': '{{ libelle }}',
            },
        },
    )
    def get_person_place_list(
        self,
        request,
        person_id,
        activity_id,
        unit_id,
        NameID=None,
        family_id=None,
        start_date=None,
        end_date=None,
        text_template=None,
    ):
        family_id = family_id or self.get_link(NameID).family_id
        reference_year = None
        if start_date and end_date:
            start_date, end_date, reference_year = self.get_start_and_end_dates(start_date, end_date)
        if not text_template:
            text_template = '{{ place.lib2|default:place.lib1 }}'

        response = self.get_person_activity_list_raw(
            family_id,
            person_id,
            reference_year=reference_year,
            start_date=start_date and start_date.strftime(utils.json_date_format),
            end_date=start_date and end_date.strftime(utils.json_date_format),
        )
        for activity in response['catalogueActivityList']:
            if activity['activity']['idActivity'] == activity_id:
                break
        else:
            raise APIError('No activity %s for person' % activity_id, err_code='no-activity')
        for unit in activity['unitInfoList']:
            if unit['idUnit'] == unit_id:
                break
        else:
            raise APIError('No unit %s for person' % unit_id, err_code='no-unit')
        data = unit.pop('placeInfoList')
        del activity['unitInfoList']
        meta = {'person': response['person'], 'activity': activity, 'unit': unit}
        for item in data:
            item['id'] = item['place']['idPlace']
            context = dict(item)
            context['meta'] = meta
            item['text'] = render_to_string(text_template, context).strip()
        return {'data': data, 'meta': meta}

    @endpoint(
        display_category='Inscriptions',
        description="Catalog geojson pour une personne",
        name='get-person-catalog-geojson',
        perm='can_access',
        parameters={
            'NameID': {'description': 'Publik NameID'},
            'family_id': {'description': 'Numéro de DUI'},
            'person_id': {'description': "Numéro du responsale légal ou de l'enfant"},
            'nature_id': {'description': "Numéro de la nature des activités"},
            'start_date': {'description': 'Début de la période'},
            'end_date': {'description': 'Fin de la période'},
            'activity_id': {'description': "Numéro de l'activités"},
            'unit_id': {'description': "Numéro de l'unité"},
            'place_id': {'description': "Numéro du lieu"},
        },
    )
    def get_person_catalog_geojson(
        self,
        request,
        person_id,
        NameID=None,
        family_id=None,
        start_date=None,
        end_date=None,
        nature_id=None,
        activity_id=None,
        unit_id=None,
        place_id=None,
    ):
        family_id = family_id or self.get_link(NameID).family_id
        reference_year = None
        if start_date and end_date:
            start_date, end_date, reference_year = self.get_start_and_end_dates(start_date, end_date)

        response = self.get_person_activity_list_raw(
            family_id,
            person_id,
            nature_id=nature_id,
            reference_year=reference_year,
            start_date=start_date and start_date.strftime(utils.json_date_format),
            end_date=start_date and end_date.strftime(utils.json_date_format),
        )

        def places(properties, place_list, place_id=None):
            for place in place_list:
                properties['place'] = place
                properties['place_id'] = place['place']['idPlace']
                if not place['place']['longitude'] or not place['place']['latitude']:
                    continue
                if place_id:
                    if properties['place_id'] == place_id:
                        yield properties
                        break
                else:
                    yield properties

        def units(properties, unit_list, unit_id=None, place_id=None):
            for unit in unit_list:
                place_list = unit.pop('placeInfoList')
                properties['unit'] = unit
                properties['unit_id'] = unit['idUnit']
                if unit_id:
                    if properties['unit_id'] == unit_id:
                        yield from places(properties, place_list, place_id)
                        break
                else:
                    yield from places(properties, place_list, place_id)

        def activities(activity_id=None, unit_id=None, place_id=None):
            for activity in response['catalogueActivityList']:
                unit_list = activity.pop('unitInfoList')
                properties = {
                    'person': response['person'],
                    'activity_id': activity['activity']['idActivity'],
                    'activity': activity,
                }
                if activity_id:
                    if properties['activity_id'] == activity_id:
                        yield from units(properties, unit_list, unit_id, place_id)
                        break
                else:
                    yield from units(properties, unit_list, unit_id, place_id)

        geojson = {
            'type': 'FeatureCollection',
            'features': [],
        }
        for item in activities(activity_id, unit_id, place_id):
            geojson['features'].append(
                {
                    'type': 'Feature',
                    'geometry': {
                        'coordinates': [
                            float(item['place']['place']['longitude']),
                            float(item['place']['place']['latitude']),
                        ],
                        'type': 'Point',
                    },
                    'properties': {
                        'id': '%s:%s:%s' % (item['activity_id'], item['unit_id'], item['place_id']),
                        'text': '%s / %s / %s'
                        % (
                            item['activity']['activity']['libelle1'],
                            item['unit']['libelle'],
                            item['place']['place']['lib1'],
                        ),
                        **item,
                    },
                }
            )
        return geojson


class Link(models.Model):
    resource = models.ForeignKey(ToulouseMaelis, on_delete=models.CASCADE)
    name_id = models.CharField(blank=False, max_length=256)
    family_id = models.CharField(blank=False, max_length=128)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    class Meta:
        unique_together = ('resource', 'name_id')


class Referential(models.Model):
    resource = models.ForeignKey(
        verbose_name='Resource',
        to=ToulouseMaelis,
        on_delete=models.CASCADE,
        related_name='referential',
    )
    referential_name = models.TextField('Name')
    item_id = models.TextField('Key')
    item_text = models.TextField('Text')
    item_unaccent_text = models.TextField('Text', null=True)
    item_data = JSONField('Data', encoder=DjangoJSONEncoder)
    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:
        ordering = ('resource', 'referential_name', 'item_text', 'item_id')
        unique_together = [['resource', 'referential_name', 'item_id']]
