Projet

Général

Profil

0003-implement-JSON-import-export-fixes-13887.patch

Benjamin Dauvergne, 06 mars 2017 13:09

Télécharger (21,2 ko)

Voir les différences:

Subject: [PATCH 3/3] implement JSON import/export (fixes #13887)

You can import/export individual resources or full sites.

  passerelle-manage export_site >export.json

  passerelle-manage import_site [--import-users] [--clean] [--overwrite] [--if-empty] export.json
 passerelle/apps/choosit/models.py                  |   3 +
 passerelle/apps/csvdatasource/models.py            |  35 +++++
 passerelle/base/management/__init__.py             |   0
 passerelle/base/management/commands/__init__.py    |   0
 passerelle/base/management/commands/export_site.py |  14 ++
 passerelle/base/management/commands/import_site.py |  29 ++++
 passerelle/base/models.py                          | 137 ++++++++++++++++-
 passerelle/repost/models.py                        |   3 +
 passerelle/utils/__init__.py                       |  57 ++++++-
 tests/test_import_export.py                        | 168 +++++++++++++++++++++
 10 files changed, 444 insertions(+), 2 deletions(-)
 create mode 100644 passerelle/base/management/__init__.py
 create mode 100644 passerelle/base/management/commands/__init__.py
 create mode 100644 passerelle/base/management/commands/export_site.py
 create mode 100644 passerelle/base/management/commands/import_site.py
 create mode 100644 tests/test_import_export.py
passerelle/apps/choosit/models.py
196 196
        reg = ChoositRegisterWS(self.url, self.key)
197 197
        ws = reg.update(subscriptions, user)
198 198
        return {"message": ws['status']}
199

  
200
    def export_json(self):
201
        raise NotImplementedError
passerelle/apps/csvdatasource/models.py
83 83
            return []
84 84
        return getattr(self, attribute).strip().splitlines()
85 85

  
86
    def export_json(self):
87
        return {
88
            'slug': self.slug,
89
            'label': self.label,
90
            'description': self.description,
91
            'filters': self.filters,
92
            'projections': self.projections,
93
            'order': self.order,
94
            'distinct': self.distinct,
95
            'structure': self.structure,
96
        }
97

  
98
    @classmethod
99
    def import_json(cls, d):
100
        return cls(**d)
101

  
86 102

  
87 103
class CsvDataSource(BaseResource):
88 104
    csv_file = models.FileField(_('CSV File'), upload_to='csv')
......
360 376
            if len(data[0]) != 1:
361 377
                raise APIError('more or less than one column', data=data)
362 378
            return data[0].values()[0]
379

  
380
    def export_json(self):
381
        d = super(CsvDataSource, self).export_json()
382
        d['queries'] = [query.export_json() for query in Query.objects.filter(resource=self)]
383
        return d
384

  
385
    @classmethod
386
    def import_json_real(cls, overwrite, instance, d, **kwargs):
387
        queries = d.pop('queries', [])
388
        instance = super(CsvDataSource, cls).import_json_real(overwrite, instance, d, **kwargs)
389
        new = []
390
        if instance and overwrite:
391
            Query.objects.filter(resource=instance).delete()
392
        for query in queries:
393
            q = Query.import_json(query)
394
            q.resource = instance
395
            new.append(q)
396
        Query.objects.bulk_create(new)
397
        return instance
passerelle/base/management/commands/export_site.py
1
import json
2
import sys
3

  
4
from django.core.management.base import BaseCommand
5

  
6
from passerelle.utils import export_site
7

  
8

  
9
class Command(BaseCommand):
10
    args = ''
11
    help = 'Export the site'
12

  
13
    def handle(self, *args, **options):
14
        json.dump(export_site(), sys.stdout, indent=4)
passerelle/base/management/commands/import_site.py
1
import json
2
from optparse import make_option
3

  
4
from django.core.management.base import BaseCommand
5

  
6
from passerelle.base.models import ApiUser, BaseResource
7
from passerelle.utils import import_site
8

  
9

  
10
class Command(BaseCommand):
11
    args = '<filename>'
12
    help = 'Import an exported site'
13
    option_list = BaseCommand.option_list + (
14
        make_option('--clean', action='store_true', default=False,
15
                    help='Clean site before importing'),
16
        make_option('--import-users', action='store_true', default=False,
17
                    help='Import users and access rights'),
18
        make_option('--if-empty', action='store_true', default=False,
19
                    help='Import only if passerelle is empty'),
20
        make_option('--overwrite', action='store_true', default=False,
21
                    help='Overwirte existing resources'),
22
    )
23

  
24
    def handle(self, filename, **options):
25
        import_site(json.load(open(filename)),
26
                    if_empty=options['if_empty'],
27
                    clean=options['clean'],
28
                    overwrite=options['overwrite'],
29
                    import_users=options['import_users'])
passerelle/base/models.py
1 1
import logging
2
import os
3
import base64
2 4

  
5
from django.apps import apps
3 6
from django.conf import settings
4 7
from django.core.exceptions import ValidationError, ObjectDoesNotExist
5 8
from django.core.urlresolvers import reverse
6
from django.db import models
9
from django.db import models, transaction
7 10
from django.db.models import Q
8 11
from django.utils.translation import ugettext_lazy as _
9 12
from django.utils.text import slugify
13
from django.core.files.base import ContentFile
10 14

  
11 15
from django.contrib.contenttypes.models import ContentType
12 16
from django.contrib.contenttypes import fields
13 17

  
18
from jsonfield import JSONField
19

  
14 20
from model_utils.managers import InheritanceManager as ModelUtilsInheritanceManager
15 21

  
16 22
import passerelle
......
44 50
        if self.keytype and not self.key:
45 51
            raise ValidationError(_('Key can not be empty for type %s.') % self.keytype)
46 52

  
53
    def export_json(self):
54
        return {
55
            '@type': 'passerelle-user',
56
            'username': self.username,
57
            'fullname': self.fullname,
58
            'description': self.description,
59
            'keytype': self.keytype,
60
            'key': self.key,
61
            'ipsource': self.ipsource,
62
        }
63

  
64
    @classmethod
65
    def import_json(self, d, overwrite=False):
66
        if d.get('@type') != 'passerelle-user':
67
            raise ValueError('not a passerelle user export')
68
        d = d.copy()
69
        d.pop('@type')
70
        api_user, created = self.objects.get_or_create(username=d['username'], defaults=d)
71
        if overwrite and not created:
72
            for key in d:
73
                setattr(api_user, key, d[key])
74
            api_user.save()
75

  
47 76

  
48 77
class TemplateVar(models.Model):
49 78
    name = models.CharField(max_length=64)
......
155 184
        return [(x, getattr(self, x.name, None)) for x in self._meta.fields if x.name not in (
156 185
            'id', 'title', 'slug', 'description', 'log_level', 'users')]
157 186

  
187
    def export_json(self):
188
        d = {
189
            '@type': 'passerelle-resource',
190
            'resource_type': '%s.%s' % (self.__class__._meta.app_label,
191
                                        self.__class__._meta.model_name),
192
            'title': self.title,
193
            'slug': self.slug,
194
            'description': self.description,
195
            'log_level': self.log_level,
196
            'access_rights': []
197
        }
198
        resource_type = ContentType.objects.get_for_model(self)
199
        for ar in AccessRight.objects.filter(resource_type=resource_type,
200
                                             resource_pk=self.pk).select_related():
201
            d['access_rights'].append({
202
                'codename': ar.codename,
203
                'apiuser': ar.apiuser.username,
204
            })
205
        for field, model in self.__class__._meta.get_concrete_fields_with_model():
206
            if field.name == 'id':
207
                continue
208
            value = getattr(self, field.attname)
209
            if isinstance(field, (models.TextField, models.CharField, models.SlugField,
210
                                  models.URLField, models.BooleanField, models.IntegerField,
211
                                  models.CommaSeparatedIntegerField, models.EmailField,
212
                                  models.IntegerField, models.PositiveIntegerField, JSONField)):
213
                d[field.name] = value
214
            elif isinstance(field, models.FileField):
215
                if value:
216
                    d[field.name] = {
217
                        'name': os.path.basename(value.name),
218
                        'content': base64.b64encode(value.read()),
219
                    }
220
                else:
221
                    d[field.name] = None
222
            else:
223
                raise Exception('export_json: field %s of ressource class %s is unsupported' % (
224
                    field, self.__class__))
225
        return d
226

  
227
    @staticmethod
228
    def import_json(d, import_users=False, overwrite=False):
229
        if d.get('@type') != 'passerelle-resource':
230
            raise ValueError('not a passerelle resource export')
231

  
232
        d = d.copy()
233
        d.pop('@type')
234
        app_label, model_name = d['resource_type'].split('.')
235
        model = apps.get_model(app_label, model_name)
236
        try:
237
            instance = model.objects.get(slug=d['slug'])
238
            if not overwrite:
239
                return
240
        except model.DoesNotExist:
241
            instance = None
242
        with transaction.atomic():
243
            # prevent semi-creation of ressources
244
            instance = model.import_json_real(overwrite, instance, d)
245
            resource_type = ContentType.objects.get_for_model(instance)
246
            # We can only connect AccessRight objects to the new Resource after its creation
247
            if import_users:
248
                for ar in d['access_rights']:
249
                    apiuser = ApiUser.objects.get(username=ar['apiuser'])
250
                    AccessRight.objects.get_or_create(
251
                        codename=ar['codename'],
252
                        resource_type=resource_type,
253
                        resource_pk=instance.pk,
254
                        apiuser=apiuser)
255
        return instance
256

  
257
    @classmethod
258
    def import_json_real(cls, overwrite, instance, d, **kwargs):
259
        init_kwargs = {
260
            'title': d['title'],
261
            'slug': d['slug'],
262
            'description': d['description'],
263
            'log_level': d['log_level'],
264
        }
265
        init_kwargs.update(kwargs)
266
        if instance:
267
            for key in init_kwargs:
268
                setattr(instance, key, init_kwargs[key])
269
        else:
270
            instance = cls(**init_kwargs)
271
        for field, model in cls._meta.get_concrete_fields_with_model():
272
            if field.name == 'id':
273
                continue
274
            value = d[field.name]
275
            if isinstance(field, (models.TextField, models.CharField, models.SlugField,
276
                                  models.URLField, models.BooleanField, models.IntegerField,
277
                                  models.CommaSeparatedIntegerField, models.EmailField,
278
                                  models.IntegerField, models.PositiveIntegerField, JSONField)):
279
                setattr(instance, field.attname, value)
280
            elif isinstance(field, models.FileField):
281
                if value:
282
                    getattr(instance, field.attname).save(
283
                        value['name'],
284
                        ContentFile(base64.b64decode(value['content'])),
285
                        save=False)
286
            else:
287
                raise Exception('import_json_real: field %s of ressource class '
288
                                '%s is unsupported' % (field, cls))
289

  
290
        instance.save()
291
        return instance
292

  
158 293

  
159 294
class AccessRight(models.Model):
160 295
    codename = models.CharField(max_length=100, verbose_name='codename')
passerelle/repost/models.py
20 20
        out_data = json.loads(p.read())
21 21
        p.close()
22 22
        return out_data
23

  
24
    def export_json(self):
25
        raise NotImplementedError
passerelle/utils/__init__.py
13 13
from django.utils.decorators import available_attrs
14 14
from django.views.generic.detail import SingleObjectMixin
15 15
from django.contrib.contenttypes.models import ContentType
16
from django.db import transaction
16 17

  
17 18
from passerelle.base.context_processors import template_vars
18
from passerelle.base.models import ApiUser, AccessRight
19
from passerelle.base.models import ApiUser, AccessRight, BaseResource
19 20
from passerelle.base.signature import check_query, check_url
20 21

  
21 22
from .jsonresponse import to_json
......
173 174
            extra={'requests_response_content': content})
