0001-environment-import-and-export-parameters-33672.patch
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 |
- |