Projet

Général

Profil

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

Nicolas Roche, 10 août 2020 18:30

Télécharger (15,4 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                     |  37 +++++-
 hobo/environment/views.py                     |  32 ++++-
 hobo/templates/hobo/home.html                 |   2 +
 tests/test_environment.py                     | 114 ++++++++++++++++++
 7 files changed, 211 insertions(+), 3 deletions(-)
 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
13 13
#
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17 17
import hashlib
18 18

  
19 19
from django.conf import settings
20 20
from django.urls import reverse
21
from django.db import connection
21
from django.db import connection, transaction
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
    with transaction.atomic():
170
        for variables in parameters.get('variables', []):
171
            obj, created = Variable.objects.get_or_create(name=variables['name'])
172
            for key, value in variables.items():
173
                setattr(obj, key, value)
174
            obj.save()
175

  
176
        for fields in parameters.get('profile', {}).get('fields', []):
177
            obj, created = AttributeDefinition.objects.get_or_create(
178
                label=fields['label'], name=fields['name'])
179
            for key, value in fields.items():
180
                setattr(obj, key, value)
181
            obj.save()
hobo/environment/views.py
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
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
from django.http import HttpResponse, HttpResponseRedirect, Http404
23
from django.http import HttpResponse, HttpResponseRedirect, Http404, JsonResponse
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
        utils.import_parameters(parameters_json)
219
        return super(ImportView, self).form_valid(form)
220

  
221

  
222
class ExportView(View):
223

  
224
    def get(self, request, *args, **kwargs):
225
        response = JsonResponse(utils.export_parameters())
226
        response['Content-Disposition'] = 'attachment; filename="hobo-export.json"'
227
        return response
228

  
229

  
202 230
def operational_check_view(request, service, slug, **kwargs):
203 231

  
204 232
    for klass in AVAILABLE_SERVICES:
205 233
        if klass.Extra.service_id == service:
206 234
            break
207 235
    else:
208 236
        raise Http404()
209 237

  
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
8
from django.db.utils import IntegrityError
6 9
from django.utils import timezone
7 10

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

  
10 14
from test_manager import login
11 15

  
12 16
pytestmark = pytest.mark.django_db
13 17

  
14 18

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

  
324

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

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

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

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

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

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

  
409
    # import invalid json
410
    resp = app.get('/', status=200)
411
    resp = resp.click('Import')
412
    resp.form['parameters_json'] = Upload(
413
        'export.json', b'garbage', 'application/json')
414
    resp = resp.form.submit()
415
    assert Variable.objects.count() == 2
416
    assert AttributeDefinition.objects.count() == 12
417

  
418
    # import corrupted json
419
    export['variables'][0]['label'] = 'foofoo'
420
    fields = export['profile']['fields']
421
    assert fields[1]['name'] == 'title'
422
    fields[1]['label'] = 'Genre'
423
    export_json = json.dumps(export)
424
    resp = app.get('/', status=200)
425
    resp = resp.click('Import')
426
    resp.form['parameters_json'] = Upload(
427
        'export.json', export_json.encode('utf-8'), 'application/json')
428
    with pytest.raises(IntegrityError):
429
        resp = resp.form.submit()
430
    assert Variable.objects.count() == 2
431
    assert AttributeDefinition.objects.count() == 12
432
    assert Variable.objects.get(name='foo').label == 'bar'
319
-