mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
Allow selecting the same add-on multiple times (#1717)
This commit is contained in:
@@ -2,24 +2,13 @@ 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
|
||||
|
||||
|
||||
@@ -126,230 +115,3 @@ class AddOnVariationField(forms.ChoiceField):
|
||||
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))
|
||||
|
||||
Reference in New Issue
Block a user