Projet

Général

Profil

0008-new-DDPACS-connector.patch

Benjamin Dauvergne, 21 octobre 2019 10:25

Télécharger (43,3 ko)

Voir les différences:

Subject: [PATCH 8/8] new DDPACS connector

 passerelle/apps/mdel_ddpacs/__init__.py       |   0
 passerelle/apps/mdel_ddpacs/abstract.py       | 267 ++++++++++++++++++
 .../mdel_ddpacs/migrations/0001_initial.py    |  60 ++++
 .../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                       |  25 ++
 tests/data/mdel_ddpacs_expected.zip           | Bin 0 -> 7465 bytes
 tests/test_mdel_ddpacs.py                     |  84 ++++++
 15 files changed, 911 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_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 xml.etree.ElementTree as ET
24

  
25
from django.db import models
26
from django.core.urlresolvers import reverse
27
from django.http import HttpResponse
28
from django.utils.translation import ugettext_lazy as _
29
from django.utils import six, timezone, functional
30

  
31
import xmlschema
32

  
33
import jsonfield
34

  
35
from passerelle.base.models import BaseResource, SkipJob
36
from passerelle.utils.api import endpoint
37
from passerelle.utils.jsonresponse import APIError
38
from passerelle.utils.zip import ZipTemplate
39

  
40
from passerelle.utils import json, xml, sftp
41

  
42
'''Base abstract models for implementing MDEL compatible requests.
43
'''
44

  
45
MDELStatus = namedtuple('MDELStatus', ['code', 'slug', 'label'])
46

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

  
57

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

  
80
    xsd_path = 'schema.xsd'
81
    xsd_root_element = None
82
    flow_type = 'flow_type CHANGEME'
83
    doc_type = 'doc_type CHANGEME'
84
    zip_manifest = 'mdel/zip/manifest.json'
85
    code_insee_id = 'CODE_INSEE'
86

  
87
    class Meta:
88
        abstract = True
89

  
90
    @classmethod
91
    def get_doc_xml_schema(cls):
92
        base_dir = os.path.dirname(inspect.getfile(cls))
93
        path = os.path.join(base_dir, cls.xsd_path)
94
        assert os.path.exists(path)
95
        return xmlschema.XMLSchema(path, converter=xmlschema.UnorderedConverter)
96

  
97
    @classmethod
98
    def get_doc_json_schema(cls):
99
        return xml.JSONSchemaFromXMLSchema(cls.get_doc_xml_schema(), cls.xsd_root_element).json_schema
100

  
101
    @classmethod
102
    def get_create_schema(cls):
103
        base_schema = cls.get_doc_json_schema()
104
        base_schema['properties'].update({
105
            'display_id': {'type': 'string'},
106
            'email': {'type': 'string'},
107
            'code_insee': {'type': 'string'},
108
        })
109
        base_schema.setdefault('required', []).append('display_id')
110
        return base_schema
111

  
112
    def _handle_create(self, request):
113
        try:
114
            raw_payload = json.loads(request.body)
115
        except (TypeError, ValueError):
116
            raise APIError('Invalid payload format: JSON expected')
117
        payload = json.unflatten(raw_payload)
118
        extra = payload.pop('extra', {})
119
        # w.c.s. pass non form fields in extra
120
        if isinstance(extra, dict):
121
            payload.update(extra)
122
        try:
123
            json.validate(payload, self.get_create_schema())
124
        except json.ValidationError as e:
125
            raise APIError('Invalid payload format: %s' % e)
126

  
127
        reference = 'A-' + payload['display_id']
128
        demand = self.demand_set.create(
129
            reference=reference,
130
            step=1,
131
            data=payload)
132
        self.add_job('push_demand', demand_id=demand.id)
133
        return self.status(request, demand)
134

  
135
    def push_demand(self, demand_id):
136
        demand = self.demand_set.get(id=demand_id)
137
        if not demand.push():
138
            raise SkipJob(after_timestamp=3600 * 6)
139

  
140
    @endpoint(perm='can_access',
141
              methods=['get'],
142
              description=_('Demand status'),
143
              pattern=r'(?P<demand_id>\d+)/$')
144
    def demand(self, request, demand_id):
145
        try:
146
            demand = self.demand_set.get(id=demand_id)
147
        except self.demand_set.model.DoesNotExist:
148
            raise APIError('demand not found', http_status=404)
149
        return self.status(request, demand)
150

  
151
    def status(self, request, demand):
152
        return {
153
            'id': demand.id,
154
            'status': demand.status,
155
            'url': request.build_absolute_uri(demand.status_url),
156
            'zip_url': request.build_absolute_uri(demand.zip_url),
157
        }
