diff --git a/dynamicforms/exports.py b/dynamicforms/exports.py index b1dacf7a1d14403c7974242d2c816062033f948a..f899f67aa5928254b31180421386751a25c5a388 100644 --- a/dynamicforms/exports.py +++ b/dynamicforms/exports.py @@ -7,6 +7,7 @@ import zipfile import csv from io import BytesIO, StringIO from .models import Form +from django.db.models.fields.files import FieldFile def _get_rows(form: Form) -> List[List[str]]: @@ -70,3 +71,18 @@ def write_zip(forms, stream=None, folder='forms'): value = csv_file.getvalue() zip.writestr(csv_filename, value.encode()) return stream + + +def files_zip(files: List[FieldFile], folder: str='') -> BytesIO: + """Write form's files into a ZIP.""" + stream = BytesIO() + with zipfile.ZipFile(stream, 'w') as zip: + for file in files: + dest = os.path.join(folder, os.path.basename(file.name)) + contents = file.read() + zip.writestr(dest, contents) + # fix for Linux zip files read in Windows + for file in zip.filelist: + file.create_system = 0 + stream.seek(0) + return stream diff --git a/dynamicforms/views.py b/dynamicforms/views.py index a257292e4171a9551123202d711a78245b69fe1e..84143d4f8ac7b0558b4542b6509791cc476f8d07 100644 --- a/dynamicforms/views.py +++ b/dynamicforms/views.py @@ -1,9 +1,10 @@ """Dynamic forms views and API endpoints.""" +from typing import Union from django.http import HttpResponse from rest_framework import mixins, viewsets -from .exports import write_zip +from .exports import write_zip, files_zip from .models import Form, FormEntry from .serializers import (FormDetailSerializer, FormEntrySerializer, FormSerializer) @@ -41,6 +42,24 @@ def download_multiple_forms_entries(request, forms): response = HttpResponse(contents, content_type='application/x-zip-compressed') - response['Content-Disposition'] = f'attachment; filename="{filename}"' + response['Content-Disposition'] = f'attachment; filename={filename}' + + return response + + +def download_files_zip(request, form: Union[Form, None], folder: str): + """Download form files in a ZIP archive.""" + if form: + files_qs = form.files.all() + files = (f.file for f in files_qs) + else: + files = () + filename = f'{folder}_files.zip' + + stream = files_zip(files, folder=folder) + + response = HttpResponse(content_type='application/zip') + response['Content-Disposition'] = f'attachment; filename={filename}' + response.write(stream.read()) return response diff --git a/projects/views.py b/projects/views.py index 6824d2a774d0eefb2ea56df88d63e5d4801eade6..7ea3aba8e542660756ad10fe252e0349d1a33b16 100644 --- a/projects/views.py +++ b/projects/views.py @@ -2,19 +2,20 @@ from django.core.exceptions import ObjectDoesNotExist from django.shortcuts import redirect +from django.utils.text import slugify from django.utils.timezone import now from django_filters import rest_framework as filters -from django_filters.rest_framework.backends import DjangoFilterBackend from rest_framework import mixins, permissions, status, viewsets from rest_framework.decorators import action from rest_framework.response import Response from dynamicforms.serializers import FormEntrySerializer +from dynamicforms.views import download_files_zip from .models import Edition, Participation, Project -from .serializers import (EditionDetailSerializer, EditionListSerializer, - ParticipationSerializer, ProjectDetailSerializer, - ProjectSerializer, EditionDocumentsSerializer) +from .serializers import (EditionDetailSerializer, EditionDocumentsSerializer, + EditionListSerializer, ParticipationSerializer, + ProjectDetailSerializer, ProjectSerializer) class ProjectViewSet(viewsets.ReadOnlyModelViewSet): @@ -225,7 +226,7 @@ class EditionViewSet(viewsets.ReadOnlyModelViewSet): 'participations', 'organizers' ) permission_classes = (permissions.IsAuthenticated,) - filter_backends = (filters.DjangoFilterBackend,) + filter_backends = (filters.backends.DjangoFilterBackend,) filter_fields = ('project', 'year',) def get_serializer_class(self): @@ -340,6 +341,22 @@ class EditionViewSet(viewsets.ReadOnlyModelViewSet): data = serializer.data return Response(data) + @action(methods=['get'], detail=True) + def documents_zip(self, request, pk=None): + """Download an edition form's documents as a ZIP archive. + + If the edition does not have a form, an empty ZIP file is sent. + """ + edition: Edition = self.get_object() + folder = slugify(edition.project.name) + + try: + form = edition.edition_form.form + except ObjectDoesNotExist: + form = None + + return download_files_zip(request, form=form, folder=folder) + class ParticipationViewSet(mixins.CreateModelMixin, viewsets.ReadOnlyModelViewSet): @@ -401,7 +418,7 @@ class ParticipationViewSet(mixins.CreateModelMixin, queryset = Participation.objects.prefetch_related('edition').all() serializer_class = ParticipationSerializer permission_classes = (permissions.IsAuthenticated,) - filter_backends = (DjangoFilterBackend,) + filter_backends = (filters.backends.DjangoFilterBackend,) filter_fields = ('user', 'state',) @action(methods=['get'], detail=True)