From a20ed6c8f4cf4a373221cb2449751aecb127520f Mon Sep 17 00:00:00 2001 From: Mikko Nieminen Date: Fri, 26 Jul 2024 14:10:38 +0200 Subject: [PATCH] add export view (#1961) --- isatemplates/templates/isatemplates/list.html | 3 +- isatemplates/tests/test_permissions.py | 29 ++++++-- isatemplates/tests/test_views.py | 66 +++++++++++++++++++ isatemplates/urls.py | 5 ++ isatemplates/views.py | 33 +++++++++- 5 files changed, 129 insertions(+), 7 deletions(-) diff --git a/isatemplates/templates/isatemplates/list.html b/isatemplates/templates/isatemplates/list.html index 1ea8b0b5..faa78227 100644 --- a/isatemplates/templates/isatemplates/list.html +++ b/isatemplates/templates/isatemplates/list.html @@ -115,7 +115,8 @@

Update Template - {# TODO #} + Export Template diff --git a/isatemplates/tests/test_permissions.py b/isatemplates/tests/test_permissions.py index 97cb7ce7..59be35a4 100644 --- a/isatemplates/tests/test_permissions.py +++ b/isatemplates/tests/test_permissions.py @@ -1,5 +1,6 @@ """Tests for UI view permissions in the isatemplates app""" +from django.test import override_settings from django.urls import reverse # Projectroles dependency @@ -21,10 +22,16 @@ def test_get_list(self): """Test ISATemplateListView GET""" url = reverse('isatemplates:list') good_users = [self.superuser] - bad_users = [self.anonymous, self.regular_user] + bad_users = [self.regular_user, self.anonymous] self.assert_response(url, good_users, 200) self.assert_response(url, bad_users, 302) + @override_settings(PROJECTROLES_ALLOW_ANONYMOUS=True) + def test_get_list_anon(self): + """Test ISATemplateListView GET with anonymous access enabled""" + url = reverse('isatemplates:list') + self.assert_response(url, self.anonymous, 302) + def test_get_detail(self): """Test ISATemplateDetailView GET""" template = self.make_isa_template(TEMPLATE_NAME, TEMPLATE_DESC, {}) @@ -33,7 +40,7 @@ def test_get_detail(self): kwargs={'cookiecutterisatemplate': template.sodar_uuid}, ) good_users = [self.superuser] - bad_users = [self.anonymous, self.regular_user] + bad_users = [self.regular_user, self.anonymous] self.assert_response(url, good_users, 200) self.assert_response(url, bad_users, 302) @@ -41,7 +48,7 @@ def test_get_create(self): """Test ISATemplateCreateView GET""" url = reverse('isatemplates:create') good_users = [self.superuser] - bad_users = [self.anonymous, self.regular_user] + bad_users = [self.regular_user, self.anonymous] self.assert_response(url, good_users, 200) self.assert_response(url, bad_users, 302) @@ -53,7 +60,7 @@ def test_get_update(self): kwargs={'cookiecutterisatemplate': template.sodar_uuid}, ) good_users = [self.superuser] - bad_users = [self.anonymous, self.regular_user] + bad_users = [self.regular_user, self.anonymous] self.assert_response(url, good_users, 200) self.assert_response(url, bad_users, 302) @@ -65,6 +72,18 @@ def test_get_delete(self): kwargs={'cookiecutterisatemplate': template.sodar_uuid}, ) good_users = [self.superuser] - bad_users = [self.anonymous, self.regular_user] + bad_users = [self.regular_user, self.anonymous] + self.assert_response(url, good_users, 200) + self.assert_response(url, bad_users, 302) + + def test_get_export(self): + """Test ISATemplateExportView GET""" + template = self.make_isa_template(TEMPLATE_NAME, TEMPLATE_DESC, {}) + url = reverse( + 'isatemplates:export', + kwargs={'cookiecutterisatemplate': template.sodar_uuid}, + ) + good_users = [self.superuser] + bad_users = [self.regular_user, self.anonymous] self.assert_response(url, good_users, 200) self.assert_response(url, bad_users, 302) diff --git a/isatemplates/tests/test_views.py b/isatemplates/tests/test_views.py index 57e46fac..9093293f 100644 --- a/isatemplates/tests/test_views.py +++ b/isatemplates/tests/test_views.py @@ -49,6 +49,7 @@ ] TEMPLATE_NAME_UPDATE = 'updated_name' TEMPLATE_DESC_UPDATE = 'Updated template name' +INVALID_UUID = '11111111-1111-1111-1111-111111111111' class ISATemplateViewTestBase( @@ -783,3 +784,68 @@ def test_post(self): self.assertEqual( ProjectEvent.objects.filter(event_name='template_delete').count(), 1 ) + + +class TestISATemplateExportView(ISATemplateViewTestBase): + """Tests for ISATemplateExportView""" + + def setUp(self): + super().setUp() + # Set up template with data + with open(TEMPLATE_JSON_PATH, 'rb') as f: + json_data = f.read().decode('utf-8') + self.template = self.make_isa_template( + name=TEMPLATE_NAME, + description=TEMPLATE_DESC, + json_data=json_data, + user=self.user, + ) + self.file_data = {} + for fn in ISA_FILE_NAMES: + fp = os.path.join(str(ISA_FILE_PATH), fn) + with open(fp, 'rb') as f: + fd = f.read().decode('utf-8') + self.file_data[fn] = fd + self.make_isa_file( + template=self.template, file_name=fn, content=fd + ) + self.url = reverse( + 'isatemplates:export', + kwargs={'cookiecutterisatemplate': self.template.sodar_uuid}, + ) + + def test_get(self): + """Test ISATemplateExportView GET""" + with self.login(self.user): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.get('Content-Disposition'), + 'attachment; filename="{}"'.format(self.template.name + '.zip'), + ) + f = io.BytesIO(response.content) + zf = zipfile.ZipFile(f, 'r') + self.assertIsNone(zf.testzip()) + name_list = zf.namelist() + self.assertIn('cookiecutter.json', name_list) + self.assertEqual( + zf.read('cookiecutter.json').decode('utf-8'), + self.template.json_data, + ) + for fn in ISA_FILE_NAMES: + fp = os.path.join(FILE_DIR, fn) + self.assertIn(fp, name_list) + f_obj = CookiecutterISAFile.objects.get( + template=self.template, file_name=fn + ) + self.assertEqual(zf.read(fp).decode('utf-8'), f_obj.content) + + def test_get_invalid_uuid(self): + """Test GET with invalid UUID""" + url = reverse( + 'isatemplates:export', + kwargs={'cookiecutterisatemplate': INVALID_UUID}, + ) + with self.login(self.user): + response = self.client.get(url) + self.assertEqual(response.status_code, 404) diff --git a/isatemplates/urls.py b/isatemplates/urls.py index 5a11cd21..88ecf698 100644 --- a/isatemplates/urls.py +++ b/isatemplates/urls.py @@ -32,4 +32,9 @@ view=views.ISATemplateDeleteView.as_view(), name='delete', ), + path( + route='export/', + view=views.ISATemplateExportView.as_view(), + name='export', + ), ] diff --git a/isatemplates/views.py b/isatemplates/views.py index 8588657d..3a89e26b 100644 --- a/isatemplates/views.py +++ b/isatemplates/views.py @@ -1,9 +1,14 @@ """UI views for the isatemplates app""" +import io +import os +import zipfile + from cubi_isa_templates import _TEMPLATES as CUBI_TEMPLATES from django.conf import settings from django.contrib import messages +from django.http import HttpResponse, Http404 from django.shortcuts import redirect from django.urls import reverse from django.views.generic import ( @@ -12,6 +17,7 @@ DeleteView, DetailView, TemplateView, + View, ) # Projectroles dependency @@ -24,6 +30,7 @@ # Local constants APP_NAME = 'isatemplates' +FILE_DIR = '{{cookiecutter.__output_dir}}' # Mixins ----------------------------------------------------------------------- @@ -159,4 +166,28 @@ def get_success_url(self): return self.handle_modify(self.object, 'delete') -# TODO: Add export view +class ISATemplateExportView(LoggedInPermissionMixin, View): + """CookiecutterISATemplate export view""" + + permission_required = 'isatemplates.view_template' + + def get(self, request, *args, **kwargs): + template = CookiecutterISATemplate.objects.filter( + sodar_uuid=kwargs.get('cookiecutterisatemplate') + ).first() + if not template: + raise Http404('Template not found') + zip_name = template.name + '.zip' + zip_io = io.BytesIO() + zf = zipfile.ZipFile(zip_io, 'w', compression=zipfile.ZIP_DEFLATED) + zf.writestr('cookiecutter.json', template.json_data) + for f in template.files.all(): + zf.writestr(os.path.join(FILE_DIR, f.file_name), f.content) + zf.close() + response = HttpResponse( + zip_io.getvalue(), content_type='application/zip' + ) + response['Content-Disposition'] = 'attachment; filename="{}"'.format( + zip_name + ) + return response