158

  
159
    @endpoint(perm='can_access',
160
              methods=['get'],
161
              description=_('Demand document'),
162
              pattern=r'(?P<demand_id>\d+)/.*$')
163
    def document(self, request, demand_id):
164
        try:
165
            demand = self.demand_set.get(id=demand_id)
166
        except self.demand_set.model.DoesNotExist:
167
            raise APIError('demand not found', http_status=404)
168
        response = HttpResponse(demand.zip_content, content_type='application/octet-stream')
169
        response['Content-Disposition'] = 'inline; filename=%s' % demand.zip_name
170
        return response
171

  
172

  
173
@six.python_2_unicode_compatible
174
class Demand(models.Model):
175
    STATUS_PENDING = 'pending'
176
    STATUS_PUSHED = 'pushed'
177

  
178
    STATUSES = [
179
        (STATUS_PENDING, _('pending')),
180
        (STATUS_PUSHED, _('pushed')),
181
    ]
182
    for mdel_status in MDEL_STATUSES:
183
        STATUSES.append((mdel_status.slug, mdel_status.label))
184

  
185
    created_at = models.DateTimeField(auto_now_add=True)
186
    updated_at = models.DateTimeField(auto_now=True)
187
    reference = models.CharField(max_length=32, null=False, unique=True)
188
    status = models.CharField(max_length=32, null=True, choices=STATUSES, default=STATUS_PENDING)
189
    pushed = models.BooleanField(default=False)
190
    step = models.IntegerField(default=0)
191
    data = jsonfield.JSONField()
192

  
193
    def push(self):
194
        if not self.resource.outcoming_sftp:
195
            return False
196
        try:
197
            with self.resource.outcoming_sftp.client() as client:
198
                with client.open(self.zip_name, mode='w') as fd:
199
                    fd.write(self.zip_content)
200
        except sftp.paramiko.SSHException as e:
201
            self.resource.logger.error('push of demand %s failed: %s', self, e)
202
        else:
203
            self.status = self.STATUS_PUSHED
204
            self.save()
205

  
206
    @functional.cached_property
207
    def zip_template(self):
208
        return ZipTemplate(self.resource.zip_manifest, ctx={
209
            'reference': self.reference,
210
            'flow_type': self.resource.flow_type,
211
            'doc_type': self.resource.doc_type,
212
            'step': self.step,
213
            'siret': self.resource.recipient_siret,
214
            'service': self.resource.recipient_service,
215
            'guichet': self.resource.recipient_guichet,
216
            'code_insee': self.data.get('code_insee', self.resource.code_insee),
217
            'document': self.document,
218
            'code_insee_id': self.resource.code_insee_id,
219
            'date': timezone.now().isoformat(),
220
            'email': self.data.get('email', ''),
221
        })
222

  
223
    @property
224
    def zip_name(self):
225
        return self.zip_template.name
226

  
227
    @property
228
    def zip_content(self):
229
        return self.zip_template.render_to_bytes()
230

  
231
    @property
232
    def document(self):
233
        xml_schema = self.resource.get_doc_xml_schema()
234
        return ET.tostring(
235
            xml_schema.elements[self.resource.xsd_root_element].encode(
236
                self.data[self.resource.xsd_root_element]))
237

  
238
    @property
239
    def status_url(self):
240
        return reverse(
241
            'generic-endpoint',
242
            kwargs={
243
                'connector': self.resource.get_connector_slug(),
244
                'slug': self.resource.slug,
245
                'endpoint': 'demand',
246
                'rest': '%s/' % self.id,
247
            })
248

  
249
    @property
250
    def zip_url(self):
251
        return reverse(
252
            'generic-endpoint',
253
            kwargs={
254
                'connector': self.resource.get_connector_slug(),
255
                'slug': self.resource.slug,
256
                'endpoint': 'document',
257
                'rest': '%s/%s' % (self.id, self.zip_name)
258
            })
259

  
260
    def __str__(self):
261
        return '<Demand %s reference:%s flow_type:%s>' % (
262
            self.id,
263
            self.reference,
264
            self.flow_type)
265

  
266
    class Meta:
267
        abstract = True
passerelle/apps/mdel_ddpacs/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.20 on 2019-10-18 08:53
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(max_length=32, null=True)),
28
                ('pushed', models.BooleanField(default=False)),
29
                ('step', models.IntegerField(default=0)),
30
                ('data', jsonfield.fields.JSONField(default=dict)),
31
            ],
32
            options={
33
                'verbose_name': 'MDEL compatible DDPACS request',
34
            },
35
        ),
