Projet

Général

Profil

0001-environment-import-and-export-parameters-33672.patch

Nicolas Roche, 10 août 2020 16:47

Télécharger (15,8 ko)

Voir les différences:

Subject: [PATCH] environment: import and export parameters (#33672)

 hobo/environment/forms.py                     |   4 +
 .../templates/environment/import.html         |  22 ++++
 hobo/environment/urls.py                      |   3 +
 hobo/environment/utils.py                     |  47 +++++++
 hobo/environment/views.py                     |  36 +++++-
 hobo/templates/hobo/home.html                 |   2 +
 tests/test_environment.py                     | 116 ++++++++++++++++++
 7 files changed, 229 insertions(+), 1 deletion(-)
 create mode 100644 hobo/environment/templates/environment/import.html
hobo/environment/forms.py
205 205
            if variable.value != form.cleaned_data[variable_name]:
206 206
                variable.value = form.cleaned_data[variable_name]
207 207
                variable.save()
208 208
                changed = True
209 209
        if changed and self.success_message:
210 210
            messages.info(self.request, self.success_message)
211 211

  
212 212
        return HttpResponseRedirect('.')
213

  
214

  
215
class ImportForm(forms.Form):
216
    parameters_json = forms.FileField(label=_('Parameters Export File'))
hobo/environment/templates/environment/import.html
1
{% extends "hobo/base.html" %}
2
{% load i18n %}
3

  
4
{% block appbar %}
5
<h2>{% trans "Parameters Import" %}</h2>
6
{% endblock %}
7

  
8
{% block breadcrumb %}
9
{{ block.super }}
10
<a href="{% url 'environment-import' %}">{% trans 'Parameters Import' %}</a>
11
{% endblock %}
12

  
13
{% block content %}
14
<form method="post" enctype="multipart/form-data">
15
  {% csrf_token %}
16
  {{ form.as_p }}
17
  <div class="buttons">
18
    <button class="submit-button">{% trans "Import" %}</button>
19
    <a class="cancel" href="{% url 'home' %}">{% trans 'Cancel' %}</a>
20
  </div>
21
</form>
22
{% endblock %}
hobo/environment/urls.py
29 29
    url(r'^check_operational/(?P<service>\w+)/(?P<slug>[\w-]+)$',
30 30
        views.operational_check_view, name='operational-check'),
31 31
    url(r'^new-(?P<service>\w+)$', views.ServiceCreateView.as_view(), name='create-service'),
32 32
    url(r'^save-(?P<service>\w+)/(?P<slug>[\w-]+)$', views.ServiceUpdateView.as_view(), name='save-service'),
33 33
    url(r'^delete-(?P<service>\w+)/(?P<slug>[\w-]+)$', views.ServiceDeleteView.as_view(), name='delete-service'),
34 34

  
35 35
    url(r'^new-variable-(?P<service>\w+)/(?P<slug>[\w-]+)$',
36 36
        views.VariableCreateView.as_view(), name='new-variable-service',),
37

  
38
    url(r'^import/$', views.ImportView.as_view(), name='environment-import'),
39
    url(r'^export/$', views.ExportView.as_view(), name='environment-export'),
37 40
    url(r'^debug.json$', views.debug_json, name='debug-json'),
38 41
]
hobo/environment/utils.py
19 19
from django.conf import settings
20 20
from django.urls import reverse
21 21
from django.db import connection
22 22
from django.utils.six.moves.urllib.parse import urlparse
23 23
from django.utils.encoding import force_text
24 24

  
25 25
from hobo.middleware.utils import StoreRequestMiddleware
26 26
from hobo.multitenant.settings_loaders import KnownServices
27
from hobo.profile.utils import get_profile_dict
27 28

  
28 29

  
29 30
def get_installed_services():
30 31
    from .models import AVAILABLE_SERVICES
31 32
    installed_services = []
32 33
    for available_service in AVAILABLE_SERVICES:
33 34
        installed_services.extend(available_service.objects.all())
34 35
    return installed_services
......
139 140
    except Variable.DoesNotExist:
140 141
        variable = Variable(
141 142
            name='SETTING_' + setting_name,
142 143
            label=label or '',
143 144
            service=service,
144 145
            auto=True)
145 146

  
146 147
    return variable
148

  
149

  
150
def export_parameters():
151
    from .models import Variable
152

  
153
    variables = []
154
    for var in Variable.objects.filter(service_pk__isnull=True):
155
        variables.append({
156
            'name': var.name,
157
            'label': var.label,
158
            'value': var.value,
159
            'auto': var.auto})
160
    parameters = {'variables': variables}
161
    parameters.update(get_profile_dict())
162
    return parameters
163

  
164

  
165
def import_parameters(parameters):
166
    from .models import Variable
167
    from hobo.profile.models import AttributeDefinition
168

  
169
    objects = []
170
    try:
171
        for variables in parameters.get('variables'):
172
            obj, created = Variable.objects.get_or_create(name=variables['name'])
173
            for key, value in variables.items():
174
                if key == 'name':
175
                    continue
176
                setattr(obj, key, value)
177
            objects.append(obj)
178

  
179
        for fields in parameters.get('profile', []).get('fields'):
180
            obj, created = AttributeDefinition.objects.get_or_create(
181
                label=fields['label'], name=fields['name'])
182
            for key, value in fields.items():
183
                if key == 'label' or key == 'name':
184
                    continue
185
                setattr(obj, key, value)
186
            objects.append(obj)
187

  
188
    except Exception as exc:
189
        return {'err': True, 'message': '%s, %s' % (exc.__class__.__name__, exc)}
190

  
191
    for obj in objects:
192
        obj.save()
193
    return {'err': False}
hobo/environment/views.py
17 17
import json
18 18
import string
19 19

  
20 20
from django.conf import settings
21 21
from django.contrib.contenttypes.models import ContentType
22 22
from django.urls import reverse_lazy
23 23
from django.http import HttpResponse, HttpResponseRedirect, Http404
24 24
from django.shortcuts import get_object_or_404
25
from django.utils.encoding import force_text
26
from django.utils.translation import ugettext_lazy as _
27
from django.views.generic import View
25 28
from django.views.generic.base import TemplateView
26
from django.views.generic.edit import CreateView, UpdateView, DeleteView
29
from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView
27 30

  
28 31
from .models import Variable, AVAILABLE_SERVICES
29 32
from . import forms, utils
30 33

  
31 34

  
32 35
class AvailableService(object):
33 36
    def __init__(self, klass):
34 37
        self.id = klass.Extra.service_id
......
194 197
        service_id = self.kwargs.pop('service')
195 198
        service_slug = self.kwargs.pop('slug')
196 199
        for service in AVAILABLE_SERVICES:
197 200
            if service.Extra.service_id == service_id:
198 201
                return service.objects.get(slug=service_slug)
199 202
        return None
200 203

  
201 204

  
205
class ImportView(FormView):
206
    form_class = forms.ImportForm
207
    template_name = 'environment/import.html'
208
    success_url = reverse_lazy('home')
209

  
210
    def form_valid(self, form):
211
        try:
212
            parameters_json = json.loads(
213
                force_text(self.request.FILES['parameters_json'].read()))
214
        except ValueError:
215
            form.add_error('parameters_json', _('File is not in the expected JSON format.'))
216
            return self.form_invalid(form)
217

  
218
        result = utils.import_parameters(parameters_json)
219
        if result.get('err'):
220
            form.add_error('parameters_json',
221
                           _('Parameters import fails: %s') % force_text(result.get('message')))
222
            return self.form_invalid(form)
223

  
224
        return super(ImportView, self).form_valid(form)
225

  
226

  
227
class ExportView(View):
228

  
229
    def get(self, request, *args, **kwargs):
230
        response = HttpResponse(content_type='application/json')
231
        response['Content-Disposition'] = 'attachment; filename="hobo-export.json"'
232
        json.dump(utils.export_parameters(), response, indent=2)
233
        return response
234

  
235

  
202 236
def operational_check_view(request, service, slug, **kwargs):
203 237

  
204 238
    for klass in AVAILABLE_SERVICES:
205 239
        if klass.Extra.service_id == service:
206 240
            break
