diff --git a/tests/test_admin_pages.py b/tests/test_admin_pages.py index 6995bbcb8..531a851e5 100644 --- a/tests/test_admin_pages.py +++ b/tests/test_admin_pages.py @@ -304,6 +304,17 @@ def test_forms_edit(pub): assert_option_display(resp, 'Limit to one form', 'Enabled') assert FormDef.get(1).only_allow_one is True + # Misc management + assert_option_display(resp, 'Management', 'Default') + resp = resp.click('Management', href='options/management') + assert resp.forms[0]['include_download_all_button'].checked is False + resp.forms[0]['include_download_all_button'].checked = True + resp = resp.forms[0].submit() + assert resp.location == 'http://example.net/backoffice/forms/1/' + resp = resp.follow() + assert_option_display(resp, 'Management', 'Custom') + assert FormDef.get(1).include_download_all_button is True + # Tracking code assert_option_display(resp, 'Tracking Code', 'Disabled') resp = resp.click('Tracking Code') diff --git a/tests/test_backoffice_pages.py b/tests/test_backoffice_pages.py index 998b9e4e1..5769dbfac 100644 --- a/tests/test_backoffice_pages.py +++ b/tests/test_backoffice_pages.py @@ -1913,6 +1913,40 @@ def test_backoffice_submission_context(pub): assert 'by %s' % user.get_display_name() in resp.text +def test_backoffice_download_as_zip(pub): + create_user(pub) + create_environment(pub) + formdef = FormDef.get_by_urlname('form-title') + formdef.fields.append(fields.FileField(id='4', label='file1 field', type='file')) + formdef.fields.append(fields.FileField(id='5', label='file2 field', type='file2')) + formdef.store() + number31 = [x for x in formdef.data_class().select() if x.data['1'] == 'FOO BAR 30'][0] + number31.data['4'] = PicklableUpload('/foo/bar', content_type='text/plain') + number31.data['4'].receive([b'hello world']) + number31.data['5'] = PicklableUpload('/foo/bar', content_type='text/plain') + number31.data['5'].receive([b'hello world2']) + number31.store() + app = login(get_app(pub)) + resp = app.get('/backoffice/management/form-title/%s/' % number31.id) + assert 'Download all files as .zip' not in resp + formdef.include_download_all_button = True + formdef.store() + resp = app.get('/backoffice/management/form-title/%s/' % number31.id) + resp = resp.click('Download all files as .zip') + zip_content = BytesIO(resp.body) + zipf = zipfile.ZipFile(zip_content, 'a') + filelist = zipf.namelist() + assert set(filelist) == {'1_bar', '2_bar'} + for zipinfo in zipf.infolist(): + content = zipf.read(zipinfo) + if zipinfo.filename == '1_bar': + assert content == b'hello world' + elif zipinfo.filename == '2_bar': + assert content == b'hello world2' + else: + assert False # unknown zip part + + def test_backoffice_sidebar_user_template(pub): user = create_user(pub) create_environment(pub) diff --git a/wcs/admin/forms.py b/wcs/admin/forms.py index e99f246bf..a7c23425f 100644 --- a/wcs/admin/forms.py +++ b/wcs/admin/forms.py @@ -155,7 +155,7 @@ class FieldsDirectory(FieldsDirectory): class OptionsDirectory(Directory): _q_exports = ['confirmation', 'only_allow_one', 'always_advertise', 'tracking_code', 'online_status', 'captcha', - 'description', 'keywords', 'category', ('360_view', 'p_360_view'), + 'description', 'keywords', 'category', 'management', 'geolocations', 'appearance', 'templates'] def __init__(self, formdef): @@ -196,12 +196,15 @@ class OptionsDirectory(Directory): value=self.formdef.has_captcha) return self.handle(form, _('CAPTCHA')) - def p_360_view(self): + def management(self): form = Form(enctype='multipart/form-data') + form.add(CheckboxWidget, 'include_download_all_button', + title=_('Include button to download all files'), + value=self.formdef.include_download_all_button) form.add(CheckboxWidget, 'skip_from_360_view', title=_('Skip from per user view'), value=self.formdef.skip_from_360_view) - return self.handle(form, _('Skip from per user view')) + return self.handle(form, _('Management')) def online_status(self): form = Form(enctype='multipart/form-data') @@ -286,6 +289,7 @@ class OptionsDirectory(Directory): 'publication_date', 'expiration_date', 'has_captcha', 'description', 'keywords', 'category_id', 'skip_from_360_view', 'geoloc_label', 'appearance_keywords', + 'include_download_all_button', 'digest_template'] for attr in attrs: widget = form.get_widget(attr) @@ -515,10 +519,10 @@ class FormDefPage(Directory): self.formdef.always_advertise and C_('display to unlogged|Enabled') or C_('display to unlogged|Disabled')) - r += add_option_line('options/360_view', - _('Skip from per user view'), - self.formdef.skip_from_360_view and - C_('skip from per user view|Enabled') or C_('skip from per user view|Disabled')) + r += add_option_line('options/management', + _('Management'), + _('Custom') if (self.formdef.skip_from_360_view or + self.formdef.include_download_all_button) else _('Default')) r += add_option_line('options/tracking_code', _('Tracking Code'), diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py index f5c73a44f..b52e45acf 100644 --- a/wcs/backoffice/management.py +++ b/wcs/backoffice/management.py @@ -21,6 +21,7 @@ import re import time import types import vobject +import zipfile try: import xlwt @@ -51,6 +52,7 @@ from ..qommon import sms from ..qommon import errors from ..qommon import ods from ..qommon.form import * +from ..qommon.form import PicklableUpload from ..qommon.storage import (Equal, NotEqual, LessOrEqual, GreaterOrEqual, Or, Intersects, ILike, FtsMatch, Contains, Null, NotNull) from ..qommon.template import Template @@ -2515,6 +2517,7 @@ class FormPage(Directory): class FormBackOfficeStatusPage(FormStatusPage): _q_exports_orig = ['', 'download', 'json', 'action', 'live', 'inspect', ('inspect-tool', 'inspect_tool'), + ('download-as-zip', 'download_as_zip'), ('user-pending-forms', 'user_pending_forms')] form_page_class = FormFillPage @@ -2641,6 +2644,24 @@ class FormBackOfficeStatusPage(FormStatusPage): 'date': formdata.anonymised.strftime(misc.date_format())} r += htmltext('') + if formdata.formdef.include_download_all_button: + has_attached_files = False + for value in (formdata.data or {}).values(): + if isinstance(value, PicklableUpload): + has_attached_files = True + if isinstance(value, dict) and isinstance(value.get('data'), list): + # block fields + for subvalue in value.get('data'): + for subvalue_elem in subvalue.values(): + if isinstance(subvalue_elem, PicklableUpload): + has_attached_files = True + break + if has_attached_files: + break + + if has_attached_files: + r += htmltext('

