Projet

Général

Profil

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

Benjamin Dauvergne, 06 mars 2017 10:56

Télécharger (17,9 ko)

Voir les différences:

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

You can import/export individual resources or full sites.
 passerelle/apps/choosit/models.py                  |   3 +
 passerelle/apps/csvdatasource/models.py            |  33 ++++++
 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 |  18 +++
 passerelle/base/models.py                          | 123 +++++++++++++++++++-
 passerelle/repost/models.py                        |   3 +
 passerelle/utils/__init__.py                       |  34 +++++-
 tests/test_import_export.py                        | 129 +++++++++++++++++++++
 10 files changed, 355 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, d, **kwargs):
387
        queries = d.pop('queries', [])
388
        instance = super(CsvDataSource, cls).import_json_real(d, **kwargs)
389
        new = []
390
        for query in queries:
391
            q = Query.import_json(query)
392
            q.resource = instance
393
            new.append(q)
394
        Query.objects.bulk_create(new)
395
        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.utils import import_site
7

  
8

  
9
class Command(BaseCommand):
10
    args = '<filename>'
11
    help = 'Import an exported site'
12
    option_list = BaseCommand.option_list + (
13
        make_option('--import-users', action='store_true', default=False,
14
                    help='Import users and access rights'),
15
    )
16

  
17
    def handle(self, filename, **options):
18
        import_site(json.load(open(filename)), 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):
66
        if d.get('@type') != 'passerelle-user':
67
            raise ValueError('not a passerelle user export')
68
        d = d.copy()
69
        d.pop('@type')
70
        return self.objects.get_or_create(username=d['username'],
71
                                          defaults=d)
72

  
47 73

  
48 74
class TemplateVar(models.Model):
49 75
    name = models.CharField(max_length=64)
......
155 181
        return [(x, getattr(self, x.name, None)) for x in self._meta.fields if x.name not in (
156 182
            'id', 'title', 'slug', 'description', 'log_level', 'users')]
157 183

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

  
221
    @staticmethod
222
    def import_json(d, import_users=False):
223
        if d.get('@type') != 'passerelle-resource':
224
            raise ValueError('not a passerelle resource export')
225

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

  
249
    @classmethod
250
    def import_json_real(cls, d, **kwargs):
251
        init_kwargs = {
252
            'title': d['title'],
253
            'slug': d['slug'],
254
            'description': d['description'],
255
            'log_level': d['log_level'],
256
        }
257
        init_kwargs.update(kwargs)
258
        instance = cls(**init_kwargs)
259
        for field, model in cls._meta.get_concrete_fields_with_model():
260
            if field.name == 'id':
261
                continue
262
            if isinstance(field, (models.TextField, models.CharField, models.SlugField,
263
                                  models.URLField, models.BooleanField, models.IntegerField,
264
                                  models.CommaSeparatedIntegerField, models.EmailField,
265
                                  models.IntegerField, models.PositiveIntegerField, JSONField)):
266
                setattr(instance, field.attname, d[field.name])
267
            elif isinstance(field, models.FileField):
268
                getattr(instance, field.attname).save(
269
                    d[field.name]['name'],
270
                    ContentFile(base64.b64decode(d[field.name]['content'])),
271
                    save=False)
272
            else:
273
                raise Exception('import_json_real: field %s of ressource class '
274
                                '%s is unsupported' % (field, cls))
275

  
276
        instance.save()
277
        return instance
278

  
158 279

  
159 280
class AccessRight(models.Model):
160 281
    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, 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
    with transaction.atomic():
202
        if import_users:
203
            for apiuser in d['apiusers']:
204
                ApiUser.import_json(apiuser)
205

  
206
        for resource in d['resources']:
207
            BaseResource.import_json(resource, 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.wsgi import get_wsgi_application
10
from webtest import TestApp
11
from django.contrib.auth.models import User
12
from django.core.files import File
13
from django.core.urlresolvers import reverse
14
from django.core.management import call_command
15
from django.contrib.contenttypes.models import ContentType
16
from django.test import Client
17

  
18
from passerelle.base.models import ApiUser, AccessRight
19
from test_manager import login, admin_user
20
from passerelle.utils import import_site, export_site
21

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

  
44
data_bom = data.decode('utf-8').encode('utf-8-sig')
45

  
46
from csvdatasource.models import CsvDataSource, Query
47

  
48
pytestmark = pytest.mark.django_db
49

  
50
TEST_BASE_DIR = os.path.join(os.path.dirname(__file__), 'data', 'csvdatasource')
51

  
52

  
53
def get_file_content(filename):
54
    return file(os.path.join(TEST_BASE_DIR, filename)).read()
55

  
56

  
57
@pytest.fixture
58
def setup():
59

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

  
73
    return maker
74

  
75

  
76
def parse_response(response):
77
    return json.loads(response.content)['data']
78

  
79

  
80
@pytest.fixture
81
def client():
82
    return Client()
83

  
84

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

  
89

  
90
def test_export_csvdatasource(app, setup, filetype):
91
    def clear():
92
        Query.objects.all().delete()
93
        CsvDataSource.objects.all().delete()
94
        ApiUser.objects.all().delete()
95

  
96
    csvdata, url = setup('id,whatever,nom,prenom,sexe', filename=filetype,
97
                         data=get_file_content(filetype))
98
    query = Query(slug='query-1_', resource=csvdata, structure='array')
99
    query.projections = '\n'.join(['id:int(id)', 'prenom:prenom'])
100
    query.save()
101

  
102
    first = export_site()
103
    clear()
104
    import_site(first, import_users=True)
105
    second = export_site()
106

  
107
    # we don't care about file names
108
    first['resources'][0]['csv_file']['name'] = 'whocares'
109
    second['resources'][0]['csv_file']['name'] = 'whocares'
110
    assert first == second
111

  
112
    old_stdout = sys.stdout
113
    output = sys.stdout = StringIO()
114
    call_command('export_site')
115
    sys.stdout = old_stdout
116

  
117
    output = output.getvalue()
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
0
-