174 175

  
175 176
        return response
177

  
178

  
179
def export_site():
180
    '''Dump passerelle configuration (users, resources and ACLs) to JSON dumpable dictionnary'''
181
    d = {}
182
    d['apiusers'] = [apiuser.export_json() for apiuser in ApiUser.objects.all()]
183
    d['resources'] = resources = []
184
    for subclass in BaseResource.__subclasses__():
185
        if subclass._meta.abstract:
186
            continue
187
        for resource in subclass.objects.all():
188
            try:
189
                resources.append(resource.export_json())
190
            except NotImplementedError:
191
                break
192
    return d
193

  
194

  
195
def import_site(d, if_empty=False, clean=False, overwrite=False, import_users=False):
196
    '''Load passerelle configuration (users, resources and ACLs) from a dictionnary loaded from
197
       JSON
198
    '''
199
    d = d.copy()
200

  
201
    def is_empty():
202
        if import_users:
203
            if ApiUser.objects.count():
204
                return False
205

  
206
        for subclass in BaseResource.__subclasses__():
207
            if subclass._meta.abstract:
208
                continue
209
            if subclass.objects.count():
210
                return False
211
        return True
212

  
213
    if if_empty and not is_empty():
214
        return
215

  
216
    if clean:
217
        for subclass in BaseResource.__subclasses__():
