Projet

Général

Profil

0008-add-MDEL-DDPACS-connector-35818.patch

Benjamin Dauvergne, 24 octobre 2019 23:05

Télécharger (54,3 ko)

Voir les différences:

Subject: [PATCH 8/8] add MDEL DDPACS connector (#35818)

 passerelle/apps/mdel_ddpacs/__init__.py       |   0
 passerelle/apps/mdel_ddpacs/abstract.py       | 388 ++++++++++++++++++
 .../mdel_ddpacs/migrations/0001_initial.py    |  59 +++
 .../apps/mdel_ddpacs/migrations/__init__.py   |   0
 passerelle/apps/mdel_ddpacs/models.py         |  48 +++
 passerelle/apps/mdel_ddpacs/schema.xsd        | 129 ++++++
 .../mdel_ddpacs/templates/mdel/zip/doc.xml    | 106 +++++
 .../mdel_ddpacs/templates/mdel/zip/entete.xml |  25 ++
 .../templates/mdel/zip/manifest.json          |  17 +
 .../templates/mdel/zip/message.xml            |  66 +++
 passerelle/apps/mdel_ddpacs/utils.py          |  83 ++++
 passerelle/settings.py                        |   1 +
 passerelle/utils/zip.py                       |  39 ++
 tests/data/mdel_ddpacs/response_manifest.json |   9 +
 tests/data/mdel_ddpacs/response_message.xml   |  31 ++
 tests/data/mdel_ddpacs_expected.zip           | Bin 0 -> 7465 bytes
 tests/test_mdel_ddpacs.py                     | 148 +++++++
 17 files changed, 1149 insertions(+)
 create mode 100644 passerelle/apps/mdel_ddpacs/__init__.py
 create mode 100644 passerelle/apps/mdel_ddpacs/abstract.py
 create mode 100644 passerelle/apps/mdel_ddpacs/migrations/0001_initial.py
 create mode 100644 passerelle/apps/mdel_ddpacs/migrations/__init__.py
 create mode 100644 passerelle/apps/mdel_ddpacs/models.py
 create mode 100644 passerelle/apps/mdel_ddpacs/schema.xsd
 create mode 100644 passerelle/apps/mdel_ddpacs/templates/mdel/zip/doc.xml
 create mode 100644 passerelle/apps/mdel_ddpacs/templates/mdel/zip/entete.xml
 create mode 100644 passerelle/apps/mdel_ddpacs/templates/mdel/zip/manifest.json
 create mode 100644 passerelle/apps/mdel_ddpacs/templates/mdel/zip/message.xml
 create mode 100644 passerelle/apps/mdel_ddpacs/utils.py
 create mode 100644 tests/data/mdel_ddpacs/response_manifest.json
 create mode 100644 tests/data/mdel_ddpacs/response_message.xml
 create mode 100644 tests/data/mdel_ddpacs_expected.zip
 create mode 100644 tests/test_mdel_ddpacs.py
passerelle/apps/mdel_ddpacs/abstract.py
1
# coding: utf-8
2
# Passerelle - uniform access to data and services
3
# Copyright (C) 2019  Entr'ouvert
4
#
5
# This program is free software: you can redistribute it and/or modify it
6
# under the terms of the GNU Affero General Public License as published
7
# by the Free Software Foundation, either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
# GNU Affero General Public License for more details.
14
#
15
# You should have received a copy of the GNU Affero General Public License
16
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
17

  
18
from __future__ import unicode_literals
19

  
20
from collections import namedtuple
21
import inspect
22
import os
23
import re
24
import xml.etree.ElementTree as ET
25
import zipfile
26

  
27
from django.db import models, IntegrityError
28
from django.core.urlresolvers import reverse
29
from django.http import HttpResponse
30
from django.utils.translation import ugettext_lazy as _
31
from django.utils import six, functional
32

  
33
import xmlschema
34

  
35
import jsonfield
36

  
37
from passerelle.base.models import BaseResource, SkipJob
38
from passerelle.utils.api import endpoint
39
from passerelle.utils.jsonresponse import APIError
40
from passerelle.utils.zip import ZipTemplate
41
from passerelle.utils.conversion import exception_to_text
42

  
43
from passerelle.utils import json, xml, sftp
44

  
45
'''Base abstract models for implementing MDEL compatible requests.
46
'''
47

  
48
MDELStatus = namedtuple('MDELStatus', ['code', 'slug', 'label'])
49

  
50
MDEL_STATUSES = map(lambda t: MDELStatus(*t), [
51
    ('100', 'closed', _('closed')),
52
    ('20', 'rejected', _('rejected')),
53
    ('19', 'accepted', _('accepted')),
54
    ('17', 'information needed', _('information needed')),
55
    ('16', 'in progress', _('in progress')),
56
    ('15', 'invalid', _('invalid')),
57
    ('14', 'imported', _('imported')),
58
])
59

  
60
MDEL_STATUSES_BY_CODE = {mdel_status.code: mdel_status for mdel_status in MDEL_STATUSES}
61

  
62

  
63
class Resource(BaseResource):
64
    outcoming_sftp = sftp.SFTPField(
65
        verbose_name=_('Outcoming SFTP'),
66
        blank=True,
67
    )
68
    incoming_sftp = sftp.SFTPField(
69
        verbose_name=_('Incoming SFTP'),
70
        blank=True,
71
    )
72
    recipient_siret = models.CharField(
73
        verbose_name=_('SIRET'),
74
        max_length=128)
75
    recipient_service = models.CharField(
76
        verbose_name=_('Service'),
77
        max_length=128)
78
    recipient_guichet = models.CharField(
79
        verbose_name=_('Guichet'),
80
        max_length=128)
81
    code_insee = models.CharField(
82
        verbose_name=_('INSEE Code'),
83
        max_length=6)
84

  
85
    xsd_path = 'schema.xsd'
86
    xsd_root_element = None
87
    flow_type = 'flow_type CHANGEME'
88
    doc_type = 'doc_type CHANGEME'
89
    zip_manifest = 'mdel/zip/manifest.json'
90
    code_insee_id = 'CODE_INSEE'
91

  
92
    class Meta:
93
        abstract = True
94

  
95
    def check_status(self):
96
        if self.outcoming_sftp:
97
            with self.outcoming_sftp.client() as out_sftp:
98
                out_sftp.listdir()
99
            if self.incoming_sftp:
100
                with self.incoming_sftp.client() as in_sftp:
101
                    in_sftp.listdir()
102

  
103
    @classmethod
104
    def get_doc_xml_schema(cls):
105
        base_dir = os.path.dirname(inspect.getfile(cls))
106
        path = os.path.join(base_dir, cls.xsd_path)
107
        assert os.path.exists(path)
108
        return xmlschema.XMLSchema(path, converter=xmlschema.UnorderedConverter)
109

  
110
    @classmethod
111
    def get_doc_json_schema(cls):
112
        return xml.JSONSchemaFromXMLSchema(cls.get_doc_xml_schema(), cls.xsd_root_element).json_schema
113

  
114
    @classmethod
115
    def get_create_schema(cls):
116
        base_schema = cls.get_doc_json_schema()
117
        base_schema['properties'].update({
118
            'display_id': {'type': 'string'},
119
            'email': {'type': 'string'},
120
            'code_insee': {'type': 'string'},
121
        })
122
        base_schema.setdefault('required', []).append('display_id')
123
        return base_schema
124

  
125
    def _handle_create(self, request):
126
        try:
127
            raw_payload = json.loads(request.body)
128
        except (TypeError, ValueError):
129
            raise APIError('Invalid payload format: JSON expected')
130
        payload = json.unflatten(raw_payload)
131
        extra = payload.pop('extra', {})
132
        # w.c.s. pass non form fields in extra
133
        if isinstance(extra, dict):
134
            payload.update(extra)
135
        try:
136
            json.validate(payload, self.get_create_schema())
137
        except json.ValidationError as e:
138
            raise APIError('Invalid payload format: %s' % e)
139

  
140
        reference = 'A-' + payload['display_id']
141
        try:
142
            demand = self.demand_set.create(
143
                reference=reference,
144
                step=1,
145
                data=payload)
146
        except IntegrityError as e:
147
            return APIError('reference-non-unique', http_status=400,
148
                            data={'original_exc': exception_to_text(e)})
149
        self.add_job('push_demand', demand_id=demand.id)
150
        return self.status(request, demand)
151

  
152
    def push_demand(self, demand_id):
153
        demand = self.demand_set.get(id=demand_id)
154
        if not demand.push():
155
            raise SkipJob(after_timestamp=3600 * 6)
156

  
157
    @endpoint(perm='can_access',
158
              methods=['get'],
159
              description=_('Demand status'),
160
              pattern=r'(?P<demand_id>\d+)/$')
161
    def demand(self, request, demand_id):
162
        try:
163
            demand = self.demand_set.get(id=demand_id)
164
        except self.demand_set.model.DoesNotExist:
165
            raise APIError('demand not found', http_status=404)
166
        return self.status(request, demand)
167

  
168
    def status(self, request, demand):
169
        return {
170
            'id': demand.id,
171
            'status': demand.status,
172
            'url': request.build_absolute_uri(demand.status_url),
173
            'zip_url': request.build_absolute_uri(demand.zip_url),
174
        }
175

  
176
    @endpoint(perm='can_access',
177
              methods=['get'],
178
              description=_('Demand document'),
179
              pattern=r'(?P<demand_id>\d+)/.*$')
180
    def document(self, request, demand_id):
181
        try:
182
            demand = self.demand_set.get(id=demand_id)
183
        except self.demand_set.model.DoesNotExist:
184
            raise APIError('demand not found', http_status=404)
185
        response = HttpResponse(demand.zip_content, content_type='application/octet-stream')
186
        response['Content-Disposition'] = 'inline; filename=%s' % demand.zip_name
187
        return response
188

  
189
    @property
190
    def response_re(self):
191
        return re.compile(
192
            r'(?P<reference>[^-]+-[^-]+-[^-]+)-%s-'
193
            r'(?P<step>\d+).zip' % self.flow_type)
194

  
195
    def hourly(self):
196
        '''Get responses'''
197
        if not self.incoming_sftp:
198
            return
199
        try:
200
            with self.incoming_sftp.client() as client:
201
                for name in client.listdir():
202
                    m = self.response_re.match(name)
203
                    if not m:
204
                        self.logger.warning(
205
                            'pull responses: unexpected file "%s" in sftp, file name does not match pattern %s',
206
                            name, self.response_re)
207
                        continue
208
                    reference = m.groupdict()['reference']
209
                    step = int(m.groupdict()['step'])
210
                    demand = self.demand_set.filter(reference=reference).first()
211
                    if not demand:
212
                        self.logger.error(
213
                            'pull responses: unexpected file "%s" in sftp, no demand for reference "%s"',
214
                            name,
215
                            reference)
216
                        continue
217
                    if step < demand.step:
218
                        demand.logger.error(
219
                            'pull responses: unexpected file "%s" in sftp: step %s is inferior to demand step %s',
220
                            name,
221
                            step,
222
                            demand.step)
223
                        continue
224
                    demand.handle_response(sftp_client=client, filename=name, step=step)
225
        except sftp.paramiko.SSHException as e:
226
            self.logger.error('pull responses: sftp error %s', e)
227
            return
228

  
229

  
230
@six.python_2_unicode_compatible
231
class Demand(models.Model):
232
    STATUS_PENDING = 'pending'
233
    STATUS_PUSHED = 'pushed'
234
    STATUS_ERROR = 'error'
235

  
236
    STATUSES = [
237
        (STATUS_PENDING, _('pending')),
238
        (STATUS_PUSHED, _('pushed')),
239
        (STATUS_ERROR, _('error')),
240
    ]
241
    for mdel_status in MDEL_STATUSES:
242
        STATUSES.append((mdel_status.slug, mdel_status.label))
243

  
244
    created_at = models.DateTimeField(auto_now_add=True)
245
    updated_at = models.DateTimeField(auto_now=True)
246
    reference = models.CharField(max_length=32, null=False, unique=True)
247
    status = models.CharField(max_length=32, null=True, choices=STATUSES, default=STATUS_PENDING)
248
    step = models.IntegerField(default=0)
249
    data = jsonfield.JSONField()
250

  
251
    @functional.cached_property
252
    def logger(self):
253
        return self.resource.logger.context(
254
            demand_id=self.id,
255
            demand_status=self.status,
256
            demand_reference=self.reference)
257

  
258
    def push(self):
259
        if not self.resource.outcoming_sftp:
260
            return False
261
        try:
262
            with self.resource.outcoming_sftp.client() as client:
263
                with client.open(self.zip_name, mode='w') as fd:
264
                    fd.write(self.zip_content)
265
        except sftp.paramiko.SSHException as e:
266
            self.logger.error('push demand: %s failed, "%s"',
267
                              self,
268
                              exception_to_text(e))
269
            self.status = self.STATUS_ERROR
270
        except Exception as e:
271
            self.logger.exception('push demand: %s failed, "%s"',
272
                                  self,
273
                                  exception_to_text(e))
274
            self.status = self.STATUS_ERROR
275
        else:
276
            self.resource.logger.info('push demand: %s success', self)
277
            self.status = self.STATUS_PUSHED
278
        self.save()
279
        return True
280

  
281
    @functional.cached_property
282
    def zip_template(self):
283
        return ZipTemplate(self.resource.zip_manifest, ctx={
284
            'reference': self.reference,
285
            'flow_type': self.resource.flow_type,
286
            'doc_type': self.resource.doc_type,
287
            'step': '1',  # We never create more than one document for a reference
288
            'siret': self.resource.recipient_siret,
289
            'service': self.resource.recipient_service,
290
            'guichet': self.resource.recipient_guichet,
291
            'code_insee': self.data.get('code_insee', self.resource.code_insee),
292
            'document': self.document,
293
            'code_insee_id': self.resource.code_insee_id,
294
            'date': self.created_at.isoformat(),
295
            'email': self.data.get('email', ''),
296
        })
297

  
298
    @property
299
    def zip_name(self):
300
        return self.zip_template.name
301

  
302
    @property
303
    def zip_content(self):
304
        return self.zip_template.render_to_bytes()
305

  
306
    @property
307
    def document(self):
308
        xml_schema = self.resource.get_doc_xml_schema()
309
        return ET.tostring(
310
            xml_schema.elements[self.resource.xsd_root_element].encode(
311
                self.data[self.resource.xsd_root_element]))
312

  
313
    @property
314
    def status_url(self):
315
        return reverse(
316
            'generic-endpoint',
317
            kwargs={
318
                'connector': self.resource.get_connector_slug(),
319
                'slug': self.resource.slug,
320
                'endpoint': 'demand',
321
                'rest': '%s/' % self.id,
322
            })
323

  
324
    @property
325
    def zip_url(self):
326
        return reverse(
327
            'generic-endpoint',
328
            kwargs={
329
                'connector': self.resource.get_connector_slug(),
330
                'slug': self.resource.slug,
331
                'endpoint': 'document',
332
                'rest': '%s/%s' % (self.id, self.zip_name)
333
            })
334

  
335
    def handle_response(self, sftp_client, filename, step):
336
        try:
337
            with sftp_client.open(filename) as fd:
338
                with zipfile.ZipFile(fd) as zip_file:
339
                    with zip_file.open('message.xml') as fd:
340
                        tree = ET.parse(fd)
341
                ns = 'http://finances.gouv.fr/dgme/pec/message/v1'
342
                etat_node = tree.find('.//{%s}Etat' % ns)
343
                if etat_node is None:
344
                    self.logger.error(
345
                        'pull responses: missing Etat node in "%s"',
346
                        filename)
347
                    return
348
                etat = etat_node.text
349
                if etat in MDEL_STATUSES_BY_CODE:
350
                    self.status = MDEL_STATUSES_BY_CODE[etat].slug
351
                else:
352
                    self.logger.error(
353
                        'pull responses: unknown etat in "%s", etat="%s"',
354
                        filename,
355
                        etat)
356
                    return
357
                commentaire_node = tree.find('.//{%s}Etat' % ns)
358
                if commentaire_node is not None:
359
                    commentaire = commentaire_node.text
360
                    self.data = self.data or {}
361
                    self.data.setdefault('commentaires', []).append(commentaire)
362
                    self.data['commentaire'] = commentaire
363
                self.step = step + 1
364
                self.save()
365
                self.logger.info('pull responses: status of demand %s changed to %s',
366
                                 self, self.status)
367
        except sftp.paramiko.SSHException as e:
368
            self.logger.error(
369
                'pull responses: failed to read response "%s", %s',
370
                filename,
371
                exception_to_text(e))
372
        else:
373
            try:
374
                sftp_client.remove(filename)
375
            except sftp.paramiko.SSHException as e:
376
                self.logger.error(
377
                    'pull responses: failed to remove response "%s", %s',
378
                    filename,
379
                    exception_to_text(e))
380

  
381
    def __str__(self):
382
        return '<Demand %s reference:%s flow_type:%s>' % (
383
            self.id,
384
            self.reference,
385
            self.resource.flow_type)
386

  
387
    class Meta:
388
        abstract = True
passerelle/apps/mdel_ddpacs/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.20 on 2019-10-24 08:59
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6
import django.db.models.deletion
7
import jsonfield.fields
8
import passerelle.utils.sftp
9

  
10

  
11
class Migration(migrations.Migration):
12

  
13
    initial = True
14

  
15
    dependencies = [
16
        ('base', '0015_auto_20190921_0347'),
17
    ]
18

  
19
    operations = [
20
        migrations.CreateModel(
21
            name='Demand',
22
            fields=[
23
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
24
                ('created_at', models.DateTimeField(auto_now_add=True)),
25
                ('updated_at', models.DateTimeField(auto_now=True)),
26
                ('reference', models.CharField(max_length=32, unique=True)),
27
                ('status', models.CharField(choices=[('pending', 'pending'), ('pushed', 'pushed'), ('error', 'error'), ('closed', 'closed'), ('rejected', 'rejected'), ('accepted', 'accepted'), ('information needed', 'information needed'), ('in progress', 'in progress'), ('invalid', 'invalid'), ('imported', 'imported')], default='pending', max_length=32, null=True)),
28
                ('step', models.IntegerField(default=0)),
29
                ('data', jsonfield.fields.JSONField(default=dict)),
30
            ],
31
            options={
32
                'verbose_name': 'MDEL compatible DDPACS request',
33
            },
34
        ),
35
        migrations.CreateModel(
36
            name='Resource',
37
            fields=[
38
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
39
                ('title', models.CharField(max_length=50, verbose_name='Title')),
40
                ('description', models.TextField(verbose_name='Description')),
41
                ('slug', models.SlugField(unique=True, verbose_name='Identifier')),
42
                ('outcoming_sftp', passerelle.utils.sftp.SFTPField(blank=True, default=None, verbose_name='Outcoming SFTP')),
43
                ('incoming_sftp', passerelle.utils.sftp.SFTPField(blank=True, default=None, verbose_name='Incoming SFTP')),
44
                ('recipient_siret', models.CharField(max_length=128, verbose_name='SIRET')),
45
                ('recipient_service', models.CharField(max_length=128, verbose_name='Service')),
46
                ('recipient_guichet', models.CharField(max_length=128, verbose_name='Guichet')),
47
                ('code_insee', models.CharField(max_length=6, verbose_name='INSEE Code')),
48
                ('users', models.ManyToManyField(blank=True, related_name='_resource_users_+', related_query_name='+', to='base.ApiUser')),
49
            ],
50
            options={
51
                'verbose_name': 'MDEL compatible DDPACS request builder',
52
            },
53
        ),
54
        migrations.AddField(
55
            model_name='demand',
56
            name='resource',
57
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mdel_ddpacs.Resource'),
58
        ),
59
    ]
passerelle/apps/mdel_ddpacs/models.py
1
# coding: utf-8
2
# Passerelle - uniform access to data and services
3
# Copyright (C) 2019  Entr'ouvert
4
#
5
# This program is free software: you can redistribute it and/or modify it
6
# under the terms of the GNU Affero General Public License as published
7
# by the Free Software Foundation, either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
# GNU Affero General Public License for more details.
14
#
15
# You should have received a copy of the GNU Affero General Public License
16
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
17

  
18
from __future__ import unicode_literals
19

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

  
23
from passerelle.utils.api import endpoint
24

  
25
from . import abstract
26

  
27

  
28
class Resource(abstract.Resource):
29
    category = _('Civil Status Connectors')
30
    xsd_root_element = 'PACS'
31
    flow_type = 'depotDossierPACS'
32
    doc_type = 'flux-pacs'
33

  
34
    class Meta:
35
        verbose_name = _('MDEL compatible DDPACS request builder')
36

  
37
    @endpoint(perm='can_access',
38
              methods=['post'],
39
              description=_('Create request'))
40
    def create(self, request):
41
        return self._handle_create(request)
42

  
43

  
44
class Demand(abstract.Demand):
45
    resource = models.ForeignKey(Resource)
46

  
47
    class Meta:
48
        verbose_name = _('MDEL compatible DDPACS request')
passerelle/apps/mdel_ddpacs/schema.xsd
1
<?xml version="1.0" encoding="UTF-8"?>
2
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
3
    <xs:element name="PACS" type="PacsType"/>
4
    <xs:complexType name="PacsType">
5
        <xs:sequence>
6
            <xs:element name="partenaire1" type="PartenaireType" />
7
            <xs:element name="partenaire2" type="PartenaireType" />
8
            <xs:element name="convention" type="ConventionType" maxOccurs="1" minOccurs="1" />
9
            <xs:element name="residenceCommune" type="AdresseType" />
10
            <xs:element name="attestationHonneur" type="AttestationHonneurType" />
11
        </xs:sequence>
12
    </xs:complexType>
13
    <xs:complexType name = "AttestationHonneurType">
14
        <xs:sequence>
15
            <xs:element name="nonParente" type="xs:boolean"/>
16
            <xs:element name="residenceCommune" type="xs:boolean"/>
17
        </xs:sequence>
18
    </xs:complexType>
19
    <xs:complexType name="PartenaireType">
20
        <xs:sequence>
21
            <xs:element name="civilite" type="CiviliteType"></xs:element>
22
            <xs:element name="nomNaissance" type="xs:string" />
23
            <xs:element name="prenoms" type="xs:string" />
24
            <xs:element name="codeNationalite" type="xs:string" maxOccurs="unbounded"/>
25
            <xs:element name="jourNaissance" type="xs:integer" maxOccurs="1" minOccurs="0" />
26
            <xs:element name="moisNaissance" type="xs:integer" maxOccurs="1" minOccurs="0" />
27
            <xs:element name="anneeNaissance" type="xs:integer" />
28
            <xs:element name="LieuNaissance" type="LieuNaissanceType" />
29
            <xs:element name="ofpra" type="xs:boolean" />
30
            <xs:element name="mesureJuridique" type="xs:boolean" />
31
            <xs:element name="adressePostale" type="AdresseType" />
32
            <xs:element name="adresseElectronique" type="xs:string" />
33
            <xs:element name="telephone" type="xs:string" minOccurs="0"/>
34
            <xs:element name="filiationParent1" type="FiliationType" minOccurs="0"/>
35
            <xs:element name="filiationParent2" type="FiliationType" minOccurs="0" />
36
            <xs:element name="titreIdentiteVerifie" type="xs:boolean"/>
37
        </xs:sequence>
38
    </xs:complexType>
39
    <xs:complexType name="ConventionType">
40
        <xs:choice>
41
            <xs:element name="conventionType" type="ConventionTypeType" />
42
            <xs:element name="conventionSpecifique" type="xs:boolean" />
43
        </xs:choice>
44
    </xs:complexType>
45
    <xs:complexType name="ConventionTypeType">
46
        <xs:sequence>
47
            <xs:element name="aideMaterielMontant" type="xs:double" maxOccurs="1" minOccurs="0"/>
48
            <xs:element name="regimePacs" type="regimePacsType" />
49
            <xs:element name="aideMateriel" type="AideMaterielType" />
50
        </xs:sequence>
51
    </xs:complexType>
52
    <xs:complexType name="AdresseType">
53
        <xs:sequence>
54
            <xs:element name="NumeroLibelleVoie" type="xs:string" minOccurs="0" />
55
            <xs:element name="Complement1" type="xs:string" minOccurs="0" />
56
            <xs:element name="Complement2" type="xs:string" minOccurs="0" />
57
            <xs:element name="LieuDitBpCommuneDeleguee" type="xs:string" minOccurs="0" />
58
            <xs:element name="CodePostal" type="codePostalType" />
59
            <xs:element name="Localite" type="localiteType" />
60
            <xs:element name="Pays" type="xs:string" />
61
        </xs:sequence>
62
    </xs:complexType>
63
    <xs:complexType name="LieuNaissanceType">
64
        <xs:sequence>
65
            <xs:element name="localite" type="localiteType"/>
66
            <xs:element name="codePostal" type="xs:string"/>
67
            <xs:element name="codeInsee" type="xs:string" minOccurs="0"/>
68
            <xs:element name="departement" type="xs:string" maxOccurs="1" minOccurs="0"/>
69
            <xs:element name="codePays" type="xs:string"/>
70
        </xs:sequence>
71
    </xs:complexType>
72
    <xs:simpleType name="localiteType">
73
        <xs:restriction base="xs:string">
74
            <xs:minLength value="1" />
75
        </xs:restriction>
76
    </xs:simpleType>
77
    <xs:simpleType name="codePostalType">
78
        <xs:restriction base="xs:string">
79
            <xs:length value="5" />
80
        </xs:restriction>
81
    </xs:simpleType>
82
    <xs:simpleType name="regimePacsType">
83
        <xs:restriction base="xs:string">
84
            <xs:enumeration value="indivision"/>
85
            <xs:enumeration value="legal"/>
86
        </xs:restriction>
87
    </xs:simpleType>
88
    <xs:complexType name="FiliationType">
89
        <xs:sequence>
90
            <xs:choice>
91
                <xs:element name="filiationInconnu" type="xs:boolean"></xs:element>
92
                <xs:element name="filiationConnu" type="FiliationConnuType">
93
                </xs:element>
94
            </xs:choice>
95
        </xs:sequence>
96
    </xs:complexType>
97
    <xs:simpleType name="CiviliteType">
98
        <xs:restriction base="xs:string">
99
            <xs:enumeration value="M"></xs:enumeration>
100
            <xs:enumeration value="MME"></xs:enumeration>
101
        </xs:restriction>
102
    </xs:simpleType>
103
    <xs:simpleType name="TypeAideMaterielType">
104
        <xs:restriction base="xs:string">
105
            <xs:enumeration value="aideFixe"/>
106
            <xs:enumeration value="aideProportionnel"/>
107
        </xs:restriction>
108
    </xs:simpleType>
109
    <xs:complexType name="AideMaterielType">
110
        <xs:sequence>
111
            <xs:element name="typeAideMateriel" type="TypeAideMaterielType"></xs:element>
112
        </xs:sequence>
113
    </xs:complexType>
114
    <xs:complexType name="FiliationConnuType">
115
        <xs:sequence>
116
            <xs:element name="sexe" type="SexeType"/>
117
            <xs:element name="nomNaissance" type="xs:string" maxOccurs="1" minOccurs="0" />
118
            <xs:element name="prenoms" type="xs:string" maxOccurs="1" minOccurs="0" />
119
            <xs:element name="dateNaissance" type="xs:string" maxOccurs="1" minOccurs="0" />
120
            <xs:element name="lieuNaissance" type="LieuNaissanceType" maxOccurs="1" minOccurs="0" />
121
        </xs:sequence>
122
    </xs:complexType>
123
    <xs:simpleType name="SexeType">
124
        <xs:restriction base="xs:string">
125
            <xs:enumeration value="M"/>
126
            <xs:enumeration value="F"/>
127
        </xs:restriction>
128
    </xs:simpleType>
129
</xs:schema>
passerelle/apps/mdel_ddpacs/templates/mdel/zip/doc.xml
1
<?xml version="1.0" encoding="UTF-8" ?>
2
<PACS xmlns:xs="http://www.w3.org/2001/XMLSchema" >
3
    <partenaire1>
4
        <civilite>{{ partenaire1.civilite }}</civilite>
5
        <nomNaissance>{{ partenaire1.nom_naissance }}</nomNaissance>
6
        <prenoms>{{ partenaire1.prenoms }}</prenoms>
7
        {% for code_nationalite in partenaire1.code_nationalite %}
8
        <codeNationalite>{{ code_nationalite }}</codeNationalite>
9
        {% endfor %}
10
        <jourNaissance>{{ partenaire1.jour_naissance }}</jourNaissance>
11
        <moisNaissance>{{ partenaire1.mois_naissance }}</moisNaissance>
12
        <anneeNaissance>{{ partenaire1.annee_naissance }}</anneeNaissance>
13
        <LieuNaissance>
14
            <localite>{{ partenaire1.localite_naissance }}</localite>
15
            <codePostal>{{ partenaire1.codepostal_naissance }}</codePostal>
16
            <codeInsee>{{ partenaire1.codeinsee_naissance }}</codeInsee>
17
            <departement>{{ partenaire1.departement_naissance }}</departement>
18
            <codePays>{{ partenaire1.codepays_naissance }}</codePays>
19
        </LieuNaissance>
20
        <ofpra>{{ partenaire1.ofpra|yesno:"true,false" }}</ofpra>
21
        <mesureJuridique>{{ partenaire1.mesure_juridique }}</mesureJuridique>
22
        <adressePostale>
23
            <NumeroLibelleVoie>{{ partenaire1.adresse_numero_voie }}</NumeroLibelleVoie>
24
            <Complement1>{{ partenaire1.adresse_complement1 }}</Complement1>
25
            <Complement2>{{ partenaire1.adresse_complement2 }}</Complement2>
26
            <LieuDitBpCommuneDeleguee>{{ partenaire1.adresse_lieuditbpcommunedeleguee }}</LieuDitBpCommuneDeleguee>
27
            <CodePostal>{{ partenaire1.adresse_codepostal }}</CodePostal>
28
            <Localite>{{ partenaire1.adresse_localite }}</Localite>
29
            <Pays>{{ partenaire1.adresse_pays }}</Pays>
30
        </adressePostale>
31
        <adresseElectronique>{{ partenaire1.email }}</adresseElectronique>
32
        <telephone>{{ partenaire1.telephone }}</telephone>
33
        <titreIdentiteVerifie>{{ partenaire1.yesno:"true,false" }}</titreIdentiteVerifie>
34
    </partenaire1>
35
    <partenaire2>
36
        <civilite>{{ partenaire2.civilite }}</civilite>
37
        <nomNaissance>{{ partenaire2.nom_naissance }}</nomNaissance>
38
        <prenoms>{{ partenaire2.prenoms }}</prenoms>
39
        {% for code_nationalite in partenaire2.code_nationalite %}
40
        <codeNationalite>{{ code_nationalite }}</codeNationalite>
41
        {% endfor %}
42
        <jourNaissance>{{ partenaire2.jour_naissance }}</jourNaissance>
43
        <moisNaissance>{{ partenaire2.mois_naissance }}</moisNaissance>
44
        <anneeNaissance>{{ partenaire2.annee_naissance }}</anneeNaissance>
45
        <LieuNaissance>
46
            <localite>{{ partenaire2.localite_naissance }}</localite>
47
            <codePostal>{{ partenaire2.codepostal_naissance }}</codePostal>
48
            <codeInsee>{{ partenaire2.codeinsee_naissance }}</codeInsee>
49
            <departement>{{ partenaire2.departement_naissance }}</departement>
50
            <codePays>{{ partenaire2.codepays_naissance }}</codePays>
51
        </LieuNaissance>
52
        <ofpra>{{ partenaire2.ofpra|yesno:"true,false" }}</ofpra>
53
        <mesureJuridique>{{ partenaire2.mesure_juridique }}</mesureJuridique>
54
        <adressePostale>
55
            <NumeroLibelleVoie>{{ partenaire2.adresse_numero_voie }}</NumeroLibelleVoie>
56
            <Complement1>{{ partenaire2.adresse_complement1 }}</Complement1>
57
            <Complement2>{{ partenaire2.adresse_complement2 }}</Complement2>
58
            <LieuDitBpCommuneDeleguee>{{ partenaire2.adresse_lieuditbpcommunedeleguee }}</LieuDitBpCommuneDeleguee>
59
            <CodePostal>{{ partenaire2.adresse_codepostal }}</CodePostal>
60
            <Localite>{{ partenaire2.adresse_localite }}</Localite>
61
            <Pays>{{ partenaire2.adresse_pays }}</Pays>
62
        </adressePostale>
63
        <adresseElectronique>{{ partenaire2.email }}</adresseElectronique>
64
        <telephone>{{ partenaire2.telephone }}</telephone>
65
        <titreIdentiteVerifie>{{ partenaire2.yesno:"true,false" }}</titreIdentiteVerifie>
66
    </partenaire2>
67
    <convention>
68
        <conventionType>
69
            <aideMaterielMontant>100000</aideMaterielMontant>
70
            <regimePacs>legal</regimePacs>
71
            <aideMateriel>
72
                <typeAideMateriel>aideFixe</typeAideMateriel>
73
            </aideMateriel>
74
        </conventionType>
75
    </convention>
76
    <residenceCommune>
77
        <NumeroLibelleVoie>3 place du test</NumeroLibelleVoie>
78
        <CodePostal>05100</CodePostal>
79
        <Localite>VILLAR ST PANCRACE</Localite>
80
        <Pays></Pays>
81
    </residenceCommune>
82
    <attestationHonneur>
83
        <nonParente>true</nonParente>
84
        <residenceCommune>true</residenceCommune>
85
    </attestationHonneur>
86
    
87
</PACS>
88

  
89

  
90

  
91

  
92

  
93

  
94

  
95

  
96

  
97

  
98

  
99

  
100

  
101

  
102

  
103

  
104

  
105

  
106

  
passerelle/apps/mdel_ddpacs/templates/mdel/zip/entete.xml
1
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2
<EnteteMetierEnveloppe xmlns="http://finances.gouv.fr/dgme/gf/composants/teledemarchexml/donnee/metier">
3
    <NumeroDemarche>{{ flow_type }}</NumeroDemarche>
4
    <Teledemarche>
5
        <NumeroTeledemarche>{{ reference }}</NumeroTeledemarche>
6
        <!-- <MotDePasse></MotDePasse> -->
7
        <!-- <Suivi></Suivi> -->
8
        <Date>{{ date }}</Date>
9
        <IdentifiantPlateforme>Publik</IdentifiantPlateforme>
10
        <Email>{{ email }}</Email>
11
    </Teledemarche>
12
    <Routage>
13
        <Donnee>
14
            <Id>{{ code_insee_id }}</Id>
15
            <Valeur>{{ code_insee }}</Valeur>
16
        </Donnee>
17
    </Routage>
18
    <Document>
19
        <Code>{{ doc_type }}</Code>
20
        <Nom>{{ doc_type }}</Nom>
21
        <FichierFormulaire>
22
            <FichierDonnees>{{ reference }}-{{ flow_type }}-doc-{{ doc_type }}-1-1.xml</FichierDonnees>
23
        </FichierFormulaire>
24
    </Document>
25
</EnteteMetierEnveloppe>
passerelle/apps/mdel_ddpacs/templates/mdel/zip/manifest.json
1
{
2
  "name_template": "{{ reference }}-{{ flow_type }}-{{ step }}.zip",
3
  "part_templates": [
4
    {
5
      "name_template": "message.xml",
6
      "template_path": "message.xml"
7
    },
8
    {
9
      "name_template": "{{ reference }}-{{ flow_type }}-doc-{{ doc_type }}-1-1.xml",
10
      "content_expression": "document"
11
    },
12
    {
13
      "name_template": "{{ reference }}-{{ flow_type }}-ent-1.xml",
14
      "template_path": "entete.xml"
15
    }
16
  ]
17
}
passerelle/apps/mdel_ddpacs/templates/mdel/zip/message.xml
1
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2
<ns2:Message xmlns:ns2="http://finances.gouv.fr/dgme/pec/message/v1" xmlns="http://finances.gouv.fr/dgme/gf/composants/teledemarchexml/donnee/metier">
3
    <ns2:Header>
4
        <ns2:Routing>
5
          <ns2:MessageId>{{ reference }} {{ step }}</ns2:MessageId>
6
            <ns2:FlowType>{{ flow_type }}</ns2:FlowType>
7
            <ns2:Sender>
8
                <ns2:Country>FR</ns2:Country>
9
                <ns2:Location xsi:type="ns2:Location_FR" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
10
                    <ns2:Siret>13000210800012</ns2:Siret>
11
                    <ns2:Service>flux_GS_PEC_AVL</ns2:Service>
12
                    <ns2:Guichet></ns2:Guichet>
13
                </ns2:Location>
14
            </ns2:Sender>
15
            <ns2:Recipients>
16
                <ns2:Recipient>
17
                    <ns2:Country>FR</ns2:Country>
18
                    <ns2:Location xsi:type="ns2:Location_FR" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
19
                        <ns2:Siret>{{ siret }}</ns2:Siret>
20
                        <ns2:Service>{{ service }}</ns2:Service>
21
                        <ns2:Guichet>{{ guichet }}</ns2:Guichet>
22
                    </ns2:Location>
23
                </ns2:Recipient>
24
            </ns2:Recipients>
25
            <ns2:AckRequired>true</ns2:AckRequired>
26
            <ns2:AckType>AVL</ns2:AckType>
27
            <ns2:AckType>ANL</ns2:AckType>
28
            <ns2:AckTo>
29
                <ns2:Country>FR</ns2:Country>
30
                <ns2:Location xsi:type="ns2:Location_FR" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
31
                    <ns2:Siret>13000210800012</ns2:Siret>
32
                    <ns2:Service>flux_GS_PEC_AVL</ns2:Service>
33
                    <ns2:Guichet></ns2:Guichet>
34
                </ns2:Location>
35
            </ns2:AckTo>
36
        </ns2:Routing>
37
    </ns2:Header>
38
    <ns2:Body>
39
        <ns2:Content>
40
            <ns2:Aller>
41
                <NumeroDemarche>{{ flow_type }}</NumeroDemarche>
42
                <Teledemarche>
43
                    <NumeroTeledemarche>{{ reference }}</NumeroTeledemarche>
44
                    <!-- <MotDePasse></MotDePasse> -->
45
                    <!-- <Suivi></Suivi> -->
46
                    <Date>{{ date }}</Date>
47
                    <IdentifiantPlateforme>Publik</IdentifiantPlateforme>
48
                    <Email>{{ email }}</Email>
49
                </Teledemarche>
50
                <Routage>
51
                    <Donnee>
52
                        <Id>{{ code_insee_id }}</Id>
53
                        <Valeur>{{ code_insee }}</Valeur>
54
                    </Donnee>
55
                </Routage>
56
                <Document>
57
                    <Code>{{ doc_type }}</Code>
58
                    <Nom>{{ doc_type }}</Nom>
59
                    <FichierFormulaire>
60
                        <FichierDonnees>{{ reference }}-{{ flow_type }}-doc-{{ doc_type }}-1-1.xml</FichierDonnees>
61
                    </FichierFormulaire>
62
                </Document>
63
            </ns2:Aller>
64
        </ns2:Content>
65
    </ns2:Body>
66
</ns2:Message>
passerelle/apps/mdel_ddpacs/utils.py
1
# Passerelle - uniform access to data and services
2
# Copyright (C) 2016  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import os
18
import zipfile
19
from xml.etree import ElementTree as etree
20

  
21
from django.utils.dateparse import parse_date as django_parse_date
22

  
23
from passerelle.utils.jsonresponse import APIError
24

  
25
def parse_date(date):
26
    try:
27
        parsed_date = django_parse_date(date)
28
    except ValueError as e:
29
        raise APIError('Invalid date: %r (%r)' % ( date, e))
30
    if not parsed_date:
31
        raise APIError('date %r not iso-formated' % date)
32
    return parsed_date.isoformat()
33

  
34

  
35
class ElementFactory(etree.Element):
36

  
37
    def __init__(self, *args, **kwargs):
38
        self.text = kwargs.pop('text', None)
39
        namespace = kwargs.pop('namespace', None)
40
        if namespace:
41
            super(ElementFactory, self).__init__(
42
                etree.QName(namespace, args[0]), **kwargs
43
            )
44
            self.namespace = namespace
45
        else:
46
            super(ElementFactory, self).__init__(*args, **kwargs)
47

  
48
    def append(self, element, allow_new=True):
49

  
50
        if not allow_new:
51
            if isinstance(element.tag, etree.QName):
52
                found = self.find(element.tag.text)
53
            else:
54
                found = self.find(element.tag)
55

  
56
            if found is not None:
57
                return self
58

  
59
        super(ElementFactory, self).append(element)
60
        return self
61

  
62
    def extend(self, elements):
63
        super(ElementFactory, self).extend(elements)
64
        return self
65

  
66

  
67
def zipdir(path):
68
    """Zip directory
69
    """
70
    archname = path + '.zip'
71
    with zipfile.ZipFile(archname, 'w', zipfile.ZIP_DEFLATED) as zipf:
72
        for root, dirs, files in os.walk(path):
73
            for f in files:
74
                fpath = os.path.join(root, f)
75
                zipf.write(fpath, os.path.basename(fpath))
76
    return archname
77

  
78

  
79
def get_file_content_from_zip(path, filename):
80
    """Rreturn file content
81
    """
82
    with zipfile.ZipFile(path, 'r') as zipf:
83
        return zipf.read(filename)
passerelle/settings.py
140 140
    'passerelle.apps.jsondatastore',
141 141
    'passerelle.apps.sp_fr',
142 142
    'passerelle.apps.mdel',
143
    'passerelle.apps.mdel_ddpacs',
143 144
    'passerelle.apps.mobyt',
144 145
    'passerelle.apps.okina',
145 146
    'passerelle.apps.opengis',
passerelle/utils/zip.py
16 16

  
17 17
from __future__ import unicode_literals, absolute_import
18 18

  
19
import difflib
19 20
import io
20 21
import os.path
21 22
import json
......
239 240
        full_path = os.path.join(str(path), self.name)
240 241
        with atomic_write(full_path, dir=tmp_dir) as fd:
241 242
            self.render_to_file(fd)
243

  
244

  
245
def diff_zip(one, two):
246
    differences = []
247

  
248
    def compute_diff(one, two, fd_one, fd_two):
249
        content_one = fd_one.read()
250
        content_two = fd_two.read()
251

  
252
        if content_one == content_two:
253
            return
254
        if one.endswith(('.xml', '.json', '.txt')):
255
            diff = list(difflib.ndiff(content_one.splitlines(),
256
                                      content_two.splitlines()))
257
            return ['File %s differs' % one] + diff
258
        return 'File %s differs' % one
259

  
260
    if not hasattr(one, 'read'):
261
        one = open(one)
262
    with one:
263
        if not hasattr(two, 'read'):
264
            two = open(two)
265
        with two:
266
            with zipfile.ZipFile(one) as one_zip:
267
                with zipfile.ZipFile(two) as two_zip:
268
                    one_nl = set(one_zip.namelist())
269
                    two_nl = set(two_zip.namelist())
270
                    for name in one_nl - two_nl:
271
                        differences.append('File %s only in %s' % (name, one))
272
                    for name in two_nl - one_nl:
273
                        differences.append('File %s only in %s' % (name, two))
274
                    for name in one_nl & two_nl:
275
                        with one_zip.open(name) as fd_one:
276
                            with two_zip.open(name) as fd_two:
277
                                difference = compute_diff(name, name, fd_one, fd_two)
278
                                if difference:
279
                                    differences.append(difference)
280
    return differences
tests/data/mdel_ddpacs/response_manifest.json
1
{
2
    "name_template": "{{ reference }}-{{ flow_type }}-{{ step }}.zip",
3
    "part_templates": [
4
        {
5
            "name_template": "message.xml",
6
            "template_path": "response_message.xml"
7
        }
8
    ]
9
}
tests/data/mdel_ddpacs/response_message.xml
1
<ns2:Message xmlns:ns2="http://finances.gouv.fr/dgme/pec/message/v1" xmlns="http://finances.gouv.fr/dgme/gf/composants/teledemarchexml/donnee/metier">
2
    <ns2:Header>
3
        <ns2:Routing>
4
            <ns2:MessageId>{{ reference }} {{ step }}</ns2:MessageId>
5
            <ns2:RefToMessageId>{{ reference }} {{ old_step }}</ns2:RefToMessageId>
6
            <ns2:FlowType>{{ flow_type }}</ns2:FlowType>
7
            <ns2:Sender/>
8
            <ns2:Recipients>
9
                <ns2:Recipient/>
10
            </ns2:Recipients>
11
        </ns2:Routing>
12
        <ns2:Security>
13
            <ns2:Horodatage>false</ns2:Horodatage>
14
        </ns2:Security>
15
    </ns2:Header>
16
    <ns2:Body>
17
        <ns2:Content>
18
            <ns2:Retour>
19
                <ns2:Enveloppe>
20
                    <ns2:NumeroTeledemarche>{{ reference }}</ns2:NumeroTeledemarche>
21
                </ns2:Enveloppe>
22
                <ns2:Instruction>
23
                    <ns2:Maj>
24
                        {% if etat %}<ns2:Etat>{{ etat }}</ns2:Etat>{% endif %}
25
                        {% if commentaire %}<ns2:Commentaire>{{ commentaire }}</ns2:Commentaire>{% endif %}
26
                    </ns2:Maj>
27
                </ns2:Instruction>
28
            </ns2:Retour>
29
        </ns2:Content>
30
    </ns2:Body>
31
</ns2:Message>
tests/test_mdel_ddpacs.py
1
# coding: utf-8
2
# Passerelle - uniform access to data and services
3
# Copyright (C) 2019  Entr'ouvert
4
#
5
# This program is free software: you can redistribute it and/or modify it
6
# under the terms of the GNU Affero General Public License as published
7
# by the Free Software Foundation, either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
# GNU Affero General Public License for more details.
14
#
15
# You should have received a.deepcopy of the GNU Affero General Public License
16
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
17

  
18
from __future__ import unicode_literals
19

  
20
import io
21
import logging
22
import os
23

  
24
import pytest
25
import utils
26

  
27
from passerelle.apps.mdel_ddpacs.models import Resource, Demand
28

  
29
from passerelle.utils import json, sftp
30
from passerelle.utils.zip import diff_zip, ZipTemplate
31

  
32

  
33
def build_response_zip(**kwargs):
34
    zip_template = ZipTemplate(os.path.abspath('tests/data/mdel_ddpacs/response_manifest.json'), ctx=kwargs)
35
    return zip_template.name, zip_template.render_to_bytes()
36

  
37

  
38
@pytest.fixture(autouse=True)
39
def resource(db):
40
    return utils.setup_access_rights(Resource.objects.create(
41
        slug='test',
42
        code_insee='66666',
43
        recipient_siret='999999',
44
        recipient_service='SERVICE',
45
        recipient_guichet='GUICHET'))
46

  
47

  
48
@pytest.fixture
49
def ddpacs_payload():
50
    xmlschema = Resource.get_doc_xml_schema()
51
    return json.flatten({'PACS': xmlschema.to_dict('tests/data/pacs-doc.xml')})
52

  
53

  
54
def test_create_demand(app, resource, ddpacs_payload, freezer, sftpserver, caplog):
55
    # paramiko log socket errors when connection is closed :/
56
    caplog.set_level(logging.CRITICAL, 'paramiko.transport')
57
    freezer.move_to('2019-01-01')
58

  
59
    # Push new demand
60
    payload = {
61
        'display_id': '1-1',
62
    }
63
    payload.update(ddpacs_payload)
64
    assert Demand.objects.count() == 0
65
    assert resource.jobs_set().count() == 0
66
    resp = app.post_json('/mdel-ddpacs/test/create?raise=1', params=payload)
67
    assert resp.json['err'] == 0
68
    assert resp.json['status'] == 'pending'
69
    assert Demand.objects.count() == 1
70
    assert resource.jobs_set().count() == 1
71

  
72
    url = resp.json['url']
73
    zip_url = resp.json['zip_url']
74

  
75
    # Check demand status URL
76
    status = app.get(url)
77
    assert status.json['err'] == 0
78
    assert status.json == resp.json
79

  
80
    # Check demand document URL
81
    zip_document = app.get(zip_url)
82
    with io.BytesIO(zip_document.body) as fd:
83
        differences = diff_zip('tests/data/mdel_ddpacs_expected.zip', fd)
84
        assert not differences, differences
85

  
86
    # Check job is skipped as no SFTP is configured
87
    assert resource.jobs_set().get().after_timestamp is None
88
    resource.jobs()
89
    assert resource.jobs_set().get().after_timestamp is not None
90
    assert resource.jobs_set().exclude(status='completed').count() == 1
91

  
92
    with sftpserver.serve_content({'input': {}, 'output': {}}):
93
        content = sftpserver.content_provider.content_object
94
        resource.outcoming_sftp = sftp.SFTP(
95
            'sftp://john:doe@{server.host}:{server.port}/output/'.format(
96
                server=sftpserver))
97
        resource.jobs()
98
        assert not content['output']
99
        # Jump over the 6 hour wait time for retry
100
        freezer.move_to('2019-01-02')
101
        resource.jobs()
102
        assert 'A-1-1-depotDossierPACS-1.zip' in content['output']
103
        # Check it's the same document than through the zip_url
104
        with open('/tmp/zip.zip', 'w') as fd:
105
            fd.write(content['output']['A-1-1-depotDossierPACS-1.zip'])
106
        with io.BytesIO(content['output']['A-1-1-depotDossierPACS-1.zip']) as fd:
107
            differences = diff_zip('tests/data/mdel_ddpacs_expected.zip', fd)
108
            assert not differences, differences
109
        # Act as if zip was consumed
110
        content['output'] = {}
111
        # Jump over the 6 hour wait time for retry
112
        freezer.move_to('2019-01-03')
113
        resource.jobs()
114
        assert not content['output']
115
        assert resource.jobs_set().exclude(status='completed').count() == 0
116

  
117
        # Check response
118
        resource.hourly()
119

  
120
        resource.incoming_sftp = sftp.SFTP(
121
            'sftp://john:doe@{server.host}:{server.port}/input/'.format(
122
                server=sftpserver))
123

  
124
        response_name, response_content = build_response_zip(
125
            reference='A-1-1',
126
            flow_type='depotDossierPACS',
127
            step=1,
128
            old_step=1,
129
            etat=100,
130
            commentaire='coucou')
131
        content['input'][response_name] = response_content
132
        resource.hourly()
133
        assert resource.demand_set.get().status == 'closed'
134
        assert response_name not in content['input']
135

  
136
        response_name, response_content = build_response_zip(
137
            reference='A-1-1',
138
            flow_type='depotDossierPACS',
139
            step=1,
140
            old_step=1,
141
            etat=1,
142
            commentaire='coucou')
143
        content['input'][response_name] = response_content
144
        resource.hourly()
145
        assert 'unexpected file "A-1-1-depotDossierPACS-1.zip"' in caplog.messages[-1]
146
        assert 'step 1 is inferior' in caplog.messages[-1]
147

  
148
        resource.check_status()
0
-