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

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

View File

@@ -5,7 +5,7 @@
{% block content %}
<h2>{% trans "Checkout" %}</h2>
<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 %}
<div class="panel-group" id="questions_group">
<div class="panel panel-default">

View File

@@ -27,7 +27,17 @@
{% endif %}
{% for q in line.questions %}
<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 %}
{% for q in line.additional_answers %}
<dt>{{ q.question }}</dt>

View File

@@ -8,7 +8,7 @@
Modify order: {{ code }}
{% endblocktrans %}
</h2>
<form class="form-horizontal" method="post">
<form class="form-horizontal" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="panel-group" id="questions_accordion">
{% 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/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<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'^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<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/pay/change',
pretix.presale.views.order.OrderPayChangeMethod.as_view(),
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>[^/]+)$',
pretix.presale.views.order.OrderDownload.as_view(),
name='event.order.download.combined'),

View File

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

View File

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

View File

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