from itertools import chain
from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Count, Prefetch
from django.utils.encoding import force_str
from django.utils.formats import number_format
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from pretix.base.forms.questions import (
BaseInvoiceAddressForm, BaseQuestionsForm,
)
from pretix.base.models import ItemVariation, Quota
from pretix.base.models.tax import TAXED_ZERO
from pretix.base.services.cart import CartError, error_messages
from pretix.base.signals import validate_cart_addons
from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.rich_text import rich_text
from pretix.base.validators import EmailBanlistValidator
from pretix.helpers.templatetags.thumb import thumb
from pretix.presale.signals import contact_form_fields
class ContactForm(forms.Form):
required_css_class = 'required'
email = forms.EmailField(label=_('E-mail'),
validators=[EmailBanlistValidator()],
widget=forms.EmailInput(attrs={'autocomplete': 'section-contact email'})
)
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
self.request = kwargs.pop('request')
self.all_optional = kwargs.pop('all_optional', False)
super().__init__(*args, **kwargs)
if self.event.settings.order_email_asked_twice:
self.fields['email_repeat'] = forms.EmailField(
label=_('E-mail address (repeated)'),
help_text=_('Please enter the same email address again to make sure you typed it correctly.'),
)
if not self.request.session.get('iframe_session', False):
# There is a browser quirk in Chrome that leads to incorrect initial scrolling in iframes if there
# is an autofocus field. Who would have thought… See e.g. here:
# https://floatboxjs.com/forum/topic.php?post=8440&usebb_sid=2e116486a9ec6b7070e045aea8cded5b#post8440
self.fields['email'].widget.attrs['autofocus'] = 'autofocus'
self.fields['email'].help_text = self.event.settings.checkout_email_helptext
responses = contact_form_fields.send(self.event, request=self.request)
for r, response in responses:
for key, value in response.items():
# We need to be this explicit, since OrderedDict.update does not retain ordering
self.fields[key] = value
if self.all_optional:
for k, v in self.fields.items():
v.required = False
v.widget.is_required = False
def clean(self):
if self.event.settings.order_email_asked_twice and self.cleaned_data.get('email') and self.cleaned_data.get('email_repeat'):
if self.cleaned_data.get('email').lower() != self.cleaned_data.get('email_repeat').lower():
raise ValidationError(_('Please enter the same email address twice.'))
class InvoiceAddressForm(BaseInvoiceAddressForm):
required_css_class = 'required'
vat_warning = True
class InvoiceNameForm(InvoiceAddressForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for f in list(self.fields.keys()):
if f != 'name_parts':
del self.fields[f]
class QuestionsForm(BaseQuestionsForm):
"""
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.
"""
required_css_class = 'required'
class AddOnRadioSelect(forms.RadioSelect):
option_template_name = 'pretixpresale/forms/addon_choice_option.html'
def optgroups(self, name, value, attrs=None):
attrs = attrs or {}
groups = []
has_selected = False
for index, (option_value, option_label, option_desc) in enumerate(chain(self.choices)):
if option_value is None:
option_value = ''
if isinstance(option_label, (list, tuple)):
raise TypeError('Choice groups are not supported here')
group_name = None
subgroup = []
groups.append((group_name, subgroup, index))
selected = (
force_str(option_value) in value and
(has_selected is False or self.allow_multiple_selected)
)
if selected is True and has_selected is False:
has_selected = True
attrs['description'] = option_desc
subgroup.append(self.create_option(
name, option_value, option_label, selected, index,
subindex=None, attrs=attrs,
))
return groups
class AddOnVariationField(forms.ChoiceField):
def valid_value(self, value):
text_value = force_str(value)
for k, v, d in self.choices:
if value == k or text_value == force_str(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, override_price=None, initial=False):
if isinstance(item_or_variation, ItemVariation):
variation = item_or_variation
item = item_or_variation.item
price = variation.price
label = variation.value
else:
item = item_or_variation
price = item.default_price
label = item.name
if override_price:
price = override_price
if self.price_included:
price = TAXED_ZERO
else:
price = item.tax(price)
if not price.gross:
n = '{name}'.format(
name=label
)
elif not price.rate:
n = _('{name} (+ {price})').format(
name=label, price=money_filter(price.gross, event.currency)
)
elif event.settings.display_net_prices:
n = _('{name} (+ {price} plus {taxes}% {taxname})').format(
name=label, price=money_filter(price.net, event.currency),
taxes=number_format(price.rate), taxname=price.name
)
else:
n = _('{name} (+ {price} incl. {taxes}% {taxname})').format(
name=label, price=money_filter(price.gross, event.currency),
taxes=number_format(price.rate), taxname=price.name
)
if not initial:
if avail[0] < Quota.AVAILABILITY_RESERVED:
n += ' – {}'.format(_('SOLD OUT'))
elif avail[0] < Quota.AVAILABILITY_OK:
n += ' – {}'.format(_('Currently unavailable'))
else:
if avail[1] is not None and item.do_show_quota_left:
n += ' – {}'.format(_('%(num)s currently available') % {'num': avail[1]})
if not isinstance(item_or_variation, ItemVariation) and item.picture:
n = escape(n)
n += '
'
n += ''.format(
item.picture.url, escape(escape(item.name)), item.id
)
n += '
'.format(
thumb(item.picture, '60x60^'),
escape(item.name)
)
n += ''
n = mark_safe(n)
return n
def __init__(self, *args, **kwargs):
"""
Takes additional keyword arguments:
:param iao: The ItemAddOn object
:param event: The event this belongs to
:param subevent: The event the parent cart position 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
"""
self.iao = kwargs.pop('iao')
category = self.iao.addon_category
self.event = kwargs.pop('event')
subevent = kwargs.pop('subevent')
current_addons = kwargs.pop('initial')
quota_cache = kwargs.pop('quota_cache')
item_cache = kwargs.pop('item_cache')
self.price_included = kwargs.pop('price_included')
self.sales_channel = kwargs.pop('sales_channel')
self.base_position = kwargs.pop('base_position')
super().__init__(*args, **kwargs)
if subevent:
item_price_override = subevent.item_price_overrides
var_price_override = subevent.var_price_overrides
else:
item_price_override = {}
var_price_override = {}
ckey = '{}-{}'.format(subevent.pk if subevent else 0, category.pk)
if ckey not in item_cache:
# Get all items to possibly show
items = category.items.filter_available(
channel=self.sales_channel,
allow_addons=True
).select_related('tax_rule').prefetch_related(
Prefetch('quotas',
to_attr='_subevent_quotas',
queryset=self.event.quotas.filter(subevent=subevent)),
Prefetch('variations', to_attr='available_variations',
queryset=ItemVariation.objects.filter(active=True, quotas__isnull=False).prefetch_related(
Prefetch('quotas',
to_attr='_subevent_quotas',
queryset=self.event.quotas.filter(subevent=subevent))
).distinct()),
'event'
).annotate(
quotac=Count('quotas'),
has_variations=Count('variations')
).filter(
quotac__gt=0
).order_by('category__position', 'category_id', 'position', 'name')
item_cache[ckey] = items
else:
items = item_cache[ckey]
self.vars_cache = {}
for i in items:
if i.hidden_if_available:
q = i.hidden_if_available.availability(_cache=quota_cache)
if q[0] == Quota.AVAILABILITY_OK:
continue
if i.has_variations:
choices = [('', _('no selection'), '')]
for v in i.available_variations:
cached_availability = v.check_quotas(subevent=subevent, _cache=quota_cache)
if self.event.settings.hide_sold_out and cached_availability[0] < Quota.AVAILABILITY_RESERVED:
continue
if v._subevent_quotas:
self.vars_cache[v.pk] = v
choices.append(
(v.pk,
self._label(self.event, v, cached_availability,
override_price=var_price_override.get(v.pk),
initial=current_addons.get(i.pk) == v.pk),
v.description)
)
n = i.name
if i.picture:
n = escape(n)
n += '
'
n += ''.format(
i.picture.url, escape(escape(i.name)), i.id
)
n += '
'.format(
thumb(i.picture, '60x60^'),
escape(i.name)
)
n += ''
n = mark_safe(n)
field = AddOnVariationField(
choices=choices,
label=n,
required=False,
widget=AddOnRadioSelect,
help_text=rich_text(str(i.description)),
initial=current_addons.get(i.pk),
)
field.item = i
if len(choices) > 1:
self.fields['item_%s' % i.pk] = field
else:
if not i._subevent_quotas:
continue
cached_availability = i.check_quotas(subevent=subevent, _cache=quota_cache)
if self.event.settings.hide_sold_out and cached_availability[0] < Quota.AVAILABILITY_RESERVED:
continue
field = forms.BooleanField(
label=self._label(self.event, i, cached_availability,
override_price=item_price_override.get(i.pk),
initial=i.pk in current_addons),
required=False,
initial=i.pk in current_addons,
help_text=rich_text(str(i.description)),
)
field.item = i
self.fields['item_%s' % i.pk] = field
def clean(self):
data = super().clean()
selected = set()
for k, v in data.items():
if v is True:
selected.add((self.fields[k].item, None))
elif v:
selected.add((self.fields[k].item, self.vars_cache.get(int(v))))
if len(selected) > self.iao.max_count:
# TODO: Proper pluralization
raise ValidationError(
_(error_messages['addon_max_count']),
'addon_max_count',
{
'base': str(self.iao.base_item.name),
'max': self.iao.max_count,
'cat': str(self.iao.addon_category.name),
}
)
elif len(selected) < self.iao.min_count:
# TODO: Proper pluralization
raise ValidationError(
_(error_messages['addon_min_count']),
'addon_min_count',
{
'base': str(self.iao.base_item.name),
'min': self.iao.min_count,
'cat': str(self.iao.addon_category.name),
}
)
try:
validate_cart_addons.send(sender=self.event, addons=selected, base_position=self.base_position,
iao=self.iao)
except CartError as e:
raise ValidationError(str(e))