Projet

Général

Profil

0001-gesbac-initial-connector-35325.patch

Serghei Mihai, 17 septembre 2019 09:47

Télécharger (26,7 ko)

Voir les différences:

Subject: [PATCH] gesbac: initial connector (#35325)

 passerelle/apps/gesbac/__init__.py            |   0
 .../apps/gesbac/migrations/0001_initial.py    |  57 ++
 passerelle/apps/gesbac/migrations/__init__.py |   0
 passerelle/apps/gesbac/models.py              | 490 ++++++++++++++++++
 passerelle/settings.py                        |   1 +
 passerelle/utils/__init__.py                  |   1 +
 tests/test_gesbac.py                          | 119 +++++
 7 files changed, 668 insertions(+)
 create mode 100644 passerelle/apps/gesbac/__init__.py
 create mode 100644 passerelle/apps/gesbac/migrations/0001_initial.py
 create mode 100644 passerelle/apps/gesbac/migrations/__init__.py
 create mode 100644 passerelle/apps/gesbac/models.py
 create mode 100644 tests/test_gesbac.py
passerelle/apps/gesbac/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.20 on 2019-09-16 16:38
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', '0014_auto_20190820_0914'),
17
    ]
18

  
19
    operations = [
20
        migrations.CreateModel(
21
            name='CSV',
22
            fields=[
23
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
24
                ('formdata_id', models.CharField(max_length=64, null=True)),
25
                ('creation_datetime', models.DateTimeField(auto_now_add=True)),
26
                ('filename', models.CharField(max_length=128)),
27
                ('sequence_number', models.IntegerField(default=0)),
28
                ('file_type', models.CharField(choices=[(b'D', b'Demand'), (b'R', b'Response')], default=b'D', max_length=1)),
29
                ('data', jsonfield.fields.JSONField(default=dict)),
30
            ],
31
            options={
32
                'get_latest_by': 'creation_datetime',
33
            },
34
        ),
35
        migrations.CreateModel(
36
            name='Gesbac',
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(default=None, verbose_name='Outcoming SFTP')),
43
                ('incoming_sftp', passerelle.utils.sftp.SFTPField(default=None, verbose_name='Incoming SFTP')),
44
                ('output_files_prefix', models.CharField(max_length=32, verbose_name='Output files prefix')),
45
                ('input_files_prefix', models.CharField(max_length=32, verbose_name='Input files prefix')),
46
                ('users', models.ManyToManyField(blank=True, related_name='_gesbac_users_+', related_query_name='+', to='base.ApiUser')),
47
            ],
48
            options={
49
                'verbose_name': 'Gesbac',
50
            },
51
        ),
52
        migrations.AddField(
53
            model_name='csv',
54
            name='resource',
55
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='gesbac.Gesbac'),
56
        ),
57
    ]
