caluire-axel: code factorization (#53704)

This commit is contained in:
Lauréline Guérin 2021-05-04 10:49:39 +02:00
parent 59186c7cf9
commit 183e7700eb
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
9 changed files with 362 additions and 363 deletions

View File

@ -32,7 +32,7 @@ from passerelle.utils.api import endpoint
from passerelle.utils.jsonresponse import APIError
from . import schemas
from . import utils
from .exceptions import AxelError
from passerelle.contrib.utils import axel
logger = logging.getLogger('passerelle.contrib.toulouse_axel')
@ -120,7 +120,7 @@ class ToulouseAxel(BaseResource):
try:
result = schemas.ref_date_gestion_dui(self)
except AxelError as e:
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
@ -141,7 +141,7 @@ class ToulouseAxel(BaseResource):
def check_dui(self, post_data):
try:
result = schemas.ref_verif_dui(self, {'PORTAIL': {'DUI': post_data}})
except AxelError as e:
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
@ -292,7 +292,7 @@ class ToulouseAxel(BaseResource):
def get_family_data(self, dui, check_registrations=False, with_management_dates=False):
try:
result = schemas.ref_famille_dui(self, {'PORTAIL': {'DUI': {'IDDUI': dui}}})
except AxelError as e:
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
@ -452,7 +452,7 @@ class ToulouseAxel(BaseResource):
flags = sorted(schemas.UPDATE_FAMILY_FLAGS.keys())
for flag in flags:
flag_value = post_data.get(flag)
flag_value = utils.encode_bool(flag_value)
flag_value = axel.encode_bool(flag_value)
# no update for the related block
if flag_value == 'OUI':
@ -579,7 +579,7 @@ class ToulouseAxel(BaseResource):
# transform ALLERGIE block
new_allergie = []
for key in ['ASTHME', 'MEDICAMENTEUSES', 'ALIMENTAIRES']:
if utils.encode_bool(child['SANITAIRE']['ALLERGIE'][key]) == 'OUI':
if axel.encode_bool(child['SANITAIRE']['ALLERGIE'][key]) == 'OUI':
new_allergie.append(
{
'TYPE': key,
@ -642,7 +642,7 @@ class ToulouseAxel(BaseResource):
# prepare data
post_data['IDDUI'] = link.dui
post_data['DATEDEMANDE'] = datetime.date.today().strftime(utils.json_date_format)
post_data['DATEDEMANDE'] = datetime.date.today().strftime(axel.json_date_format)
self.sanitize_update_family_data(dui=link.dui, post_data=post_data)
@ -653,7 +653,7 @@ class ToulouseAxel(BaseResource):
try:
result = schemas.form_maj_famille_dui(self, {'PORTAIL': {'DUI': post_data}})
except AxelError as e:
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
@ -680,7 +680,7 @@ class ToulouseAxel(BaseResource):
try:
result = schemas.ref_facture_a_payer(self, {'PORTAIL': {'DUI': {'IDDUI': dui}}})
except AxelError as e:
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
@ -701,7 +701,7 @@ class ToulouseAxel(BaseResource):
result = schemas.list_dui_factures(
self, {'LISTFACTURE': {'NUMDUI': link.dui, 'DEBUT': '1970-01-01'}}
)
except AxelError as e:
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
@ -829,7 +829,7 @@ class ToulouseAxel(BaseResource):
result = schemas.ref_facture_pdf(
self, {'PORTAIL': {'FACTUREPDF': {'IDFACTURE': int(invoice['display_id'])}}}
)
except AxelError as e:
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
@ -876,10 +876,10 @@ class ToulouseAxel(BaseResource):
transaction_amount = invoice['amount']
transaction_id = data['transaction_id']
transaction_date = utils.parse_datetime(data['transaction_date'])
transaction_date = axel.parse_datetime(data['transaction_date'])
if transaction_date is None:
raise APIError('invalid transaction_date')
transaction_date = utils.encode_datetime(transaction_date)
transaction_date = axel.encode_datetime(transaction_date)
post_data = {
'IDFACTURE': int(invoice_id),
'IDREGIEENCAISSEMENT': '',
@ -889,7 +889,7 @@ class ToulouseAxel(BaseResource):
}
try:
schemas.form_paiement_dui(self, {'PORTAIL': {'DUI': post_data}})
except AxelError as e:
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
@ -913,7 +913,7 @@ class ToulouseAxel(BaseResource):
}
},
)
except AxelError as e:
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
@ -993,8 +993,8 @@ class ToulouseAxel(BaseResource):
{
'IDACTIVITE': activity_id,
'ANNEEREFERENCE': str(reference_year),
'DATEDEBUT': start_date.strftime(utils.json_date_format),
'DATEDFIN': end_date.strftime(utils.json_date_format),
'DATEDEBUT': start_date.strftime(axel.json_date_format),
'DATEDFIN': end_date.strftime(axel.json_date_format),
}
)
try:
@ -1012,7 +1012,7 @@ class ToulouseAxel(BaseResource):
}
},
)
except AxelError as e:
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
@ -1044,12 +1044,12 @@ class ToulouseAxel(BaseResource):
for activity in child_activities.get('ACTIVITE', []):
activity['id'] = activity['IDACTIVITE']
start_date = datetime.datetime.strptime(activity['DATEENTREE'], utils.json_date_format)
end_date = datetime.datetime.strptime(activity['DATESORTIE'], utils.json_date_format)
start_date = datetime.datetime.strptime(activity['DATEENTREE'], axel.json_date_format)
end_date = datetime.datetime.strptime(activity['DATESORTIE'], axel.json_date_format)
activity['text'] = u'{} (inscription du {} au {})'.format(
activity['LIBELLEACTIVITE'],
start_date.strftime(utils.xml_date_format),
end_date.strftime(utils.xml_date_format),
start_date.strftime(axel.xml_date_format),
end_date.strftime(axel.xml_date_format),
)
activity['annee_reference'] = reference_year
activity['annee_reference_short'] = str(reference_year)[2:]
@ -1080,7 +1080,7 @@ class ToulouseAxel(BaseResource):
# get pivot date
try:
pivot_date = datetime.datetime.strptime(
'%s-%s' % (reference_year, pivot_date), utils.json_date_format
'%s-%s' % (reference_year, pivot_date), axel.json_date_format
).date()
except ValueError:
raise APIError('bad date format, should be MM-DD', err_code='bad-request', http_status=400)
@ -1094,7 +1094,7 @@ class ToulouseAxel(BaseResource):
'id': str(reference_year),
'text': '%s/%s' % (reference_year, reference_year + 1),
'type': 'encours',
'refdate': today.strftime(utils.json_date_format),
'refdate': today.strftime(axel.json_date_format),
}
]
if today < pivot_date:
@ -1115,7 +1115,7 @@ class ToulouseAxel(BaseResource):
'id': str(reference_year + 1),
'text': '%s/%s' % (reference_year + 1, reference_year + 2),
'type': 'suivante',
'refdate': next_ref_date.strftime(utils.json_date_format),
'refdate': next_ref_date.strftime(axel.json_date_format),
}
)
return {'data': data}
@ -1133,7 +1133,7 @@ class ToulouseAxel(BaseResource):
def clae_children_activities_info(self, request, NameID, booking_date):
link = self.get_link(NameID)
try:
booking_date = datetime.datetime.strptime(booking_date, utils.json_date_format)
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)
@ -1159,8 +1159,8 @@ class ToulouseAxel(BaseResource):
def clae_booking_activities_info(self, request, NameID, idpersonne, start_date, end_date):
link = self.get_link(NameID)
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()
start_date = datetime.datetime.strptime(start_date, axel.json_date_format).date()
end_date = datetime.datetime.strptime(end_date, axel.json_date_format).date()
except ValueError:
raise APIError('bad date format, should be YYYY-MM-DD', err_code='bad-request', http_status=400)
@ -1174,8 +1174,8 @@ class ToulouseAxel(BaseResource):
booking_data = {d['TYPEACTIVITE']: d for d in booking_data}
start_date, end_date = utils.get_week_dates_from_date(week_start_date)
week = 'week:%s:%s' % (
start_date.strftime(utils.json_date_format),
end_date.strftime(utils.json_date_format),
start_date.strftime(axel.json_date_format),
end_date.strftime(axel.json_date_format),
)
day_date = week_start_date
while day_date <= week_end_date:
@ -1187,7 +1187,7 @@ class ToulouseAxel(BaseResource):
booked = activity['booking']['days'][day]
if booked is not None:
yield {
'day': day_date.strftime(utils.json_date_format),
'day': day_date.strftime(axel.json_date_format),
'activity_id': activity['id'],
'activity_type': activity_type,
'activity_label': activity['LIBELLEACTIVITE'],
@ -1218,8 +1218,8 @@ class ToulouseAxel(BaseResource):
entree_dates = [act['DATEENTREE'] for act in child_activities.get('ACTIVITE', [])]
sortie_dates = [act['DATESORTIE'] for act in child_activities.get('ACTIVITE', [])]
return (
datetime.datetime.strptime(max(entree_dates), utils.json_date_format).date(),
datetime.datetime.strptime(min(sortie_dates), utils.json_date_format).date(),
datetime.datetime.strptime(max(entree_dates), axel.json_date_format).date(),
datetime.datetime.strptime(min(sortie_dates), axel.json_date_format).date(),
)
@endpoint(
@ -1240,8 +1240,8 @@ class ToulouseAxel(BaseResource):
):
link = self.get_link(NameID)
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()
start_date = datetime.datetime.strptime(start_date, axel.json_date_format).date()
end_date = datetime.datetime.strptime(end_date, axel.json_date_format).date()
except ValueError:
raise APIError('bad date format, should be YYYY-MM-DD', err_code='bad-request', http_status=400)
if activity_type not in ['MAT', 'MIDI', 'SOIR', 'GARD']:
@ -1286,7 +1286,7 @@ class ToulouseAxel(BaseResource):
day = WEEKDAYS[day_date.weekday()]
activity_day = {
'id': '{}:{}:{}:{}'.format(
idpersonne, activity_type, activity['id'], day_date.strftime(utils.json_date_format)
idpersonne, activity_type, activity['id'], day_date.strftime(axel.json_date_format)
),
'text': dateformat.format(day_date, 'l j F Y'),
'disabled': activity['booking']['days'][day] is None,
@ -1328,7 +1328,7 @@ class ToulouseAxel(BaseResource):
):
link = self.get_link(NameID)
try:
booking_date = datetime.datetime.strptime(booking_date, utils.json_date_format).date()
booking_date = datetime.datetime.strptime(booking_date, axel.json_date_format).date()
except ValueError:
raise APIError('bad date format, should be YYYY-MM-DD', err_code='bad-request', http_status=400)
if activity_type not in ['MAT', 'MIDI', 'SOIR', 'GARD']:
@ -1408,12 +1408,10 @@ class ToulouseAxel(BaseResource):
# check dates
today = datetime.date.today()
start_date_min = today + datetime.timedelta(days=8)
start_date = datetime.datetime.strptime(
post_data['booking_start_date'], utils.json_date_format
).date()
start_date = datetime.datetime.strptime(post_data['booking_start_date'], axel.json_date_format).date()
reference_year = utils.get_reference_year_from_date(start_date)
end_date_max = datetime.date(reference_year + 1, 7, 31)
end_date = datetime.datetime.strptime(post_data['booking_end_date'], utils.json_date_format).date()
end_date = datetime.datetime.strptime(post_data['booking_end_date'], axel.json_date_format).date()
if start_date > end_date:
raise APIError(
'booking_start_date should be before booking_end_date',
@ -1461,7 +1459,7 @@ class ToulouseAxel(BaseResource):
post_data['child_id'],
activity_type,
activity_id,
day_date.strftime(utils.json_date_format),
day_date.strftime(axel.json_date_format),
)
if key in post_data['booking_list_%s' % activity_type]:
week_pattern = (
@ -1483,8 +1481,8 @@ class ToulouseAxel(BaseResource):
week_pattern = get_week_pattern(real_start_date, real_end_date, activity_type, activity_id)
activity['PERIODE'].append(
{
'DATEDEBUT': real_start_date.strftime(utils.json_date_format),
'DATEDFIN': real_end_date.strftime(utils.json_date_format),
'DATEDEBUT': real_start_date.strftime(axel.json_date_format),
'DATEDFIN': real_end_date.strftime(axel.json_date_format),
'SEMAINETYPE': week_pattern,
}
)
@ -1494,7 +1492,7 @@ class ToulouseAxel(BaseResource):
# build data
data = {
'IDDUI': link.dui,
'DATEDEMANDE': today.strftime(utils.json_date_format),
'DATEDEMANDE': today.strftime(axel.json_date_format),
'ENFANT': [
{
'IDPERSONNE': post_data['child_id'],
@ -1510,7 +1508,7 @@ class ToulouseAxel(BaseResource):
try:
result = schemas.reservation_annuelle(self, {'PORTAIL': {'DUI': data}})
except AxelError as e:
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
@ -1562,7 +1560,7 @@ class ToulouseAxel(BaseResource):
# build dates of the period
today = datetime.date.today()
start_date_min = today + datetime.timedelta(days=8)
start_date = datetime.datetime.strptime(post_data['booking_date'], utils.json_date_format).date()
start_date = datetime.datetime.strptime(post_data['booking_date'], axel.json_date_format).date()
start_date = max(start_date, start_date_min)
reference_year = utils.get_reference_year_from_date(start_date)
end_date = datetime.date(reference_year + 1, 7, 31)
@ -1593,8 +1591,8 @@ class ToulouseAxel(BaseResource):
'ANNEEREFERENCE': str(reference_year),
'PERIODE': [
{
'DATEDEBUT': start_date.strftime(utils.json_date_format),
'DATEDFIN': end_date.strftime(utils.json_date_format),
'DATEDEBUT': start_date.strftime(axel.json_date_format),
'DATEDFIN': end_date.strftime(axel.json_date_format),
'SEMAINETYPE': week_pattern,
}
],
@ -1603,7 +1601,7 @@ class ToulouseAxel(BaseResource):
# build data
data = {
'IDDUI': link.dui,
'DATEDEMANDE': today.strftime(utils.json_date_format),
'DATEDEMANDE': today.strftime(axel.json_date_format),
'ENFANT': [
{
'IDPERSONNE': post_data['child_id'],
@ -1619,7 +1617,7 @@ class ToulouseAxel(BaseResource):
try:
result = schemas.reservation_annuelle(self, {'PORTAIL': {'DUI': data}})
except AxelError as e:
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',

View File

@ -15,156 +15,16 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import copy
import datetime
import os
import re
import xml.etree.ElementTree as ET
from collections import namedtuple
from django.utils.encoding import force_text
import xmlschema
from passerelle.utils.xml import JSONSchemaFromXMLSchema
from . import utils
from .exceptions import AxelError
from passerelle.contrib.utils import axel
BASE_XSD_PATH = os.path.join(os.path.dirname(__file__), 'xsd')
class AxelSchema(JSONSchemaFromXMLSchema):
type_map = {
'{urn:AllAxelTypes}DATEREQUIREDType': 'date',
'{urn:AllAxelTypes}DATEType': 'date_optional',
'{urn:AllAxelTypes}OUINONREQUIREDType': 'bool',
'{urn:AllAxelTypes}OUINONType': 'bool_optional',
}
@classmethod
def schema_date(cls):
return {
'type': 'string',
'pattern': '[0-9]{4}-[0-9]{2}-[0-9]{2}',
}
def encode_date(self, obj):
try:
return datetime.datetime.strptime(obj, utils.json_date_format).strftime(utils.xml_date_format)
except ValueError:
return obj
def encode_date_optional(self, obj):
if not obj:
return obj
return self.encode_date(obj)
def decode_date(self, data):
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):
if not data.text:
return data
return self.decode_date(data)
@classmethod
def schema_bool(cls):
return copy.deepcopy(utils.boolean_type)
def encode_bool(self, obj):
return utils.encode_bool(obj)
def decode_bool(self, data):
value = False
if data.text.lower() == 'oui':
value = True
return xmlschema.ElementData(
tag=data.tag, text=value, content=data.content, attributes=data.attributes
)
@classmethod
def schema_bool_optional(cls):
schema_bool_optional = cls.schema_bool()
schema_bool_optional['oneOf'].append({'type': 'string', 'enum': ['']})
return schema_bool_optional
def encode_bool_optional(self, obj):
return self.encode_bool(obj)
def decode_bool_optional(self, data):
if not data.text:
return data
return self.decode_bool(data)
def xml_schema_converter(name, root_element):
xsd_path = os.path.join(BASE_XSD_PATH, name)
if not os.path.exists(xsd_path):
return None
return AxelSchema(xsd_path, root_element)
OperationResult = namedtuple('OperationResult', ['json_response', 'xml_request', 'xml_response'])
class Operation(object):
def __init__(self, operation, prefix='Dui/', request_root_element='PORTAIL'):
self.operation = operation
self.request_converter = xml_schema_converter(
'%sQ_%s.xsd' % (prefix, operation), request_root_element
)
self.response_converter = xml_schema_converter('%sR_%s.xsd' % (prefix, operation), 'PORTAILSERVICE')
self.name = re.sub(
'(.?)([A-Z])', lambda s: s.group(1) + ('-' if s.group(1) else '') + s.group(2).lower(), operation
)
self.snake_name = self.name.replace('-', '_')
@property
def request_schema(self):
schema = self.request_converter.json_schema
schema['flatten'] = True
schema['merge_extra'] = True
return schema
def __call__(self, resource, request_data=None):
client = resource.soap_client()
serialized_request = ''
if self.request_converter:
try:
serialized_request = self.request_converter.encode(request_data)
except xmlschema.XMLSchemaValidationError as e:
raise AxelError('invalid request %s' % str(e))
utils.indent(serialized_request)
serialized_request = force_text(ET.tostring(serialized_request))
try:
self.request_converter.xml_schema.validate(serialized_request)
except xmlschema.XMLSchemaValidationError as e:
raise AxelError('invalid request %s' % str(e), xml_request=serialized_request)
result = client.service.getData(
self.operation, serialized_request, ''
) # FIXME: What is the user parameter for ?
xml_result = ET.fromstring(result.encode('utf-8'))
utils.indent(xml_result)
pretty_result = force_text(ET.tostring(xml_result))
if xml_result.find('RESULTAT/STATUS').text != 'OK':
msg = xml_result.find('RESULTAT/COMMENTAIRES').text
raise AxelError(msg, xml_request=serialized_request, xml_response=pretty_result)
try:
return OperationResult(
json_response=self.response_converter.decode(xml_result),
xml_request=serialized_request,
xml_response=pretty_result,
)
except xmlschema.XMLSchemaValidationError as e:
raise AxelError(
'invalid response %s' % str(e), xml_request=serialized_request, xml_response=pretty_result
)
class Operation(axel.Operation):
base_xsd_path = BASE_XSD_PATH
default_prefix = 'Dui/'
ref_date_gestion_dui = Operation('RefDateGestionDui')
@ -183,7 +43,7 @@ reservation_annuelle = Operation('ReservationAnnuelle')
PAYMENT_SCHEMA = {
'type': 'object',
'properties': {
'transaction_date': copy.deepcopy(utils.datetime_type),
'transaction_date': copy.deepcopy(axel.datetime_type),
'transaction_id': {
'type': 'string',
},
@ -229,7 +89,7 @@ UPDATE_FAMILY_SCHEMA = copy.deepcopy(
)
for flag in sorted(UPDATE_FAMILY_FLAGS.keys()):
flag_type = copy.deepcopy(utils.boolean_type)
flag_type = copy.deepcopy(axel.boolean_type)
if flag not in UPDATE_FAMILY_REQUIRED_FLAGS:
flag_type['oneOf'].append({'type': 'null'})
flag_type['oneOf'].append({'type': 'string', 'enum': ['']})
@ -283,7 +143,7 @@ sanitaire_properties['ALLERGIE'] = {
}
sanitaire_required.append('ALLERGIE')
for key in ['ASTHME', 'MEDICAMENTEUSES', 'ALIMENTAIRES']:
flag_type = copy.deepcopy(utils.boolean_type)
flag_type = copy.deepcopy(axel.boolean_type)
flag_type['oneOf'].append({'type': 'null'})
flag_type['oneOf'].append({'type': 'string', 'enum': ['']})
sanitaire_properties['ALLERGIE']['properties'][key] = flag_type
@ -303,8 +163,8 @@ UPDATE_FAMILY_SCHEMA['unflatten'] = True
BOOKING_SCHEMA = {
'type': 'object',
'properties': {
'booking_start_date': copy.deepcopy(utils.date_type),
'booking_end_date': copy.deepcopy(utils.date_type),
'booking_start_date': copy.deepcopy(axel.date_type),
'booking_end_date': copy.deepcopy(axel.date_type),
'booking_list_MAT': {
'oneOf': [
{'type': 'null'},
@ -429,7 +289,7 @@ ANNUAL_BOOKING_SCHEMA = {
'maxLength': 8,
},
'regime': {'oneOf': [{'type': 'null'}, {'type': 'string', 'enum': ['', 'SV', 'AV']}]},
'booking_date': copy.deepcopy(utils.date_type),
'booking_date': copy.deepcopy(axel.date_type),
},
'required': [
'booking_list_MAT',

View File

@ -19,39 +19,12 @@ from __future__ import unicode_literals
from collections import OrderedDict
import datetime
import unicodedata
import xml.etree.ElementTree as ET
import pytz
from django.utils.six import string_types
from passerelle.utils.conversion import normalize
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',
},
]
}
date_type = {
'type': 'string',
'pattern': '[0-9]{4}-[0-9]{2}-[0-9]{2}',
}
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'
situation_familiale_mapping = OrderedDict(
[
('C', 'Célibataire'),
@ -113,63 +86,6 @@ def get_label(mapping, code):
return mapping.get(code, code)
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 parse_datetime(value):
try:
dt = datetime.datetime.strptime(value, json_datetime_format)
except ValueError:
return None
return pytz.utc.localize(dt)
def encode_datetime(dt):
return dt.astimezone(pytz.timezone('Europe/Paris')).strftime(xml_datetime_format)
def upperize(data):
if isinstance(data, dict):
for key, val in data.items():

View File

View File

@ -0,0 +1,264 @@
# 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 copy
import datetime
import os
import re
import xml.etree.ElementTree as ET
from collections import namedtuple
import pytz
from django.utils.encoding import force_text
import xmlschema
from passerelle.utils.xml import JSONSchemaFromXMLSchema
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',
},
]
}
date_type = {
'type': 'string',
'pattern': '[0-9]{4}-[0-9]{2}-[0-9]{2}',
}
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=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 parse_datetime(value):
try:
dt = datetime.datetime.strptime(value, json_datetime_format)
except ValueError:
return None
return pytz.utc.localize(dt)
def encode_datetime(dt):
return dt.astimezone(pytz.timezone('Europe/Paris')).strftime(xml_datetime_format)
class AxelError(Exception):
def __init__(self, message, xml_request=None, xml_response=None, *args):
self.message = message
self.xml_request = xml_request
self.xml_response = xml_response
super(AxelError, self).__init__(message, *args)
def __str__(self):
return self.message
class AxelSchema(JSONSchemaFromXMLSchema):
type_map = {
'{urn:AllAxelTypes}DATEREQUIREDType': 'date',
'{urn:AllAxelTypes}DATEType': 'date_optional',
'{urn:AllAxelTypes}OUINONREQUIREDType': 'bool',
'{urn:AllAxelTypes}OUINONType': 'bool_optional',
}
@classmethod
def schema_date(cls):
return {
'type': 'string',
'pattern': '[0-9]{4}-[0-9]{2}-[0-9]{2}',
}
def encode_date(self, obj):
try:
return datetime.datetime.strptime(obj, json_date_format).strftime(xml_date_format)
except ValueError:
return obj
def encode_date_optional(self, obj):
if not obj:
return obj
return self.encode_date(obj)
def decode_date(self, data):
value = datetime.datetime.strptime(data.text, xml_date_format).strftime(json_date_format)
return xmlschema.ElementData(
tag=data.tag, text=value, content=data.content, attributes=data.attributes
)
def decode_date_optional(self, data):
if not data.text:
return data
return self.decode_date(data)
@classmethod
def schema_bool(cls):
return copy.deepcopy(boolean_type)
def encode_bool(self, obj):
return encode_bool(obj)
def decode_bool(self, data):
value = False
if data.text.lower() == 'oui':
value = True
return xmlschema.ElementData(
tag=data.tag, text=value, content=data.content, attributes=data.attributes
)
@classmethod
def schema_bool_optional(cls):
schema_bool_optional = cls.schema_bool()
schema_bool_optional['oneOf'].append({'type': 'string', 'enum': ['']})
return schema_bool_optional
def encode_bool_optional(self, obj):
return self.encode_bool(obj)
def decode_bool_optional(self, data):
if not data.text:
return data
return self.decode_bool(data)
def xml_schema_converter(base_xsd_path, name, root_element):
xsd_path = os.path.join(base_xsd_path, name)
if not os.path.exists(xsd_path):
return None
return AxelSchema(xsd_path, root_element)
OperationResult = namedtuple('OperationResult', ['json_response', 'xml_request', 'xml_response'])
class Operation(object):
base_xsd_path = None
default_prefix = ''
def __init__(self, operation, prefix=None, request_root_element='PORTAIL'):
if prefix is None:
prefix = self.default_prefix
self.operation = operation
self.request_converter = xml_schema_converter(
self.base_xsd_path, '%sQ_%s.xsd' % (prefix, operation), request_root_element
)
self.response_converter = xml_schema_converter(
self.base_xsd_path, '%sR_%s.xsd' % (prefix, operation), 'PORTAILSERVICE'
)
self.name = re.sub(
'(.?)([A-Z])', lambda s: s.group(1) + ('-' if s.group(1) else '') + s.group(2).lower(), operation
)
self.snake_name = self.name.replace('-', '_')
@property
def request_schema(self):
schema = self.request_converter.json_schema
schema['flatten'] = True
schema['merge_extra'] = True
return schema
def __call__(self, resource, request_data=None):
client = resource.soap_client()
serialized_request = ''
if self.request_converter:
try:
serialized_request = self.request_converter.encode(request_data)
except xmlschema.XMLSchemaValidationError as e:
raise AxelError('invalid request %s' % str(e))
indent(serialized_request)
serialized_request = force_text(ET.tostring(serialized_request))
try:
self.request_converter.xml_schema.validate(serialized_request)
except xmlschema.XMLSchemaValidationError as e:
raise AxelError('invalid request %s' % str(e), xml_request=serialized_request)
result = client.service.getData(
self.operation, serialized_request, ''
) # FIXME: What is the user parameter for ?
xml_result = ET.fromstring(result.encode('utf-8'))
indent(xml_result)
pretty_result = force_text(ET.tostring(xml_result))
if xml_result.find('RESULTAT/STATUS').text != 'OK':
msg = xml_result.find('RESULTAT/COMMENTAIRES').text
raise AxelError(msg, xml_request=serialized_request, xml_response=pretty_result)
try:
return OperationResult(
json_response=self.response_converter.decode(xml_result),
xml_request=serialized_request,
xml_response=pretty_result,
)
except xmlschema.XMLSchemaValidationError as e:
raise AxelError(
'invalid response %s' % str(e), xml_request=serialized_request, xml_response=pretty_result
)

View File

@ -1,5 +1,5 @@
# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2020 Entr'ouvert
# 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
@ -14,13 +14,20 @@
# 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.utils.axel import (
parse_datetime,
encode_datetime,
)
class AxelError(Exception):
def __init__(self, message, xml_request=None, xml_response=None, *args):
self.message = message
self.xml_request = xml_request
self.xml_response = xml_response
super(AxelError, self).__init__(message, *args)
def __str__(self):
return self.message
def test_parse_datetime():
# wrong format
assert parse_datetime('foo') is None
assert parse_datetime('2019-12-12') is None
assert parse_datetime('2019-12-12T12:01:72') is None
# ok
assert parse_datetime('2019-12-12T12:01:42').isoformat() == '2019-12-12T12:01:42+00:00'
def test_encode_datetime():
assert encode_datetime(parse_datetime('2019-12-12T23:40:42')) == '13/12/2019 00:40:42'

View File

@ -33,7 +33,6 @@ import pytest
import xmlschema
import jsonschema
from passerelle.contrib.toulouse_axel.exceptions import AxelError
from passerelle.contrib.toulouse_axel.models import (
Link,
Lock,
@ -48,6 +47,8 @@ from passerelle.contrib.toulouse_axel.utils import (
regime_mapping,
upperize,
)
from passerelle.contrib.utils.axel import AxelError
from passerelle.contrib.utils.axel import OperationResult
from passerelle.utils.jsonresponse import APIError
from passerelle.utils.soap import SOAPError
import utils
@ -675,7 +676,7 @@ def test_link_endpoint_axel_error(app, resource, link_params):
% content
)
with mock_getdata(content, 'RefVerifDui'):
with mock.patch('passerelle.contrib.toulouse_axel.schemas.AxelSchema.decode') as decode:
with mock.patch('passerelle.contrib.utils.axel.AxelSchema.decode') as decode:
decode.side_effect = xmlschema.XMLSchemaValidationError(None, None)
resp = app.post_json('/toulouse-axel/test/link?NameID=yyy', params=link_params)
assert resp.json['err_desc'].startswith("Axel error: invalid response")
@ -1278,7 +1279,7 @@ def test_update_family_info_endpoint(app, resource, update_params, family_data):
assert 'xml_response' in resp.json['data']
with mock.patch('passerelle.contrib.toulouse_axel.schemas.form_maj_famille_dui') as operation:
operation.return_value = schemas.OperationResult(json_response={}, xml_request='', xml_response='')
operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
with mock.patch(
'passerelle.contrib.toulouse_axel.models.ToulouseAxel.get_family_data', return_value=family_data
):
@ -1292,7 +1293,7 @@ def test_update_family_info_endpoint(app, resource, update_params, family_data):
link.person_id = '35'
link.save()
with mock.patch('passerelle.contrib.toulouse_axel.schemas.form_maj_famille_dui') as operation:
operation.return_value = schemas.OperationResult(json_response={}, xml_request='', xml_response='')
operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
with mock.patch(
'passerelle.contrib.toulouse_axel.models.ToulouseAxel.get_family_data', return_value=family_data
):
@ -2735,9 +2736,7 @@ def test_clae_children_activities_info_check(app, resource, activities, expected
}
}
with mock.patch('passerelle.contrib.toulouse_axel.schemas.enfants_activites') as operation:
operation.return_value = schemas.OperationResult(
json_response=result, xml_request='', xml_response=''
)
operation.return_value = OperationResult(json_response=result, xml_request='', xml_response='')
resp = app.get('/toulouse-axel/test/clae_children_activities_info?NameID=yyy&booking_date=2020-01-20')
assert resp.json['err'] == 0
assert len(resp.json['data']) == expected and 1 or 0
@ -4000,9 +3999,7 @@ def test_clae_booking_endpoint(app, resource, booking_params, child_activities_d
assert 'xml_response' in resp.json['data']
with mock.patch('passerelle.contrib.toulouse_axel.schemas.reservation_annuelle') as operation:
operation.return_value = schemas.OperationResult(
json_response={}, xml_request='', xml_response=''
)
operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
resp = app.post_json('/toulouse-axel/test/clae_booking?NameID=yyy', params=booking_params)
payload = operation.call_args_list[0][0][1]['PORTAIL']['DUI']
assert payload == {
@ -4069,9 +4066,7 @@ def test_clae_booking_endpoint(app, resource, booking_params, child_activities_d
new_booking_params['booking_list_SOIR'] = []
new_booking_params['booking_list_GARD'] = []
with mock.patch('passerelle.contrib.toulouse_axel.schemas.reservation_annuelle') as operation:
operation.return_value = schemas.OperationResult(
json_response={}, xml_request='', xml_response=''
)
operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
resp = app.post_json('/toulouse-axel/test/clae_booking?NameID=yyy', params=new_booking_params)
payload = operation.call_args_list[0][0][1]['PORTAIL']['DUI']
assert len(payload['ENFANT']) == 1
@ -4109,9 +4104,7 @@ def test_clae_booking_endpoint(app, resource, booking_params, child_activities_d
new_booking_params['booking_list_SOIR'] = None
new_booking_params['booking_list_GARD'] = None
with mock.patch('passerelle.contrib.toulouse_axel.schemas.reservation_annuelle') as operation:
operation.return_value = schemas.OperationResult(
json_response={}, xml_request='', xml_response=''
)
operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
resp = app.post_json('/toulouse-axel/test/clae_booking?NameID=yyy', params=new_booking_params)
payload = operation.call_args_list[0][0][1]['PORTAIL']['DUI']
assert 'ACTIVITE' not in payload['ENFANT'][0]
@ -4134,9 +4127,7 @@ def test_clae_booking_endpoint(app, resource, booking_params, child_activities_d
new_booking_params['booking_list_SOIR'] = None
new_booking_params['booking_list_GARD'] = None
with mock.patch('passerelle.contrib.toulouse_axel.schemas.reservation_annuelle') as operation:
operation.return_value = schemas.OperationResult(
json_response={}, xml_request='', xml_response=''
)
operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
with mock.patch('django.core.cache.cache.delete') as mock_cache_delete:
resp = app.post_json('/toulouse-axel/test/clae_booking?NameID=yyy', params=new_booking_params)
assert mock_cache_delete.call_args_list == [
@ -4180,25 +4171,19 @@ def test_clae_booking_regime(app, resource, booking_params, child_activities_dat
):
booking_params['regime'] = None
with mock.patch('passerelle.contrib.toulouse_axel.schemas.reservation_annuelle') as operation:
operation.return_value = schemas.OperationResult(
json_response={}, xml_request='', xml_response=''
)
operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
app.post_json('/toulouse-axel/test/clae_booking?NameID=yyy', params=booking_params)
payload = operation.call_args_list[0][0][1]['PORTAIL']['DUI']
assert payload['ENFANT'][0]['REGIME'] == 'SV'
booking_params['regime'] = ''
with mock.patch('passerelle.contrib.toulouse_axel.schemas.reservation_annuelle') as operation:
operation.return_value = schemas.OperationResult(
json_response={}, xml_request='', xml_response=''
)
operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
app.post_json('/toulouse-axel/test/clae_booking?NameID=yyy', params=booking_params)
payload = operation.call_args_list[0][0][1]['PORTAIL']['DUI']
assert payload['ENFANT'][0]['REGIME'] == 'SV'
del booking_params['regime']
with mock.patch('passerelle.contrib.toulouse_axel.schemas.reservation_annuelle') as operation:
operation.return_value = schemas.OperationResult(
json_response={}, xml_request='', xml_response=''
)
operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
app.post_json('/toulouse-axel/test/clae_booking?NameID=yyy', params=booking_params)
payload = operation.call_args_list[0][0][1]['PORTAIL']['DUI']
assert payload['ENFANT'][0]['REGIME'] == 'SV'
@ -4228,9 +4213,7 @@ def test_clae_booking_endpoint_next_year(app, resource, child_activities_data):
) as mock_activities:
mock_activities.return_value = activities
with mock.patch('passerelle.contrib.toulouse_axel.schemas.reservation_annuelle') as operation:
operation.return_value = schemas.OperationResult(
json_response={}, xml_request='', xml_response=''
)
operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
app.post_json('/toulouse-axel/test/clae_booking?NameID=yyy', params=booking_params)
assert mock_activities.call_args_list == [
mock.call(child_id='3535', dui='XXX', reference_year=2020),
@ -4305,9 +4288,7 @@ def test_clae_booking_annual_endpoint(app, resource, annual_booking_params, chil
assert 'xml_response' in resp.json['data']
with mock.patch('passerelle.contrib.toulouse_axel.schemas.reservation_annuelle') as operation:
operation.return_value = schemas.OperationResult(
json_response={}, xml_request='', xml_response=''
)
operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
resp = app.post_json(
'/toulouse-axel/test/clae_booking_annual?NameID=yyy', params=annual_booking_params
)
@ -4360,9 +4341,7 @@ def test_clae_booking_annual_endpoint(app, resource, annual_booking_params, chil
new_booking_params['booking_list_SOIR'] = []
new_booking_params['booking_list_GARD'] = []
with mock.patch('passerelle.contrib.toulouse_axel.schemas.reservation_annuelle') as operation:
operation.return_value = schemas.OperationResult(
json_response={}, xml_request='', xml_response=''
)
operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
resp = app.post_json(
'/toulouse-axel/test/clae_booking_annual?NameID=yyy', params=new_booking_params
)
@ -4402,9 +4381,7 @@ def test_clae_booking_annual_endpoint(app, resource, annual_booking_params, chil
new_booking_params['booking_list_SOIR'] = None
new_booking_params['booking_list_GARD'] = None
with mock.patch('passerelle.contrib.toulouse_axel.schemas.reservation_annuelle') as operation:
operation.return_value = schemas.OperationResult(
json_response={}, xml_request='', xml_response=''
)
operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
resp = app.post_json(
'/toulouse-axel/test/clae_booking_annual?NameID=yyy', params=new_booking_params
)
@ -4434,9 +4411,7 @@ def test_clae_booking_annual_next_year(app, resource, child_activities_data):
'passerelle.contrib.toulouse_axel.models.ToulouseAxel.get_child_activities', return_value=activities
):
with mock.patch('passerelle.contrib.toulouse_axel.schemas.reservation_annuelle') as operation:
operation.return_value = schemas.OperationResult(
json_response={}, xml_request='', xml_response=''
)
operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
app.post_json('/toulouse-axel/test/clae_booking_annual?NameID=yyy', params=annual_booking_params)
payload = operation.call_args_list[0][0][1]['PORTAIL']['DUI']
assert payload['ENFANT'][0]['ACTIVITE'][0]['PERIODE'][0]['DATEDEBUT'] == '2020-08-01'
@ -4452,25 +4427,19 @@ def test_clae_booking_annual_regime(app, resource, annual_booking_params, child_
):
annual_booking_params['regime'] = None
with mock.patch('passerelle.contrib.toulouse_axel.schemas.reservation_annuelle') as operation:
operation.return_value = schemas.OperationResult(
json_response={}, xml_request='', xml_response=''
)
operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
app.post_json('/toulouse-axel/test/clae_booking_annual?NameID=yyy', params=annual_booking_params)
payload = operation.call_args_list[0][0][1]['PORTAIL']['DUI']
assert payload['ENFANT'][0]['REGIME'] == 'SV'
annual_booking_params['regime'] = ''
with mock.patch('passerelle.contrib.toulouse_axel.schemas.reservation_annuelle') as operation:
operation.return_value = schemas.OperationResult(
json_response={}, xml_request='', xml_response=''
)
operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
app.post_json('/toulouse-axel/test/clae_booking_annual?NameID=yyy', params=annual_booking_params)
payload = operation.call_args_list[0][0][1]['PORTAIL']['DUI']
assert payload['ENFANT'][0]['REGIME'] == 'SV'
del annual_booking_params['regime']
with mock.patch('passerelle.contrib.toulouse_axel.schemas.reservation_annuelle') as operation:
operation.return_value = schemas.OperationResult(
json_response={}, xml_request='', xml_response=''
)
operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
app.post_json('/toulouse-axel/test/clae_booking_annual?NameID=yyy', params=annual_booking_params)
payload = operation.call_args_list[0][0][1]['PORTAIL']['DUI']
assert payload['ENFANT'][0]['REGIME'] == 'SV'

View File

@ -16,7 +16,7 @@
import os
from passerelle.contrib.toulouse_axel.schemas import AxelSchema
from passerelle.contrib.utils.axel import AxelSchema
import pytest
import xmlschema

View File

@ -18,26 +18,11 @@ import datetime
import pytest
from passerelle.contrib.toulouse_axel.utils import (
parse_datetime,
encode_datetime,
get_booking,
get_reference_year_from_date,
get_week_dates_from_date,
json_date_format,
)
def test_parse_datetime():
# wrong format
assert parse_datetime('foo') is None
assert parse_datetime('2019-12-12') is None
assert parse_datetime('2019-12-12T12:01:72') is None
# ok
assert parse_datetime('2019-12-12T12:01:42').isoformat() == '2019-12-12T12:01:42+00:00'
def test_encode_datetime():
assert encode_datetime(parse_datetime('2019-12-12T23:40:42')) == '13/12/2019 00:40:42'
from passerelle.contrib.utils.axel import json_date_format
@pytest.mark.parametrize(