0003-base-add-import-export-UI-15269.patch
passerelle/base/forms.py | ||
---|---|---|
1 | 1 |
from django import forms |
2 |
from django.utils.translation import ugettext_lazy as _ |
|
2 | 3 | |
3 | 4 |
from .models import ApiUser, AccessRight, AvailabilityParameters |
4 | 5 | |
... | ... | |
27 | 28 |
widgets = { |
28 | 29 |
'notification_delays': forms.TextInput, |
29 | 30 |
} |
31 | ||
32 | ||
33 |
class ImportSiteForm(forms.Form): |
|
34 |
site_json = forms.FileField(label=_('Site Export File')) |
|
35 |
import_users = forms.BooleanField(label=_('Import users and access rights'), required=False) |
passerelle/base/urls.py | ||
---|---|---|
2 | 2 | |
3 | 3 |
from .views import ApiUserCreateView, ApiUserUpdateView, ApiUserDeleteView, \ |
4 | 4 |
ApiUserListView, AccessRightDeleteView, AccessRightCreateView, \ |
5 |
LoggingParametersUpdateView, ManageAvailabilityView |
|
5 |
LoggingParametersUpdateView, ManageAvailabilityView, ImportSiteView, \ |
|
6 |
ExportSiteView |
|
6 | 7 | |
7 | 8 |
access_urlpatterns = [ |
8 | 9 |
url(r'^$', ApiUserListView.as_view(), name='apiuser-list'), |
... | ... | |
20 | 21 |
ManageAvailabilityView.as_view(), name='manage-availability') |
21 | 22 | |
22 | 23 |
] |
24 | ||
25 |
import_export_urlpatterns = [ |
|
26 |
url(r'^import$', ImportSiteView.as_view(), name='import-site'), |
|
27 |
url(r'^export$', ExportSiteView.as_view(), name='export-site'), |
|
28 |
] |
passerelle/base/views.py | ||
---|---|---|
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 | 17 |
import datetime |
18 |
import json |
|
18 | 19 | |
19 | 20 |
from dateutil import parser as date_parser |
20 | 21 | |
... | ... | |
25 | 26 |
from django.db.models import Q |
26 | 27 |
from django.forms import models as model_forms |
27 | 28 |
from django.views.generic import ( |
28 |
DetailView, ListView, CreateView, UpdateView, DeleteView, FormView) |
|
29 |
from django.http import Http404 |
|
29 |
View, DetailView, ListView, CreateView, UpdateView, DeleteView, FormView)
|
|
30 |
from django.http import Http404, HttpResponse
|
|
30 | 31 |
from django.utils.timezone import make_aware |
31 | 32 |
from django.utils.translation import ugettext_lazy as _ |
32 | 33 | |
33 | 34 |
from .models import ApiUser, AccessRight, LoggingParameters, ResourceStatus, Job |
34 |
from .forms import ApiUserForm, AccessRightForm, AvailabilityParametersForm |
|
35 |
from .forms import ApiUserForm, AccessRightForm, AvailabilityParametersForm, ImportSiteForm
|
|
35 | 36 |
from ..views import GenericConnectorMixin |
36 |
from ..utils import get_trusted_services |
|
37 |
from ..utils import get_trusted_services, import_site, export_site
|
|
37 | 38 | |
38 | 39 | |
39 | 40 |
class ResourceView(DetailView): |
... | ... | |
277 | 278 |
except Job.DoesNotExist: |
278 | 279 |
raise Http404() |
279 | 280 |
return context |
281 | ||
282 | ||
283 |
class ImportSiteView(FormView): |
|
284 |
template_name = 'passerelle/manage/import_site.html' |
|
285 |
form_class = ImportSiteForm |
|
286 | ||
287 |
def get_success_url(self): |
|
288 |
return reverse('manage-home') |
|
289 | ||
290 |
def form_valid(self, form): |
|
291 |
try: |
|
292 |
site_json = json.load(self.request.FILES['site_json']) |
|
293 |
except ValueError: |
|
294 |
form.add_error('site_json', _('File is not in the expected JSON format.')) |
|
295 |
return self.form_invalid(form) |
|
296 | ||
297 |
results = import_site(site_json, overwrite=True, |
|
298 |
import_users=form.cleaned_data['import_users']) |
|
299 |
return super(ImportSiteView, self).form_valid(form) |
|
300 | ||
301 | ||
302 |
class ExportSiteView(View): |
|
303 | ||
304 |
def get(self, request, *args, **kwargs): |
|
305 |
response = HttpResponse(content_type='application/json') |
|
306 |
json.dump(export_site(), response, indent=2) |
|
307 |
return response |
passerelle/templates/passerelle/manage.html | ||
---|---|---|
4 | 4 |
{% block appbar %} |
5 | 5 |
<h2>{% trans 'Web Services' %}</h2> |
6 | 6 |
<span class="actions"> |
7 |
<a class="extra-actions-menu-opener"></a> |
|
7 | 8 |
<a href="{% url 'apiuser-list' %}">{% trans 'Access Management' %}</a> |
8 | 9 |
<a href="{% url 'add-connector' %}">{% trans 'Add Connector' %}</a> |
9 | 10 |
</span> |
11 |
<ul class="extra-actions-menu"> |
|
12 |
<li><a href="{% url 'import-site' %}" rel="popup">{% trans 'Import' %}</a></li> |
|
13 |
<li><a download href="{% url 'export-site' %}">{% trans 'Export' %}</a></li> |
|
14 |
</ul> |
|
10 | 15 |
{% endblock %} |
11 | 16 | |
12 | 17 |
{% block content %} |
passerelle/templates/passerelle/manage/import_site.html | ||
---|---|---|
1 |
{% extends "passerelle/manage.html" %} |
|
2 |
{% load i18n %} |
|
3 | ||
4 |
{% block appbar %} |
|
5 |
<h2>{% trans "Site Import" %}</h2> |
|
6 |
{% endblock %} |
|
7 | ||
8 |
{% block content %} |
|
9 |
<form method="post" enctype="multipart/form-data"> |
|
10 |
{% csrf_token %} |
|
11 |
{{ form.as_p }} |
|
12 |
<div class="buttons"> |
|
13 |
<button class="submit-button">{% trans "Import" %}</button> |
|
14 |
<a class="cancel" href="{% url 'manage-home' %}">{% trans 'Cancel' %}</a> |
|
15 |
</div> |
|
16 |
</form> |
|
17 |
{% endblock %} |
passerelle/templates/passerelle/manage/service_view.html | ||
---|---|---|
15 | 15 |
{% endwith %} |
16 | 16 |
</h2> |
17 | 17 |
<span class="actions"> |
18 |
<a class="extra-actions-menu-opener"></a> |
|
18 | 19 |
{% if object|can_edit:request.user and has_check_status %} |
19 | 20 |
<a rel="popup" href="{% url 'manage-availability' resource_type=object|resource_type resource_pk=object.id %}">{% trans 'availability check parameters' %}</a> |
20 | 21 |
{% endif %} |
... | ... | |
28 | 29 |
<a rel="popup" href="{{ object.get_delete_url }}">{% trans 'delete' %}</a> |
29 | 30 |
{% endif %} |
30 | 31 |
</span> |
32 |
<ul class="extra-actions-menu"> |
|
33 |
<li><a download href="{% url 'export-connector' connector=object.get_connector_slug slug=object.slug %}">{% trans 'Export' %}</a></li> |
|
34 |
</ul> |
|
31 | 35 |
{% endblock %} |
32 | 36 | |
33 | 37 |
{% block content %} |
passerelle/urls.py | ||
---|---|---|
9 | 9 |
from .views import (HomePageView, ManageView, ManageAddView, |
10 | 10 |
GenericCreateConnectorView, GenericDeleteConnectorView, |
11 | 11 |
GenericEditConnectorView, GenericEndpointView, GenericConnectorView, |
12 |
GenericViewLogsConnectorView, GenericLogView, |
|
12 |
GenericViewLogsConnectorView, GenericLogView, GenericExportConnectorView,
|
|
13 | 13 |
login, logout, menu_json) |
14 | 14 |
from .base.views import GenericViewJobsConnectorView, GenericJobView |
15 | 15 |
from .urls_utils import decorated_includes, required, app_enabled, manager_required |
16 |
from .base.urls import access_urlpatterns |
|
16 |
from .base.urls import access_urlpatterns, import_export_urlpatterns
|
|
17 | 17 |
from .plugins import register_apps_urls |
18 | 18 | |
19 | 19 |
from passerelle.apps.pastell import urls as pastell_urls |
... | ... | |
34 | 34 | |
35 | 35 |
url(r'^manage/access/', |
36 | 36 |
decorated_includes(manager_required, include(access_urlpatterns))), |
37 |
url(r'^manage/', |
|
38 |
decorated_includes(manager_required, include(import_export_urlpatterns))), |
|
37 | 39 |
] |
38 | 40 | |
39 | 41 |
urlpatterns += required( |
... | ... | |
73 | 75 |
GenericViewJobsConnectorView.as_view(), name='view-jobs-connector'), |
74 | 76 |
url(r'^(?P<slug>[\w,-]+)/jobs/(?P<job_pk>\d+)/$', |
75 | 77 |
GenericJobView.as_view(), name='view-job'), |
78 |
url(r'^(?P<slug>[\w,-]+)/export$', |
|
79 |
GenericExportConnectorView.as_view(), name='export-connector'), |
|
76 | 80 |
]))) |
77 | 81 |
] |
78 | 82 |
passerelle/views.py | ||
---|---|---|
477 | 477 | |
478 | 478 |
def delete(self, request, *args, **kwargs): |
479 | 479 |
return self.get(request, *args, **kwargs) |
480 | ||
481 | ||
482 |
class GenericExportConnectorView(GenericConnectorMixin, DetailView): |
|
483 | ||
484 |
def get(self, request, *args, **kwargs): |
|
485 |
response = HttpResponse(content_type='application/json') |
|
486 |
json.dump({'resources': [self.get_object().export_json()]}, response, indent=2) |
|
487 |
return response |
tests/test_manager.py | ||
---|---|---|
2 | 2 |
import re |
3 | 3 |
from StringIO import StringIO |
4 | 4 | |
5 |
from webtest import Upload |
|
6 | ||
5 | 7 |
from django.contrib.auth.models import User |
6 | 8 |
from django.contrib.contenttypes.models import ContentType |
7 | 9 |
from django.core.files import File |
... | ... | |
336 | 338 |
base_url = re.findall(r'data-job-base-url="(.*)"', resp.text)[0] |
337 | 339 |
resp = app.get(base_url + job_pk + '/') |
338 | 340 |
resp = app.get(base_url + '12345' + '/', status=404) |
341 | ||
342 | ||
343 |
def test_manager_import_export(app, admin_user): |
|
344 |
data = StringIO('1;Foo\n2;Bar\n3;Baz') |
|
345 |
csv = CsvDataSource.objects.create(csv_file=File(data, 't.csv'), |
|
346 |
columns_keynames='id, text', slug='test', title='a title', description='a description') |
|
347 |
csv2 = CsvDataSource.objects.create(csv_file=File(data, 't.csv'), |
|
348 |
columns_keynames='id, text', slug='test2', title='a title', description='a description') |
|
349 |
api = ApiUser.objects.create(username='public', |
|
350 |
fullname='public', |
|
351 |
description='access for all', |
|
352 |
keytype='', key='') |
|
353 |
obj_type = ContentType.objects.get_for_model(csv) |
|
354 |
AccessRight.objects.create(codename='can_access', |
|
355 |
apiuser=api, |
|
356 |
resource_type=obj_type, |
|
357 |
resource_pk=csv.pk, |
|
358 |
) |
|
359 | ||
360 |
# export site |
|
361 |
app = login(app) |
|
362 |
resp = app.get('/manage/') |
|
363 |
resp = resp.click('Export') |
|
364 |
assert resp.headers['content-type'] == 'application/json' |
|
365 |
site_export = resp.text |
|
366 | ||
367 |
# invalid json |
|
368 |
resp = app.get('/manage/', status=200) |
|
369 |
resp = resp.click('Import') |
|
370 |
resp.form['site_json'] = Upload('export.json', b'garbage', 'application/json') |
|
371 |
resp = resp.form.submit() |
|
372 |
assert 'File is not in the expected JSON format.' in resp.text |
|
373 | ||
374 |
# empty json |
|
375 |
resp = app.get('/manage/', status=200) |
|
376 |
resp = resp.click('Import') |
|
377 |
resp.form['site_json'] = Upload('export.json', b'{}', 'application/json') |
|
378 |
resp = resp.form.submit().follow() |
|
379 |
assert CsvDataSource.objects.count() == 2 |
|
380 | ||
381 |
# import site |
|
382 |
CsvDataSource.objects.all().delete() |
|
383 |
resp = app.get('/manage/', status=200) |
|
384 |
resp = resp.click('Import') |
|
385 |
resp.form['site_json'] = Upload('export.json', site_export.encode('utf-8'), 'application/json') |
|
386 |
resp = resp.form.submit().follow() |
|
387 |
assert CsvDataSource.objects.count() == 2 |
|
388 | ||
389 |
# export connector |
|
390 |
resp = app.get('/%s/%s/' % (csv.get_connector_slug(), csv.slug), status=200) |
|
391 |
resp = resp.click('Export') |
|
392 |
assert resp.headers['content-type'] == 'application/json' |
|
393 |
connector_export = resp.text |
|
394 | ||
395 |
# import connector |
|
396 |
csv.delete() |
|
397 |
resp = app.get('/manage/', status=200) |
|
398 |
resp = resp.click('Import') |
|
399 |
resp.form['site_json'] = Upload('export.json', connector_export.encode('utf-8'), |
|
400 |
'application/json') |
|
401 |
resp = resp.form.submit().follow() |
|
402 |
assert CsvDataSource.objects.count() == 2 |
|
403 |
assert CsvDataSource.objects.filter(slug='test').exists() |
|
404 | ||
405 |
# import users |
|
406 |
ApiUser.objects.all().delete() |
|
407 |
AccessRight.objects.all().delete() |
|
408 |
resp = app.get('/manage/', status=200) |
|
409 |
resp = resp.click('Import') |
|
410 |
resp.form['site_json'] = Upload('export.json', site_export.encode('utf-8'), 'application/json') |
|
411 |
resp = resp.form.submit().follow() |
|
412 |
assert not ApiUser.objects.exists() |
|
413 |
assert not AccessRight.objects.exists() |
|
414 |
resp = resp.click('Import') |
|
415 |
resp.form['import_users'] = True |
|
416 |
resp.form['site_json'] = Upload('export.json', site_export.encode('utf-8'), 'application/json') |
|
417 |
resp = resp.form.submit().follow() |
|
418 |
assert ApiUser.objects.filter(username='public').exists() |
|
419 |
assert AccessRight.objects.filter(codename='can_access').exists() |
|
339 |
- |