36
        migrations.CreateModel(
37
            name='Resource',
38
            fields=[
39
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
40
                ('title', models.CharField(max_length=50, verbose_name='Title')),
41
                ('description', models.TextField(verbose_name='Description')),
42
                ('slug', models.SlugField(unique=True, verbose_name='Identifier')),
43
                ('outcoming_sftp', passerelle.utils.sftp.SFTPField(blank=True, default=None, verbose_name='Outcoming SFTP')),
44
                ('incoming_sftp', passerelle.utils.sftp.SFTPField(blank=True, default=None, verbose_name='Incoming SFTP')),
45
                ('recipient_siret', models.CharField(max_length=128, verbose_name='SIRET')),
46
                ('recipient_service', models.CharField(max_length=128, verbose_name='Service')),
47
                ('recipient_guichet', models.CharField(max_length=128, verbose_name='Guichet')),
48
                ('code_insee', models.CharField(max_length=6, verbose_name='INSEE Code')),
49
                ('users', models.ManyToManyField(blank=True, related_name='_resource_users_+', related_query_name='+', to='base.ApiUser')),
50
            ],
51
            options={
52
                'verbose_name': 'MDEL compatible DDPACS request builder',
53
            },
54
        ),
55
        migrations.AddField(
56
            model_name='demand',
57
            name='resource',
58
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mdel_ddpacs.Resource'),
59
        ),
60
    ]
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
227 227
        full_path = os.path.join(str(path), self.name)
228 228
        with atomic_write(full_path, dir=tmp_dir) as fd:
229 229
            self.render_to_file(fd)
230

  
231

  
232
def diff_zip(one, two):
233
    differences = []
234

  
235
    if not hasattr(one, 'read'):
236
        one = open(one)
237
    with one:
238
        if not hasattr(two, 'read'):
239
            two = open(two)
240
        with two:
241
            with zipfile.ZipFile(one) as one_zip:
242
                with zipfile.ZipFile(two) as two_zip:
243
                    one_nl = set(one_zip.namelist())
244
                    two_nl = set(two_zip.namelist())
245
                    for name in one_nl - two_nl:
246
                        differences.append('File %s only in %s' % (name, one))
247
                    for name in two_nl - one_nl:
248
                        differences.append('File %s only in %s' % (name, two))
249
                    for name in one_nl & two_nl:
250
                        with one_zip.open(name) as fd_one:
251
                            with two_zip.open(name) as fd_two:
252
                                if fd_one.read() != fd_two.read():
253
                                    differences.append('File %s differs' % name)
254
    return differences
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 zipfile
22

  
23
import pytest
24
import utils
25

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

  
28
from passerelle.utils import json
29
from passerelle.utils.zip import diff_zip
30

  
31

  
32
@pytest.fixture(autouse=True)
33
def resource(db):
34
    return utils.setup_access_rights(Resource.objects.create(
35
        slug='test',
36
        code_insee='66666',
37
        recipient_siret='999999',
38
        recipient_service='SERVICE',
39
        recipient_guichet='GUICHET'))
40

  
41

  
42
@pytest.fixture
43
def ddpacs_payload():
44
    xmlschema = Resource.get_doc_xml_schema()
45
    return json.flatten({'PACS': xmlschema.to_dict('tests/data/pacs-doc.xml')})
46

  
47

  
48
def test_create_demand(app, resource, ddpacs_payload, freezer):
49
    freezer.move_to('2019-01-01')
50

  
51
    # Push new demand
52
    payload = {
53
        'display_id': '1-1',
54
    }
55
    payload.update(ddpacs_payload)
56
    assert Demand.objects.count() == 0
57
    assert resource.jobs_set().count() == 0
58
    resp = app.post_json('/mdel-ddpacs/test/create?raise=1', params=payload)
59
    assert resp.json['err'] == 0
60
    assert resp.json['status'] == 'pending'
61
    assert Demand.objects.count() == 1
62
    assert resource.jobs_set().count() == 1
63

  
64
    url = resp.json['url']
65
    zip_url = resp.json['zip_url']
66

  
67
    # Check demand status URL
68
    status = app.get(url)
69
    assert status.json['err'] == 0
70
    assert status.json == resp.json
71

  
72
    # Check demand document URL
73
    zip_document = app.get(zip_url)
74
    with io.BytesIO(zip_document.body) as fd:
75
        differences = diff_zip('tests/data/mdel_ddpacs_expected.zip', fd)
76
        assert not differences, differences
77

  
78
    # Check job is skipped as no SFTP is configured
79
    assert resource.jobs_set().get().after_timestamp is None
80
    resource.jobs()
81
    assert resource.jobs_set().get().after_timestamp is not None
82

  
83

  
84

  
0
-