From 941db3db3a5e2f77b49eac69fbbe3320b89b61cf Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Wed, 4 Mar 2015 12:42:36 +0100 Subject: [PATCH] Checkout: Ask questions --- src/pretix/base/models.py | 4 +- .../event/checkout_questions.html | 41 +++++ .../templates/pretixpresale/event/index.html | 2 +- src/pretix/presale/views/__init__.py | 11 +- src/pretix/presale/views/checkout.py | 174 +++++++++++++++++- 5 files changed, 216 insertions(+), 16 deletions(-) create mode 100644 src/pretix/presale/templates/pretixpresale/event/checkout_questions.html diff --git a/src/pretix/base/models.py b/src/pretix/base/models.py index 79420f4a04..d6ee02934f 100644 --- a/src/pretix/base/models.py +++ b/src/pretix/base/models.py @@ -1480,7 +1480,9 @@ class OrganizerSetting(Versionable): """ DEFAULTS = { 'user_mail_required': 'False', - 'max_items_per_order': '10' + 'max_items_per_order': '10', + 'attendee_names_asked': 'True', + 'attendee_names_required': 'False', } organizer = VersionedForeignKey(Organizer, related_name='setting_objects') key = models.CharField(max_length=255) diff --git a/src/pretix/presale/templates/pretixpresale/event/checkout_questions.html b/src/pretix/presale/templates/pretixpresale/event/checkout_questions.html new file mode 100644 index 0000000000..104952fc69 --- /dev/null +++ b/src/pretix/presale/templates/pretixpresale/event/checkout_questions.html @@ -0,0 +1,41 @@ +{% extends "pretixpresale/event/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% block title %}{% trans "Checkout" %}{% endblock %} +{% block content %} +

{% trans "Checkout" %}

+

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

+
+ {% csrf_token %} +
+ {% for form in forms %} + + {% endfor %} +
+
+
+ +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/pretix/presale/templates/pretixpresale/event/index.html b/src/pretix/presale/templates/pretixpresale/event/index.html index 39c8e495ec..5d5c2a2cf7 100644 --- a/src/pretix/presale/templates/pretixpresale/event/index.html +++ b/src/pretix/presale/templates/pretixpresale/event/index.html @@ -100,7 +100,7 @@ {% endfor %}
-
diff --git a/src/pretix/presale/views/__init__.py b/src/pretix/presale/views/__init__.py index 2940eef048..5066000531 100644 --- a/src/pretix/presale/views/__init__.py +++ b/src/pretix/presale/views/__init__.py @@ -35,17 +35,15 @@ class EventLoginRequiredMixin: class CartDisplayMixin: def get_cart(self): - qw = Q(user=self.request.user) - - cartpos = list(CartPosition.objects.current.filter( - qw & Q(event=self.request.event) + cartpos = CartPosition.objects.current.filter( + Q(user=self.request.user) & Q(event=self.request.event) ).order_by( 'item', 'variation' ).select_related( 'item', 'variation' ).prefetch_related( 'variation__values', 'variation__values__prop' - )) + ) # Group items of the same variation # We do this by list manipulations instead of a GROUP BY query, as @@ -54,7 +52,7 @@ class CartDisplayMixin: return pos.item_id, pos.variation_id, pos.price positions = [] - for k, g in groupby(sorted(cartpos, key=keyfunc), key=keyfunc): + for k, g in groupby(sorted(list(cartpos), key=keyfunc), key=keyfunc): g = list(g) group = g[0] group.count = len(g) @@ -63,6 +61,7 @@ class CartDisplayMixin: return { 'positions': positions, + 'raw': cartpos, 'total': sum(p.total for p in positions), 'minutes_left': ( max(min(p.expires for p in positions) - now(), timedelta()).seconds // 60 diff --git a/src/pretix/presale/views/checkout.py b/src/pretix/presale/views/checkout.py index 69d88e2c92..55103ab99a 100644 --- a/src/pretix/presale/views/checkout.py +++ b/src/pretix/presale/views/checkout.py @@ -1,23 +1,181 @@ from django.contrib import messages from django.core.urlresolvers import reverse +from django.db.models import Q +from django import forms from django.shortcuts import redirect -from django.views.generic import View +from django.utils.functional import cached_property +from django.views.generic import View, TemplateView from django.utils.translation import ugettext_lazy as _ +from pretix.base.models import CartPosition, Question, QuestionAnswer from pretix.presale.views import EventViewMixin, CartDisplayMixin, EventLoginRequiredMixin -class CheckoutStart(EventViewMixin, CartDisplayMixin, EventLoginRequiredMixin, View): +class QuestionsForm(forms.Form): + """ + This form class is responsible for asking order-related questions. This includes + the attendee name for admission tickets, if the corresponding setting is enabled, + as well as additional questions defined by the organizer. + """ - def get_failure_url(self): + def __init__(self, *args, **kwargs): + """ + Takes two additional keyword arguments: + + :param cartpos: The cart position the form should be for + :param event: The event this belongs to + """ + cartpos = kwargs.pop('cartpos') + item = cartpos.item + questions = list(item.questions.all()) + event = kwargs.pop('event') + + super().__init__(*args, **kwargs) + + if item.admission and event.settings.attendee_names_asked == 'True': + self.fields['attendee_name'] = forms.CharField( + max_length=255, required=(event.settings.attendee_names_required == 'True'), + label=_('Attendee name'), + initial=cartpos.attendee_name + ) + + for q in questions: + # Do we already have an answer? Provide it as the initial value + answers = [a for a in cartpos.answers.all() if a.question_id == q.identity] + if answers: + initial = answers[0].answer + else: + initial = None + if q.type == Question.TYPE_BOOLEAN: + field = forms.BooleanField( + label=q.question, required=q.required, + initial=initial + ) + elif q.type == Question.TYPE_NUMBER: + field = forms.DecimalField( + label=q.question, required=q.required, + initial=initial + ) + elif q.type == Question.TYPE_STRING: + field = forms.CharField( + label=q.question, required=q.required, + initial=initial + ) + elif q.type == Question.TYPE_TEXT: + field = forms.CharField( + label=q.question, required=q.required, + widget=forms.Textarea, + initial=initial + ) + field.question = q + if answers: + # Cache the answer object for later use + field.answer = answers[0] + self.fields['question_%s' % q.identity] = field + + +class CheckoutStart(EventViewMixin, CartDisplayMixin, EventLoginRequiredMixin, TemplateView): + template_name = "pretixpresale/event/checkout_questions.html" + + def get_success_url(self): return reverse('presale:event.index', kwargs={ 'event': self.request.event.slug, 'organizer': self.request.event.organizer.slug, }) - def get(self, *args, **kwargs): - cart = self.get_cart() - if not cart['positions']: + def get_url(self): + return reverse('presale:event.checkout.start', kwargs={ + 'event': self.request.event.slug, + 'organizer': self.request.event.organizer.slug, + }) + + def get_previous_url(self): + return reverse('presale:event.index', kwargs={ + 'event': self.request.event.slug, + 'organizer': self.request.event.organizer.slug, + }) + + @cached_property + def forms(self): + """ + A list of forms with one form for each cart cart position that has questions + the user can answer. All forms have a custom prefix, so that they can all be + submitted at once. + """ + formlist = [] + for cr in self.cartpos: + form = QuestionsForm(event=self.request.event, + prefix=cr.identity, + cartpos=cr, + data=(self.request.POST if self.request.method == 'POST' else None)) + form.cartpos = cr + if len(form.fields) > 0: + formlist.append(form) + return formlist + + @cached_property + def cartpos(self): + """ + A list of this users cart position + """ + return list(CartPosition.objects.current.filter( + Q(user=self.request.user) & Q(event=self.request.event) + ).order_by( + 'item', 'variation' + ).select_related( + 'item', 'variation' + ).prefetch_related( + 'variation__values', 'variation__values__prop', + 'item__questions', 'answers' + )) + + def post(self, *args, **kwargs): + failed = False + for form in self.forms: + # Every form represents a CartPosition with questions attached + if not form.is_valid(): + failed = True + else: + # This form was correctly filled, so we store the data as + # answers to the questions / in the CartPosition object + for k, v in form.cleaned_data.items(): + if k == 'attendee_name': + form.cartpos = form.cartpos.clone() + form.cartpos.attendee_name = v + form.cartpos.save() + elif k.startswith('question_'): + field = form.fields[k] + if hasattr(field, 'answer'): + # We already have a cached answer object, so we don't + # have to create a new one + field.answer = field.answer.clone() + field.answer.answer = v + field.answer.save() + else: + QuestionAnswer.objects.create( + cartposition=form.cartpos, + question=field.question, + answer=v + ) + if failed: messages.error(self.request, - _("Your cart is empty") % self.event.max_items_per_order) - return redirect(self.get_failure_url()) + _("We had difficulties processing your input. Please review the errors below.")) + return self.get(*args, **kwargs) + return redirect(self.get_success_url()) + + def get(self, *args, **kwargs): + if not self.cartpos: + messages.error(self.request, + _("Your cart is empty")) + return redirect(self.get_previous_url()) + + if not self.forms: + # Nothing to do here + return redirect(self.get_success_url()) + + return super().get(*args, **kwargs) + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + ctx['forms'] = self.forms + return ctx