passerelle/apps/gesbac/models.py
1
# -*- coding: utf-8 -*-
2
# Copyright (C) 2019  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 csv
18
import os
19

  
20
from django.db import models
21
from django.utils.translation import ugettext_lazy as _
22
from django.utils.timezone import now, make_aware
23
from django.core.files.storage import default_storage
24

  
25
from passerelle.base.models import BaseResource
26
from passerelle.utils.api import endpoint, APIError
27
from passerelle.utils import SFTPField
28

  
29
from jsonfield import JSONField
30

  
31
CSV_DELIMITER = ';'
32

  
33
DEMAND_SCHEMA = {
34
    "$schema": "http://json-schema.org/draft-03/schema#",
35
    "title": "Gesbac",
36
    "description": "",
37
    "type": "object",
38
    "properties": {
39
        "formdata_id": {
40
            "description": "formdata id",
41
            "type": "string",
42
            "required": True
43
        },
44
        "demand_date": {
45
            "description": "demand date",
46
            "type": "string",
47
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$",
48
            "required": True
49
        },
50
        "demand_time": {
51
            "description": "demand time",
52
            "type": "string",
53
            "pattern": "^[0-9]{2}:[0-9]{2}:[0-9]{2}$",
54
            "required": True
55
        },
56
        "producer_code": {
57
            "description": "producer code",
58
            "type": "integer",
59
            "required": True
60
        },
61
        "invariant_number": {
62
            "description": "invariant number",
63
            "type": "string",
64
            "required": False,
65
            "maxLength": 10,
66
            "default": ""
67
        },
68
        "city_insee_code": {
69
            "type": "string",
70
            "required": False,
71
            "default": ""
72
        },
73
        "street_rivoli_code": {
74
            "type": "string",
75
            "required": False,
76
            "default": ""
77
        },
78
        "street_name": {
79
            "type": "string",
80
            "required": False,
81
            "default": ""
82
        },
83
        "address_complement": {
84
            "type": "string",
85
            "maxLength": 32,
86
            "required": False,
87
            "default": ""
88
        },
89
        "street_number": {
90
            "type": "integer",
91
            "required": False,
92
            "default": 0
93
        },
94
        "bis_ter": {
95
            "type": "string",
96
            "maxLength": 3,
97
            "required": False,
98
            "default": ""
99
        },
100
        "building": {
101
            "type": "string",
102
            "maxLength": 5,
103
            "required": False,
104
            "default": ""
105
        },
106
        "hall": {
107
            "type": "string",
108
            "maxLength": 5,
109
            "required": False,
110
            "default": ""
111
        },
112
        "appartment_number": {
113
            "type": "string",
114
            "maxLength": 5,
115
            "required": False,
116
            "default": ""
117
        },
118
        "producer_social_reason": {
119
            "type": "string",
120
            "maxLength": 38,
121
            "required": False,
122
            "default": ""
123
        },
124
        "producer_title_code": {
125
            "type": "integer",
126
            "required": False,
127
            "default": 0
128
        },
129
        "producer_last_name": {
130
            "type": "string",
131
            "maxLength": 38,
132
            "required": False,
133
            "default": ""
134
        },
135
        "producer_first_name": {
136
            "type": "string",
137
            "maxLength": 32,
138
            "required": False,
139
            "default": ""
140
        },
141
        "producer_phone": {
142
            "type": "string",
143
            "maxLength": 20,
144
            "required": False,
145
            "default": ""
146
        },
147
        "producer_email": {
148
            "type": "string",
149
            "maxLength": 50,
150
            "required": False,
151
            "default": ""
152
        },
153
        "owner_last_name": {
154
            "type": "string",
155
            "maxLength": 38,
156
            "required": False,
157
            "default": ""
158
        },
159
        "owner_first_name": {
160
            "type": "string",
161
            "maxLength": 32,
162
            "required": False,
163
            "default": ""
164
        },
165
        "owner_phone": {
166
            "type": "string",
167
            "maxLength": 20,
168
            "required": False,
169
            "default": ""
170
        },
171
        "owner_email": {
172
            "type": "string",
173
            "maxLength": 50,
174
            "required": False,
175
            "default": ""
176
        },
177
        "activity_code": {
178
            "type": "integer",
179
            "required": False,
180
            "default": 0
181
        },
182
        "family_members_number": {
183
            "type": "integer",
184
            "required": False,
185
            "default": 0
186
        },
187
        "houses_number": {
188
            "type": "integer",
189
            "required": False,
190
            "default": 0
191
        },
192
        "t1_flats_number": {
193
            "type": "integer",
194
            "required": False,
195
            "default": 0
196
        },
197
        "t2_flats_number": {
198
            "type": "integer",
199
            "required": False,
200
            "default": 0
201
        },
202
        "t3_flats_number": {
203
            "type": "integer",
204
            "required": False,
205
            "default": 0
206
        },
207
        "t4_flats_number": {
208
            "type": "integer",
209
            "required": False,
210
            "default": 0
211
        },
212
        "t5_flats_number": {
213
            "type": "integer",
214
            "required": False,
215
            "default": 0
216
        },
217
        "t6_flats_number": {
218
            "type": "integer",
219
            "required": False,
220
            "default": 0
221
        },
222
        "shops_number": {
223
            "type": "integer",
224
            "required": False,
225
            "default": 0
226
        },
227
        "garden_size": {
228
            "type": "integer",
229
            "required": False,
230
            "default": 0
231
        },
232
        "expected_date": {
233
            "type": "string",
234
            "pattern": "^[0-9]{8}$",
235
            "required": False,
236
            "default": ""
237
        },
238
        "expected_time": {
239
            "type": "string",
240
            "pattern": "^[0-9]{2}$",
241
            "required": False,
242
            "default": ""
243
        },
244
        "modification_code": {
245
            "type": "integer",
246
            "required": False,
247
            "default": 0
248
        },
249
        "demand_reason_label": {
250
            "type": "string",
251
            "required": False,
252
            "default": ""
253
        },
254
        "comment": {
255
            "type": "string",
256
            "maxLength": 500,
257
            "required": False,
258
            "default": ""
259
        },
260
        "card_subject": {
261
            "type": "integer",
262
            "required": True
263
        },
264
        "card_type": {
265
            "type": "integer",
266
            "required": True
267
        },
268
        "card_demand_reason": {
269
            "type": "integer",
270
            "required": True
271
        },
272
        "cards_number": {
273
            "type": "integer",
274
            "required": True,
275
            "default": 1
276
        },
277
        "card_number": {
278
            "type": "string",
279
            "maxLength": 20,
280
            "required": False
281
        },
282
        "card_bar_code": {
283
            "type": "string",
284
            "maxLength": 20,
285
            "required": False,
286
            "default": "",
287
        },
288
        "card_code": {
289
            "type": "string",
290
            "maxLength": 20,
291
            "required": False,
292
            "default": "",
293
        },
294
        "card_validity_start_date": {
295
            "type": "string",
296
            "required": False,
297
            "pattern": "^[0-9]{8}$",
298
            "default": "",
299
        },
300
        "card_validity_end_date": {
301
            "type": "string",
302
            "required": False,
303
            "pattern": "^[0-9]{8}$",
304
            "default": "",
305
        },
306
        "card_comment": {
307
            "type": "string",
308
            "required": False,
309
            "maxLength": 100,
310
            "default": "",
311
        }
312
    }
313
}
314

  
315

  
316
class Gesbac(BaseResource):
317
    outcoming_sftp = SFTPField(verbose_name=_('Outcoming SFTP'))
