From 7b60e533eccb0eb0198311b680b5711de8707f50 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Fri, 24 Jan 2020 15:30:56 +0100
Subject: [PATCH 2/3] toulouse_axel: move some functions in utils (#39126)

---
 passerelle/contrib/toulouse_axel/models.py | 134 +++------------------
 passerelle/contrib/toulouse_axel/utils.py  | 124 +++++++++++++++++++
 tests/test_toulouse_axel_schema.py         |  10 --
 tests/test_toulouse_axel_utils.py          |  28 +++++
 4 files changed, 167 insertions(+), 129 deletions(-)
 create mode 100644 passerelle/contrib/toulouse_axel/utils.py
 create mode 100644 tests/test_toulouse_axel_utils.py

diff --git a/passerelle/contrib/toulouse_axel/models.py b/passerelle/contrib/toulouse_axel/models.py
index 753bfba2..6a086822 100644
--- a/passerelle/contrib/toulouse_axel/models.py
+++ b/passerelle/contrib/toulouse_axel/models.py
@@ -1,5 +1,5 @@
 # passerelle - uniform access to multiple data sources and services
-# Copyright (C) 2019  Entr'ouvert
+# Copyright (C) 2020  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
@@ -36,35 +36,17 @@ from passerelle.base.models import BaseResource
 from passerelle.utils.api import endpoint
 from passerelle.utils.jsonresponse import APIError
 from passerelle.utils.xml import JSONSchemaFromXMLSchema
+from . import utils
 
 logger = logging.getLogger('passerelle.contrib.toulouse_axel')
 
 BASE_XSD_PATH = os.path.join(os.path.dirname(__file__), 'xsd')
 
 
-boolean_type = {
-    'oneOf': [
-        {'type': 'boolean'},
-        {
-            'type': 'string',
-            'pattern': '[Oo][Uu][Ii]|[Nn][Oo][Nn]|[Tt][Rr][Uu][Ee]|[Ff][Aa][Ll][Ss][Ee]|1|0',
-        }
-    ]
-}
-datetime_type = {
-    'type': 'string',
-    'pattern': '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}',
-}
-json_date_format = '%Y-%m-%d'
-json_datetime_format = '%Y-%m-%dT%H:%M:%S'
-xml_date_format = '%d/%m/%Y'
-xml_datetime_format = '%d/%m/%Y %H:%M:%S'
-
-
 PAYMENT_SCHEMA = {
     'type': 'object',
     'properties': {
-        'transaction_date': copy.deepcopy(datetime_type),
+        'transaction_date': copy.deepcopy(utils.datetime_type),
         'transaction_id': {
             'type': 'string',
         }
@@ -73,58 +55,6 @@ PAYMENT_SCHEMA = {
 }
 
 
-def indent(tree, space="  ", level=0):
-    # backport from Lib/xml/etree/ElementTree.py python 3.9
-    if isinstance(tree, ET.ElementTree):
-        tree = tree.getroot()
-    if level < 0:
-        raise ValueError("Initial indentation level must be >= 0, got {level}".format(level))
-    if not len(tree):
-        return
-
-    # Reduce the memory consumption by reusing indentation strings.
-    indentations = ["\n" + level * space]
-
-    def _indent_children(elem, level):
-        # Start a new indentation level for the first child.
-        child_level = level + 1
-        try:
-            child_indentation = indentations[child_level]
-        except IndexError:
-            child_indentation = indentations[level] + space
-            indentations.append(child_indentation)
-
-        if not elem.text or not elem.text.strip():
-            elem.text = child_indentation
-
-        for child in elem:
-            if len(child):
-                _indent_children(child, child_level)
-            if not child.tail or not child.tail.strip():
-                child.tail = child_indentation
-
-        # Dedent after the last child by overwriting the previous indentation.
-        if not child.tail.strip():
-            child.tail = indentations[level]
-
-    _indent_children(tree, 0)
-
-
-def encode_bool(obj):
-    if obj is True or str(obj).lower() in ['true', 'oui', '1']:
-        return 'OUI'
-    if obj is False or str(obj).lower() in ['false', 'non', '0']:
-        return 'NON'
-    return obj
-
-
-def encode_datetime(obj):
-    try:
-        return datetime.datetime.strptime(obj, json_datetime_format).strftime(xml_datetime_format)
-    except ValueError:
-        return obj
-
-
 class AxelSchema(JSONSchemaFromXMLSchema):
     type_map = {
         '{urn:AllAxelTypes}DATEREQUIREDType': 'date',
@@ -142,7 +72,7 @@ class AxelSchema(JSONSchemaFromXMLSchema):
 
     def encode_date(self, obj):
         try:
-            return datetime.datetime.strptime(obj, json_date_format).strftime(xml_date_format)
+            return datetime.datetime.strptime(obj, utils.json_date_format).strftime(utils.xml_date_format)
         except ValueError:
             return obj
 
@@ -152,7 +82,7 @@ class AxelSchema(JSONSchemaFromXMLSchema):
         return self.encode_date(obj)
 
     def decode_date(self, data):
-        value = datetime.datetime.strptime(data.text, xml_date_format).strftime(json_date_format)
+        value = datetime.datetime.strptime(data.text, utils.xml_date_format).strftime(utils.json_date_format)
         return xmlschema.ElementData(tag=data.tag, text=value, content=data.content, attributes=data.attributes)
 
     def decode_date_optional(self, data):
@@ -162,10 +92,10 @@ class AxelSchema(JSONSchemaFromXMLSchema):
 
     @classmethod
     def schema_bool(cls):
-        return copy.deepcopy(boolean_type)
+        return copy.deepcopy(utils.boolean_type)
 
     def encode_bool(self, obj):
-        return encode_bool(obj)
+        return utils.encode_bool(obj)
 
     def decode_bool(self, data):
         value = False
@@ -236,7 +166,7 @@ class Operation(object):
                 serialized_request = self.request_converter.encode(request_data)
             except xmlschema.XMLSchemaValidationError as e:
                 raise AxelError('invalid request %s' % str(e))
-            indent(serialized_request)
+            utils.indent(serialized_request)
             serialized_request = ET.tostring(serialized_request)
             try:
                 self.request_converter.xml_schema.validate(serialized_request)
@@ -251,7 +181,7 @@ class Operation(object):
             '')  # FIXME: What is the user parameter for ?
 
         xml_result = ET.fromstring(result.encode('utf-8'))
-        indent(xml_result)
+        utils.indent(xml_result)
         pretty_result = ET.tostring(xml_result)
         if xml_result.find('RESULTAT/STATUS').text != 'OK':
             msg = xml_result.find('RESULTAT/COMMENTAIRES').text
@@ -335,7 +265,7 @@ class ToulouseAxel(BaseResource):
         form_maj_famille_dui.request_schema['properties']['PORTAIL']['properties']['DUI'])
 
     for flag in sorted(UPDATE_FAMILY_FLAGS.keys()):
-        flag_type = copy.deepcopy(boolean_type)
+        flag_type = copy.deepcopy(utils.boolean_type)
         if flag not in UPDATE_FAMILY_REQUIRED_FLAGS:
             flag_type['oneOf'].append({'type': 'null'})
             flag_type['oneOf'].append({'type': 'string', 'enum': ['']})
@@ -384,7 +314,7 @@ class ToulouseAxel(BaseResource):
         'required': ['ASTHME', 'MEDICAMENTEUSES', 'ALIMENTAIRES', 'AUTRES'],
     }
     for key in ['ASTHME', 'MEDICAMENTEUSES', 'ALIMENTAIRES']:
-        sanitaire_properties['ALLERGIE']['properties'][key] = copy.deepcopy(boolean_type)
+        sanitaire_properties['ALLERGIE']['properties'][key] = copy.deepcopy(utils.boolean_type)
     sanitaire_properties['ALLERGIE']['properties']['AUTRES'] = {
         'oneOf': [
             {'type': 'null'},
@@ -576,7 +506,7 @@ class ToulouseAxel(BaseResource):
         flags = sorted(self.UPDATE_FAMILY_FLAGS.keys())
         for flag in flags:
             flag_value = post_data.get(flag)
-            flag_value = encode_bool(flag_value)
+            flag_value = utils.encode_bool(flag_value)
 
             # no update for the related block
             if flag_value == 'OUI':
@@ -754,40 +684,6 @@ class ToulouseAxel(BaseResource):
             }
         }
 
-    def normalize_invoice(self, invoice, dui, historical=False, vendor_base=None):
-        vendor = vendor_base or {}
-        vendor.update(invoice)
-        invoice_id = '%s-%s' % (dui, invoice['IDFACTURE'])
-        if historical:
-            invoice_id = 'historical-%s' % invoice_id
-        data = {
-            'id': invoice_id,
-            'display_id': str(invoice['IDFACTURE']),
-            'label': invoice['LIBELLE'],
-            'paid': False,
-            'vendor': {'toulouse-axel': vendor},
-        }
-        if historical:
-            data.update({
-                'amount': 0,
-                'total_amount': invoice['MONTANT'],
-                'created': invoice['EMISSION'],
-                'pay_limit_date': '',
-                'online_payment': False,
-                'has_pdf': invoice['IPDF'] == '1',
-            })
-        else:
-            data.update({
-                'amount': invoice['RESTEAPAYER'],
-                'total_amount': invoice['MONTANTTOTAL'],
-                'created': invoice['DATEEMISSION'],
-                'pay_limit_date': invoice['DATEECHEANCE'],
-                'has_pdf': invoice['EXISTEPDF'] == '1',
-            })
-            pay_limit_date = datetime.datetime.strptime(invoice['DATEECHEANCE'], '%Y-%m-%d').date()
-            data['online_payment'] = data['amount'] > 0 and pay_limit_date >= datetime.date.today()
-        return data
-
     def get_invoices(self, regie_id, dui=None, name_id=None):
         assert name_id or dui
         if name_id:
@@ -807,7 +703,7 @@ class ToulouseAxel(BaseResource):
         for facture in data.get('FACTURES', []):
             if facture['IDREGIE'] != regie_id:
                 continue
-            result.append(self.normalize_invoice(facture, dui))
+            result.append(utils.normalize_invoice(facture, dui))
         return result
 
     def get_historical_invoices(self, name_id):
@@ -828,7 +724,7 @@ class ToulouseAxel(BaseResource):
         for direction in data.get('DIRECTION', []):
             for facture in direction.get('FACTURE', []):
                 result.append(
-                    self.normalize_invoice(
+                    utils.normalize_invoice(
                         facture,
                         link.dui,
                         historical=True,
@@ -968,7 +864,7 @@ class ToulouseAxel(BaseResource):
 
         transaction_amount = invoice['amount']
         transaction_id = data['transaction_id']
-        transaction_date = encode_datetime(data['transaction_date'])
+        transaction_date = utils.encode_datetime(data['transaction_date'])
         post_data = {
             'IDFACTURE': int(invoice_id),
             'IDREGIEENCAISSEMENT': '',
diff --git a/passerelle/contrib/toulouse_axel/utils.py b/passerelle/contrib/toulouse_axel/utils.py
new file mode 100644
index 00000000..c856eff0
--- /dev/null
+++ b/passerelle/contrib/toulouse_axel/utils.py
@@ -0,0 +1,124 @@
+# passerelle - uniform access to multiple data sources and services
+# Copyright (C) 2020  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 xml.etree.ElementTree as ET
+
+
+boolean_type = {
+    'oneOf': [
+        {'type': 'boolean'},
+        {
+            'type': 'string',
+            'pattern': '[Oo][Uu][Ii]|[Nn][Oo][Nn]|[Tt][Rr][Uu][Ee]|[Ff][Aa][Ll][Ss][Ee]|1|0',
+        }
+    ]
+}
+datetime_type = {
+    'type': 'string',
+    'pattern': '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}',
+}
+json_date_format = '%Y-%m-%d'
+json_datetime_format = '%Y-%m-%dT%H:%M:%S'
+xml_date_format = '%d/%m/%Y'
+xml_datetime_format = '%d/%m/%Y %H:%M:%S'
+
+
+def indent(tree, space="  ", level=0):
+    # backport from Lib/xml/etree/ElementTree.py python 3.9
+    if isinstance(tree, ET.ElementTree):
+        tree = tree.getroot()
+    if level < 0:
+        raise ValueError("Initial indentation level must be >= 0, got {level}".format(level))
+    if not len(tree):
+        return
+
+    # Reduce the memory consumption by reusing indentation strings.
+    indentations = ["\n" + level * space]
+
+    def _indent_children(elem, level):
+        # Start a new indentation level for the first child.
+        child_level = level + 1
+        try:
+            child_indentation = indentations[child_level]
+        except IndexError:
+            child_indentation = indentations[level] + space
+            indentations.append(child_indentation)
+
+        if not elem.text or not elem.text.strip():
+            elem.text = child_indentation
+
+        for child in elem:
+            if len(child):
+                _indent_children(child, child_level)
+            if not child.tail or not child.tail.strip():
+                child.tail = child_indentation
+
+        # Dedent after the last child by overwriting the previous indentation.
+        if not child.tail.strip():
+            child.tail = indentations[level]
+
+    _indent_children(tree, 0)
+
+
+def encode_bool(obj):
+    if obj is True or str(obj).lower() in ['true', 'oui', '1']:
+        return 'OUI'
+    if obj is False or str(obj).lower() in ['false', 'non', '0']:
+        return 'NON'
+    return obj
+
+
+def encode_datetime(obj):
+    try:
+        return datetime.datetime.strptime(obj, json_datetime_format).strftime(xml_datetime_format)
+    except ValueError:
+        return obj
+
+
+def normalize_invoice(invoice, dui, historical=False, vendor_base=None):
+    vendor = vendor_base or {}
+    vendor.update(invoice)
+    invoice_id = '%s-%s' % (dui, invoice['IDFACTURE'])
+    if historical:
+        invoice_id = 'historical-%s' % invoice_id
+    data = {
+        'id': invoice_id,
+        'display_id': str(invoice['IDFACTURE']),
+        'label': invoice['LIBELLE'],
+        'paid': False,
+        'vendor': {'toulouse-axel': vendor},
+    }
+    if historical:
+        data.update({
+            'amount': 0,
+            'total_amount': invoice['MONTANT'],
+            'created': invoice['EMISSION'],
+            'pay_limit_date': '',
+            'online_payment': False,
+            'has_pdf': invoice['IPDF'] == '1',
+        })
+    else:
+        data.update({
+            'amount': invoice['RESTEAPAYER'],
+            'total_amount': invoice['MONTANTTOTAL'],
+            'created': invoice['DATEEMISSION'],
+            'pay_limit_date': invoice['DATEECHEANCE'],
+            'has_pdf': invoice['EXISTEPDF'] == '1',
+        })
+        pay_limit_date = datetime.datetime.strptime(invoice['DATEECHEANCE'], '%Y-%m-%d').date()
+        data['online_payment'] = data['amount'] > 0 and pay_limit_date >= datetime.date.today()
+    return data
diff --git a/tests/test_toulouse_axel_schema.py b/tests/test_toulouse_axel_schema.py
index ac514af6..85cb301c 100644
--- a/tests/test_toulouse_axel_schema.py
+++ b/tests/test_toulouse_axel_schema.py
@@ -17,7 +17,6 @@
 import os
 
 from passerelle.contrib.toulouse_axel.models import AxelSchema
-from passerelle.contrib.toulouse_axel.models import encode_datetime
 
 import pytest
 import xmlschema
@@ -61,15 +60,6 @@ def test_date_mapping(date_type):
         assert json_data['DATE'] is None
 
 
-def test_encode_datetime():
-    # wrong format
-    assert encode_datetime('foo') == 'foo'
-    assert encode_datetime('2019-12-12') == '2019-12-12'
-    assert encode_datetime('2019-12-12T12:01:72') == '2019-12-12T12:01:72'
-    # ok
-    assert encode_datetime('2019-12-12T12:01:42') == '12/12/2019 12:01:42'
-
-
 @pytest.mark.parametrize('bool_type', ['OUINONREQUIREDType', 'OUINONType'])
 @pytest.mark.parametrize('value, expected, py_expected', [
     ('OUI', 'OUI', True),
diff --git a/tests/test_toulouse_axel_utils.py b/tests/test_toulouse_axel_utils.py
new file mode 100644
index 00000000..ea47f239
--- /dev/null
+++ b/tests/test_toulouse_axel_utils.py
@@ -0,0 +1,28 @@
+# passerelle - uniform access to multiple data sources and services
+# Copyright (C) 2020 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/>.
+
+from passerelle.contrib.toulouse_axel.utils import (
+    encode_datetime,
+)
+
+
+def test_encode_datetime():
+    # wrong format
+    assert encode_datetime('foo') == 'foo'
+    assert encode_datetime('2019-12-12') == '2019-12-12'
+    assert encode_datetime('2019-12-12T12:01:72') == '2019-12-12T12:01:72'
+    # ok
+    assert encode_datetime('2019-12-12T12:01:42') == '12/12/2019 12:01:42'
-- 
2.20.1

