Projet

Général

Profil

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

Benjamin Dauvergne, 27 janvier 2017 00:10

Télécharger (17,7 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/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                          | 124 +++++++++++++++++++-
 passerelle/repost/models.py                        |   3 +
 passerelle/utils/__init__.py                       |  30 ++++-
 tests/test_import_export.py                        | 129 +++++++++++++++++++++
 10 files changed, 352 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')
......
359 375
            if len(data[0]) != 1:
360 376
                raise APIError('more or less than one column', data=data)
361 377
            return data[0].values()[0]
378

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

  
384
    @classmethod
385
    def import_json_real(cls, d, **kwargs):
386
        queries = d.pop('queries', [])
387
        instance = super(CsvDataSource, cls).import_json_real(d, **kwargs)
388
        new = []
389
        for query in queries:
390
            q = Query.import_json(query)
391
            q.resource = instance
392
            new.append(q)
393
        Query.objects.bulk_create(new)
394
        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
            # FIXME: Should we dump users ? I think it's dead.
193
            'log_level': self.log_level,
194
            'access_rights': []
195
        }
196
        resource_type = ContentType.objects.get_for_model(self)
197
        for ar in AccessRight.objects.filter(resource_type=resource_type,
198
                                             resource_pk=self.pk).select_related():
199
            d['access_rights'].append({
200
                'codename': ar.codename,
201
                'apiuser': ar.apiuser.username,
202
            })
203
        for field, model in self.__class__._meta.get_concrete_fields_with_model():
204
            if field.name == 'id':
205
                continue
206
            if isinstance(field, (models.TextField, models.CharField, models.SlugField,
207
                                  models.URLField, models.BooleanField, models.IntegerField,
208
                                  models.CommaSeparatedIntegerField, models.EmailField,
209
                                  models.IntegerField, models.PositiveIntegerField, JSONField)):
210
                d[field.name] = getattr(self, field.attname)
211
            elif isinstance(field, models.FileField):
212
                value = getattr(self, field.attname)
213
                d[field.name] = {
214
                    'name': os.path.basename(value.name),
215
                    'content': base64.b64encode(value.read()),
216
                }
217
            else:
218
                raise Exception('export_json: field %s of ressource class %s is unsupported' % (
219
                    field, self.__class__))
220
        return d
221

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

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

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

  
277
        instance.save()
278
        return instance
279

  
158 280

  
159 281
class AccessRight(models.Model):
160 282
    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
    d = {}
181
    d['apiusers'] = [apiuser.export_json() for apiuser in ApiUser.objects.all()]
182
    d['resources'] = resources = []
183
    for subclass in BaseResource.__subclasses__():
184
        if subclass._meta.abstract:
185
            continue
186
        for resource in subclass.objects.all():
187
            try:
188
                resources.append(resource.export_json())
189
            except NotImplementedError:
190
                break
191
    return d
192

  
193

  
194
def import_site(d, import_users=False):
195
    d = d.copy()
196

  
197
    with transaction.atomic():
198
        if import_users:
199
            for apiuser in d['apiusers']:
200
                ApiUser.import_json(apiuser)
201

  
202
        for resource in d['resources']:
203
            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
-