318
    incoming_sftp = SFTPField(verbose_name=_('Incoming SFTP'))
319
    output_files_prefix = models.CharField(_('Output files prefix'),
320
                                           blank=False, max_length=32)
321
    input_files_prefix = models.CharField(_('Input files prefix'),
322
                                          blank=False, max_length=32)
323

  
324
    category = _('Business Process Connectors')
325

  
326
    class Meta:
327
        verbose_name = u'Gesbac'
328

  
329
    def check_status(self):
330
        with self.outcoming_sftp.client() as out_sftp:
331
            out_sftp.listdir()
332
        with self.incoming_sftp.client() as in_sftp:
333
            in_sftp.listdir()
334

  
335
    @property
336
    def file_handler(self):
337
        return FileHandler(resource=self)
338

  
339
    @endpoint(name='create-demand',
340
              perm='can_access',
341
              description=_('Create demand'),
342
              post = {
343
                  'description': _('Creates a demand file'),
344
                  'request_body': {
345
                      'schema': {
346
                          'application/json': DEMAND_SCHEMA
347
                      }
348
                  }
349
              }
350
    )
351
    def create_demand(self, request, post_data):
352
        return {'data': self.file_handler.send(post_data['formdata_id'], post_data)}
353

  
354
    @endpoint(name='get-response', perm='can_access',
355
              description=_('Get response'),
356
              parameters={
357
                  'formdata_id': {
358
                      'description': _('Form data identifier'),
359
                      'example_value': '42-01'
360
                  }
361
              }
362
    )
363
    def get_response(self, request, formdata_id):
