Add a file upload type to questions (#534)

* Initial stuff

* More features
This commit is contained in:
Raphael Michel
2017-07-03 14:22:31 +02:00
committed by GitHub
parent 678d510e29
commit 0db5d062be
18 changed files with 334 additions and 20 deletions

View File

@@ -22,6 +22,7 @@ type string The expected ty
* ``B`` boolean * ``B`` boolean
* ``C`` choice from a list * ``C`` choice from a list
* ``M`` multiple choice from a list * ``M`` multiple choice from a list
* ``F`` file upload
required boolean If ``True``, the question needs to be filled out. required boolean If ``True``, the question needs to be filled out.
position integer An integer, used for sorting position integer An integer, used for sorting
items list of integers List of item IDs this question is assigned to. items list of integers List of item IDs this question is assigned to.

View File

@@ -1,3 +1,4 @@
from .answers import * # noqa
from .invoices import * # noqa from .invoices import * # noqa
from .json import * # noqa from .json import * # noqa
from .mail import * # noqa from .mail import * # noqa

View File

@@ -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

View File

@@ -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'),
),
]

View File

@@ -16,7 +16,7 @@ def cachedfile_name(instance, filename: str) -> str:
class CachedFile(models.Model): 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) id = models.UUIDField(primary_key=True, default=uuid.uuid4)
expires = models.DateTimeField(null=True, blank=True) expires = models.DateTimeField(null=True, blank=True)

View File

@@ -446,6 +446,7 @@ class Question(LoggedModel):
* a multi-line string (``TYPE_TEXT``) * a multi-line string (``TYPE_TEXT``)
* a boolean (``TYPE_BOOLEAN``) * a boolean (``TYPE_BOOLEAN``)
* a multiple choice option (``TYPE_CHOICE`` and ``TYPE_CHOICE_MULTIPLE``) * a multiple choice option (``TYPE_CHOICE`` and ``TYPE_CHOICE_MULTIPLE``)
* a file upload (``TYPE_FILE``))
:param event: The event this question belongs to :param event: The event this question belongs to
:type event: Event :type event: Event
@@ -463,13 +464,15 @@ class Question(LoggedModel):
TYPE_BOOLEAN = "B" TYPE_BOOLEAN = "B"
TYPE_CHOICE = "C" TYPE_CHOICE = "C"
TYPE_CHOICE_MULTIPLE = "M" TYPE_CHOICE_MULTIPLE = "M"
TYPE_FILE = "F"
TYPE_CHOICES = ( TYPE_CHOICES = (
(TYPE_NUMBER, _("Number")), (TYPE_NUMBER, _("Number")),
(TYPE_STRING, _("Text (one line)")), (TYPE_STRING, _("Text (one line)")),
(TYPE_TEXT, _("Multiline text")), (TYPE_TEXT, _("Multiline text")),
(TYPE_BOOLEAN, _("Yes/No")), (TYPE_BOOLEAN, _("Yes/No")),
(TYPE_CHOICE, _("Choose one from a list")), (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( event = models.ForeignKey(

View File

@@ -12,7 +12,10 @@ from django.db.models import F, Sum
from django.db.models.signals import post_delete from django.db.models.signals import post_delete
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.crypto import get_random_string 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.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.timezone import now
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@@ -340,6 +343,17 @@ class Order(LoggedModel):
return True 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): class QuestionAnswer(models.Model):
""" """
The answer to a Question, connected to an OrderPosition or CartPosition. 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 QuestionOption, related_name='answers', blank=True
) )
answer = models.TextField() 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("<a href='{}'>{}</a>".format(
url,
escape(self.file.name.split('.', 1)[-1])
))
return ""
def __str__(self): def __str__(self):
if self.question.type == Question.TYPE_BOOLEAN and self.answer == "True": if self.question.type == Question.TYPE_BOOLEAN and self.answer == "True":
return str(_("Yes")) return str(_("Yes"))
elif self.question.type == Question.TYPE_BOOLEAN and self.answer == "False": elif self.question.type == Question.TYPE_BOOLEAN and self.answer == "False":
return str(_("No")) return str(_("No"))
elif self.question.type == Question.TYPE_FILE:
return str(_("<file>"))
else: else:
return self.answer return self.answer
@@ -684,3 +725,17 @@ def cachedticket_delete(sender, instance, **kwargs):
if instance.file: if instance.file:
# Pass false so FileField doesn't save the model. # Pass false so FileField doesn't save the model.
instance.file.delete(False) 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)

View File

@@ -198,8 +198,17 @@
{% endif %} {% endif %}
{% for q in line.questions %} {% for q in line.questions %}
<dt>{{ q.question }}</dt> <dt>{{ q.question }}</dt>
<dd>{% if q.answer %}{{ q.answer|linebreaksbr }}{% else %} <dd>
<em>{% trans "not answered" %}</em>{% endif %}</dd> {% if q.answer %}
{% if q.answer.file %}
<span class="fa fa-file"></span> {{ q.answer.file_link }}
{% else %}
{{ q.answer|linebreaksbr }}
{% endif %}
{% else %}
<em>{% trans "not answered" %}</em>
{% endif %}
</dd>
{% endfor %} {% endfor %}
{% for q in line.additional_fields %} {% for q in line.additional_fields %}
<dt>{{ q.question }}</dt> <dt>{{ q.question }}</dt>

View File

@@ -414,7 +414,14 @@ class QuestionView(EventPermissionRequiredMixin, QuestionMixin, ChartContainingV
i = self.request.GET.get("item", "") i = self.request.GET.get("item", "")
qs = qs.filter(orderposition__item_id__in=(i,)) 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')\ qs = qs.order_by('options').values('options', 'options__answer')\
.annotate(count=Count('id')).order_by('-count') .annotate(count=Count('id')).order_by('-count')
for a in qs: for a in qs:

View File

@@ -1,3 +1,4 @@
import os
from decimal import Decimal from decimal import Decimal
from itertools import chain from itertools import chain
@@ -10,8 +11,9 @@ from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from pretix.base.models import ItemVariation, Question 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.base.templatetags.rich_text import rich_text
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.signals import contact_form_fields, question_form_fields 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.')) 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): class QuestionsForm(forms.Form):
""" """
This form class is responsible for asking order-related questions. This includes This form class is responsible for asking order-related questions. This includes
@@ -169,6 +207,12 @@ class QuestionsForm(forms.Form):
widget=forms.CheckboxSelectMultiple, widget=forms.CheckboxSelectMultiple,
initial=initial.options.all() if initial else None, 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 field.question = q
if answers: if answers:
# Cache the answer object for later use # Cache the answer object for later use

View File

@@ -5,7 +5,7 @@
{% block content %} {% block content %}
<h2>{% trans "Checkout" %}</h2> <h2>{% trans "Checkout" %}</h2>
<p>{% trans "Before we continue, we need you to answer some questions." %}</p> <p>{% trans "Before we continue, we need you to answer some questions." %}</p>
<form class="form-horizontal" method="post"> <form class="form-horizontal" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<div class="panel-group" id="questions_group"> <div class="panel-group" id="questions_group">
<div class="panel panel-default"> <div class="panel panel-default">

View File

@@ -27,7 +27,17 @@
{% endif %} {% endif %}
{% for q in line.questions %} {% for q in line.questions %}
<dt>{{ q.question }}</dt> <dt>{{ q.question }}</dt>
<dd>{% if q.answer %}{{ q.answer|linebreaksbr }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}</dd> <dd>
{% if q.answer %}
{% if q.answer.file %}
<span class="fa fa-file"></span> {{ q.answer.file_link }}
{% else %}
{{ q.answer|linebreaksbr }}
{% endif %}
{% else %}
<em>{% trans "not answered" %}</em>
{% endif %}
</dd>
{% endfor %} {% endfor %}
{% for q in line.additional_answers %} {% for q in line.additional_answers %}
<dt>{{ q.question }}</dt> <dt>{{ q.question }}</dt>

View File

@@ -8,7 +8,7 @@
Modify order: {{ code }} Modify order: {{ code }}
{% endblocktrans %} {% endblocktrans %}
</h2> </h2>
<form class="form-horizontal" method="post"> <form class="form-horizontal" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<div class="panel-group" id="questions_accordion"> <div class="panel-group" id="questions_accordion">
{% if event.settings.invoice_address_asked %} {% if event.settings.invoice_address_asked %}

View File

@@ -16,6 +16,9 @@ event_patterns = [
url(r'^cart/add$', pretix.presale.views.cart.CartAdd.as_view(), name='event.cart.add'), 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/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/clear$', pretix.presale.views.cart.CartClear.as_view(), name='event.cart.clear'),
url(r'^cart/answer/(?P<answer>[^/]+)/$',
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'^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'^checkout/start$', pretix.presale.views.checkout.CheckoutView.as_view(), name='event.checkout.start'),
url(r'^redeem/?$', pretix.presale.views.cart.RedeemView.as_view(), url(r'^redeem/?$', pretix.presale.views.cart.RedeemView.as_view(),
@@ -48,6 +51,9 @@ event_patterns = [
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/pay/change', url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/pay/change',
pretix.presale.views.order.OrderPayChangeMethod.as_view(), pretix.presale.views.order.OrderPayChangeMethod.as_view(),
name='event.order.pay.change'), name='event.order.pay.change'),
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/answer/(?P<answer>[^/]+)/$',
pretix.presale.views.order.AnswerDownload.as_view(),
name='event.order.download.answer'),
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/download/(?P<output>[^/]+)$', url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/download/(?P<output>[^/]+)$',
pretix.presale.views.order.OrderDownload.as_view(), pretix.presale.views.order.OrderDownload.as_view(),
name='event.order.download.combined'), name='event.order.download.combined'),

View File

@@ -1,14 +1,17 @@
import mimetypes
import os
from django.contrib import messages from django.contrib import messages
from django.db.models import Count, Q from django.db.models import Count, Q
from django.http import JsonResponse from django.http import FileResponse, Http404, JsonResponse
from django.shortcuts import redirect from django.shortcuts import get_object_or_404, redirect
from django.utils import translation from django.utils import translation
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.generic import TemplateView, View from django.views.generic import TemplateView, View
from pretix.base.decimal import round_decimal 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 ( from pretix.base.services.cart import (
CartError, add_items_to_cart, clear_cart, remove_cart_position, 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 redirect(eventreverse(request.event, 'presale:event.index'))
return super().dispatch(request, *args, **kwargs) 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

View File

@@ -1,15 +1,18 @@
import mimetypes
import os
from django.contrib import messages from django.contrib import messages
from django.db import transaction from django.db import transaction
from django.db.models import Sum from django.db.models import Sum
from django.http import FileResponse, Http404, JsonResponse 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.functional import cached_property
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView, View from django.views.generic import TemplateView, View
from pretix.base.models import CachedTicket, Invoice, Order, OrderPosition 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.payment import PaymentException
from pretix.base.services.invoices import ( from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified, generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
@@ -29,13 +32,13 @@ from pretix.presale.views.questions import QuestionsViewMixin
class OrderDetailMixin: class OrderDetailMixin:
@cached_property @cached_property
def order(self): def order(self):
try: order = self.request.event.orders.filter(code=self.kwargs['order']).select_related('event').first()
order = self.request.event.orders.get(code=self.kwargs['order']) if order:
if order.secret.lower() == self.kwargs['secret'].lower(): if order.secret.lower() == self.kwargs['secret'].lower():
return order return order
else: else:
return None return None
except Order.DoesNotExist: else:
# Do a comparison as well to harden timing attacks # Do a comparison as well to harden timing attacks
if 'abcdefghijklmnopq'.lower() == self.kwargs['secret'].lower(): if 'abcdefghijklmnopq'.lower() == self.kwargs['secret'].lower():
return None return None
@@ -89,7 +92,7 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TemplateView):
ctx['download_buttons'] = self.download_buttons ctx['download_buttons'] = self.download_buttons
ctx['cart'] = self.get_cart( ctx['cart'] = self.get_cart(
answers=True, downloads=ctx['can_download'], 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 payment_fee=self.order.payment_fee, payment_fee_tax_rate=self.order.payment_fee_tax_rate
) )
ctx['invoices'] = list(self.order.invoices.all()) ctx['invoices'] = list(self.order.invoices.all())
@@ -488,6 +491,23 @@ class OrderCancelDo(EventViewMixin, OrderDetailMixin, AsyncAction, View):
return _('The order has been canceled.') 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): class OrderDownload(EventViewMixin, OrderDetailMixin, View):
def get_self_url(self): def get_self_url(self):

View File

@@ -2,6 +2,7 @@ import json
from collections import defaultdict from collections import defaultdict
from django import forms from django import forms
from django.core.files.uploadedfile import UploadedFile
from django.utils.functional import cached_property from django.utils.functional import cached_property
from pretix.base.models import CartPosition, OrderPosition, QuestionAnswer from pretix.base.models import CartPosition, OrderPosition, QuestionAnswer
@@ -39,7 +40,8 @@ class QuestionsViewMixin:
prefix=cr.id, prefix=cr.id,
cartpos=cartpos, cartpos=cartpos,
orderpos=orderpos, 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 form.pos = cartpos or orderpos
if len(form.fields) > 0: if len(form.fields) > 0:
formlist.append(form) formlist.append(form)
@@ -78,7 +80,9 @@ class QuestionsViewMixin:
if hasattr(field, 'answer'): if hasattr(field, 'answer'):
# We already have a cached answer object, so we don't # We already have a cached answer object, so we don't
# have to create a new one # 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() field.answer.delete()
else: else:
self._save_to_answer(field, field.answer, v) self._save_to_answer(field, field.answer, v)
@@ -119,5 +123,9 @@ class QuestionsViewMixin:
answer.options.clear() answer.options.clear()
answer.options.add(value) answer.options.add(value)
answer.answer = value.answer 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: else:
answer.answer = value answer.answer = value

View File

@@ -1,9 +1,11 @@
import datetime import datetime
import os
from datetime import timedelta from datetime import timedelta
from decimal import Decimal from decimal import Decimal
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from django.conf import settings from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase from django.test import TestCase
from django.utils.timezone import now from django.utils.timezone import now
@@ -110,6 +112,43 @@ class CheckoutTestCase(TestCase):
self.assertEqual(cr1.answers.filter(question=q2).count(), 1) self.assertEqual(cr1.answers.filter(question=q2).count(), 1)
self.assertFalse(cr2.answers.filter(question=q2).exists()) 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): def test_attendee_email_required(self):
self.event.settings.set('attendee_emails_asked', True) self.event.settings.set('attendee_emails_asked', True)
self.event.settings.set('attendee_emails_required', True) self.event.settings.set('attendee_emails_required', True)