207 241
    else:
208 242
        raise Http404()
209 243

  
hobo/templates/hobo/home.html
13 13
    <li><a href="{% url 'emails-home' %}">{% trans 'Emails' %}</a></li>
14 14
    {% if has_authentic %}
15 15
    <li><a href="{% url 'franceconnect-home' %}">FranceConnect</a></li>
16 16
    {% endif %}
17 17
    <li><a href="{% url 'matomo-home' %}">{% trans 'User tracking' %}</a></li>
18 18
    <li><a href="{% url 'seo-home' %}">{% trans 'Indexing' %}</a></li>
19 19
    <li><a href="{% url 'environment-home' %}">{% trans 'Services' %}</a></li>
20 20
    <li><a href="{% url 'environment-variables' %}">{% trans 'Variables' %}</a></li>
21
    <li><a rel="popup" href="{% url 'environment-import' %}">{% trans 'Import' %}</a></li>
22
    <li><a href="{% url 'environment-export' %}">{% trans 'Export' %}</a></li>
21 23
    <li><a href="{% url 'debug-home' %}">{% trans 'Debugging' %}</a></li>
22 24
  </ul>
23 25
  </span>
24 26
{% endblock %}
25 27

  
26 28
{% block content %}
27 29

  
28 30
{% if not has_global_title %}
tests/test_environment.py
1 1
# -*- coding: utf-8 -*-
2
import json
2 3
import pytest
4
from webtest import Upload
3 5

  
4 6
from django.core.exceptions import ValidationError
5 7
from django.core.management import call_command
6 8
from django.utils import timezone
7 9

  
8 10
from hobo.environment.models import AVAILABLE_SERVICES, Combo, Passerelle, ServiceBase, Variable
11
from hobo.profile.models import AttributeDefinition
9 12

  
10 13
from test_manager import login
11 14

  
12 15
pytestmark = pytest.mark.django_db
13 16

  
14 17

  
15 18
def test_service_id():
16 19
    for service in AVAILABLE_SERVICES:
......
311 314
    combo.last_operational_success_timestamp = '2022-2-22'
312 315
    combo.save()
313 316
    call_command('check_operational', '-v2')
314 317
    captured = capsys.readouterr()
315 318
    assert captured.out.split('\n')[:-1] == [
316 319
        'foo is NOT operational',
317 320
        '  last operational success: 2022-02-22 00:00:00+00:00'
318 321
    ]
322

  
323

  
324
def test_export_import_view(app, admin_user):
325
    combo = Combo.objects.create(base_url='https://combo.agglo.love',
326
                                 template_name='...portal-user...',
327
                                 slug='portal')
328
    Variable.objects.create(name='foo', value='bar').save()
329
    Variable.objects.create(name='foo2', value='bar2', service=combo).save()
330
    app = login(app, 'admin', 'password')
331
    resp = app.get('/sites/export/', status=200)
332
    assert sorted(resp.json.keys()) == ['profile', 'variables']
333
    assert resp.json['variables'] == [
334
        {'name': 'foo', 'label': '', 'value': 'bar', 'auto': False}]
335
    assert resp.json['profile']['fields'][0]['name'] == 'title'
336
    assert resp.json['profile']['fields'][0]['required'] is False
337
    assert resp.json['profile']['fields'][1]['name'] == 'first_name'
338
    assert resp.json['profile']['fields'][1]['required'] is True
339

  
340
    # modify exported file
341
    export = resp.json
342
    export['variables'][0]['label'] = 'bar'
343
    fields = export['profile']['fields']
344
    assert fields[0]['name'] == 'title'
345
    assert fields[1]['name'] == 'first_name'
346
    fields[0]['description'] = 'genre'
347
    fields[0], fields[1] = fields[1], fields[0]
348
    export_json = json.dumps(export)
349

  
350
    # add new content
351
    Variable.objects.create(name='foo3', value='bar3').save()
352
    AttributeDefinition.objects.create(name='prefered_color').save()
353
    assert Variable.objects.count() == 3
354
    assert AttributeDefinition.objects.count() == 12
355
    assert Variable.objects.get(name='foo').label == ''
