diff --git a/doc/api/resources/questions.rst b/doc/api/resources/questions.rst index d20e11217f..937bc843f6 100644 --- a/doc/api/resources/questions.rst +++ b/doc/api/resources/questions.rst @@ -22,6 +22,7 @@ type string The expected ty * ``B`` – boolean * ``C`` – choice from a list * ``M`` – multiple choice from a list + * ``F`` – file upload required boolean If ``True``, the question needs to be filled out. position integer An integer, used for sorting items list of integers List of item IDs this question is assigned to. diff --git a/src/pretix/base/exporters/__init__.py b/src/pretix/base/exporters/__init__.py index eefda5866d..aa774ae6cf 100644 --- a/src/pretix/base/exporters/__init__.py +++ b/src/pretix/base/exporters/__init__.py @@ -1,3 +1,4 @@ +from .answers import * # noqa from .invoices import * # noqa from .json import * # noqa from .mail import * # noqa diff --git a/src/pretix/base/exporters/answers.py b/src/pretix/base/exporters/answers.py new file mode 100644 index 0000000000..1f1f9de014 --- /dev/null +++ b/src/pretix/base/exporters/answers.py @@ -0,0 +1,61 @@ +import os +import tempfile +from collections import OrderedDict +from zipfile import ZipFile + +from django import forms +from django.dispatch import receiver +from django.utils.translation import ugettext_lazy as _ + +from pretix.base.models import QuestionAnswer + +from ..exporter import BaseExporter +from ..signals import register_data_exporters + + +class AnswerFilesExporter(BaseExporter): + identifier = 'answerfiles' + verbose_name = _('Answers to file upload questions') + + @property + def export_form_fields(self): + return OrderedDict( + [ + ('questions', + forms.ModelMultipleChoiceField( + queryset=self.event.questions.filter(type='F'), + label=_('Questions'), + widget=forms.CheckboxSelectMultiple, + required=False + )), + ] + ) + + def render(self, form_data: dict): + qs = QuestionAnswer.objects.filter( + orderposition__order__event=self.event, + ).select_related('orderposition', 'orderposition__order', 'question') + if form_data.get('questions'): + qs = qs.filter(question__in=form_data['questions']) + with tempfile.TemporaryDirectory() as d: + with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf: + for i in qs: + if i.file: + i.file.open('r') + fname = '{}-{}-{}-q{}-{}'.format( + self.event.slug.upper(), + i.orderposition.order.code, + i.orderposition.positionid, + i.question.pk, + os.path.basename(i.file.name).split('.', 1)[1] + ) + zipf.writestr(fname, i.file.read()) + i.file.close() + + with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf: + return 'answers.zip', 'application/zip', zipf.read() + + +@receiver(register_data_exporters, dispatch_uid="exporter_answers") +def register_anwers_export(sender, **kwargs): + return AnswerFilesExporter diff --git a/src/pretix/base/migrations/0064_auto_20170703_0912.py b/src/pretix/base/migrations/0064_auto_20170703_0912.py new file mode 100644 index 0000000000..7a7c3eedfc --- /dev/null +++ b/src/pretix/base/migrations/0064_auto_20170703_0912.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.2 on 2017-07-03 09:12 +from __future__ import unicode_literals + +from django.db import migrations, models + +import pretix.base.models.orders + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0063_auto_20170702_1711'), + ] + + operations = [ + migrations.AddField( + model_name='questionanswer', + name='file', + field=models.FileField(blank=True, null=True, upload_to=pretix.base.models.orders.answerfile_name), + ), + migrations.AlterField( + model_name='question', + name='type', + field=models.CharField(choices=[('N', 'Number'), ('S', 'Text (one line)'), ('T', 'Multiline text'), ('B', 'Yes/No'), ('C', 'Choose one from a list'), ('M', 'Choose multiple from a list'), ('F', 'File upload')], max_length=5, verbose_name='Question type'), + ), + ] diff --git a/src/pretix/base/models/base.py b/src/pretix/base/models/base.py index fc40dcfea8..8f5f576328 100644 --- a/src/pretix/base/models/base.py +++ b/src/pretix/base/models/base.py @@ -16,7 +16,7 @@ def cachedfile_name(instance, filename: str) -> str: class CachedFile(models.Model): """ - A cached file (e.g. pre-generated ticket PDF) + An uploaded file, with an optional expiry date. """ id = models.UUIDField(primary_key=True, default=uuid.uuid4) expires = models.DateTimeField(null=True, blank=True) diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index a7593276fb..af82081926 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -446,6 +446,7 @@ class Question(LoggedModel): * a multi-line string (``TYPE_TEXT``) * a boolean (``TYPE_BOOLEAN``) * a multiple choice option (``TYPE_CHOICE`` and ``TYPE_CHOICE_MULTIPLE``) + * a file upload (``TYPE_FILE``)) :param event: The event this question belongs to :type event: Event @@ -463,13 +464,15 @@ class Question(LoggedModel): TYPE_BOOLEAN = "B" TYPE_CHOICE = "C" TYPE_CHOICE_MULTIPLE = "M" + TYPE_FILE = "F" TYPE_CHOICES = ( (TYPE_NUMBER, _("Number")), (TYPE_STRING, _("Text (one line)")), (TYPE_TEXT, _("Multiline text")), (TYPE_BOOLEAN, _("Yes/No")), (TYPE_CHOICE, _("Choose one from a list")), - (TYPE_CHOICE_MULTIPLE, _("Choose multiple from a list")) + (TYPE_CHOICE_MULTIPLE, _("Choose multiple from a list")), + (TYPE_FILE, _("File upload")), ) event = models.ForeignKey( diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 8a37d3a30f..835010d1db 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -12,7 +12,10 @@ from django.db.models import F, Sum from django.db.models.signals import post_delete from django.dispatch import receiver from django.utils.crypto import get_random_string +from django.utils.encoding import escape_uri_path from django.utils.functional import cached_property +from django.utils.html import escape +from django.utils.safestring import mark_safe from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ @@ -340,6 +343,17 @@ class Order(LoggedModel): return True +def answerfile_name(instance, filename: str) -> str: + secret = get_random_string(length=32, allowed_chars=string.ascii_letters + string.digits) + event = (instance.cartposition if instance.cartposition else instance.orderposition.order).event + return 'cachedfiles/answers/{org}/{ev}/{secret}.{filename}'.format( + org=event.organizer.slug, + ev=event.slug, + secret=secret, + filename=escape_uri_path(filename), + ) + + class QuestionAnswer(models.Model): """ The answer to a Question, connected to an OrderPosition or CartPosition. @@ -370,12 +384,39 @@ class QuestionAnswer(models.Model): QuestionOption, related_name='answers', blank=True ) answer = models.TextField() + file = models.FileField( + null=True, blank=True, upload_to=answerfile_name + ) + + @property + def file_link(self): + from pretix.multidomain.urlreverse import eventreverse + + if self.file: + if self.orderposition: + url = eventreverse(self.orderposition.order.event, 'presale:event.order.download.answer', kwargs={ + 'order': self.orderposition.order.code, + 'secret': self.orderposition.order.secret, + 'answer': self.pk, + }) + else: + url = eventreverse(self.cartposition.event, 'presale:event.cart.download.answer', kwargs={ + 'answer': self.pk, + }) + + return mark_safe("{}".format( + url, + escape(self.file.name.split('.', 1)[-1]) + )) + return "" def __str__(self): if self.question.type == Question.TYPE_BOOLEAN and self.answer == "True": return str(_("Yes")) elif self.question.type == Question.TYPE_BOOLEAN and self.answer == "False": return str(_("No")) + elif self.question.type == Question.TYPE_FILE: + return str(_("")) else: return self.answer @@ -684,3 +725,17 @@ def cachedticket_delete(sender, instance, **kwargs): if instance.file: # Pass false so FileField doesn't save the model. instance.file.delete(False) + + +@receiver(post_delete, sender=CachedCombinedTicket) +def cachedcombinedticket_delete(sender, instance, **kwargs): + if instance.file: + # Pass false so FileField doesn't save the model. + instance.file.delete(False) + + +@receiver(post_delete, sender=QuestionAnswer) +def answer_delete(sender, instance, **kwargs): + if instance.file: + # Pass false so FileField doesn't save the model. + instance.file.delete(False) diff --git a/src/pretix/control/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html index 3764859e28..3bb1290f67 100644 --- a/src/pretix/control/templates/pretixcontrol/order/index.html +++ b/src/pretix/control/templates/pretixcontrol/order/index.html @@ -198,8 +198,17 @@ {% endif %} {% for q in line.questions %}
{{ q.question }}
-
{% if q.answer %}{{ q.answer|linebreaksbr }}{% else %} - {% trans "not answered" %}{% endif %}
+
+ {% if q.answer %} + {% if q.answer.file %} + {{ q.answer.file_link }} + {% else %} + {{ q.answer|linebreaksbr }} + {% endif %} + {% else %} + {% trans "not answered" %} + {% endif %} +
{% endfor %} {% for q in line.additional_fields %}
{{ q.question }}
diff --git a/src/pretix/control/views/item.py b/src/pretix/control/views/item.py index 835a38fde7..adfab590bf 100644 --- a/src/pretix/control/views/item.py +++ b/src/pretix/control/views/item.py @@ -414,7 +414,14 @@ class QuestionView(EventPermissionRequiredMixin, QuestionMixin, ChartContainingV i = self.request.GET.get("item", "") qs = qs.filter(orderposition__item_id__in=(i,)) - if self.object.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE): + if self.object.type == Question.TYPE_FILE: + qs = [ + { + 'answer': ugettext('File uploaded'), + 'count': qs.filter(file__isnull=False).count() + } + ] + elif self.object.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE): qs = qs.order_by('options').values('options', 'options__answer')\ .annotate(count=Count('id')).order_by('-count') for a in qs: diff --git a/src/pretix/presale/forms/checkout.py b/src/pretix/presale/forms/checkout.py index 104c75ea02..e49ebb726d 100644 --- a/src/pretix/presale/forms/checkout.py +++ b/src/pretix/presale/forms/checkout.py @@ -1,3 +1,4 @@ +import os from decimal import Decimal from itertools import chain @@ -10,8 +11,9 @@ from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ from pretix.base.models import ItemVariation, Question -from pretix.base.models.orders import InvoiceAddress +from pretix.base.models.orders import InvoiceAddress, OrderPosition from pretix.base.templatetags.rich_text import rich_text +from pretix.multidomain.urlreverse import eventreverse from pretix.presale.signals import contact_form_fields, question_form_fields @@ -73,6 +75,42 @@ class InvoiceAddressForm(forms.ModelForm): raise ValidationError(_('You need to provide either a company name or your name.')) +class UploadedFileWidget(forms.ClearableFileInput): + + def __init__(self, *args, **kwargs): + self.position = kwargs.pop('position') + self.event = kwargs.pop('event') + self.answer = kwargs.pop('answer') + super().__init__(*args, **kwargs) + + class FakeFile: + def __init__(self, file, position, event, answer): + self.file = file + self.position = position + self.event = event + self.answer = answer + + def __str__(self): + return os.path.basename(self.file.name).split('.', 1)[-1] + + @property + def url(self): + if isinstance(self.position, OrderPosition): + return eventreverse(self.event, 'presale:event.order.download.answer', kwargs={ + 'order': self.position.order.code, + 'secret': self.position.order.secret, + 'answer': self.answer.pk, + }) + else: + return eventreverse(self.event, 'presale:event.cart.download.answer', kwargs={ + 'answer': self.answer.pk, + }) + + def format_value(self, value): + if self.is_initial(value): + return self.FakeFile(value, self.position, self.event, self.answer) + + class QuestionsForm(forms.Form): """ This form class is responsible for asking order-related questions. This includes @@ -169,6 +207,12 @@ class QuestionsForm(forms.Form): widget=forms.CheckboxSelectMultiple, initial=initial.options.all() if initial else None, ) + elif q.type == Question.TYPE_FILE: + field = forms.FileField( + label=q.question, required=q.required, + initial=initial.file if initial else None, + widget=UploadedFileWidget(position=pos, event=event, answer=initial) + ) field.question = q if answers: # Cache the answer object for later use diff --git a/src/pretix/presale/templates/pretixpresale/event/checkout_questions.html b/src/pretix/presale/templates/pretixpresale/event/checkout_questions.html index da509576b9..8116cce309 100644 --- a/src/pretix/presale/templates/pretixpresale/event/checkout_questions.html +++ b/src/pretix/presale/templates/pretixpresale/event/checkout_questions.html @@ -5,7 +5,7 @@ {% block content %}

{% trans "Checkout" %}

{% trans "Before we continue, we need you to answer some questions." %}

-
+ {% csrf_token %}
diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html b/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html index 66b37c3cad..b9e0cefba7 100644 --- a/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html +++ b/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html @@ -27,7 +27,17 @@ {% endif %} {% for q in line.questions %}
{{ q.question }}
-
{% if q.answer %}{{ q.answer|linebreaksbr }}{% else %}{% trans "not answered" %}{% endif %}
+
+ {% if q.answer %} + {% if q.answer.file %} + {{ q.answer.file_link }} + {% else %} + {{ q.answer|linebreaksbr }} + {% endif %} + {% else %} + {% trans "not answered" %} + {% endif %} +
{% endfor %} {% for q in line.additional_answers %}
{{ q.question }}
diff --git a/src/pretix/presale/templates/pretixpresale/event/order_modify.html b/src/pretix/presale/templates/pretixpresale/event/order_modify.html index a91f5fb217..5954cac4a5 100644 --- a/src/pretix/presale/templates/pretixpresale/event/order_modify.html +++ b/src/pretix/presale/templates/pretixpresale/event/order_modify.html @@ -8,7 +8,7 @@ Modify order: {{ code }} {% endblocktrans %} - + {% csrf_token %}
{% if event.settings.invoice_address_asked %} diff --git a/src/pretix/presale/urls.py b/src/pretix/presale/urls.py index 7328719255..758b042a14 100644 --- a/src/pretix/presale/urls.py +++ b/src/pretix/presale/urls.py @@ -16,6 +16,9 @@ event_patterns = [ url(r'^cart/add$', pretix.presale.views.cart.CartAdd.as_view(), name='event.cart.add'), url(r'^cart/remove$', pretix.presale.views.cart.CartRemove.as_view(), name='event.cart.remove'), url(r'^cart/clear$', pretix.presale.views.cart.CartClear.as_view(), name='event.cart.clear'), + url(r'^cart/answer/(?P[^/]+)/$', + pretix.presale.views.cart.AnswerDownload.as_view(), + name='event.cart.download.answer'), url(r'^waitinglist', pretix.presale.views.waiting.WaitingView.as_view(), name='event.waitinglist'), url(r'^checkout/start$', pretix.presale.views.checkout.CheckoutView.as_view(), name='event.checkout.start'), url(r'^redeem/?$', pretix.presale.views.cart.RedeemView.as_view(), @@ -48,6 +51,9 @@ event_patterns = [ url(r'^order/(?P[^/]+)/(?P[A-Za-z0-9]+)/pay/change', pretix.presale.views.order.OrderPayChangeMethod.as_view(), name='event.order.pay.change'), + url(r'^order/(?P[^/]+)/(?P[A-Za-z0-9]+)/answer/(?P[^/]+)/$', + pretix.presale.views.order.AnswerDownload.as_view(), + name='event.order.download.answer'), url(r'^order/(?P[^/]+)/(?P[A-Za-z0-9]+)/download/(?P[^/]+)$', pretix.presale.views.order.OrderDownload.as_view(), name='event.order.download.combined'), diff --git a/src/pretix/presale/views/cart.py b/src/pretix/presale/views/cart.py index e095e1be2c..8bce1a33ca 100644 --- a/src/pretix/presale/views/cart.py +++ b/src/pretix/presale/views/cart.py @@ -1,14 +1,17 @@ +import mimetypes +import os + from django.contrib import messages from django.db.models import Count, Q -from django.http import JsonResponse -from django.shortcuts import redirect +from django.http import FileResponse, Http404, JsonResponse +from django.shortcuts import get_object_or_404, redirect from django.utils import translation from django.utils.timezone import now from django.utils.translation import ugettext as _ from django.views.generic import TemplateView, View from pretix.base.decimal import round_decimal -from pretix.base.models import CartPosition, Quota, Voucher +from pretix.base.models import CartPosition, QuestionAnswer, Quota, Voucher from pretix.base.services.cart import ( CartError, add_items_to_cart, clear_cart, remove_cart_position, ) @@ -266,3 +269,23 @@ class RedeemView(EventViewMixin, TemplateView): return redirect(eventreverse(request.event, 'presale:event.index')) return super().dispatch(request, *args, **kwargs) + + +class AnswerDownload(EventViewMixin, View): + def get(self, request, *args, **kwargs): + answid = kwargs.get('answer') + answer = get_object_or_404( + QuestionAnswer, + cartposition__cart_id=self.request.session.session_key, + id=answid + ) + if not answer.file: + return Http404() + + ftype, _ = mimetypes.guess_type(answer.file.name) + resp = FileResponse(answer.file, content_type=ftype or 'application/binary') + resp['Content-Disposition'] = 'attachment; filename="{}-cart-{}"'.format( + self.request.event.slug.upper(), + os.path.basename(answer.file.name).split('.', 1)[1] + ) + return resp diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py index 1de61fb391..e940a5f2d3 100644 --- a/src/pretix/presale/views/order.py +++ b/src/pretix/presale/views/order.py @@ -1,15 +1,18 @@ +import mimetypes +import os + from django.contrib import messages from django.db import transaction from django.db.models import Sum from django.http import FileResponse, Http404, JsonResponse -from django.shortcuts import redirect, render +from django.shortcuts import get_object_or_404, redirect, render from django.utils.functional import cached_property from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ from django.views.generic import TemplateView, View from pretix.base.models import CachedTicket, Invoice, Order, OrderPosition -from pretix.base.models.orders import InvoiceAddress +from pretix.base.models.orders import InvoiceAddress, QuestionAnswer from pretix.base.payment import PaymentException from pretix.base.services.invoices import ( generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified, @@ -29,13 +32,13 @@ from pretix.presale.views.questions import QuestionsViewMixin class OrderDetailMixin: @cached_property def order(self): - try: - order = self.request.event.orders.get(code=self.kwargs['order']) + order = self.request.event.orders.filter(code=self.kwargs['order']).select_related('event').first() + if order: if order.secret.lower() == self.kwargs['secret'].lower(): return order else: return None - except Order.DoesNotExist: + else: # Do a comparison as well to harden timing attacks if 'abcdefghijklmnopq'.lower() == self.kwargs['secret'].lower(): return None @@ -89,7 +92,7 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TemplateView): ctx['download_buttons'] = self.download_buttons ctx['cart'] = self.get_cart( answers=True, downloads=ctx['can_download'], - queryset=OrderPosition.objects.filter(order=self.order), + queryset=self.order.positions.all(), payment_fee=self.order.payment_fee, payment_fee_tax_rate=self.order.payment_fee_tax_rate ) ctx['invoices'] = list(self.order.invoices.all()) @@ -488,6 +491,23 @@ class OrderCancelDo(EventViewMixin, OrderDetailMixin, AsyncAction, View): return _('The order has been canceled.') +class AnswerDownload(EventViewMixin, OrderDetailMixin, View): + def get(self, request, *args, **kwargs): + answid = kwargs.get('answer') + answer = get_object_or_404(QuestionAnswer, orderposition__order=self.order, id=answid) + if not answer.file: + return Http404() + + ftype, _ = mimetypes.guess_type(answer.file.name) + resp = FileResponse(answer.file, content_type=ftype or 'application/binary') + resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}"'.format( + self.request.event.slug.upper(), self.order.code, + answer.orderposition.positionid, + os.path.basename(answer.file.name).split('.', 1)[1] + ) + return resp + + class OrderDownload(EventViewMixin, OrderDetailMixin, View): def get_self_url(self): diff --git a/src/pretix/presale/views/questions.py b/src/pretix/presale/views/questions.py index fa522313b8..971f4fad85 100644 --- a/src/pretix/presale/views/questions.py +++ b/src/pretix/presale/views/questions.py @@ -2,6 +2,7 @@ import json from collections import defaultdict from django import forms +from django.core.files.uploadedfile import UploadedFile from django.utils.functional import cached_property from pretix.base.models import CartPosition, OrderPosition, QuestionAnswer @@ -39,7 +40,8 @@ class QuestionsViewMixin: prefix=cr.id, cartpos=cartpos, orderpos=orderpos, - data=(self.request.POST if self.request.method == 'POST' else None)) + data=(self.request.POST if self.request.method == 'POST' else None), + files=(self.request.FILES if self.request.method == 'POST' else None)) form.pos = cartpos or orderpos if len(form.fields) > 0: formlist.append(form) @@ -78,7 +80,9 @@ class QuestionsViewMixin: if hasattr(field, 'answer'): # We already have a cached answer object, so we don't # have to create a new one - if v == '': + if v == '' or v is None or (isinstance(field, forms.FileField) and v is False): + if field.answer.file: + field.answer.file.delete() field.answer.delete() else: self._save_to_answer(field, field.answer, v) @@ -119,5 +123,9 @@ class QuestionsViewMixin: answer.options.clear() answer.options.add(value) answer.answer = value.answer + elif isinstance(field, forms.FileField): + if isinstance(value, UploadedFile): + answer.file.save(value.name, value) + answer.answer = 'file://' + value.name else: answer.answer = value diff --git a/src/tests/presale/test_checkout.py b/src/tests/presale/test_checkout.py index b2850e4ae1..c42c9b2677 100644 --- a/src/tests/presale/test_checkout.py +++ b/src/tests/presale/test_checkout.py @@ -1,9 +1,11 @@ import datetime +import os from datetime import timedelta from decimal import Decimal from bs4 import BeautifulSoup from django.conf import settings +from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase from django.utils.timezone import now @@ -110,6 +112,43 @@ class CheckoutTestCase(TestCase): self.assertEqual(cr1.answers.filter(question=q2).count(), 1) self.assertFalse(cr2.answers.filter(question=q2).exists()) + def test_question_file_upload(self): + q1 = Question.objects.create( + event=self.event, question='Student ID', type=Question.TYPE_FILE, + required=False + ) + self.ticket.questions.add(q1) + cr1 = CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=23, expires=now() + timedelta(minutes=10) + ) + response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True) + doc = BeautifulSoup(response.rendered_content, "lxml") + + self.assertEqual(len(doc.select('input[name=%s-question_%s]' % (cr1.id, q1.id))), 1) + + f = SimpleUploadedFile("testfile.txt", b"file_content") + response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), { + '%s-question_%s' % (cr1.id, q1.id): f, + 'email': 'admin@localhost' + }, follow=True) + self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), + target_status_code=200) + + cr1 = CartPosition.objects.get(id=cr1.id) + a = cr1.answers.get(question=q1) + assert a.file + assert a.file.read() == b"file_content" + assert os.path.exists(os.path.join(settings.MEDIA_ROOT, a.file.name)) + + # Delete + self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), { + '%s-question_%s-clear' % (cr1.id, q1.id): 'on', + 'email': 'admin@localhost' + }, follow=True) + assert not cr1.answers.exists() + assert not os.path.exists(os.path.join(settings.MEDIA_ROOT, a.file.name)) + def test_attendee_email_required(self): self.event.settings.set('attendee_emails_asked', True) self.event.settings.set('attendee_emails_required', True)