364
        return {'data': self.file_handler.get(formdata_id)}
365

  
366

  
367
CSV_FILE_TYPES = (
368
    ('D', 'Demand'),
369
    ('R', 'Response')
370
)
371

  
372

  
373
class CSV(models.Model):
374
    resource = models.ForeignKey(Gesbac)
375
    formdata_id = models.CharField(max_length=64, null=True)
376
    creation_datetime = models.DateTimeField(auto_now_add=True)
377
    filename = models.CharField(max_length=128)
378
    sequence_number = models.IntegerField(default=0)
379
    file_type = models.CharField(max_length=1, choices=CSV_FILE_TYPES, default='D')
380
    data = JSONField()
381

  
382
    class Meta:
383
        get_latest_by = 'creation_datetime'
384

  
385

  
386
class FileHandler(object):
387

  
388
    def __init__(self, resource):
389
        self.resource = resource
390

  
391
    def get_file_suffix(self, formdata_id, sequence_number):
392
        sequence = "%03d" % sequence_number
393
        # replace '-' from formdata id with sequence number because integer is
394
        # expected
395
        return formdata_id.replace('-', sequence)
396

  
397
    def get_filename(self, prefix, formdata_id, sequence_number):
398
        timestamp = now().strftime('%y%m%d-%H%M%S')
399
        suffix = self.get_file_suffix(formdata_id, sequence_number)
400
        return '%s%s-%s.csv' % (prefix, timestamp, suffix)
401

  
402
    def send(self, formdata_id, data):
403

  
404
        try:
405
            demand_file = self.resource.csv_set.filter(formdata_id=formdata_id, file_type='D').latest()
406
            sequence_number = demand_file.sequence_number + 1
407
        except CSV.DoesNotExist:
408
            sequence_number = 0
409

  
410
        filename = self.get_filename(self.resource.output_files_prefix,
411
                                     formdata_id, sequence_number)
412

  
413
        with self.resource.outcoming_sftp.client() as client:
414
            with client.open(filename, mode='w') as fd:
415
                writer = csv.writer(fd, delimiter=CSV_DELIMITER)
416
                applicant_data = ['E', formdata_id]
417
                # get applicant attributes
418
                for param in ['demand_date', 'demand_time', 'producer_code',
419
                              'invariant_number', 'city_insee_code', 'street_rivoli_code',
420
                              'street_name', 'address_complement', 'street_number',
421
                              'bis_ter', 'building', 'hall', 'appartment_number',
422
                              'producer_social_reason', 'producer_title_code',
423
                              'producer_last_name', 'producer_first_name',
424
                              'producer_phone', 'producer_email', 'owner_last_name',
425
                              'owner_first_name', 'owner_phone', 'owner_email',
426
                              'activity_code', 'family_members_number', 'houses_number',
427
                              't1_flats_number', 't2_flats_number', 't3_flats_number',
428
                              't4_flats_number', 't5_flats_number', 't6_flats_number',
429
                              'shops_number', 'garden_size', 'expected_date',
430
                              'expected_time', 'modification_code',
431
                              'demand_reason_label', 'comment']:
432
                    item = data.get(param)
433
                    if isinstance(item, basestring):
434
                        item = item.encode('utf-8')
435
                    applicant_data.append(item)
436
                writer.writerow(applicant_data)
437
                # get card attributes
438
                card_data = ['CARTE', formdata_id]
439
                for param in ['card_demand_purpose', 'card_type', 'card_demand_reason',
440
                             'card_demanded_number', 'card_number', 'card_bar_code',
441
                             'card_code', 'card_validity_start_date',
442
                             'card_validity_end_date', 'card_comment']:
443
                    item = data.get(param)
444
                    if isinstance(item, basestring):
445
                        item = item.encode('utf-8')
446
                    card_data.append(item)
447
                writer.writerow(card_data)
448

  
449
        CSV.objects.create(formdata_id=formdata_id,
450
                           resource=self.resource,
451
                           sequence_number=sequence_number,
452
                           filename=filename,
453
                           data=data)