218
            if subclass._meta.abstract:
219
                continue
220
            subclass.objects.all().delete()
221
        if import_users:
222
            ApiUser.objects.all().delete()
223

  
224
    with transaction.atomic():
225
        if import_users:
226
            for apiuser in d['apiusers']:
227
                ApiUser.import_json(apiuser, overwrite=overwrite)
228

  
229
        for resource in d['resources']:
230
            BaseResource.import_json(resource, overwrite=overwrite, import_users=import_users)
tests/test_import_export.py
1
# -*- coding: utf-8 -*-
2
import sys
3
import json
4
import os
5
import pytest
6
from StringIO import StringIO
7
import tempfile
8

  
9
from django.core.files import File
10
from django.core.urlresolvers import reverse
11
from django.core.management import call_command
12
from django.contrib.contenttypes.models import ContentType
13
from django.test import Client
14

  
15
from passerelle.base.models import ApiUser, AccessRight
16
from passerelle.utils import import_site, export_site
17

  
18
data = """121;69981;DELANOUE;Eliot;H
19
525;6;DANIEL WILLIAMS;Shanone;F
20
253;67742;MARTIN;Sandra;F
21
511;38142;MARCHETTI;Lucie;F
22
235;22;MARTIN;Sandra;F
23
620;52156;ARNAUD;Mathis;H
24
902;36;BRIGAND;Coline;F
25
2179;48548;THEBAULT;Salima;F
26
3420;46;WILSON-LUZAYADIO;Anaëlle;F
27
1421;55486;WONE;Fadouma;F
28
2841;51;FIDJI;Zakia;F
29
2431;59;BELICARD;Sacha;H
30
4273;60;GOUBERT;Adrien;H
31
4049;64;MOVSESSIAN;Dimitri;H
32
4605;67;ABDOU BACAR;Kyle;H
33
4135;22231;SAVERIAS;Marius;H
34
4809;75;COROLLER;Maelys;F
35
5427;117;KANTE;Aliou;H
36
116642;118;ZAHMOUM;Yaniss;H
37
216352;38;Dupont;Benoît;H
38
"""
39

  
40
data_bom = data.decode('utf-8').encode('utf-8-sig')
41

  
42
from csvdatasource.models import CsvDataSource, Query
43
from bdp.models import Bdp
44

  
45
pytestmark = pytest.mark.django_db
46

  
47
TEST_BASE_DIR = os.path.join(os.path.dirname(__file__), 'data', 'csvdatasource')
48

  
49

  
50
def get_file_content(filename):
51
    return file(os.path.join(TEST_BASE_DIR, filename)).read()
