0006-cityweb-add-demands-processing-15883.patch
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 |
- |