Files
pretix_cgo/src/pretix/presale/forms/checkout.py
Raphael Michel af23d6e4bf Upgrade to Django 3.0 and other dependencies (#1568)
* Upgrade Django to 3.0 and other dependencies to recent versions

* Fix otp version contsraint

* Remove six dependency

* Resolve some warnings

* Fix failing tests

* Update django-countries

* Resolve all RemovedInDjango31Warnings in test suite

* Run isort

* Fix import

* Update PostgreSQL version on travis
2020-03-23 15:02:20 +01:00

356 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 += '<br>'
n += '<a href="{}" class="productpicture" data-title="{}" data-lightbox={}>'.format(
item.picture.url, escape(escape(item.name)), item.id
)
n += '<img src="{}" alt="{}">'.format(
thumb(item.picture, '60x60^'),
escape(item.name)
)
n += '</a>'
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 += '<br>'
n += '<a href="{}" class="productpicture" data-title="{}" data-lightbox="{}">'.format(
i.picture.url, escape(escape(i.name)), i.id
)
n += '<img src="{}" alt="{}">'.format(
thumb(i.picture, '60x60^'),
escape(i.name)
)
n += '</a>'
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))