%s

') % _('Download all files as .zip') + r += htmltext('') if formdata.submission_context or formdata.submission_channel: @@ -2717,6 +2738,34 @@ class FormBackOfficeStatusPage(FormStatusPage): return r.getvalue() + def download_as_zip(self): + formdata = self.filled + zip_content = BytesIO() + zip_file = zipfile.ZipFile(zip_content, 'w') + counter = {'value': 0} + + def add_zip_file(upload): + counter['value'] += 1 + filename = '%s_%s' % (counter['value'], upload.base_filename) + zip_file.writestr(filename, upload.get_content()) + + for value in formdata.data.values(): + if isinstance(value, PicklableUpload): + add_zip_file(value) + if isinstance(value, dict) and isinstance(value.get('data'), list): + for subvalue in value.get('data'): + for subvalue_elem in subvalue.values(): + if isinstance(subvalue_elem, PicklableUpload): + add_zip_file(subvalue_elem) + + zip_file.close() + + response = get_response() + response.set_content_type('application/zip') + response.set_header('content-disposition', + 'attachment; filename=files-%s.zip' % formdata.get_display_id()) + return zip_content.getvalue() + def get_user_pending_forms(self): from wcs import sql formdata = self.filled diff --git a/wcs/formdef.py b/wcs/formdef.py index 1e137e375..97cf23117 100644 --- a/wcs/formdef.py +++ b/wcs/formdef.py @@ -111,6 +111,7 @@ class FormDef(StorableObject): expiration_date = None has_captcha = False skip_from_360_view = False + include_download_all_button = False appearance_keywords = None digest_template = None @@ -134,7 +135,7 @@ class FormDef(StorableObject): 'digest_template'] BOOLEAN_ATTRIBUTES = ['discussion', 'detailed_emails', 'disabled', 'only_allow_one', 'enable_tracking_codes', 'confirmation', - 'always_advertise', + 'always_advertise', 'include_download_all_button', 'has_captcha', 'skip_from_360_view'] def __init__(self, *args, **kwargs):