52

  
53

  
54
@pytest.fixture
55
def setup():
56

  
57
    def maker(columns_keynames='fam,id,lname,fname,sex', filename='data.csv', sheet_name='Feuille2',
58
              data=''):
59
        api = ApiUser.objects.create(username='all', keytype='', key='')
60
        csv = CsvDataSource.objects.create(csv_file=File(StringIO(data), filename),
61
                                           sheet_name=sheet_name, columns_keynames=columns_keynames,
62
                                           slug='test', title='a title',
63
                                           description='a description')
64
        obj_type = ContentType.objects.get_for_model(csv)
65
        AccessRight.objects.create(codename='can_access', apiuser=api, resource_type=obj_type,
66
                                   resource_pk=csv.pk)
67
        url = reverse('csvdatasource-data', kwargs={'slug': csv.slug})
68
        return csv, url
69

  
70
    return maker
71

  
72

  
73
def parse_response(response):
74
    return json.loads(response.content)['data']
75

  
76

  
77
@pytest.fixture
78
def client():
79
    return Client()
80

  
81

  
82
@pytest.fixture(params=['data.csv', 'data.ods', 'data.xls', 'data.xlsx'])
83
def filetype(request):
84
    return request.param
85

  
86

  
87
def get_output_of_command(command, *args, **kwargs):
88
    old_stdout = sys.stdout
