Skip to content
Snippets Groups Projects
Commit 5fca08a2 authored by florimondmanca's avatar florimondmanca
Browse files

add zip export of form entries in admin

parent 70bb2a1a
No related branches found
No related tags found
No related merge requests found
"""Dynamic forms admin panels."""
from django.contrib import admin
from .models import Form, Section, Question, FormEntry, Answer, File
from .models import Answer, File, Form, FormEntry, Question, Section
from .views import download_multiple_forms_entries
class SectionInline(admin.StackedInline):
......@@ -31,10 +33,19 @@ class FormAdmin(admin.ModelAdmin):
list_display = ('title', 'created',)
list_filter = ('created',)
readonly_fields = ('slug',)
readonly_fields = ('slug', 'created',)
search_fields = ('title',)
inlines = (SectionInline, FileInline,)
actions = ['download_csv']
def download_csv(self, request, queryset):
"""Download entries of selected forms under a ZIP file."""
return download_multiple_forms_entries(request, queryset)
download_csv.short_description = (
'Télécharger les résponses des formulaires sélectionnés')
@admin.register(Section)
class SectionAdmin(admin.ModelAdmin):
......@@ -61,6 +72,7 @@ class FormEntryAdmin(admin.ModelAdmin):
list_display = ('form', 'submitted',)
list_filter = ('form', 'submitted',)
readonly_fields = ('submitted',)
search_fields = ('form__title',)
......
"""Data export utilities."""
from typing import List
import os
import zipfile
import csv
from io import BytesIO, StringIO
from .models import Form
def _get_rows(form: Form) -> List[List[str]]:
"""Return CSV rows of form entries, including the header row."""
rows = []
sections = form.sections.prefetch_related('questions').all()
def sections_questions():
for section in sections:
for question in section.questions.all():
yield section, question
def build_row(entry) -> List[str]:
row = [entry.submitted.strftime("%Y-%m-%d %H:%M")]
for _, question in sections_questions():
answer = entry.answers.filter(question=question).first()
value: str = answer and (answer.answer or '') or ''
row.append(value)
return row
# Build the header row
headers = ['Soumis le']
for section, question in sections_questions():
headers.append(f'{section.title}: {question.text}')
rows.append(headers)
# Add a row for each form entry
for entry in form.entries.prefetch_related('answers').all():
row = build_row(entry)
rows.append(row)
return rows
def _write_csv(form: Form, stream):
"""Write a form's entries into a stream in CSV format."""
writer = csv.writer(stream)
for row in _get_rows(form):
writer.writerow(row)
return stream
def write_zip(forms_queryset, stream=None, folder='forms'):
"""Write form entries into a zip of CSV files."""
if stream is None:
stream = BytesIO()
# See https://stackoverflow.com/a/32075279
assert not isinstance(stream, StringIO), (
'cannot write binary zip file contents to a StringIO object, '
'consider using BytesIO instead'
)
with zipfile.ZipFile(stream, 'w') as zip:
for form in forms_queryset:
csv_file = StringIO()
_write_csv(form, csv_file)
csv_filename = os.path.join(folder, f'{form.slug}.csv')
value = csv_file.getvalue()
zip.writestr(csv_filename, value.encode())
return stream
......@@ -25,6 +25,10 @@ class Form(models.Model):
created = models.DateTimeField('créé le', auto_now_add=True)
sections: models.Manager
entries: models.Manager
files: models.Manager
class Meta: # noqa
ordering = ('-created',)
verbose_name = 'formulaire'
......@@ -55,6 +59,8 @@ class Section(models.Model):
verbose_name='formulaire',
help_text="Formulaire associé à la section.")
questions: models.Manager
def __str__(self):
return str(self.title)
......@@ -102,6 +108,8 @@ class Question(models.Model):
verbose_name='section',
help_text="Section de formulaire associée à la question.")
answers: models.Manager
def __str__(self) -> str:
return f'{self.text}{self.required and "*" or ""}'
......@@ -121,6 +129,8 @@ class FormEntry(models.Model):
auto_now_add=True,
help_text="Date et heure de soumission de l'entrée.")
answers: models.Manager
class Meta: # noqa
ordering = ('-submitted',)
verbose_name = 'entrée de formulaire'
......
"""Dynamic forms tests."""
from io import StringIO, BytesIO
from django.test import TestCase
from .models import Form, Section, Question, FormEntry, Answer
from .exports import _write_csv, _get_rows, write_zip
class WriteCsvTest(TestCase):
"""Test the CSV export utility."""
def setUp(self):
# Create a form
form = Form.objects.create(title='Animals')
# Add a section
section = Section.objects.create(
form=form,
title='Dogs')
# Add two questions in the section
have_dog = Question.objects.create(
text='Do you have a dog?',
type=Question.TYPE_YES_NO,
section=section)
Question.objects.create(
text='If yes, what is its name?',
type=Question.TYPE_TEXT_SMALL,
required=False,
section=section)
# Create an entry
entry = FormEntry.objects.create(form=form)
# Add 1 answer to the entry, leave the other answer blank
Answer.objects.create(question=have_dog, entry=entry, answer='No')
self.form = Form.objects.get(pk=form.pk)
self.entry = FormEntry.objects.get(pk=entry.pk)
def test_get_rows(self):
rows = _get_rows(self.form)
self.assertEqual(len(rows), 2)
expected_headers = [
'Soumis le',
'Dogs: If yes, what is its name?',
'Dogs: Do you have a dog?',
]
self.assertListEqual(rows[0], expected_headers)
expected_entry_row = [
self.entry.submitted.strftime("%Y-%m-%d %H:%M"),
'',
'No',
]
self.assertListEqual(rows[1], expected_entry_row)
def test_write_csv_runs_without_failing(self):
stream = StringIO()
_write_csv(self.form, stream)
stream.seek(0)
stream.read()
def test_write_zip_runs_without_failing(self):
stream = BytesIO()
write_zip([self.form], stream=stream)
stream.seek(0)
stream.read()
"""Dynamic forms views and API endpoints."""
from django.http import HttpResponse
from rest_framework import mixins, viewsets
from .exports import write_zip
from .models import Form, FormEntry
from .serializers import (FormDetailSerializer, FormEntrySerializer,
FormSerializer)
......@@ -24,3 +26,20 @@ class FormEntryViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
serializer_class = FormEntrySerializer
queryset = FormEntry.objects.all()
def download_multiple_forms_entries(request, queryset):
"""Download form entries in a ZIP file containing CSV files.
Note: this is not a proper Django view as it expects a queryset.
"""
stream = write_zip(queryset, folder='reponses')
stream.seek(0)
contents = stream.read()
filename = 'responses.zip'
response = HttpResponse(contents,
content_type='application/x-zip-compressed')
response['Content-Disposition'] = f'attachment; filename="{filename}"'
return response
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment