From 1483ddd8c160322b48afa23c153a0c1a7780577a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Mon, 10 May 2021 11:50:54 +0200
Subject: [PATCH 1/2] caluire-axel: child_schooling_info enpoint (#53853)

---
 functests/caluire_axel/test_caluire_axel.py   |  14 ++
 passerelle/contrib/caluire_axel/models.py     |  61 +++++++-
 passerelle/contrib/caluire_axel/schemas.py    |   1 +
 passerelle/contrib/caluire_axel/utils.py      |  23 +++
 .../caluire_axel/xsd/Q_GetIndividu.xsd        |  26 ++++
 .../caluire_axel/xsd/R_GetIndividu.xsd        |  63 ++++++++
 tests/data/caluire_axel/schooling_info.xml    |  36 +++++
 tests/test_caluire_axel.py                    | 144 +++++++++++++++++-
 tests/test_caluire_axel_utils.py              |  36 +++++
 9 files changed, 394 insertions(+), 10 deletions(-)
 create mode 100644 passerelle/contrib/caluire_axel/utils.py
 create mode 100644 passerelle/contrib/caluire_axel/xsd/Q_GetIndividu.xsd
 create mode 100644 passerelle/contrib/caluire_axel/xsd/R_GetIndividu.xsd
 create mode 100644 tests/data/caluire_axel/schooling_info.xml
 create mode 100644 tests/test_caluire_axel_utils.py

diff --git a/functests/caluire_axel/test_caluire_axel.py b/functests/caluire_axel/test_caluire_axel.py
index 20c881f8..8c0f91d2 100644
--- a/functests/caluire_axel/test_caluire_axel.py
+++ b/functests/caluire_axel/test_caluire_axel.py
@@ -1,3 +1,4 @@
+import datetime
 import pprint
 
 import requests
@@ -48,6 +49,19 @@ def test_link(conn, user):
         assert res['err'] == 0
         print('\n')
 
+        print("and GET school info")
+        url = conn + '/child_schooling_info?NameID=%s&idpersonne=%s&booking_date=%s' % (
+            name_id,
+            child['IDENT'],
+            datetime.date.today().strftime('%Y-%m-%d'),
+        )
+        resp = requests.get(url)
+        resp.raise_for_status()
+        res = resp.json()
+        pprint.pprint(res)
+        assert res['err'] == 0
+        print('\n')
+
     print("Deleting link")
     url = conn + '/unlink?NameID=%s' % name_id
     resp = requests.post(url)
diff --git a/passerelle/contrib/caluire_axel/models.py b/passerelle/contrib/caluire_axel/models.py
index de3b0b1a..22ede3b4 100644
--- a/passerelle/contrib/caluire_axel/models.py
+++ b/passerelle/contrib/caluire_axel/models.py
@@ -14,6 +14,8 @@
 # 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 datetime
+
 from django.db import models
 from django.utils.translation import ugettext_lazy as _
 
@@ -22,7 +24,7 @@ from passerelle.contrib.utils import axel
 from passerelle.utils.api import endpoint
 from passerelle.utils.jsonresponse import APIError
 
-from . import schemas
+from . import schemas, utils
 
 
 class CaluireAxel(BaseResource):
@@ -32,7 +34,7 @@ class CaluireAxel(BaseResource):
     )
 
     category = _('Business Process Connectors')
-    _category_ordering = [_('Family account')]
+    _category_ordering = [_('Family account'), _('Schooling')]
 
     class Meta:
         verbose_name = _('Caluire Axel')
@@ -150,6 +152,13 @@ class CaluireAxel(BaseResource):
 
         return family_data
 
+    def get_child_data(self, family_id, child_id):
+        family_data = self.get_family_data(family_id)
+        for child in family_data.get('MEMBRE', []):
+            if child['IDENT'] == child_id:
+                return child
+        return None
+
     @endpoint(
         display_category=_('Family account'),
         display_order=3,
@@ -188,15 +197,51 @@ class CaluireAxel(BaseResource):
             'idpersonne': {'description': _('Child ID')},
         },
     )
-    def child_info(self, request, idpersonne, NameID):
+    def child_info(self, request, NameID, idpersonne):
         link = self.get_link(NameID)
-        family_data = self.get_family_data(link.family_id)
+        child_data = self.get_child_data(link.family_id, idpersonne)
+        if child_data is None:
+            raise APIError('Child not found', err_code='not-found')
+        return {'data': child_data}
 
-        for child in family_data.get('MEMBRE', []):
-            if child['IDENT'] == idpersonne:
-                return {'data': child}
+    @endpoint(
+        display_category=_('Schooling'),
+        display_order=1,
+        description=_("Get information about schooling of a child"),
+        perm='can_access',
+        parameters={
+            'NameID': {'description': _('Publik ID')},
+            'idpersonne': {'description': _('Child ID')},
+            'booking_date': {'description': _('Booking date (to get reference year)')},
+        },
+    )
+    def child_schooling_info(self, request, NameID, idpersonne, booking_date):
+        link = self.get_link(NameID)
+        try:
+            booking_date = datetime.datetime.strptime(booking_date, axel.json_date_format)
+        except ValueError:
+            raise APIError('bad date format, should be YYYY-MM-DD', err_code='bad-request', http_status=400)
+
+        child_data = self.get_child_data(link.family_id, idpersonne)
+        if child_data is None:
+            raise APIError('Child not found', err_code='not-found')
+
+        reference_year = utils.get_reference_year_from_date(booking_date)
+        try:
+            result = schemas.get_individu(
+                self,
+                {'PORTAIL': {'GETINDIVIDU': {'IDENTINDIVIDU': idpersonne, 'ANNEE': str(reference_year)}}},
+            )
+        except axel.AxelError as e:
+            raise APIError(
+                'Axel error: %s' % e,
+                err_code='error',
+                data={'xml_request': e.xml_request, 'xml_response': e.xml_response},
+            )
+
+        schooling_data = result.json_response['DATA']['PORTAIL']['GETINDIVIDU']
 
-        raise APIError('Child not found', err_code='not-found')
+        return {'data': schooling_data}
 
 
 class Link(models.Model):
diff --git a/passerelle/contrib/caluire_axel/schemas.py b/passerelle/contrib/caluire_axel/schemas.py
index f1d579d8..eba5b858 100644
--- a/passerelle/contrib/caluire_axel/schemas.py
+++ b/passerelle/contrib/caluire_axel/schemas.py
@@ -74,6 +74,7 @@ class Operation(axel.Operation):
 
 find_individus = Operation('FindIndividus')
 get_famille_individus = Operation('GetFamilleIndividus')
+get_individu = Operation('GetIndividu')
 
 
 LINK_SCHEMA = copy.deepcopy(
diff --git a/passerelle/contrib/caluire_axel/utils.py b/passerelle/contrib/caluire_axel/utils.py
new file mode 100644
index 00000000..49a3377d
--- /dev/null
+++ b/passerelle/contrib/caluire_axel/utils.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+# passerelle - uniform access to multiple data sources and services
+# Copyright (C) 2021  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/>.
+
+
+def get_reference_year_from_date(booking_date):
+    if booking_date.month <= 8:
+        # between january and august, reference year is the year just before
+        return booking_date.year - 1
+    return booking_date.year
diff --git a/passerelle/contrib/caluire_axel/xsd/Q_GetIndividu.xsd b/passerelle/contrib/caluire_axel/xsd/Q_GetIndividu.xsd
new file mode 100644
index 00000000..e4248a49
--- /dev/null
+++ b/passerelle/contrib/caluire_axel/xsd/Q_GetIndividu.xsd
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:all="urn:AllAxelTypes">
+	
+	<xsd:import schemaLocation="./AllAxelTypes.xsd" namespace="urn:AllAxelTypes"  />
+	
+	<xsd:complexType name="PORTAILType">
+		<xsd:sequence>
+			<xsd:element ref="GETINDIVIDU" minOccurs="0" maxOccurs="1"/>
+		</xsd:sequence>  
+	</xsd:complexType>
+	
+	<xsd:complexType name="GETINDIVIDUType">
+		<xsd:sequence>
+			<xsd:element ref="IDENTINDIVIDU" />
+			<xsd:element ref="ANNEE" />
+		</xsd:sequence> 
+	</xsd:complexType>
+	
+	<xsd:element name="IDENTINDIVIDU" type="all:IDENTREQUIREDType"/>
+	<xsd:element name="ANNEE" type="all:ANNEEType"/>
+	
+	<xsd:element name="GETINDIVIDU" type="GETINDIVIDUType"/>
+	
+	<xsd:element name="PORTAIL" type="PORTAILType"/>	
+		
+</xsd:schema>
\ No newline at end of file
diff --git a/passerelle/contrib/caluire_axel/xsd/R_GetIndividu.xsd b/passerelle/contrib/caluire_axel/xsd/R_GetIndividu.xsd
new file mode 100644
index 00000000..8768ae75
--- /dev/null
+++ b/passerelle/contrib/caluire_axel/xsd/R_GetIndividu.xsd
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<xsd:schema	xmlns:all="urn:AllAxelTypes" xmlns:ind="urn:Individu"  xmlns:xsd="http://www.w3.org/2001/XMLSchema" >
+	
+	<xsd:import schemaLocation="./Individu.xsd" namespace="urn:Individu" />
+	<xsd:import schemaLocation="./AllAxelTypes.xsd" namespace="urn:AllAxelTypes" />
+	
+	<xsd:redefine schemaLocation="./R_ShemaResultat.xsd">
+	    <xsd:simpleType name="TYPEType">
+			<xsd:restriction base="TYPEType">
+				<xsd:enumeration value="GetIndividu" />
+			</xsd:restriction>
+	    </xsd:simpleType>
+		
+		<xsd:complexType name="PORTAILType">
+			<xsd:complexContent>
+				<xsd:extension base="PORTAILType">
+					<xsd:sequence>
+							<xsd:element ref="GETINDIVIDU" minOccurs="0" maxOccurs="1"/>
+					</xsd:sequence>
+				</xsd:extension>
+			 </xsd:complexContent>
+		</xsd:complexType>
+	</xsd:redefine>	
+	
+	<xsd:complexType name="SCOLAIREType">
+		<xsd:sequence>
+			<xsd:element ref="IDENTECOLE" />
+			<xsd:element ref="LIBELLEECOLE"/>
+			<xsd:element ref="IDENTNIVEAU"/>
+			<xsd:element ref="LIBELLENIVEAU"/>
+		</xsd:sequence> 
+	</xsd:complexType>	
+	
+	<xsd:simpleType name="PLACEType">
+		<xsd:restriction base="xsd:string">
+			<xsd:enumeration value="" />
+			<xsd:enumeration value="1" />
+			<xsd:enumeration value="2" />
+			<xsd:enumeration value="3" />
+		</xsd:restriction>
+	</xsd:simpleType>
+	
+	<xsd:complexType name="GETINDIVIDUType">
+		<xsd:sequence>
+			<xsd:element ref="CODE" />
+			<xsd:element ref="SCOLAIRE" minOccurs="0" maxOccurs="1" />
+			<xsd:element ref="INDIVIDU" minOccurs="0" maxOccurs="1" />
+		</xsd:sequence> 
+	</xsd:complexType>
+	
+	<xsd:element name="CODE" type="xsd:integer"/>
+	<xsd:element name="INDIVIDU" type="ind:INDIVIDUType"/>
+	<xsd:element name="IDENTFAMILLE" type="all:IDENTType"/>
+	<xsd:element name="SCOLAIRE" type="SCOLAIREType"/>
+	<xsd:element name="PLACE" type="PLACEType"/>
+	<xsd:element name="IDENTECOLE" type="all:IDType"/>
+	<xsd:element name="IDENTNIVEAU" type="all:IDType"/>
+	<xsd:element name="LIBELLEECOLE" type="all:LIBELLEType"/>
+	<xsd:element name="LIBELLENIVEAU" type="all:LIBELLEType"/>
+	
+	<xsd:element name="GETINDIVIDU" type="GETINDIVIDUType"/>
+		
+</xsd:schema>
diff --git a/tests/data/caluire_axel/schooling_info.xml b/tests/data/caluire_axel/schooling_info.xml
new file mode 100644
index 00000000..b46d872a
--- /dev/null
+++ b/tests/data/caluire_axel/schooling_info.xml
@@ -0,0 +1,36 @@
+<PORTAIL>
+  <GETINDIVIDU>
+    <CODE>0</CODE>
+    <SCOLAIRE>
+      <IDENTECOLE>ECOLE1</IDENTECOLE>
+      <LIBELLEECOLE>Ecole El&#233;mentaire</LIBELLEECOLE>
+      <IDENTNIVEAU>CE2</IDENTNIVEAU>
+      <LIBELLENIVEAU>Cours &#233;l&#233;mentaire 2&#232;me ann&#233;e</LIBELLENIVEAU>
+    </SCOLAIRE>
+    <INDIVIDU>
+      <IDENT>50632</IDENT>
+      <CIVILITE />
+      <NOM>CALUIRE TEST</NOM>
+      <PRENOM>Enfant 1 </PRENOM>
+      <NAISSANCE>10/10/2013</NAISSANCE>
+      <SEXE>M</SEXE>
+      <NOMJF />
+      <TELFIXE />
+      <TELPORTABLE />
+      <MAIL />
+      <PAI>N</PAI>
+      <GARDEALTERNEE>O</GARDEALTERNEE>
+      <ADRESSE>
+        <ADRESSE3 />
+        <ADRESSE4 />
+        <NORUE>30</NORUE>
+        <ADRESSE1>RUE PASTEUR</ADRESSE1>
+        <ADRESSE2 />
+        <CODEPOSTAL>69300</CODEPOSTAL>
+        <VILLE>CALUIRE ET CUIRE</VILLE>
+        <PAYS />
+        <NPAI />
+      </ADRESSE>
+    </INDIVIDU>
+  </GETINDIVIDU>
+</PORTAIL>
diff --git a/tests/test_caluire_axel.py b/tests/test_caluire_axel.py
index 637558e3..49d80aa7 100644
--- a/tests/test_caluire_axel.py
+++ b/tests/test_caluire_axel.py
@@ -16,6 +16,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import os
+import xml.etree.ElementTree as ET
 from contextlib import contextmanager
 
 import mock
@@ -45,6 +46,33 @@ def link_params():
     }
 
 
+@pytest.fixture
+def family_data():
+    filepath = os.path.join(os.path.dirname(__file__), 'data/caluire_axel/family_info.xml')
+    with open(filepath) as xml:
+        content = xml.read()
+    resp = (
+        '''
+    <?xml version="1.0"?>
+    <PORTAILSERVICE>
+      <RESULTAT>
+        <TYPE>GetFamilleIndividus</TYPE>
+        <STATUS>OK</STATUS>
+        <DATE>10/10/2010 10:10:01</DATE>
+        <COMMENTAIRES><![CDATA[]]></COMMENTAIRES>
+      </RESULTAT>
+      <DATA>
+        %s
+      </DATA>
+    </PORTAILSERVICE>
+    '''.strip()
+        % content
+    )
+    return schemas.get_famille_individus.response_converter.decode(ET.fromstring(resp))['DATA']['PORTAIL'][
+        'GETFAMILLE'
+    ]
+
+
 @contextmanager
 def mock_getdata(content, operation):
     with mock.patch('passerelle.contrib.caluire_axel.models.CaluireAxel.soap_client') as client:
@@ -131,6 +159,48 @@ def test_operation_find_individus(resource, content):
             )
 
 
+@pytest.mark.parametrize(
+    'content',
+    [
+        '<PORTAIL><GETFAMILLE/></PORTAIL>',
+    ],
+)
+def test_operation_get_famille_individus(resource, content):
+    with mock_getdata(content, 'GetFamilleIndividus'):
+        with pytest.raises(AxelError):
+            schemas.get_famille_individus(
+                resource,
+                {
+                    'PORTAIL': {
+                        'GETFAMILLE': {
+                            'IDENTFAMILLE': 'XXX',
+                        }
+                    }
+                },
+            )
+
+
+@pytest.mark.parametrize(
+    'content',
+    [
+        '<PORTAIL><GETINDIVIDU/></PORTAIL>',
+    ],
+)
+def test_operation_get_individu(resource, content):
+    with mock_getdata(content, 'GetIndividu'):
+        with pytest.raises(AxelError):
+            schemas.get_individu(
+                resource,
+                {
+                    'PORTAIL': {
+                        'GETINDIVIDU': {
+                            'IDENTINDIVIDU': 'XXX',
+                        }
+                    }
+                },
+            )
+
+
 def test_link_endpoint_nameid_empty(app, resource, link_params):
     resp = app.post_json('/caluire-axel/test/link?NameID=', params=link_params, status=400)
     assert resp.json['err_desc'] == "NameID is empty"
@@ -440,13 +510,13 @@ def test_child_info_endpoint_axel_error(app, resource):
     Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42')
     with mock.patch('passerelle.contrib.caluire_axel.schemas.get_famille_individus') as operation:
         operation.side_effect = AxelError('FooBar')
-        resp = app.get('/caluire-axel/test/child_info?NameID=yyy&idpersonne=zzz')
+        resp = app.get('/caluire-axel/test/child_info?NameID=yyy&idpersonne=50632')
     assert resp.json['err_desc'] == "Axel error: FooBar"
     assert resp.json['err'] == 'error'
 
 
 def test_child_info_endpoint_no_result(app, resource):
-    resp = app.get('/caluire-axel/test/child_info?NameID=yyy&idpersonne=zzz')
+    resp = app.get('/caluire-axel/test/child_info?NameID=yyy&idpersonne=50632')
     assert resp.json['err_desc'] == "Person not found"
     assert resp.json['err'] == 'not-found'
 
@@ -490,3 +560,73 @@ def test_child_info_endpoint(app, resource):
     )
     assert resp.json['data']['id'] == '50632'
     assert resp.json['data']['text'] == 'Enfant 1 CALUIRE TEST'
+
+
+def test_child_schooling_info_endpoint_axel_error(app, resource):
+    Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42')
+    with mock.patch('passerelle.contrib.caluire_axel.schemas.get_famille_individus') as operation:
+        operation.side_effect = AxelError('FooBar')
+        resp = app.get(
+            '/caluire-axel/test/child_schooling_info?NameID=yyy&idpersonne=50632&booking_date=2021-05-10'
+        )
+    assert resp.json['err_desc'] == "Axel error: FooBar"
+    assert resp.json['err'] == 'error'
+
+    filepath = os.path.join(os.path.dirname(__file__), 'data/caluire_axel/family_info.xml')
+    with open(filepath) as xml:
+        content = xml.read()
+    with mock_getdata(content, 'GetFamilleIndividus'):
+        with mock.patch('passerelle.contrib.caluire_axel.schemas.get_individu') as operation:
+            operation.side_effect = AxelError('FooBar')
+            resp = app.get(
+                '/caluire-axel/test/child_schooling_info?NameID=yyy&idpersonne=50632&booking_date=2021-05-10'
+            )
+    assert resp.json['err_desc'] == "Axel error: FooBar"
+    assert resp.json['err'] == 'error'
+
+
+@pytest.mark.parametrize('value', ['foo', '20/01/2020', '2020'])
+def test_child_schooling_info_endpoint_bad_date_format(app, resource, value):
+    Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42')
+    resp = app.get(
+        '/caluire-axel/test/child_schooling_info?NameID=yyy&idpersonne=50632&booking_date=%s' % value,
+        status=400,
+    )
+    assert resp.json['err_desc'] == "bad date format, should be YYYY-MM-DD"
+    assert resp.json['err'] == 'bad-request'
+
+
+def test_child_schooling_info_endpoint_no_result(app, resource):
+    resp = app.get(
+        '/caluire-axel/test/child_schooling_info?NameID=yyy&idpersonne=50632&booking_date=2021-05-10'
+    )
+    assert resp.json['err_desc'] == "Person not found"
+    assert resp.json['err'] == 'not-found'
+
+    Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42')
+    filepath = os.path.join(os.path.dirname(__file__), 'data/caluire_axel/family_info.xml')
+    with open(filepath) as xml:
+        content = xml.read()
+    with mock_getdata(content, 'GetFamilleIndividus'):
+        resp = app.get(
+            '/caluire-axel/test/child_schooling_info?NameID=yyy&idpersonne=zzz&booking_date=2021-05-10'
+        )
+    assert resp.json['err_desc'] == "Child not found"
+    assert resp.json['err'] == 'not-found'
+
+
+def test_child_schooling_info(app, resource, family_data):
+    Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42')
+    filepath = os.path.join(os.path.dirname(__file__), 'data/caluire_axel/schooling_info.xml')
+    with open(filepath) as xml:
+        content = xml.read()
+    with mock_getdata(content, 'GetIndividu'):
+        with mock.patch(
+            'passerelle.contrib.caluire_axel.models.CaluireAxel.get_family_data',
+            return_value=family_data,
+        ):
+            resp = app.get(
+                '/caluire-axel/test/child_schooling_info?NameID=yyy&idpersonne=50632&booking_date=2021-05-10'
+            )
+    assert resp.json['err'] == 0
+    assert set(resp.json['data'].keys()) == set(['CODE', 'INDIVIDU', 'SCOLAIRE'])
diff --git a/tests/test_caluire_axel_utils.py b/tests/test_caluire_axel_utils.py
new file mode 100644
index 00000000..d4dc5207
--- /dev/null
+++ b/tests/test_caluire_axel_utils.py
@@ -0,0 +1,36 @@
+# passerelle - uniform access to multiple data sources and services
+# Copyright (C) 2021 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 datetime
+
+import pytest
+
+from passerelle.contrib.caluire_axel.utils import get_reference_year_from_date
+from passerelle.contrib.utils.axel import json_date_format
+
+
+@pytest.mark.parametrize(
+    'value, expected',
+    [
+        ('2021-01-01', 2020),
+        ('2021-08-31', 2020),
+        ('2021-09-01', 2021),
+        ('2021-12-31', 2021),
+    ],
+)
+def test_get_reference_year_from_date(value, expected):
+    in_date = datetime.datetime.strptime(value, json_date_format)
+    assert get_reference_year_from_date(in_date) == expected
-- 
2.30.2