89
    output = sys.stdout = StringIO()
90
    call_command(command, *args, **kwargs)
91
    sys.stdout = old_stdout
92
    return output.getvalue()
93

  
94

  
95
def test_export_csvdatasource(app, setup, filetype):
96
    def clear():
97
        Query.objects.all().delete()
98
        CsvDataSource.objects.all().delete()
99
        ApiUser.objects.all().delete()
100

  
101
    csvdata, url = setup('id,whatever,nom,prenom,sexe', filename=filetype,
102
                         data=get_file_content(filetype))
103
    query = Query(slug='query-1_', resource=csvdata, structure='array')
104
    query.projections = '\n'.join(['id:int(id)', 'prenom:prenom'])
105
    query.save()
106

  
107
    first = export_site()
108
    clear()
109
    import_site(first, import_users=True)
110
    second = export_site()
111

  
112
    # we don't care about file names
113
    first['resources'][0]['csv_file']['name'] = 'whocares'
114
    second['resources'][0]['csv_file']['name'] = 'whocares'
115
    assert first == second
116

  
117
    output = get_output_of_command('export_site')
118
    third = json.loads(output)
119
    third['resources'][0]['csv_file']['name'] = 'whocares'
120
    assert first == third
121

  
122
    clear()
123
    with tempfile.NamedTemporaryFile() as f:
124
        f.write(output)
125
        f.flush()
126
        call_command('import_site', f.name, import_users=True)
127
    fourth = export_site()
128
    fourth['resources'][0]['csv_file']['name'] = 'whocares'
129
    assert first == fourth
130

  
131
    Query.objects.all().delete()
132

  
133
    with tempfile.NamedTemporaryFile() as f:
134
        f.write(output)
135
        f.flush()
136
        call_command('import_site', f.name, import_users=True)
137
    assert Query.objects.count() == 0
138

  
139
    with tempfile.NamedTemporaryFile() as f:
140
        f.write(output)
141
        f.flush()
142
        call_command('import_site', f.name, overwrite=True, import_users=True)
143
    assert Query.objects.count() == 1
144

  
145
    query = Query(slug='query-2_', resource=CsvDataSource.objects.get(), structure='array')
146
    query.projections = '\n'.join(['id:int(id)', 'prenom:prenom'])
147
    query.save()
148

  
149
    assert Query.objects.count() == 2
150

  
151
    with tempfile.NamedTemporaryFile() as f:
152
        f.write(output)
153
        f.flush()
154
        call_command('import_site', f.name, clean=True, overwrite=True, import_users=True)
155
    assert Query.objects.count() == 1
156
    assert Query.objects.filter(slug='query-1_').exists()
157

  
158

  
159
def test_import_export_empty_filefield(app, setup, filetype):
160
    Bdp.objects.create(service_url='https://bdp.example.com/')
161

  
162
    first = export_site()
163
    import_site(first, import_users=True)
164
    second = export_site()
165

  
166
    # we don't care about file names
167
    assert first['resources'][0]['keystore'] is None
168
    assert second['resources'][0]['keystore'] is None
0
-