Projet

Général

Profil

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

Nicolas Roche, 10 août 2020 19:20

Télécharger (15,7 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                     |  36 +++++-
 hobo/environment/views.py                     |  32 ++++-
 hobo/templates/hobo/home.html                 |   2 +
 tests/test_environment.py                     | 119 ++++++++++++++++++
 7 files changed, 215 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(name=fields['name'])
178
            for key, value in fields.items():
179
                setattr(obj, key, value)
180
            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'][0]['description'] == ''
339
    assert resp.json['profile']['fields'][2]['name'] == 'last_name'
340
    assert resp.json['profile']['fields'][2]['required'] is True
341
    assert resp.json['profile']['fields'][2]['label'] == 'Nom'
342

  
343
    # modify exported file
344
    export = resp.json
345
    export['variables'][0]['label'] = 'bar'
346
    fields = export['profile']['fields']
347
    assert fields[0]['name'] == 'title'
348
    assert fields[2]['name'] == 'last_name'
349
    fields[0]['description'] = 'genre'
350
    fields[2]['label'] = 'Nom de naissance'
351
    fields[0], fields[2] = fields[2], fields[0]
352
    export_json = json.dumps(export)
353

  
354
    # add new content
355
    Variable.objects.create(name='foo3', value='bar3').save()
356
    AttributeDefinition.objects.create(name='prefered_color', label='not empty').save()
357
    assert Variable.objects.count() == 3
358
    assert AttributeDefinition.objects.count() == 12
359
    assert Variable.objects.get(name='foo').label == ''
360
    assert AttributeDefinition.objects.get(name='title').description == ''
361
    assert AttributeDefinition.objects.get(name='title').order == 1
362
    assert AttributeDefinition.objects.get(name='last_name').order == 3
363
    assert AttributeDefinition.objects.get(name='prefered_color').order == 12
364

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

  
380
    # import empty json
381
    resp = app.get('/', status=200)
382
    resp = resp.click('Import')
383
    resp.form['parameters_json'] = Upload(
384
        'export.json', b'{}', 'application/json')
385
    resp = resp.form.submit()
386
    assert Variable.objects.count() == 3
387
    assert AttributeDefinition.objects.count() == 12
388
    assert Variable.objects.get(name='foo').label == 'bar'
389
    assert AttributeDefinition.objects.get(name='title').description == 'genre'
390
    assert AttributeDefinition.objects.get(name='title').order == 1
391
    assert AttributeDefinition.objects.get(name='last_name').label == 'Nom de naissance'
392
    assert AttributeDefinition.objects.get(name='last_name').order == 3
393
    assert AttributeDefinition.objects.get(name='prefered_color').order == 12
394

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

  
414
    # import invalid json
415
    resp = app.get('/', status=200)
416
    resp = resp.click('Import')
417
    resp.form['parameters_json'] = Upload(
418
        'export.json', b'garbage', 'application/json')
419
    resp = resp.form.submit()
420
    assert Variable.objects.count() == 2
421
    assert AttributeDefinition.objects.count() == 12
422

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