356
    assert AttributeDefinition.objects.get(name='title').description == ''
357
    assert AttributeDefinition.objects.get(name='title').order == 1
358
    assert AttributeDefinition.objects.get(name='first_name').order == 2
359
    assert AttributeDefinition.objects.get(name='prefered_color').order == 12
360

  
361
    # import valid content
362
    resp = app.get('/', status=200)
363
    resp = resp.click('Import')
364
    resp.form['parameters_json'] = Upload(
365
        'export.json', export_json.encode('utf-8'), 'application/json')
366
    resp = resp.form.submit()
367
    assert Variable.objects.count() == 3
368
    assert AttributeDefinition.objects.count() == 12
369
    assert Variable.objects.get(name='foo').label == 'bar'
370
    assert AttributeDefinition.objects.get(name='title').description == 'genre'
371
    assert AttributeDefinition.objects.get(name='title').order == 1
372
    assert AttributeDefinition.objects.get(name='first_name').order == 2
373
    assert AttributeDefinition.objects.get(name='prefered_color').order == 12
374

  
375
    # import empty json
376
    resp = app.get('/', status=200)
377
    resp = resp.click('Import')
378
    resp.form['parameters_json'] = Upload(
379
        'export.json', b'{}', 'application/json')
380
    resp = resp.form.submit()
381
    assert Variable.objects.count() == 3
382
    assert AttributeDefinition.objects.count() == 12
383
    assert Variable.objects.get(name='foo').label == 'bar'
384
    assert AttributeDefinition.objects.get(name='title').description == 'genre'
385
    assert AttributeDefinition.objects.get(name='title').order == 1
386
    assert AttributeDefinition.objects.get(name='first_name').order == 2
387
    assert AttributeDefinition.objects.get(name='prefered_color').order == 12
388

  
389
    # import from scratch
390
    Variable.objects.all().delete()
391
    AttributeDefinition.objects.all().delete()
392
    Variable.objects.create(name='foo2', value='bar2', service=combo).save()
393
    AttributeDefinition.objects.create(name='prefered_color').save()
394
    assert Variable.objects.count() == 1
395
    assert AttributeDefinition.objects.count() == 1
396
    resp = app.get('/', status=200)
397
    resp = resp.click('Import')
398
    resp.form['parameters_json'] = Upload(
399
        'export.json', export_json.encode('utf-8'), 'application/json')
400
    resp = resp.form.submit()
401
    assert Variable.objects.count() == 2
402
    assert AttributeDefinition.objects.count() == 12
403
    assert Variable.objects.get(name='foo').label == 'bar'
404
    assert AttributeDefinition.objects.get(name='title').order == 3
405
    assert AttributeDefinition.objects.get(name='first_name').order == 2
406
    assert AttributeDefinition.objects.get(name='prefered_color').order == 1
407

  
408
    # import invalid json
409
    resp = app.get('/', status=200)
410
    resp = resp.click('Import')
411
    resp.form['parameters_json'] = Upload(
412
        'export.json', b'garbage', 'application/json')
413
    resp = resp.form.submit()
414
    assert resp.html.find('ul', {'class': 'errorlist'}).li.text == \
415
        'File is not in the expected JSON format.'
416
    assert Variable.objects.count() == 2
417
    assert AttributeDefinition.objects.count() == 12
418

  
419
    # import corrupted json
420
    export['variables'][0]['label'] = 'foofoo'
421
    fields = export['profile']['fields']
422
    assert fields[1]['name'] == 'title'
423
    fields[1]['label'] = 'Genre'
424
    export_json = json.dumps(export)
425
    resp = app.get('/', status=200)
426
    resp = resp.click('Import')
427
    resp.form['parameters_json'] = Upload(
428
        'export.json', export_json.encode('utf-8'), 'application/json')
429
    resp = resp.form.submit()
430
    assert 'Parameters import fails: IntegrityError' in resp.html.find(
431
        'ul', {'class': 'errorlist'}).li.text
432
    assert Variable.objects.count() == 2
433
    assert AttributeDefinition.objects.count() == 12
434
    assert Variable.objects.get(name='foo').label == 'bar'
319
-