454
        return {'filename': filename}
455

  
456
    def get(self, formdata_id):
457
        data = []
458
        try:
459
            demand = self.resource.csv_set.filter(formdata_id=formdata_id,
460
                                                  file_type='D').latest()
461
        except CSV.DoesNotExist:
462
            raise APIError('Unknown demand')
463

  
464
        try:
465
            response = self.resource.csv_set.filter(formdata_id=formdata_id,
466
                                                    sequence_number=demand.sequence_number,
467
                                                    file_type='R').latest()
468
            return response.data
469
        except CSV.DoesNotExist:
470
            pass
471

  
472
        with self.resource.incoming_sftp.client() as client:
473
            response_filename = demand.filename.replace(self.resource.output_files_prefix,
474
                                                        self.resource.input_files_prefix)
475
            for csv_file in client.listdir():
476
                if csv_file.endswith('%s.csv' % self.get_file_suffix(formdata_id,
477
                                                                     demand.sequence_number)):
478
                    response_filename = csv_file
479
                    break
480
            else:
481
                raise APIError('Response doest not exist yet')
482

  
483
            with client.open(response_filename, mode='r') as fd:
484
                for row in csv.reader(fd, delimiter=CSV_DELIMITER):
485
                    data.append(row)
486
            CSV.objects.create(resource=self.resource, formdata_id=formdata_id,
487
                               filename=response_filename,
488
                               sequence_number=demand.sequence_number,
489
                               file_type='R', data=data)
490
        return data
passerelle/settings.py
136 136
    'passerelle.apps.family',
137 137
    'passerelle.apps.feeds',
138 138
    'passerelle.apps.gdc',
139
    'passerelle.apps.gesbac',
139 140
    'passerelle.apps.jsondatastore',
140 141
    'passerelle.apps.sp_fr',
141 142
    'passerelle.apps.mobyt',
passerelle/utils/__init__.py
337 337
# legacy import, other modules keep importing to_json from passerelle.utils
338 338
from .jsonresponse import to_json
339 339
from .soap import SOAPClient, SOAPTransport
340
from .sftp import SFTPField, SFTP
tests/test_gesbac.py
1
# -*- coding: utf-8 -*-
2

  
3
# tests/test_gesbac.py
4
# Copyright (C) 2019  Entr'ouvert
5
#
6
# This program is free software: you can redistribute it and/or modify it
7
# under the terms of the GNU Affero General Public License as published
8
# by the Free Software Foundation, either version 3 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU Affero General Public License for more details.
15
#
16
# You should have received a copy of the GNU Affero General Public License
17
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
18

  
19
import datetime
20
import pytest
21

  
22
from django.utils.timezone import now
23
from .utils import make_resource
24

  
25
from passerelle.utils import SFTP
26
from passerelle.apps.gesbac.models import Gesbac, CSV
27

  
28

  
29
@pytest.fixture
30
def resource(db, sftpserver):
31
    return make_resource(
32
        Gesbac,
33
        slug='test',
34
        title='Gesbac',
35
        description='gesbac',
36
        outcoming_sftp=SFTP('sftp://foo:bar@{server.host}:{server.port}/output/'.format(server=sftpserver)),
37
        incoming_sftp=SFTP('sftp://foo:bar@{server.host}:{server.port}/input/'.format(server=sftpserver)),
38
        output_files_prefix='output-',
39
        input_files_prefix='input-')
40

  
41

  
42
def test_check_status(app, resource, sftpserver):
43
    with sftpserver.serve_content({'input': {'test': 'content'},
44
                                   'output': {'file': 'content'}}):
45
        resource.check_status()
46

  
47

  
48
def test_create_demand(app, resource, sftpserver, freezer):
49
    assert resource.csv_set.count() == 0
50
    timestamp = now()
