From 107b8d71882f4f6aa33eec2f94943d5fb4f71ee4 Mon Sep 17 00:00:00 2001 From: Josue Kouka Date: Tue, 23 May 2017 00:40:18 +0200 Subject: [PATCH] cityweb: add cityweb connector (#15883) --- debian/control | 1 + passerelle/apps/cityweb/__init__.py | 0 passerelle/apps/cityweb/cityweb.py | 342 +++++++++++++++++++++ passerelle/apps/cityweb/migrations/0001_initial.py | 39 +++ passerelle/apps/cityweb/migrations/__init__.py | 0 passerelle/apps/cityweb/models.py | 146 +++++++++ passerelle/settings.py | 1 + setup.py | 1 + tests/data/cityweb/cityweb.xsd | 181 +++++++++++ tests/data/cityweb/payload_birth.json | 50 +++ tests/data/cityweb/payload_death.json | 37 +++ tests/data/cityweb/payload_mariage.json | 68 ++++ tests/test_cityweb.py | 189 ++++++++++++ tox.ini | 1 + 14 files changed, 1056 insertions(+) create mode 100644 passerelle/apps/cityweb/__init__.py create mode 100644 passerelle/apps/cityweb/cityweb.py create mode 100644 passerelle/apps/cityweb/migrations/0001_initial.py create mode 100644 passerelle/apps/cityweb/migrations/__init__.py create mode 100644 passerelle/apps/cityweb/models.py create mode 100644 tests/data/cityweb/cityweb.xsd create mode 100644 tests/data/cityweb/payload_birth.json create mode 100644 tests/data/cityweb/payload_death.json create mode 100644 tests/data/cityweb/payload_mariage.json create mode 100644 tests/test_cityweb.py diff --git a/debian/control b/debian/control index ed3549c..b48c0ab 100644 --- a/debian/control +++ b/debian/control @@ -24,6 +24,7 @@ Depends: ${python:Depends}, python-magic, python-suds, python-cmislib (>= 0.5), python-cmislib (< 0.6), + python-lxml Recommends: python-soappy, python-phpserialize Suggests: python-sqlalchemy, python-mako Description: Uniform access to multiple data sources and services (Python module) diff --git a/passerelle/apps/cityweb/__init__.py b/passerelle/apps/cityweb/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/passerelle/apps/cityweb/cityweb.py b/passerelle/apps/cityweb/cityweb.py new file mode 100644 index 0000000..f7602fe --- /dev/null +++ b/passerelle/apps/cityweb/cityweb.py @@ -0,0 +1,342 @@ +# Copyright (C) 2017 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 . + +import os +import zipfile +from dateutil.parser import parse as dateutil_parse +from lxml import etree, objectify as xobject + +from django.core.files.storage import default_storage +from django.core.files.base import ContentFile + +from passerelle.utils.jsonresponse import APIError + + +def is_clean(element): + if not element.getchildren() and element.text is None: + return False + return all(is_clean(child) for child in element.iterchildren()) + + +class BaseType(object): + """Base data binding object + """ + tagname = None + + def __repr__(self): + return '<%s>' % self.tagname + + @classmethod + def make_element(cls, tagname, value=None, namespace=None, nsmap=None): + M = xobject.ElementMaker(annotate=False, namespace=namespace, + nsmap=nsmap) + return M(tagname, value) + + @property + def xml(self): + if isinstance(self, (SimpleType, DateType)): + return self.make_element(self.tagname, self.value, namespace=self.namespace) + tag = self.make_element(self.tagname, namespace=self.namespace, nsmap=self.nsmap) + for subelt in self.sequence: + attr = getattr(self, subelt, None) + if not attr: + continue + if isinstance(attr, (str, unicode)) and attr: + tag.append(self.make_element(subelt, attr, namespace=self.namespace)) + else: + xml = attr.xml + if not is_clean(xml): + continue + tag.append(xml) + xobject.deannotate(tag, xsi_nil=True) + return tag + + def __str__(self): + return etree.tostring(self.xml, pretty_print=True) + + +class CityWebType(BaseType): + namespace = 'http://tempuri.org/XMLSchema.xsd' + nsmap = {'xs': 'http://tempuri.org/XMLSchema.xsd'} + + +class SimpleType(CityWebType): + """Data binding class for SimpleType + """ + allowed_values = None + + def __init__(self, value): + if value not in self.allowed_values: + raise APIError('<%s> value (%s) not in %s' % (self.tagname, value, + self.allowed_values)) + self.value = value + + +class DateType(CityWebType): + + def __init__(self, value): + try: + self.value = dateutil_parse(value).date().isoformat() + except (ValueError,) as exc: + raise APIError(exc.message) + + +class ComplexType(CityWebType): + """Data binding class for ComplexType + """ + sequence = None + pattern = None + + def __init__(self, data): + if self.pattern: + data = self.extract_by_pattern(data) + self.data = data + + def extract_by_pattern(self, data): + data = {key: value for key, value in data.iteritems() if self.pattern in key} + data = {key.replace(self.pattern, ''): value for key, value in data.iteritems()} + return data + + +class BirthDate(DateType): + tagname = 'date' + + +class StartDate(DateType): + tagname = 'dateDebut' + + +class EndDate(DateType): + tagname = 'dateFin' + + +# SIMPLE TYPES + + +class Document(SimpleType): + tagname = 'natureDocument' + allowed_values = ('CPI', 'EXTAF', 'EXTSF', 'EXTPL') + + +class Origin(SimpleType): + tagname = 'origine' + allowed_values = ('internet', 'guichet', 'courrier') + + +class Title(SimpleType): + tagname = 'genre' + allowed_values = ('M', 'Mme', 'Mlle') + + +class Sex(SimpleType): + tagname = 'sexe' + allowed_values = ('F', 'M', 'NA') + + +class Certificate(SimpleType): + tagname = 'natureEvenement' + allowed_values = ('NAI', 'DEC', 'MAR', 'REC') + + +class ConcernedKind(SimpleType): + tagname = 'typeInteresse' + allowed_values = ('reconnu', 'auteur') + + +# COMPLEX TYPES + + +class Names(ComplexType): + tagname = 'noms' + sequence = ('nomDeFamille', 'nomUsgae', 'typeUsage') + + def __init__(self, data): + super(Names, self).__init__(data) + if self.data.get('lastname'): + self.nomDeFamille = self.data['lastname'] + self.nomUsage = self.data.get('usual_name', '') + self.typeUsage = self.data.get('name_usage', '') + + +class Place(ComplexType): + tagname = 'lieu' + sequence = ('ville', 'province', 'pays') + + def __init__(self, data): + super(Place, self).__init__(data) + if self.data.get('city'): + self.ville = self.data['city'] + self.province = self.data.get('county', '') + self.pays = self.data.get('country', '') + + +class Address(ComplexType): + tagname = 'adresse' + sequence = ('ligneAdr1', 'ligneAdr2', 'codePostal', + 'lieu', 'mail', 'tel') + pattern = 'address_' + + def __init__(self, data): + super(Address, self).__init__(data) + self.ligneAdr1 = self.data.get('street', '') + self.ligneAdr2 = self.data.get('complement', '') + self.lieu = Place(self.data) + self.mail = self.data.get('email', '') + self.tel = self.data.get('phone', '') + + +class Birth(ComplexType): + tagname = 'naissance' + sequence = ('date', 'lieu') + pattern = 'birth_' + + def __init__(self, data): + super(Birth, self).__init__(data) + birthdate = self.data.get('date', None) + if birthdate: + self.date = BirthDate(birthdate) + self.lieu = Place(self.data) + + +class EventDate(ComplexType): + tagname = 'dateEvenement' + sequence = ('dateDebut', 'dateFin') + + def __init__(self, data): + super(EventDate, self).__init__(data) + self.dateDebut = StartDate(self.data['event_date_start']) + if data.get('event_date_end', None): + self.dateFin = EndDate(self.data['event_date_end']) + + +class EventPlace(Place): + tagname = 'lieuEvenement' + pattern = 'event_' + + +class Person(ComplexType): + sequence = ('noms', 'prenoms', 'genre', 'adresse', 'sexe', + 'pere', 'mere', 'naissance') + + def __init__(self, data): + super(Person, self).__init__(data) + self.noms = Names(self.data) + self.prenoms = self.data.get('firstnames', '') + if self.data.get('title', None): + self.genre = Title(self.data['title']) + self.adresse = Address(self.data) + if self.data.get('sex', None): + self.sexe = Sex(self.data['sex']) + self.naissance = Birth(self.data) + + +class ApplicantPerson(Person): + tagname = 'individu' + pattern = 'applicant_' + sequence = ('noms', 'prenoms', 'genre', 'adresse') + + +class Parent(Person): + sequence = ('noms', 'prenoms', 'genre') + + def __init__(self, data, pattern): + self.pattern = pattern + super(Parent, self).__init__(data) + sex = self.data.get('sex') + if sex: + if sex == 'M': + self.tagname = 'pere' + else: + self.tagname = 'mere' + else: + if pattern.startswith('parent1'): + self.tagname = 'pere' + else: + self.tagname = 'mere' + + +class ConcernedCommon(Person): + sequence = ('noms', 'prenoms', 'genre', 'sexe', + 'parent1', 'parent2', 'naissance') + + def __init__(self, data): + super(ConcernedCommon, self).__init__(data) + self.parent1 = Parent(self.data, 'parent1_') + self.parent2 = Parent(self.data, 'parent2_') + + +class Concerned(ConcernedCommon): + tagname = 'interesse' + pattern = 'concerned_' + + +class Partner(ConcernedCommon): + tagname = 'conjoint' + pattern = 'partner_' + + +class Applicant(ComplexType): + tagname = 'demandeur' + sequence = ('qualiteDemandeur', 'individu') + + def __init__(self, data): + self.qualiteDemandeur = data.get('applicant_status', '') + self.individu = ApplicantPerson(data) + + +class Event(ComplexType): + tagname = 'evenement' + sequence = ('interesse', 'conjoint', 'natureEvenement', + 'typeInteresse', 'dateEvenement', 'lieuEvenement') + + def __init__(self, data): + certificate_type = data['certificate_type'] + self.interesse = Concerned(data) + if certificate_type == 'MAR': + self.conjoint = Partner(data) + self.natureEvenement = Certificate(data['certificate_type']) + if data.get('concerned_kind', None): + self.typeInteresse = ConcernedKind(data['concerned_kind']) + self.dateEvenement = EventDate(data) + self.lieuEvenement = EventPlace(data) + + +class CivilStatusApplication(ComplexType): + tagname = 'demandeEtatCivil' + sequence = ( + 'identifiant', 'demandeur', 'natureDocument', 'nbExemplaire', + 'dateDemande', 'evenement', 'motif', 'origine', 'commentaire') + + def __init__(self, data): + self.identifiant = data['application_id'] + self.demandeur = Applicant(data) + self.natureDocument = Document(data['document_type']) + self.nbExemplaire = data['document_copies'] + self.dateDemande = data['application_time'] + self.evenement = Event(data) + self.motif = data.get('application_reason', '') + if data.get('application_origin', None): + self.origine = Origin(data['application_origin']) + self.commentaire = data.get('application_comment', '') + + def zip(self, path, filename): + basename = os.path.join(path, filename) + archname = basename + '.zip' + filepath = basename + '.xml' + content = etree.tostring(self.xml, pretty_print=True) + default_storage.save(filepath, ContentFile(content)) + with zipfile.ZipFile(archname, 'w') as zipf: + zipf.write(filepath, os.path.basename(filepath)) + return archname diff --git a/passerelle/apps/cityweb/migrations/0001_initial.py b/passerelle/apps/cityweb/migrations/0001_initial.py new file mode 100644 index 0000000..122a40c --- /dev/null +++ b/passerelle/apps/cityweb/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0005_resourcelog'), + ] + + operations = [ + migrations.CreateModel( + name='CityWeb', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('title', models.CharField(max_length=50)), + ('slug', models.SlugField()), + ('description', models.TextField()), + ('log_level', models.CharField(default=b'INFO', max_length=10, verbose_name='Log Level', choices=[(b'NOTSET', b'NOTSET'), (b'DEBUG', b'DEBUG'), (b'INFO', b'INFO'), (b'WARNING', b'WARNING'), (b'ERROR', b'ERROR'), (b'CRITICAL', b'CRITICAL'), (b'FATAL', b'FATAL')])), + ('users', models.ManyToManyField(to='base.ApiUser', blank=True)), + ], + options={ + 'verbose_name': "CityWeb - Demande d'acte d'\xe9tat civil", + }, + ), + migrations.CreateModel( + name='Demand', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('received_at', models.DateTimeField()), + ('demand_id', models.CharField(max_length=32, serialize=False, primary_key=True)), + ('kind', models.CharField(max_length=32)), + ('resource', models.ForeignKey(to='cityweb.CityWeb')), + ], + ), + ] diff --git a/passerelle/apps/cityweb/migrations/__init__.py b/passerelle/apps/cityweb/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/passerelle/apps/cityweb/models.py b/passerelle/apps/cityweb/models.py new file mode 100644 index 0000000..82986f8 --- /dev/null +++ b/passerelle/apps/cityweb/models.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2017 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 . + +import os +import json + +from dateutil.parser import parse as dateutil_parse + +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from django.core.files.storage import default_storage + +from passerelle.base.models import BaseResource +from passerelle.utils.api import endpoint +from passerelle.utils.jsonresponse import APIError + +from .cityweb import CivilStatusApplication + + +CERTIFICATE_TYPES = [ + {"id": "NAI", "text": "Naissance"}, + {"id": "MAR", "text": "Mariage"}, + {"id": "REC", "text": "Reconnaissance"}, + {"id": "DEC", "text": "Décès"} +] + +SEXES = [ + {"id": "M", "text": "Homme"}, + {"id": "F", "text": "Femme"}, + {"id": "NA", "text": "Autre"} +] + +TITLES = [ + {"id": "M", "text": "Monsieur"}, + {"id": "Mme", "text": "Madame"}, + {"id": "Mlle", "text": "Mademoiselle"} +] + +DOCUMENT_TYPES = [ + {"id": "CPI", "text": "Copie intégrale"}, + {"id": "EXTAF", "text": "Extrait avec filiation"}, + {"id": "EXTSF", "text": "Extrait sans filiation"}, + {"id": "EXTPL", "text": "Extrait plurilingue"} +] + +CONCERNED = [ + {"id": "reconnu", "text": "Reconnu"}, + {"id": "auteur", "text": "Auteur"} +] + +ORIGINS = [ + {"id": "internet", "text": "Internet"}, + {"id": "guichet", "text": "Guichet"}, + {"id": "courrier", "text": "Courrier"} +] + + +class CityWeb(BaseResource): + category = _('Business Process Connectors') + + class Meta: + verbose_name = "CityWeb - Demande d'acte d'état civil" + + @classmethod + def get_verbose_name(cls): + return cls._meta.verbose_name + + @endpoint(perm='can_access', methods=['post'], description=_('Create a demand')) + def create(self, request, *args, **kwargs): + payload = json.loads(request.body) + # check mandatory keys + for key in ('application_id', 'application_time', 'certificate_type'): + if key not in payload: + raise APIError('<%s> is required' % key) + + attrs = { + 'demand_id': '%s-%s' % (payload['application_id'], payload['certificate_type']), + 'kind': payload['certificate_type'], 'resource': self, + 'received_at': dateutil_parse(payload['application_time'])} + + demand, created = Demand.objects.get_or_create(**attrs) + result = demand.create(payload) + return {'data': {'demand_id': result}} + + @endpoint(perm='can_access', description=_('Get title list')) + def titles(self, request): + return {'data': TITLES} + + @endpoint(perm='can_access', description=_('Get sex list')) + def sexes(self, request): + return {'data': SEXES} + + @endpoint(perm='can_access', description=_('Get concerned status list')) + def concerned(self, request): + return {'data': CONCERNED} + + @endpoint(perm='can_access', description=_('Get application origin list')) + def origins(self, request): + return {'data': ORIGINS} + + @endpoint(name='certificate-types', perm='can_access', + description=_('Get certificate type list'), parameters={'exclude': {'example_value': 'REC'}}) + def certificate_types(self, request, exclude=''): + return {'data': [item for item in CERTIFICATE_TYPES + if item.get('id') not in exclude.split(',')]} + + @endpoint(name='document-types', perm='can_access', + description=_('Get document type list'), parameters={'exclude': {'example_value': 'EXTPL'}}) + def document_types(self, request, exclude=''): + return {'data': [item for item in DOCUMENT_TYPES + if item.get('id') not in exclude.split(',')]} + + +class Demand(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + received_at = models.DateTimeField() + resource = models.ForeignKey(CityWeb) + demand_id = models.CharField(max_length=32, primary_key=True) + kind = models.CharField(max_length=32) + + def __unicode__(self): + return '%s - %s' % (self.resource.slug, self.demand_id) + + @property + def basepath(self): + return os.path.join( + default_storage.path('cityweb'), self.resource.slug) + + def create(self, data): + application = CivilStatusApplication(data) + application.zip(self.basepath, self.demand_id) + return self.demand_id diff --git a/passerelle/settings.py b/passerelle/settings.py index 23abc37..446d77b 100644 --- a/passerelle/settings.py +++ b/passerelle/settings.py @@ -116,6 +116,7 @@ INSTALLED_APPS = ( 'passerelle.apps.airquality', 'passerelle.apps.okina', 'passerelle.apps.cmis', + 'passerelle.apps.cityweb', # backoffice templates and static 'gadjo', ) diff --git a/setup.py b/setup.py index 8baa535..82da648 100755 --- a/setup.py +++ b/setup.py @@ -98,6 +98,7 @@ setup(name='passerelle', 'pyexcel-xls >= 0.4, <0.5', 'cmislib >= 0.5, <0.6', 'feedparser', + 'lxml' ], cmdclass={ 'build': build, diff --git a/tests/data/cityweb/cityweb.xsd b/tests/data/cityweb/cityweb.xsd new file mode 100644 index 0000000..be64220 --- /dev/null +++ b/tests/data/cityweb/cityweb.xsd @@ -0,0 +1,181 @@ + + + + + + Root element + + + + + + + utile si différent de l'interessé + + + + + copie intégrale, extrait avec ou sans filiation, plurilingue + + + + + + + + + origine de la demande pour l'état civil : courrier, guichet ou internet + + + + + champ commentaire + + + + + + + + + Naissance, mariage, décès, reconnaissance + + + + + + + + + + + + + + + + + + + + + + + Informations sur le demandeur + + + + + avocat, notaire, père, mère... + + + + + + + + permet de gérer le sexe indeterminé + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Seulement pour les mariages + + + + + naissance, mariage, décès, reconnaissance + + + + + necessaire pour les reconnaissances seulement, l'interessé pouvant etre le parent ou le reconnu, il faut donc préciser. + + + + + + + + + + + + + précission sur le nom d'usage: nom d'épouse, veuve, d'usage... + + + + + + + + + + + + + + + + + + + + + + + + + + Permet de gérer les dates incomplètes + + + + diff --git a/tests/data/cityweb/payload_birth.json b/tests/data/cityweb/payload_birth.json new file mode 100644 index 0000000..d58b518 --- /dev/null +++ b/tests/data/cityweb/payload_birth.json @@ -0,0 +1,50 @@ +{ + "applicant_address_city": "Nancy", + "applicant_address_complement": "Bat A", + "applicant_address_country": "France", + "applicant_address_county": "Meurthe-et-Moselle", + "applicant_address_email": "chelsea@whatever.com", + "applicant_address_phone": "+33 6 55 44 22 11", + "applicant_address_street": "37 Rue du Cheval Blanc", + "applicant_address_zipcode": "54000", + "applicant_firstnames": "Kim Chelsea", + "applicant_lastname": "Whatever", + "applicant_name_usage": "nom d'epouse", + "applicant_status": "concerne", + "applicant_title": "Mme", + "applicant_title_label": "Madame", + "applicant_usual_name": "Whatever", + "application_id": "15-4", + "application_origin": "internet", + "application_reason": "They are just messy", + "application_time": "2016-10-20T14:41:20Z", + "certificate_type": "NAI", + "certificate_type_label": "Acte de naissance", + "concerned_birth_city": "Harare", + "concerned_birth_country": "Zimbabwe", + "concerned_birth_county": "", + "concerned_birth_date": "1980-02-29", + "concerned_firstnames": "Kevin", + "concerned_lastname": "Whatever", + "concerned_name_usage": "", + "concerned_parent1_firstnames": "John Oliver", + "concerned_parent1_lastname": "Smith", + "concerned_parent1_name_usage": "Smith", + "concerned_parent1_title": "M", + "concerned_parent1_title_label": "Monsieur", + "concerned_parent1_usual_name": "Smith", + "concerned_parent2_firstnames": "Kim", + "concerned_parent2_lastname": "Smith", + "concerned_parent2_name_usage": "nom d'\u00e9pouse", + "concerned_parent2_title": "Mme", + "concerned_parent2_usual_name": "Smith", + "concerned_sex": "M", + "concerned_title": "M", + "concerned_usual_name": "Whatever", + "document_copies": "1", + "document_type": "CPI", + "document_type_label": "Copie Integrale", + "event_city": "Nancy", + "event_date_end": "", + "event_date_start": "2012-07-14" +} diff --git a/tests/data/cityweb/payload_death.json b/tests/data/cityweb/payload_death.json new file mode 100644 index 0000000..2264692 --- /dev/null +++ b/tests/data/cityweb/payload_death.json @@ -0,0 +1,37 @@ +{ + "applicant_address_city": "Nancy", + "applicant_address_complement": "Bat A", + "applicant_address_country": "France", + "applicant_address_county": "Meurthe-et-Moselle", + "applicant_address_email": "chelsea@whatever.com", + "applicant_address_phone": "+33 6 55 44 22 11", + "applicant_address_street": "37 Rue du Cheval Blanc", + "applicant_address_zipcode": "54000", + "applicant_firstnames": "Kim Chelsea", + "applicant_lastname": "Whatever", + "applicant_name_usage": "nom d'epouse", + "applicant_status": "concerne", + "applicant_title": "Mme", + "applicant_title_label": "Madame", + "applicant_usual_name": "Whatever", + "application_origin": "internet", + "application_id": "17-1", + "application_reason": "", + "application_time": "2016-10-20T14:41:20Z", + "certificate_type": "DEC", + "certificate_type_label": "Acte de d\u00e9c\u00e8s", + "concerned_birth_city": "Harare", + "concerned_birth_country": "Zimbabwe", + "concerned_birth_county": "", + "concerned_birth_date": "1980-02-29", + "concerned_firstnames": "Kevin", + "concerned_lastname": "Whatever", + "concerned_sex": "M", + "concerned_title": "M", + "concerned_usual_name": "Whatever", + "document_copies": "1", + "document_type": "EXTSF", + "document_type_label": "Extrait sans filiation", + "event_city": "Nancy", + "event_date_start": "2012-07-14" +} diff --git a/tests/data/cityweb/payload_mariage.json b/tests/data/cityweb/payload_mariage.json new file mode 100644 index 0000000..a64f826 --- /dev/null +++ b/tests/data/cityweb/payload_mariage.json @@ -0,0 +1,68 @@ +{ + "applicant_address_city": "Nancy", + "applicant_address_complement": "Bat A", + "applicant_address_country": "France", + "applicant_address_county": "Meurthe-et-Moselle", + "applicant_address_email": "chelsea@whatever.com", + "applicant_address_phone": "+33 6 55 44 22 11", + "applicant_address_street": "37 Rue du Cheval Blanc", + "applicant_address_zipcode": "54000", + "applicant_firstnames": "Kim Chelsea", + "applicant_lastname": "Whatever", + "applicant_name_usage": "nom d'epouse", + "applicant_status": "concerne", + "applicant_title": "Mme", + "applicant_title_label": "Madame", + "applicant_usual_name": "Whatever", + "application_id": "16-1", + "application_origin": "internet", + "application_reason": "Happy mariage", + "application_time": "2016-10-20T14:41:20Z", + "certificate_type": "MAR", + "certificate_type_label": "Acte de naissance", + "concerned_birth_city": "Harare", + "concerned_birth_country": "Zimbabwe", + "concerned_birth_county": "", + "concerned_birth_date": "1980-02-29", + "concerned_parent1_firstnames": "John Oliver", + "concerned_parent1_lastname": "Smith", + "concerned_parent1_name_usage": "Smith", + "concerned_parent1_title": "M", + "concerned_parent1_title_label": "Monsieur", + "concerned_parent1_usual_name": "Smith", + "concerned_firstnames": "Kevin", + "concerned_lastname": "Whatever", + "concerned_parent2_firstnames": "Kim", + "concerned_parent2_lastname": "Smith", + "concerned_parent2_name_usage": "nom d'\u00e9pouse", + "concerned_parent2_title": "Mme", + "concerned_parent2_usual_name": "Smith", + "concerned_name_usage": "", + "concerned_sex": "M", + "concerned_title": "M", + "document_copies": "1", + "document_type": "CPI", + "document_type_label": "Copie Integrale", + "event_city": "Nancy", + "event_date_end": "", + "event_date_start": "2012-07-14", + "partner_birth_city": "Harare", + "partner_birth_country": "Zimbabwe", + "partner_birth_county": "", + "partner_birth_date": "1984-02-29", + "partner_parent1_firstnames": "Antonio", + "partner_parent1_lastname": "Scaramucci", + "partner_parent1_title": "M", + "partner_parent1_title_label": "Monsieur", + "partner_firstnames": "Chelsea", + "partner_lastname": "Contrao", + "partner_parent2_firstnames": "Marguerite", + "partner_parent2_lastname": "Scaramucci", + "partner_parent2_name_usage": "nom d'\u00e9pouse", + "partner_parent2_title": "Mme", + "partner_parent2_usual_name": "Gaye", + "partner_name_usage": "", + "partner_sex": "F", + "partner_title": "Mme", + "partner_usual_name": "Scaramucci" +} diff --git a/tests/test_cityweb.py b/tests/test_cityweb.py new file mode 100644 index 0000000..1a5696f --- /dev/null +++ b/tests/test_cityweb.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- +# Passerelle - uniform access to data and services +# Copyright (C) 2017 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; exclude 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.deepcopy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import unicode_literals + +import os +import json +import shutil + +import pytest +import mock +from lxml import etree, objectify as xobject + +import utils + +from passerelle.apps.cityweb.models import CityWeb + + +def get_test_base_dir(name): + return os.path.join(os.path.dirname(__file__), 'data', name) + + +def get_file_from_test_base_dir(filename): + path = os.path.join(get_test_base_dir('cityweb'), filename) + with open(path, 'rb') as fd: + return fd.read() + + +@pytest.fixture +def setup(db): + return utils.setup_access_rights(CityWeb.objects.create(slug='test')) + + +PAYLOAD = [ + { + 'birth': json.loads(get_file_from_test_base_dir('payload_birth.json')) + }, + { + 'mariage': json.loads(get_file_from_test_base_dir('payload_mariage.json')) + }, + { + 'death': json.loads(get_file_from_test_base_dir('payload_death.json')) + } +] + + +@pytest.fixture(params=PAYLOAD) +def payload(request): + return request.param + + +def assert_xml_doc(doc): + schema = etree.XMLSchema( + etree.parse(os.path.join(get_test_base_dir('cityweb'), 'cityweb.xsd'))) + schema.assertValid(doc) + + +@mock.patch('passerelle.apps.cityweb.models.default_storage.path', get_test_base_dir) +def test_demand_creation(app, setup, payload): + url = '/cityweb/test/create/' + if 'birth' in payload: + response = app.post_json(url, params=payload['birth']) + assert response.json['data']['demand_id'] == '15-4-NAI' + doc = xobject.parse( + os.path.join(get_test_base_dir('cityweb'), 'test', '15-4-NAI.xml')) + assert_xml_doc(doc) + + elif 'mariage' in payload: + response = app.post_json(url, params=payload['mariage']) + assert response.json['data']['demand_id'] == '16-1-MAR' + doc = etree.parse( + os.path.join(get_test_base_dir('cityweb'), 'test', '16-1-MAR.xml')) + assert_xml_doc(doc) + else: + response = app.post_json(url, params=payload['death']) + assert response.json['data']['demand_id'] == '17-1-DEC' + doc = etree.parse( + os.path.join(get_test_base_dir('cityweb'), 'test', '17-1-DEC.xml')) + assert_xml_doc(doc) + + +def test_datasource_titles(app, setup): + response = app.get('/cityweb/test/titles/') + data = response.json['data'] + assert len(data) == 3 + for datum in data: + if datum['id'] == 'M': + assert datum['text'] == 'Monsieur' + elif datum['id'] == 'Mme': + assert datum['text'] == 'Madame' + else: + assert datum['id'] == 'Mlle' + assert datum['text'] == 'Mademoiselle' + + +def test_datasource_sexes(app, setup): + response = app.get('/cityweb/test/sexes/') + data = response.json['data'] + assert len(data) == 3 + for datum in data: + if datum['id'] == 'M': + assert datum['text'] + elif datum['id'] == 'F': + assert datum['text'] == 'Femme' + else: + assert datum['id'] == 'NA' + assert datum['text'] == 'Autre' + + +def test_datasource_concerned(app, setup): + response = app.get('/cityweb/test/concerned/') + data = response.json['data'] + assert len(data) == 2 + for datum in data: + if datum['id'] == 'reconnu': + assert datum['text'] == 'Reconnu' + else: + assert datum['id'] == 'auteur' + assert datum['text'] == 'Auteur' + + +def test_datasource_origins(app, setup): + response = app.get('/cityweb/test/origins/') + data = response.json['data'] + assert len(data) == 3 + for datum in data: + if datum['id'] == 'internet': + assert datum['text'] == 'Internet' + elif datum['id'] == 'guichet': + assert datum['text'] == 'Guichet' + else: + assert datum['id'] == 'courrier' + assert datum['text'] == 'Courrier' + + +def test_datasource_document_types(app, setup): + response = app.get('/cityweb/test/document-types/') + data = response.json['data'] + assert len(data) == 4 + for datum in data: + if datum['id'] == 'CPI': + assert datum['text'] == 'Copie intégrale' + elif datum['id'] == 'EXTAF': + assert datum['text'] == 'Extrait avec filiation' + elif datum['id'] == 'EXTSF': + assert datum['text'] == 'Extrait sans filiation' + else: + datum['id'] == 'EXTPL' + datum['text'] == 'Extrait plurilingue' + + params = {'exclude': 'EXTAF,EXTSF,EXTPL'} + response = app.get('/cityweb/test/document-types/', params=params) + data = response.json['data'] + assert len(data) == 1 + assert data[0]['id'] == 'CPI' + assert data[0]['text'] == 'Copie intégrale' + + +def test_datasource_certificate_types(app, setup): + response = app.get('/cityweb/test/certificate-types/') + data = response.json['data'] + assert len(data) == 4 + for datum in data: + if datum['id'] == 'NAI': + assert datum['text'] == 'Naissance' + elif datum['id'] == 'MAR': + assert datum['text'] == 'Mariage' + elif datum['id'] == 'REC': + assert datum['text'] == 'Reconnaissance' + else: + assert datum['id'] == 'DEC' + assert datum['text'] == 'Décès' + + +def teardown_module(module): + shutil.rmtree(os.path.join(get_test_base_dir('cityweb'), 'test'), ignore_errors=True) diff --git a/tox.ini b/tox.ini index 8c2d0fc..b416d7c 100644 --- a/tox.ini +++ b/tox.ini @@ -23,6 +23,7 @@ deps = pylint pylint-django django-webtest + lxml commands = ./getmagic.sh py.test {env:FAST:} {env:COVERAGE:} {posargs:tests/} -- 2.11.0