mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
320 lines
13 KiB
Python
320 lines
13 KiB
Python
from decimal import Decimal
|
||
|
||
from django import forms
|
||
from django.core.exceptions import ValidationError
|
||
from django.db.models import Count, Prefetch, Q
|
||
from django.forms.widgets import RadioChoiceInput, RadioFieldRenderer
|
||
from django.utils.encoding import force_text
|
||
from django.utils.formats import number_format
|
||
from django.utils.html import format_html
|
||
from django.utils.safestring import mark_safe
|
||
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.templatetags.rich_text import rich_text
|
||
|
||
|
||
class ContactForm(forms.Form):
|
||
email = forms.EmailField(label=_('E-mail'),
|
||
help_text=_('Make sure to enter a valid email address. We will send you an order '
|
||
'confirmation including a link that you need in case you want to make '
|
||
'modifications to your order or download your ticket later.'))
|
||
|
||
|
||
class InvoiceAddressForm(forms.ModelForm):
|
||
|
||
class Meta:
|
||
model = InvoiceAddress
|
||
fields = ('company', 'name', 'street', 'zipcode', 'city', 'country', 'vat_id')
|
||
widgets = {
|
||
'street': forms.Textarea(attrs={'rows': 2, 'placeholder': _('Street and Number')}),
|
||
}
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
self.event = event = kwargs.pop('event')
|
||
super().__init__(*args, **kwargs)
|
||
if not event.settings.invoice_address_vatid:
|
||
del self.fields['vat_id']
|
||
if not event.settings.invoice_address_required:
|
||
for k, f in self.fields.items():
|
||
f.required = False
|
||
f.widget.is_required = False
|
||
if 'required' in f.widget.attrs:
|
||
del f.widget.attrs['required']
|
||
|
||
def clean(self):
|
||
data = self.cleaned_data
|
||
if not data['name'] and not data['company'] and self.event.settings.invoice_address_required:
|
||
raise ValidationError(_('You need to provide either a company name or your name.'))
|
||
|
||
|
||
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 __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 = self.cartpos = kwargs.pop('cartpos', None)
|
||
orderpos = self.orderpos = kwargs.pop('orderpos', None)
|
||
item = cartpos.item if cartpos else orderpos.item
|
||
questions = list(item.questions.all())
|
||
event = kwargs.pop('event')
|
||
|
||
super().__init__(*args, **kwargs)
|
||
|
||
if item.admission and event.settings.attendee_names_asked:
|
||
self.fields['attendee_name'] = forms.CharField(
|
||
max_length=255, required=event.settings.attendee_names_required,
|
||
label=_('Attendee name'),
|
||
initial=(cartpos.attendee_name if cartpos else orderpos.attendee_name)
|
||
)
|
||
if item.admission and event.settings.attendee_emails_asked:
|
||
self.fields['attendee_email'] = forms.EmailField(
|
||
required=event.settings.attendee_emails_required,
|
||
label=_('Attendee email'),
|
||
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email)
|
||
)
|
||
|
||
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 cartpos else orderpos.answers.all())
|
||
if a.question_id == q.id
|
||
]
|
||
if answers:
|
||
initial = answers[0]
|
||
else:
|
||
initial = None
|
||
if q.type == Question.TYPE_BOOLEAN:
|
||
if q.required:
|
||
# For some reason, django-bootstrap3 does not set the required attribute
|
||
# itself.
|
||
widget = forms.CheckboxInput(attrs={'required': 'required'})
|
||
else:
|
||
widget = forms.CheckboxInput()
|
||
|
||
if initial:
|
||
initialbool = (initial.answer == "True")
|
||
else:
|
||
initialbool = False
|
||
|
||
field = forms.BooleanField(
|
||
label=q.question, required=q.required,
|
||
initial=initialbool, widget=widget
|
||
)
|
||
elif q.type == Question.TYPE_NUMBER:
|
||
field = forms.DecimalField(
|
||
label=q.question, required=q.required,
|
||
initial=initial.answer if initial else None,
|
||
min_value=Decimal('0.00')
|
||
)
|
||
elif q.type == Question.TYPE_STRING:
|
||
field = forms.CharField(
|
||
label=q.question, required=q.required,
|
||
initial=initial.answer if initial else None,
|
||
)
|
||
elif q.type == Question.TYPE_TEXT:
|
||
field = forms.CharField(
|
||
label=q.question, required=q.required,
|
||
widget=forms.Textarea,
|
||
initial=initial.answer if initial else None,
|
||
)
|
||
elif q.type == Question.TYPE_CHOICE:
|
||
field = forms.ModelChoiceField(
|
||
queryset=q.options.all(),
|
||
label=q.question, required=q.required,
|
||
widget=forms.RadioSelect,
|
||
initial=initial.options.first() if initial else None,
|
||
)
|
||
elif q.type == Question.TYPE_CHOICE_MULTIPLE:
|
||
field = forms.ModelMultipleChoiceField(
|
||
queryset=q.options.all(),
|
||
label=q.question, required=q.required,
|
||
widget=forms.CheckboxSelectMultiple,
|
||
initial=initial.options.all() if initial else None,
|
||
)
|
||
field.question = q
|
||
if answers:
|
||
# Cache the answer object for later use
|
||
field.answer = answers[0]
|
||
self.fields['question_%s' % q.id] = field
|
||
|
||
|
||
# The following will get totally different once Django 1.11 is integrated
|
||
class AddOnVariationSelectInput(RadioChoiceInput):
|
||
|
||
def __init__(self, name, value, attrs, choice, index):
|
||
super().__init__(name, value, attrs, choice, index)
|
||
self.description = force_text(choice[2])
|
||
|
||
def render(self, name=None, value=None, attrs=None):
|
||
if self.id_for_label:
|
||
label_for = format_html(' for="{}"', self.id_for_label)
|
||
else:
|
||
label_for = ''
|
||
attrs = dict(self.attrs, **attrs) if attrs else self.attrs
|
||
if self.description:
|
||
return format_html(
|
||
'<label{}>{} {}</label> <span class="fa fa-info-circle toggle-variation-description"></span>'
|
||
'<div class="variation-description addon-variation-description">{}</div>',
|
||
label_for, self.tag(attrs), self.choice_label,
|
||
rich_text(str(self.description))
|
||
)
|
||
else:
|
||
return format_html(
|
||
'<label{}>{} {}</label>',
|
||
label_for, self.tag(attrs), self.choice_label,
|
||
)
|
||
|
||
|
||
class AddOnVariationSelectRenderer(RadioFieldRenderer):
|
||
choice_input_class = AddOnVariationSelectInput
|
||
|
||
def render(self):
|
||
id_ = self.attrs.get('id')
|
||
output = []
|
||
for i, choice in enumerate(self.choices):
|
||
w = self.choice_input_class(self.name, self.value, self.attrs.copy(), choice, i)
|
||
output.append(format_html(self.inner_html, choice_value=force_text(w), sub_widgets=''))
|
||
return format_html(
|
||
self.outer_html,
|
||
id_attr=format_html(' id="{}"', id_) if id_ else '',
|
||
content=mark_safe('\n'.join(output)),
|
||
)
|
||
|
||
|
||
class AddOnVariationSelect(forms.RadioSelect):
|
||
renderer = AddOnVariationSelectRenderer
|
||
|
||
|
||
class AddOnVariationField(forms.ChoiceField):
|
||
|
||
def valid_value(self, value):
|
||
text_value = force_text(value)
|
||
for k, v, d in self.choices:
|
||
if value == k or text_value == force_text(k):
|
||
return True
|
||
return False
|
||
|
||
|
||
class AddOnsForm(forms.Form):
|
||
"""
|
||
This form class is responsible for selecting add-ons to a product in the cart.
|
||
"""
|
||
|
||
def _label(self, event, item_or_variation, avail):
|
||
if isinstance(item_or_variation, ItemVariation):
|
||
variation = item_or_variation
|
||
item = item_or_variation.item
|
||
price = variation.price
|
||
price_net = variation.net_price
|
||
label = variation.value
|
||
else:
|
||
item = item_or_variation
|
||
price = item.default_price
|
||
price_net = item.default_price_net
|
||
label = item.name
|
||
|
||
if not price:
|
||
n = '{name}'.format(
|
||
name=label
|
||
)
|
||
elif not item.tax_rate:
|
||
n = _('{name} (+ {currency} {price})').format(
|
||
name=label, currency=event.currency, price=number_format(price)
|
||
)
|
||
elif event.settings.display_net_prices:
|
||
n = _('{name} (+ {currency} {price} plus {taxes}% taxes)').format(
|
||
name=label, currency=event.currency, price=number_format(price_net),
|
||
taxes=number_format(item.tax_rate)
|
||
)
|
||
else:
|
||
n = _('{name} (+ {currency} {price} incl. {taxes}% taxes)').format(
|
||
name=label, currency=event.currency, price=number_format(price),
|
||
taxes=number_format(item.tax_rate)
|
||
)
|
||
|
||
if avail[0] < 20:
|
||
n += ' – {}'.format(_('SOLD OUT'))
|
||
elif avail[0] < 100:
|
||
n += ' – {}'.format(_('Currently unavailable'))
|
||
|
||
return n
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
"""
|
||
Takes additional keyword arguments:
|
||
|
||
:param category: The category to choose from
|
||
:param event: The event this belongs to
|
||
:param initial: The current set of add-ons
|
||
:param quota_cache: A shared dictionary for quota caching
|
||
:param item_cache: A shared dictionary for item/category caching
|
||
"""
|
||
category = kwargs.pop('category')
|
||
event = kwargs.pop('event')
|
||
current_addons = kwargs.pop('initial')
|
||
quota_cache = kwargs.pop('quota_cache')
|
||
item_cache = kwargs.pop('item_cache')
|
||
|
||
super().__init__(*args, **kwargs)
|
||
|
||
if category.pk not in item_cache:
|
||
# Get all items to possibly show
|
||
items = category.items.filter(
|
||
Q(active=True)
|
||
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
|
||
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
|
||
& Q(hide_without_voucher=False)
|
||
).prefetch_related(
|
||
'variations__quotas', # for .availability()
|
||
Prefetch('quotas', queryset=event.quotas.all()),
|
||
Prefetch('variations', to_attr='available_variations',
|
||
queryset=ItemVariation.objects.filter(active=True, quotas__isnull=False).distinct()),
|
||
).annotate(
|
||
quotac=Count('quotas'),
|
||
has_variations=Count('variations')
|
||
).filter(
|
||
quotac__gt=0
|
||
).order_by('category__position', 'category_id', 'position', 'name')
|
||
item_cache[category.pk] = items
|
||
else:
|
||
items = item_cache[category.pk]
|
||
|
||
for i in items:
|
||
if i.has_variations:
|
||
choices = [('', _('no selection'), '')]
|
||
for v in i.available_variations:
|
||
cached_availability = v.check_quotas(_cache=quota_cache)
|
||
choices.append((v.pk, self._label(event, v, cached_availability), v.description))
|
||
|
||
field = AddOnVariationField(
|
||
choices=choices,
|
||
label=i.name,
|
||
required=False,
|
||
widget=AddOnVariationSelect,
|
||
help_text=rich_text(str(i.description)),
|
||
initial=current_addons.get(i.pk),
|
||
)
|
||
else:
|
||
cached_availability = i.check_quotas(_cache=quota_cache)
|
||
field = forms.BooleanField(
|
||
label=self._label(event, i, cached_availability),
|
||
required=False,
|
||
initial=i.pk in current_addons,
|
||
help_text=rich_text(str(i.description)),
|
||
)
|
||
|
||
self.fields['item_%s' % i.pk] = field
|