diff --git a/hobo/environment/forms.py b/hobo/environment/forms.py index afc0e08..b54f30c 100644 --- a/hobo/environment/forms.py +++ b/hobo/environment/forms.py @@ -210,3 +210,7 @@ class VariablesFormMixin(object): messages.info(self.request, self.success_message) return HttpResponseRedirect('.') + + +class ImportForm(forms.Form): + parameters_json = forms.FileField(label=_('Parameters Export File')) diff --git a/hobo/environment/urls.py b/hobo/environment/urls.py index 657403b..95e8d15 100644 --- a/hobo/environment/urls.py +++ b/hobo/environment/urls.py @@ -34,5 +34,8 @@ urlpatterns = [ url(r'^new-variable-(?P\w+)/(?P[\w-]+)$', views.VariableCreateView.as_view(), name='new-variable-service',), + + url(r'^import/$', views.ImportView.as_view(), name='environment-import'), + url(r'^export/$', views.ExportView.as_view(), name='environment-export'), url(r'^debug.json$', views.debug_json, name='debug-json'), ] diff --git a/hobo/environment/utils.py b/hobo/environment/utils.py index fee3792..9587c10 100644 --- a/hobo/environment/utils.py +++ b/hobo/environment/utils.py @@ -18,12 +18,13 @@ import hashlib from django.conf import settings from django.urls import reverse -from django.db import connection +from django.db import connection, transaction from django.utils.six.moves.urllib.parse import urlparse from django.utils.encoding import force_text from hobo.middleware.utils import StoreRequestMiddleware from hobo.multitenant.settings_loaders import KnownServices +from hobo.profile.utils import get_profile_dict def get_installed_services(): @@ -144,3 +145,36 @@ def get_setting_variable(setting_name, label=None, service=None): auto=True) return variable + + +def export_parameters(): + from .models import Variable + + variables = [] + for var in Variable.objects.filter(service_pk__isnull=True): + variables.append({ + 'name': var.name, + 'label': var.label, + 'value': var.value, + 'auto': var.auto}) + parameters = {'variables': variables} + parameters.update(get_profile_dict()) + return parameters + + +def import_parameters(parameters): + from .models import Variable + from hobo.profile.models import AttributeDefinition + + with transaction.atomic(): + for variables in parameters.get('variables', []): + obj, created = Variable.objects.get_or_create(name=variables['name']) + for key, value in variables.items(): + setattr(obj, key, value) + obj.save() + + for fields in parameters.get('profile', {}).get('fields', []): + obj, created = AttributeDefinition.objects.get_or_create(name=fields['name']) + for key, value in fields.items(): + setattr(obj, key, value) + obj.save() diff --git a/hobo/environment/views.py b/hobo/environment/views.py index 69c8f19..b761542 100644 --- a/hobo/environment/views.py +++ b/hobo/environment/views.py @@ -20,10 +20,13 @@ import string from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.urls import reverse_lazy -from django.http import HttpResponse, HttpResponseRedirect, Http404 +from django.http import HttpResponse, HttpResponseRedirect, Http404, JsonResponse from django.shortcuts import get_object_or_404 +from django.utils.encoding import force_text +from django.utils.translation import ugettext_lazy as _ +from django.views.generic import View from django.views.generic.base import TemplateView -from django.views.generic.edit import CreateView, UpdateView, DeleteView +from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView from .models import Variable, AVAILABLE_SERVICES from . import forms, utils @@ -199,6 +202,31 @@ class ServiceDeleteView(DeleteView): return None +class ImportView(FormView): + form_class = forms.ImportForm + template_name = 'environment/import.html' + success_url = reverse_lazy('home') + + def form_valid(self, form): + try: + parameters_json = json.loads( + force_text(self.request.FILES['parameters_json'].read())) + except ValueError: + form.add_error('parameters_json', _('File is not in the expected JSON format.')) + return self.form_invalid(form) + + utils.import_parameters(parameters_json) + return super(ImportView, self).form_valid(form) + + +class ExportView(View): + + def get(self, request, *args, **kwargs): + response = JsonResponse(utils.export_parameters(), json_dumps_params={'indent': 2}) + response['Content-Disposition'] = 'attachment; filename="hobo-export.json"' + return response + + def operational_check_view(request, service, slug, **kwargs): for klass in AVAILABLE_SERVICES: diff --git a/hobo/templates/hobo/home.html b/hobo/templates/hobo/home.html index 9a53eb4..6734721 100644 --- a/hobo/templates/hobo/home.html +++ b/hobo/templates/hobo/home.html @@ -19,6 +19,8 @@
  • {% trans 'Indexing' %}
  • {% trans 'Services' %}
  • {% trans 'Variables' %}
  • +
  • {% trans 'Import' %}
  • +
  • {% trans 'Export' %}
  • {% trans 'Debugging' %}
  • diff --git a/tests/test_environment.py b/tests/test_environment.py index 02ce816..0480082 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -1,11 +1,15 @@ # -*- coding: utf-8 -*- +import json import pytest +from webtest import Upload from django.core.exceptions import ValidationError from django.core.management import call_command +from django.db.utils import IntegrityError from django.utils import timezone from hobo.environment.models import AVAILABLE_SERVICES, Combo, Passerelle, ServiceBase, Variable +from hobo.profile.models import AttributeDefinition from test_manager import login @@ -316,3 +320,118 @@ def test_check_operational_command(monkeypatch, capsys): 'foo is NOT operational', ' last operational success: 2022-02-22 00:00:00+00:00' ] + + +def test_export_import_view(app, admin_user): + combo = Combo.objects.create(base_url='https://combo.agglo.love', + template_name='...portal-user...', + slug='portal') + Variable.objects.create(name='foo', value='bar').save() + Variable.objects.create(name='foo2', value='bar2', service=combo).save() + app = login(app, 'admin', 'password') + resp = app.get('/sites/export/', status=200) + assert sorted(resp.json.keys()) == ['profile', 'variables'] + assert resp.json['variables'] == [ + {'name': 'foo', 'label': '', 'value': 'bar', 'auto': False}] + assert resp.json['profile']['fields'][0]['name'] == 'title' + assert resp.json['profile']['fields'][0]['required'] is False + assert resp.json['profile']['fields'][0]['description'] == '' + assert resp.json['profile']['fields'][2]['name'] == 'last_name' + assert resp.json['profile']['fields'][2]['required'] is True + assert resp.json['profile']['fields'][2]['label'] == 'Nom' + + # modify exported file + export = resp.json + export['variables'][0]['label'] = 'bar' + fields = export['profile']['fields'] + assert fields[0]['name'] == 'title' + assert fields[2]['name'] == 'last_name' + fields[0]['description'] = 'genre' + fields[2]['label'] = 'Nom de naissance' + fields[0], fields[2] = fields[2], fields[0] + export_json = json.dumps(export) + + # add new content + Variable.objects.create(name='foo3', value='bar3').save() + AttributeDefinition.objects.create(name='prefered_color', label='not empty').save() + assert Variable.objects.count() == 3 + assert AttributeDefinition.objects.count() == 12 + assert Variable.objects.get(name='foo').label == '' + assert AttributeDefinition.objects.get(name='title').description == '' + assert AttributeDefinition.objects.get(name='title').order == 1 + assert AttributeDefinition.objects.get(name='last_name').order == 3 + assert AttributeDefinition.objects.get(name='prefered_color').order == 12 + + # import valid content + resp = app.get('/', status=200) + resp = resp.click('Import') + resp.form['parameters_json'] = Upload( + 'export.json', export_json.encode('utf-8'), 'application/json') + resp = resp.form.submit() + assert Variable.objects.count() == 3 + assert AttributeDefinition.objects.count() == 12 + assert Variable.objects.get(name='foo').label == 'bar' + assert AttributeDefinition.objects.get(name='title').description == 'genre' + assert AttributeDefinition.objects.get(name='title').order == 1 + assert AttributeDefinition.objects.get(name='last_name').label == 'Nom de naissance' + assert AttributeDefinition.objects.get(name='last_name').order == 3 + assert AttributeDefinition.objects.get(name='prefered_color').order == 12 + + # import empty json + resp = app.get('/', status=200) + resp = resp.click('Import') + resp.form['parameters_json'] = Upload( + 'export.json', b'{}', 'application/json') + resp = resp.form.submit() + assert Variable.objects.count() == 3 + assert AttributeDefinition.objects.count() == 12 + assert Variable.objects.get(name='foo').label == 'bar' + assert AttributeDefinition.objects.get(name='title').description == 'genre' + assert AttributeDefinition.objects.get(name='title').order == 1 + assert AttributeDefinition.objects.get(name='last_name').label == 'Nom de naissance' + assert AttributeDefinition.objects.get(name='last_name').order == 3 + assert AttributeDefinition.objects.get(name='prefered_color').order == 12 + + # import from scratch + Variable.objects.all().delete() + AttributeDefinition.objects.all().delete() + Variable.objects.create(name='foo2', value='bar2', service=combo).save() + AttributeDefinition.objects.create(name='prefered_color', label='not empty').save() + assert Variable.objects.count() == 1 + assert AttributeDefinition.objects.count() == 1 + resp = app.get('/', status=200) + resp = resp.click('Import') + resp.form['parameters_json'] = Upload( + 'export.json', export_json.encode('utf-8'), 'application/json') + resp = resp.form.submit() + assert Variable.objects.count() == 2 + assert AttributeDefinition.objects.count() == 12 + assert Variable.objects.get(name='foo').label == 'bar' + assert AttributeDefinition.objects.get(name='title').order == 4 + assert AttributeDefinition.objects.get(name='last_name').order == 2 + assert AttributeDefinition.objects.get(name='prefered_color').order == 1 + + # import invalid json + resp = app.get('/', status=200) + resp = resp.click('Import') + resp.form['parameters_json'] = Upload( + 'export.json', b'garbage', 'application/json') + resp = resp.form.submit() + assert Variable.objects.count() == 2 + assert AttributeDefinition.objects.count() == 12 + + # import corrupted json + export['variables'][0]['label'] = 'foofoo' + fields = export['profile']['fields'] + assert fields[2]['name'] == 'title' + fields[2]['label'] = 'Nom de naissance' + export_json = json.dumps(export) + resp = app.get('/', status=200) + resp = resp.click('Import') + resp.form['parameters_json'] = Upload( + 'export.json', export_json.encode('utf-8'), 'application/json') + with pytest.raises(IntegrityError): + resp = resp.form.submit() + assert Variable.objects.count() == 2 + assert AttributeDefinition.objects.count() == 12 + assert Variable.objects.get(name='foo').label == 'bar'