From 58805e970cc414e4613221b44ce2e70fe8b2ff36 Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Tue, 12 May 2020 10:28:54 +0200
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                     | 35 ++++++++++-
 hobo/templates/hobo/home.html                 |  2 +
 tests/test_environment.py                     | 58 +++++++++++++++++++
 7 files changed, 160 insertions(+), 1 deletion(-)
 create mode 100644 hobo/environment/templates/environment/import.html

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
@@ -205,8 +205,12 @@ class VariablesFormMixin(object):
             if variable.value != form.cleaned_data[variable_name]:
                 variable.value = form.cleaned_data[variable_name]
                 variable.save()
                 changed = True
         if changed and self.success_message:
             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/templates/environment/import.html b/hobo/environment/templates/environment/import.html
new file mode 100644
index 0000000..099cb65
--- /dev/null
+++ b/hobo/environment/templates/environment/import.html
@@ -0,0 +1,22 @@
+{% extends "hobo/base.html" %}
+{% load i18n %}
+
+{% block appbar %}
+<h2>{% trans "Parameters Import" %}</h2>
+{% endblock %}
+
+{% block breadcrumb %}
+{{ block.super }}
+<a href="{% url 'environment-import' %}">{% trans 'Parameters Import' %}</a>
+{% endblock %}
+
+{% block content %}
+<form method="post" enctype="multipart/form-data">
+  {% csrf_token %}
+  {{ form.as_p }}
+  <div class="buttons">
+    <button class="submit-button">{% trans "Import" %}</button>
+    <a class="cancel" href="{% url 'home' %}">{% trans 'Cancel' %}</a>
+  </div>
+</form>
+{% endblock %}
diff --git a/hobo/environment/urls.py b/hobo/environment/urls.py
index 657403b..94c736d 100644
--- a/hobo/environment/urls.py
+++ b/hobo/environment/urls.py
@@ -29,10 +29,13 @@ urlpatterns = [
     url(r'^check_operational/(?P<service>\w+)/(?P<slug>[\w-]+)$',
         views.operational_check_view, name='operational-check'),
     url(r'^new-(?P<service>\w+)$', views.ServiceCreateView.as_view(), name='create-service'),
     url(r'^save-(?P<service>\w+)/(?P<slug>[\w-]+)$', views.ServiceUpdateView.as_view(), name='save-service'),
     url(r'^delete-(?P<service>\w+)/(?P<slug>[\w-]+)$', views.ServiceDeleteView.as_view(), name='delete-service'),
 
     url(r'^new-variable-(?P<service>\w+)/(?P<slug>[\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 1d4b957..f56c283 100644
--- a/hobo/environment/utils.py
+++ b/hobo/environment/utils.py
@@ -16,16 +16,17 @@
 
 from django.conf import settings
 from django.core.urlresolvers import reverse
 from django.db import connection
 from django.utils.six.moves.urllib.parse import urlparse
 from django.utils.encoding import force_text
 
 from hobo.middleware.utils import StoreRequestMiddleware
+from hobo.profile.utils import get_profile_dict
 
 
 def get_installed_services():
     from .models import AVAILABLE_SERVICES
     installed_services = []
     for available_service in AVAILABLE_SERVICES:
         installed_services.extend(available_service.objects.all())
     return installed_services
@@ -121,8 +122,44 @@ def get_setting_variable(setting_name, label=None, service=None):
     except Variable.DoesNotExist:
         variable = Variable(
             name='SETTING_' + setting_name,
             label=label or '',
             service=service,
             auto=True)
 
     return variable
+
+
+def export_parameters():
+    from .models import Variable
+
+    # TODO : export too : label, auto?
+    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
+
+    objects = []
+    try:
+        for var in parameters['variables']:
+            objects.append(Variable(**var))
+        for field in parameters['profile']['fields']:
+            objects.append(AttributeDefinition(**field))
+    except Exception as exc:
+        return {'err': True, 'message': '%s, %s' % (exc.__class__.__name__, exc)}
+
+    Variable.objects.filter(service_pk__isnull=True).delete()
+    AttributeDefinition.objects.all().delete()
+    for obj in objects:
+        obj.save()
+    return {'err': False}
diff --git a/hobo/environment/views.py b/hobo/environment/views.py
index 3e4c444..ca87be1 100644
--- a/hobo/environment/views.py
+++ b/hobo/environment/views.py
@@ -17,18 +17,21 @@
 import json
 import string
 
 from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.core.urlresolvers import reverse_lazy
 from django.http import HttpResponse, HttpResponseRedirect, Http404
 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
 
 
 class AvailableService(object):
     def __init__(self, klass):
         self.id = klass.Extra.service_id
@@ -194,16 +197,46 @@ class ServiceDeleteView(DeleteView):
         service_id = self.kwargs.pop('service')
         service_slug = self.kwargs.pop('slug')
         for service in AVAILABLE_SERVICES:
             if service.Extra.service_id == service_id:
                 return service.objects.get(slug=service_slug)
         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)
+
+        result = utils.import_parameters(parameters_json)
+        if result.get('err'):
+            form.add_error('parameters_json',
+                           _('Parameters import fails: %s') % result.get('message'))
+            return self.form_invalid(form)
+
+        return super(ImportView, self).form_valid(form)
+
+
+class ExportView(View):
+
+    def get(self, request, *args, **kwargs):
+        response = HttpResponse(content_type='application/json')
+        json.dump(utils.export_parameters(), response, indent=2)
+        return response
+
+
 def operational_check_view(request, service, slug, **kwargs):
 
     for klass in AVAILABLE_SERVICES:
         if klass.Extra.service_id == service:
             break
     else:
         raise Http404()
 
diff --git a/hobo/templates/hobo/home.html b/hobo/templates/hobo/home.html
index bb890a3..7dd3d09 100644
--- a/hobo/templates/hobo/home.html
+++ b/hobo/templates/hobo/home.html
@@ -13,16 +13,18 @@
     <li><a href="{% url 'emails-home' %}">{% trans 'Emails' %}</a></li>
     {% if has_authentic %}
     <li><a href="{% url 'franceconnect-home' %}">FranceConnect</a></li>
     {% endif %}
     <li><a href="{% url 'matomo-home' %}">{% trans 'User tracking' %}</a></li>
     <li><a href="{% url 'seo-home' %}">{% trans 'Indexing' %}</a></li>
     <li><a href="{% url 'environment-home' %}">{% trans 'Services' %}</a></li>
     <li><a href="{% url 'environment-variables' %}">{% trans 'Variables' %}</a></li>
+    <li><a rel="popup" href="{% url 'environment-import' %}">{% trans 'Import' %}</a></li>
+    <li><a download href="{% url 'environment-export' %}">{% trans 'Export' %}</a></li>
     <li><a href="{% url 'debug-home' %}">{% trans 'Debugging' %}</a></li>
   </ul>
   </span>
 {% endblock %}
 
 {% block content %}
 
 {% if services %}
diff --git a/tests/test_environment.py b/tests/test_environment.py
index 9bf77b6..8e81974 100644
--- a/tests/test_environment.py
+++ b/tests/test_environment.py
@@ -1,10 +1,12 @@
 # -*- 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.utils import timezone
 
 from hobo.environment.models import AVAILABLE_SERVICES, Combo, Passerelle, ServiceBase, Variable
 
 from test_manager import login
@@ -311,8 +313,64 @@ def test_check_operational_command(monkeypatch, capsys):
     combo.last_operational_success_timestamp = '2022-2-22'
     combo.save()
     call_command('check_operational', '-v2')
     captured = capsys.readouterr()
     assert captured.out.split('\n')[:-1] == [
         'foo is NOT operational',
         '  last operational success: 2022-02-22 00:00:00+00:00'
     ]
+
+
+def test_export_import_view(app, admin_user):
+    Variable.objects.create(name='foo', value='bar')
+    app = login(app, 'admin', 'password')
+    resp = app.get('/sites/export', status=200)
+    assert [x for x in 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'][1]['name'] == 'first_name'
+    assert resp.json['profile']['fields'][1]['required'] is True
+
+    # modify exported file
+    export = resp.json
+    export['variables'][0]['label'] = 'foo'
+    fields = export['profile']['fields']
+    fields[0], fields[1] = fields[1], fields[0]
+    export_json = json.dumps(export)
+
+    # 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 resp.html.find('ul', {'class': 'errorlist'}).li.text == \
+        'File is not in the expected JSON format.'
+
+    # 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 resp.html.find('ul', {'class': 'errorlist'}).li.text == \
+        "Parameters import fails: KeyError, 'variables'"
+
+    # 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().follow()
+
+    # check imported parameters
+    app = login(app, 'admin', 'password')
+    resp = app.get('/sites/export', status=200)
+    assert [x for x in sorted(resp.json.keys())] == ['profile', 'variables']
+    assert resp.json['variables'] == [
+        {'name': 'foo', 'label': 'foo', 'value': 'bar', 'auto': False}]
+    assert resp.json['profile']['fields'][1]['name'] == 'title'
+    assert resp.json['profile']['fields'][1]['required'] is False
+    assert resp.json['profile']['fields'][0]['name'] == 'first_name'
+    assert resp.json['profile']['fields'][0]['required'] is True
-- 
2.26.2