51
    payload = {
52
        'formdata_id': '42-42',
53
        'demand_date': timestamp.strftime('%Y-%m-%d'),
54
        'demand_time': timestamp.strftime('%H:%M:%S'),
55
        'producer_code': 1,
56
        'city_insee_code': '75114',
57
        'street_rivoli_code': '4',
58
        'street_name': 'Château',
59
        'producer_social_reason': 'SCOP',
60
        'producer_last_name': 'Bar',
61
        'producer_first_name': 'Foo',
62
        'producer_email': 'foo@example.com',
63
        'owner_last_name': 'Bar',
64
        'owner_first_name': 'Foo',
65
        'owner_email': 'foo@example.com',
66
        'family_members_number': 5,
67
        'houses_number': 1,
68
        'card_type': 1,
69
        'card_subject': 1,
70
        'card_demand_reason': 1,
71
        'card_demand_purpose': 1,
72
        'cards_number': 1
73
    }
74
    with sftpserver.serve_content({'output': {'file': 'content'}}):
75
        response = app.post_json('/gesbac/test/create-demand/', params=payload)
76
        assert response.json['data']['filename'] == '%s%s-4200042.csv' % (resource.output_files_prefix,
77
                                                                          timestamp.strftime('%y%m%d-%H%M%S'))
78
        assert resource.csv_set.filter(formdata_id='42-42', file_type='D').count() == 1
79
        csv = resource.csv_set.filter(formdata_id='42-42', file_type='D').latest()
80
        assert csv.filename == 'output-' + timestamp.strftime('%y%m%d-%H%M%S') + '-4200042.csv'
81
        freezer.move_to(datetime.timedelta(seconds=5))
82
        timestamp = now()
83
        response = app.post_json('/gesbac/test/create-demand/', params=payload)
84
        payload['cards_number'] = 2
85
        assert resource.csv_set.filter(formdata_id='42-42', file_type='D').count() == 2
86
        csv = resource.csv_set.filter(formdata_id='42-42', file_type='D').latest()
87
        assert csv.filename == '%s%s-4200142.csv' % (resource.output_files_prefix,
88
                                                      timestamp.strftime('%y%m%d-%H%M%S'))
89

  
90

  
91
def test_get_demand_response(app, resource, sftpserver):
92
    response = app.get('/gesbac/test/get-response/', params={'formdata_id': '42-42'})
93
    assert response.json['err'] == 1
94
    assert response.json['err_desc'] == 'Unknown demand'
95
    assert response.json['err_class'] == 'passerelle.utils.jsonresponse.APIError'
96

  
97
    demand_filename = '%s91001-090000-4200042.csv' % resource.output_files_prefix
98
    CSV.objects.create(resource=resource, formdata_id='42-42',
99
                       filename=demand_filename)
100
    response_filename = '%s91001-090300-4200042.csv' % resource.input_files_prefix
101
    CSV.objects.create(resource=resource, formdata_id='42-42',
102
                       filename=response_filename, file_type='R', data=['CARTE', '42-42'])
103
    response = app.get('/gesbac/test/get-response/', params={'formdata_id': '42-42'})
104
    assert response.json['err'] == 0
105
    assert response.json['data'] == ['CARTE', '42-42']
106

  
107
    CSV.objects.filter(resource=resource, file_type='R').delete()
108

  
109
    with sftpserver.serve_content({'input': {'test': 'content'}}):
110
        response = app.get('/gesbac/test/get-response/', params={'formdata_id': '42-42'})
111
        assert response.json['err'] == 1
112
        assert response.json['err_desc'] == 'Response doest not exist yet'
113
        assert response.json['err_class'] == 'passerelle.utils.jsonresponse.APIError'
114

  
115
    with sftpserver.serve_content({'input': {response_filename: 'CARTE;42-42;3;2;;;;;;;;;'}}):
116
        response = app.get('/gesbac/test/get-response/', params={'formdata_id': '42-42'})
117
        assert response.json['err'] == 0
118
        assert response.json['data'] == [['CARTE', '42-42', '3', '2', '', '', '', '', '', '', '', '', '']]
119
        assert resource.csv_set.filter(file_type='R', filename=response_filename).count() == 1
0
-