Projet

Général

Profil

0006-cityweb-add-demands-processing-15883.patch

Josué Kouka, 31 mai 2017 13:53

Télécharger (9,84 ko)

Voir les différences:

Subject: [PATCH 6/6] cityweb: add demands processing (#15883)

 passerelle/contrib/cityweb/cityweb.py | 96 +++++++++++++++++++++++++++++++++++
 passerelle/contrib/cityweb/models.py  | 34 ++++++++++---
 tests/test_cityweb.py                 | 36 ++++++++++---
 3 files changed, 152 insertions(+), 14 deletions(-)
 create mode 100644 passerelle/contrib/cityweb/cityweb.py
passerelle/contrib/cityweb/cityweb.py
1
# Copyright (C) 2017  Entr'ouvert
2
#
3
# This program is free software: you can redistribute it and/or modify it
4
# under the terms of the GNU Affero General Public License as published
5
# by the Free Software Foundation, either version 3 of the License, or
6
# (at your option) any later version.
7
#
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU Affero General Public License for more details.
12
#
13
# You should have received a copy of the GNU Affero General Public License
14
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15

  
16
import os
17
from collections import OrderedDict
18

  
19
from dateutil.parser import parse as dateutil_parse
20

  
21
from django.core.files.storage import default_storage
22
from django.core.files.base import ContentFile
23

  
24
from passerelle.utils.jsonresponse import APIError
25
from .utils import ElementFactory, json_to_xml, zipdir, etree
26
from .mapping import MAPPING
27

  
28

  
29
ALLOWED_VALUES = {
30
    '_title_raw': ('M', 'Mme', 'Mlle'),  # {applicant_, concerned_, partner_}{father, mother}
31
    '_gender_raw': ('M', 'F', 'NA'),  # {applicant_, concerned_, partner_}{father, mother}
32
    'document_kind_raw': ('CPI', 'EXTAF', 'EXTSF', 'EXTPL'),
33
    'concerned_kind_raw': ('reconnu', 'auteur'),
34
    'event_kind_raw': ('NAI', 'MAR', 'REC', 'DEC'),
35
    'canal_raw': ('internet', 'guichet', 'courrier')
36
}
37

  
38

  
39
def is_value_allowed(key, value, allowed_values):
40
    if value not in allowed_values:
41
        raise APIError('Invalid value for %s: %s not in %s'
42
                       % (key, value, allowed_values))
43

  
44

  
45
class CivilStatusApplication(object):
46
    root_element = 'demandeEtatCivil'
47
    mapping = MAPPING
48
    namespace = 'http://tempuri.org/XMLSchema.xsd'
49

  
50
    def __init__(self, demand_id, data):
51
        self.application_id = demand_id
52
        self.data = self.validate(data)
53

  
54
    def validate(self, data):
55
        data['application_id'] = self.application_id
56
        data['application_date'] = data['receipt_time']
57
        applicant_is_concerned = data.pop('applicant_is_concerned', False)
58
        if applicant_is_concerned:
59
            data['concerned_kind'] = 'auteur'
60

  
61
        for key in data.keys():
62
            # set all dates to ISO format
63
            if '_date' in key and data.get(key):
64
                if key == 'application_date':
65
                    continue
66
                data[key] = dateutil_parse(data[key]).date().isoformat()
67
            # check value validity for key with value constraint
68
            for pattern, allowed_values in ALLOWED_VALUES.items():
69
                if key.endswith(pattern) and data.get(key):
70
                    is_value_allowed(pattern, data[key], allowed_values)
71
            # if the applicant is the concerned person, then all
72
            # applicant vars values are copied
73
            if key.startswith('applicant_') and applicant_is_concerned:
74
                if key == 'applicant_kind':
75
                    continue
76
                data[key.replace('applicant', 'concerned')] = data[key]
77
        return data
78

  
79
    @property
80
    def xml(self):
81
        elements = OrderedDict()
82
        for var, elt in self.mapping.items():
83
            if self.data.get(var):
84
                elements[elt] = self.data[var]
85

  
86
        etree.register_namespace('xs', 'http://tempuri.org/XMLSchema.xsd')
87
        root = ElementFactory(self.root_element, namespace=self.namespace)
88
        for key, value in elements.items():
89
            path = key.split('_')
90
            root.append(json_to_xml(path, value, root, namespace=self.namespace), allow_new=False)
91
        return root
92

  
93
    def save(self, path):
94
        filepath = os.path.join(path, self.application_id + '.xml')
95
        default_storage.save(filepath, ContentFile(self.xml.to_pretty_xml()))
96
        return zipdir(path)
passerelle/contrib/cityweb/models.py
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import os
17 18
import json
18 19

  
20
from dateutil.parser import parse as dateutil_parse
21

  
19 22
from django.db import models
20 23
from django.utils.translation import ugettext_lazy as _
21 24

  
22 25
from passerelle.base.models import BaseResource
23 26
from passerelle.utils.api import endpoint
27
from passerelle.utils.jsonresponse import APIError
28

  
29
from .cityweb import CivilStatusApplication
30
from .utils import get_resource_base_dir, flatten_payload
24 31

  
25 32

  
26 33
EVENTS_KIND = [
......
74 81
    @endpoint(serializer_type='json-api', perm='can_access', methods=['post'])
75 82
    def create(self, request, *args, **kwargs):
76 83
        formdata = json.loads(request.body)
77
        demand, created = Demand.objects.get_or_create(resource=self, demand_id='demand_id',
78
                                                       kind='kind')
84
        formdata.update(flatten_payload(formdata.pop('fields'), formdata.pop('extra')))
85
        # check mandatory keys
86
        for key in ('display_id', 'receipt_time', 'event_kind_raw'):
87
            if key not in formdata:
88
                raise APIError('<%s> is required' % key)
89

  
90
        attrs = {
91
            'demand_id': '%s-%s' % (formdata['display_id'], formdata['event_kind_raw']),
92
            'kind': formdata['event_kind_raw'], 'resource': self,
93
            'received_at': dateutil_parse(formdata['receipt_time'])}
94

  
95
        demand, created = Demand.objects.get_or_create(**attrs)
79 96
        result = demand.create(formdata)
80 97
        return {'demand_id': result}
81 98

  
......
109 126
class Demand(models.Model):
110 127
    created_at = models.DateTimeField(auto_now_add=True)
111 128
    updated_at = models.DateTimeField(auto_now=True)
129
    received_at = models.DateTimeField()
112 130
    resource = models.ForeignKey(CityWeb)
113
    name = models.CharField(max_length=32)
131
    demand_id = models.CharField(max_length=32, primary_key=True)
114 132
    kind = models.CharField(max_length=32)
115 133

  
116 134
    def __unicode__(self):
117
        return '%s - %s - %s' % (self.resource.slug, self.name, self.kind)
135
        return '%s - %s - %s' % (self.resource.slug, self.demand_id)
118 136

  
119 137
    @property
120
    def filename(self):
121
        pass
138
    def filepath(self):
139
        return os.path.join(get_resource_base_dir(), self.resource.slug, self.demand_id)
122 140

  
123 141
    def create(self, data):
124
        pass
142
        application = CivilStatusApplication(self.demand_id, data)
143
        application.save(self.filepath)
144
        return self.demand_id
tests/test_cityweb.py
22 22

  
23 23
import pytest
24 24
import mock
25
from lxml import etree as letree
25 26

  
26 27
import utils
27 28

  
28 29
from passerelle.contrib.cityweb.models import CityWeb
29 30

  
30 31

  
31
CITYWEB_TEST_BASE_DIR = os.path.join(os.path.dirname(__file__), 'data', 'cityweb')
32
def get_test_base_dir():
33
    return os.path.join(os.path.dirname(__file__), 'data', 'cityweb')
32 34

  
33 35

  
34 36
def get_file_from_test_base_dir(filename):
35
    path = os.path.join(CITYWEB_TEST_BASE_DIR, filename)
37
    path = os.path.join(get_test_base_dir(), filename)
36 38
    with open(path, 'rb') as fd:
37 39
        return fd.read()
38 40

  
......
60 62
    return request.param
61 63

  
62 64

  
63
@mock.patch('passerelle.contrib.cityweb.utils.get_resource_base_dir', CITYWEB_TEST_BASE_DIR)
65
def assert_xml_doc(doc):
66
    schema = letree.XMLSchema(
67
        letree.parse(os.path.join(get_test_base_dir(), 'cityweb.xsd')))
68
    schema.assertValid(doc)
69

  
70

  
71
@mock.patch('passerelle.contrib.cityweb.models.get_resource_base_dir', get_test_base_dir)
64 72
def test_demand_creation(app, setup, payload):
73
    url = '/cityweb/test/create/'
65 74
    if 'naissance' in payload:
66
        pass
75
        response = app.post_json(url, params=payload['naissance'])
76
        assert response.json['data']['demand_id'] == '15-4-NAI'
77
        doc = letree.parse(
78
            os.path.join(get_test_base_dir(), 'test', '15-4-NAI', '15-4-NAI.xml'))
79
        assert_xml_doc(doc)
80

  
67 81
    elif 'mariage' in payload:
68
        pass
82
        response = app.post_json(url, params=payload['mariage'])
83
        assert response.json['data']['demand_id'] == '16-1-MAR'
84
        doc = letree.parse(
85
            os.path.join(get_test_base_dir(), 'test', '16-1-MAR', '16-1-MAR.xml'))
86
        assert_xml_doc(doc)
69 87
    else:
70
        pass
88
        response = app.post_json(url, params=payload['deces'])
89
        assert response.json['data']['demand_id'] == '17-1-DEC'
90
        doc = letree.parse(
91
            os.path.join(get_test_base_dir(), 'test', '17-1-DEC', '17-1-DEC.xml'))
92
        assert_xml_doc(doc)
71 93

  
72 94

  
73 95
def test_datasource_titles(app, setup):
......
165 187

  
166 188
def teardown_module(module):
167 189
    # remove test/inputs from fs
168
    shutil.rmtree(os.path.join(CITYWEB_TEST_BASE_DIR, 'test', 'inputs'), ignore_errors=True)
190
    shutil.rmtree(os.path.join(get_test_base_dir(), 'test'), ignore_errors=True)
169
-