mirror of
https://github.com/pretix/pretix.git
synced 2025-12-06 21:42:49 +00:00
Improve UI to configure unavailable items handling (Z#23131828) (#3739)
* start impl of unavailability modes ui * add db migration * use new widget for more fields * improve contrast * use new widget for hide_without_voucher field * improved wording * rebase migration * undo changes to require_membership_hidden * code formatting * move unavail_reason logic around * enforce consistent state of hide_without_voucher / require_voucher * annotate unavailability info in get_grouped_items * remove MSIE6 compat * add unavailability reasons to widget * remove test output * Apply suggestions from code review text improvements Co-authored-by: Richard Schreiber <schreiber@rami.io> * add css fix for jumping items due to tooltip * dynamically retrieve unavailability reason message * widget: simplify logic conditions * add available_{from,until}_mode to api and api docs * rebase migration * rebase migration * add unavailable_*_mode to ItemVariation * add available_*_mode to API docs for items * fix wrong reference * fix test cases * add available_*_mode to item variation form * apply unavailability modes to subevents and variations (presale) * /o\ * apply unavailability modes to subevents and variations (widget) * display unavailability mode in subevent product settings * fix widget test * fix api item tests * copy available_*_mode when copying an item * Apply suggestions from code review Co-authored-by: Raphael Michel <michel@rami.io> * Add unavail mode indicator to bulk create and edit forms --------- Co-authored-by: Richard Schreiber <schreiber@rami.io> Co-authored-by: Raphael Michel <michel@rami.io>
This commit is contained in:
@@ -45,8 +45,16 @@ sales_channels list of strings Sales channels
|
||||
available.
|
||||
available_from datetime The first date time at which this variation can be bought
|
||||
(or ``null``).
|
||||
available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
|
||||
if unavailable due to the available_from setting.
|
||||
If ``info``, the variation is visible, but can't be purchased,
|
||||
and a note explaining the unavailability is displayed.
|
||||
available_until datetime The last date time at which this variation can be bought
|
||||
(or ``null``).
|
||||
available_until_mode string If ``hide`` (the default), this variation is hidden in the shop
|
||||
if unavailable due to the available_until setting.
|
||||
If ``info``, the variation is visible, but can't be purchased,
|
||||
and a note explaining the unavailability is displayed.
|
||||
hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
|
||||
redemption process, but not in the normal shop
|
||||
frontend.
|
||||
@@ -105,7 +113,9 @@ Endpoints
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": {
|
||||
"en": "Test2"
|
||||
@@ -131,7 +141,9 @@ Endpoints
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": {},
|
||||
"position": 1,
|
||||
@@ -192,7 +204,9 @@ Endpoints
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 0,
|
||||
@@ -232,7 +246,9 @@ Endpoints
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 0,
|
||||
@@ -263,7 +279,9 @@ Endpoints
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 0,
|
||||
@@ -325,7 +343,9 @@ Endpoints
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"position": 1,
|
||||
|
||||
@@ -50,8 +50,16 @@ sales_channels list of strings Sales channel
|
||||
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
|
||||
available_from datetime The first date time at which this item can be bought
|
||||
(or ``null``).
|
||||
available_from_mode string If ``hide`` (the default), this item is hidden in the shop
|
||||
if unavailable due to the ``available_from`` setting.
|
||||
If ``info``, the item is visible, but can't be purchased,
|
||||
and a note explaining the unavailability is displayed.
|
||||
available_until datetime The last date time at which this item can be bought
|
||||
(or ``null``).
|
||||
available_until_mode string If ``hide`` (the default), this item is hidden in the shop
|
||||
if unavailable due to the ``available_until`` setting.
|
||||
If ``info``, the item is visible, but can't be purchased,
|
||||
and a note explaining the unavailability is displayed.
|
||||
hidden_if_available integer **DEPRECATED** The internal ID of a quota object, or ``null``. If
|
||||
set, this item won't be shown publicly as long as this
|
||||
quota is available.
|
||||
@@ -156,8 +164,16 @@ variations list of objects A list with o
|
||||
available.
|
||||
├ available_from datetime The first date time at which this variation can be bought
|
||||
(or ``null``).
|
||||
├ available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
|
||||
if unavailable due to the ``available_from`` setting.
|
||||
If ``info``, the variation is visible, but can't be purchased,
|
||||
and a note explaining the unavailability is displayed.
|
||||
├ available_until datetime The last date time at which this variation can be bought
|
||||
(or ``null``).
|
||||
├ available_until_mode string If ``hide`` (the default), this variation is hidden in the shop
|
||||
if unavailable due to the ``available_until`` setting.
|
||||
If ``info``, the variation is visible, but can't be purchased,
|
||||
and a note explaining the unavailability is displayed.
|
||||
├ hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
|
||||
redemption process, but not in the normal shop
|
||||
frontend.
|
||||
@@ -279,7 +295,9 @@ Endpoints
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hidden_if_available": null,
|
||||
"hidden_if_item_available": null,
|
||||
"require_voucher": false,
|
||||
@@ -324,7 +342,9 @@ Endpoints
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
@@ -344,7 +364,9 @@ Endpoints
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
@@ -417,7 +439,9 @@ Endpoints
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hidden_if_available": null,
|
||||
"hidden_if_item_available": null,
|
||||
"require_voucher": false,
|
||||
@@ -463,7 +487,9 @@ Endpoints
|
||||
"description": null,
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"meta_data": {},
|
||||
"position": 0
|
||||
@@ -482,7 +508,9 @@ Endpoints
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
@@ -536,7 +564,9 @@ Endpoints
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hidden_if_available": null,
|
||||
"hidden_if_item_available": null,
|
||||
"require_voucher": false,
|
||||
@@ -580,7 +610,9 @@ Endpoints
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
@@ -600,7 +632,9 @@ Endpoints
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
@@ -642,7 +676,9 @@ Endpoints
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hidden_if_available": null,
|
||||
"hidden_if_item_available": null,
|
||||
"require_voucher": false,
|
||||
@@ -687,7 +723,9 @@ Endpoints
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
@@ -707,7 +745,9 @@ Endpoints
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
@@ -780,7 +820,9 @@ Endpoints
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hidden_if_available": null,
|
||||
"hidden_if_item_available": null,
|
||||
"require_voucher": false,
|
||||
@@ -825,7 +867,9 @@ Endpoints
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
@@ -845,7 +889,9 @@ Endpoints
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": false,
|
||||
"description": null,
|
||||
"meta_data": {},
|
||||
|
||||
@@ -61,7 +61,8 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
||||
fields = ('id', 'value', 'active', 'description',
|
||||
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
|
||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
||||
'checkin_attention', 'checkin_text', 'available_from', 'available_until',
|
||||
'checkin_attention', 'checkin_text',
|
||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||
'sales_channels', 'hide_without_voucher', 'meta_data')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -85,7 +86,8 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
|
||||
fields = ('id', 'value', 'active', 'description',
|
||||
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
|
||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
||||
'checkin_attention', 'checkin_text', 'available_from', 'available_until',
|
||||
'checkin_attention', 'checkin_text',
|
||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||
'sales_channels', 'hide_without_voucher', 'meta_data')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -235,7 +237,8 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
model = Item
|
||||
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
|
||||
'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
|
||||
'personalized', 'position', 'picture', 'available_from', 'available_until',
|
||||
'personalized', 'position', 'picture',
|
||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
||||
'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations',
|
||||
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
||||
|
||||
22
src/pretix/base/migrations/0255_item_unavail_modes.py
Normal file
22
src/pretix/base/migrations/0255_item_unavail_modes.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 4.2.4 on 2023-11-22 20:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("pretixbase", "0254_alter_logentry_organizer_link_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="item",
|
||||
name="available_from_mode",
|
||||
field=models.CharField(default="hide", max_length=16),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="item",
|
||||
name="available_until_mode",
|
||||
field=models.CharField(default="hide", max_length=16),
|
||||
)
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 4.2.4 on 2024-01-11 15:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("pretixbase", "0255_item_unavail_modes"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="itemvariation",
|
||||
name="available_from_mode",
|
||||
field=models.CharField(default="hide", max_length=16),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="itemvariation",
|
||||
name="available_until_mode",
|
||||
field=models.CharField(default="hide", max_length=16),
|
||||
),
|
||||
]
|
||||
@@ -263,8 +263,8 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False):
|
||||
# IMPORTANT: If this is updated, also update the ItemVariation query
|
||||
# in models/event.py: EventMixin.annotated()
|
||||
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(Q(available_from__isnull=True) | Q(available_from__lte=now()) | Q(available_from_mode='info'))
|
||||
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()) | Q(available_until_mode='info'))
|
||||
& Q(sales_channels__contains=channel) & Q(require_bundling=False)
|
||||
)
|
||||
if not allow_addons:
|
||||
@@ -374,6 +374,13 @@ class Item(LoggedModel):
|
||||
(VALIDITY_MODE_DYNAMIC, _('Dynamic validity')),
|
||||
)
|
||||
|
||||
UNAVAIL_MODE_HIDDEN = "hide"
|
||||
UNAVAIL_MODE_INFO = "info"
|
||||
UNAVAIL_MODES = (
|
||||
(UNAVAIL_MODE_HIDDEN, _("Hide product if unavailable")),
|
||||
(UNAVAIL_MODE_INFO, _("Show info text if unavailable")),
|
||||
)
|
||||
|
||||
MEDIA_POLICY_REUSE = 'reuse'
|
||||
MEDIA_POLICY_NEW = 'new'
|
||||
MEDIA_POLICY_REUSE_OR_NEW = 'reuse_or_new'
|
||||
@@ -487,11 +494,21 @@ class Item(LoggedModel):
|
||||
null=True, blank=True,
|
||||
help_text=_('This product will not be sold before the given date.')
|
||||
)
|
||||
available_from_mode = models.CharField(
|
||||
choices=UNAVAIL_MODES,
|
||||
default=UNAVAIL_MODE_HIDDEN,
|
||||
max_length=16,
|
||||
)
|
||||
available_until = models.DateTimeField(
|
||||
verbose_name=_("Available until"),
|
||||
null=True, blank=True,
|
||||
help_text=_('This product will not be sold after the given date.')
|
||||
)
|
||||
available_until_mode = models.CharField(
|
||||
choices=UNAVAIL_MODES,
|
||||
default=UNAVAIL_MODE_HIDDEN,
|
||||
max_length=16,
|
||||
)
|
||||
hidden_if_available = models.ForeignKey(
|
||||
'Quota',
|
||||
null=True, blank=True,
|
||||
@@ -703,6 +720,8 @@ class Item(LoggedModel):
|
||||
return str(self.internal_name or self.name)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.hide_without_voucher:
|
||||
self.require_voucher = True
|
||||
super().save(*args, **kwargs)
|
||||
if self.event:
|
||||
self.event.cache.clear()
|
||||
@@ -780,6 +799,24 @@ class Item(LoggedModel):
|
||||
return False
|
||||
return True
|
||||
|
||||
def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]:
|
||||
now_dt = now_dt or now()
|
||||
subevent_item = subevent and subevent.item_overrides.get(self.pk)
|
||||
if not self.active:
|
||||
return 'active'
|
||||
elif self.available_from and self.available_from > now_dt:
|
||||
return 'available_from'
|
||||
elif self.available_until and self.available_until < now_dt:
|
||||
return 'available_until'
|
||||
elif (self.require_voucher or self.hide_without_voucher) and not has_voucher:
|
||||
return 'require_voucher'
|
||||
elif subevent_item and subevent_item.available_from and subevent_item.available_from > now_dt:
|
||||
return 'available_from'
|
||||
elif subevent_item and subevent_item.available_until and subevent_item.available_until < now_dt:
|
||||
return 'available_until'
|
||||
else:
|
||||
return None
|
||||
|
||||
def _get_quotas(self, ignored_quotas=None, subevent=None):
|
||||
check_quotas = set(getattr(
|
||||
self, '_subevent_quotas', # Utilize cache in product list
|
||||
@@ -1078,11 +1115,21 @@ class ItemVariation(models.Model):
|
||||
null=True, blank=True,
|
||||
help_text=_('This variation will not be sold before the given date.')
|
||||
)
|
||||
available_from_mode = models.CharField(
|
||||
choices=Item.UNAVAIL_MODES,
|
||||
default=Item.UNAVAIL_MODE_HIDDEN,
|
||||
max_length=16,
|
||||
)
|
||||
available_until = models.DateTimeField(
|
||||
verbose_name=_("Available until"),
|
||||
null=True, blank=True,
|
||||
help_text=_('This variation will not be sold after the given date.')
|
||||
)
|
||||
available_until_mode = models.CharField(
|
||||
choices=Item.UNAVAIL_MODES,
|
||||
default=Item.UNAVAIL_MODE_HIDDEN,
|
||||
max_length=16,
|
||||
)
|
||||
sales_channels = fields.MultiStringField(
|
||||
verbose_name=_('Sales channels'),
|
||||
default=_all_sales_channels_identifiers,
|
||||
@@ -1260,6 +1307,22 @@ class ItemVariation(models.Model):
|
||||
return False
|
||||
return True
|
||||
|
||||
def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]:
|
||||
now_dt = now_dt or now()
|
||||
subevent_var = subevent and subevent.var_overrides.get(self.pk)
|
||||
if not self.active:
|
||||
return 'active'
|
||||
elif self.available_from and self.available_from > now_dt:
|
||||
return 'available_from'
|
||||
elif self.available_until and self.available_until < now_dt:
|
||||
return 'available_until'
|
||||
elif subevent_var and subevent_var.available_from and subevent_var.available_from > now_dt:
|
||||
return 'available_from'
|
||||
elif subevent_var and subevent_var.available_until and subevent_var.available_until < now_dt:
|
||||
return 'available_until'
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def meta_data(self):
|
||||
data = self.item.meta_data
|
||||
|
||||
@@ -415,3 +415,17 @@ class FontSelect(forms.RadioSelect):
|
||||
class ItemMultipleChoiceField(SafeModelMultipleChoiceField):
|
||||
def label_from_instance(self, obj):
|
||||
return str(obj) if obj.active else mark_safe(f'<strike class="text-muted">{escape(obj)}</strike>')
|
||||
|
||||
|
||||
class ButtonGroupRadioSelect(forms.RadioSelect):
|
||||
template_name = 'pretixcontrol/button_group_radio.html'
|
||||
option_template_name = 'pretixcontrol/button_group_radio_option.html'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.option_icons = kwargs.pop('option_icons')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
|
||||
attrs['icon'] = self.option_icons[value]
|
||||
opt = super().create_option(name, value, label, selected, index, subindex, attrs)
|
||||
return opt
|
||||
|
||||
@@ -64,8 +64,8 @@ from pretix.base.models import (
|
||||
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
|
||||
from pretix.base.signals import item_copy_data
|
||||
from pretix.control.forms import (
|
||||
ItemMultipleChoiceField, SizeValidationMixin, SplitDateTimeField,
|
||||
SplitDateTimePickerWidget,
|
||||
ButtonGroupRadioSelect, ItemMultipleChoiceField, SizeValidationMixin,
|
||||
SplitDateTimeField, SplitDateTimePickerWidget,
|
||||
)
|
||||
from pretix.control.forms.widgets import Select2
|
||||
from pretix.helpers.models import modelcopy
|
||||
@@ -380,7 +380,9 @@ class ItemCreateForm(I18nModelForm):
|
||||
'description',
|
||||
'active',
|
||||
'available_from',
|
||||
'available_from_mode',
|
||||
'available_until',
|
||||
'available_until_mode',
|
||||
'require_voucher',
|
||||
'hide_without_voucher',
|
||||
'allow_cancel',
|
||||
@@ -562,6 +564,34 @@ class ItemUpdateForm(I18nModelForm):
|
||||
)
|
||||
change_decimal_field(self.fields['default_price'], self.event.currency)
|
||||
|
||||
self.fields['available_from_mode'].widget = ButtonGroupRadioSelect(
|
||||
choices=self.fields['available_from_mode'].choices,
|
||||
option_icons={
|
||||
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
|
||||
Item.UNAVAIL_MODE_INFO: 'info'
|
||||
}
|
||||
)
|
||||
|
||||
self.fields['available_until_mode'].widget = ButtonGroupRadioSelect(
|
||||
choices=self.fields['available_until_mode'].choices,
|
||||
option_icons={
|
||||
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
|
||||
Item.UNAVAIL_MODE_INFO: 'info'
|
||||
}
|
||||
)
|
||||
|
||||
self.fields['hide_without_voucher'].widget = ButtonGroupRadioSelect(
|
||||
choices=(
|
||||
(True, _("Hide product if unavailable")),
|
||||
(False, _("Show product with info on why it’s unavailable")),
|
||||
),
|
||||
option_icons={
|
||||
True: 'eye-slash',
|
||||
False: 'info'
|
||||
},
|
||||
attrs={'data-checkbox-dependency': '#id_require_voucher'}
|
||||
)
|
||||
|
||||
if self.instance.hidden_if_available_id:
|
||||
self.fields['hidden_if_available'].queryset = self.event.quotas.all()
|
||||
self.fields['hidden_if_available'].help_text = format_html(
|
||||
@@ -656,6 +686,9 @@ class ItemUpdateForm(I18nModelForm):
|
||||
)
|
||||
)
|
||||
|
||||
if not d.get('require_voucher'):
|
||||
d['hide_without_voucher'] = False
|
||||
|
||||
if d.get('require_membership') and not d.get('require_membership_types'):
|
||||
self.add_error(
|
||||
'require_membership_types',
|
||||
@@ -713,7 +746,9 @@ class ItemUpdateForm(I18nModelForm):
|
||||
'free_price_suggestion',
|
||||
'tax_rule',
|
||||
'available_from',
|
||||
'available_from_mode',
|
||||
'available_until',
|
||||
'available_until_mode',
|
||||
'require_voucher',
|
||||
'require_approval',
|
||||
'hide_without_voucher',
|
||||
@@ -850,6 +885,22 @@ class ItemVariationForm(I18nModelForm):
|
||||
|
||||
self.fields['free_price_suggestion'].widget.attrs['data-display-dependency'] = '#id_free_price'
|
||||
|
||||
self.fields['available_from_mode'].widget = ButtonGroupRadioSelect(
|
||||
choices=self.fields['available_from_mode'].choices,
|
||||
option_icons={
|
||||
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
|
||||
Item.UNAVAIL_MODE_INFO: 'info'
|
||||
}
|
||||
)
|
||||
|
||||
self.fields['available_until_mode'].widget = ButtonGroupRadioSelect(
|
||||
choices=self.fields['available_until_mode'].choices,
|
||||
option_icons={
|
||||
Item.UNAVAIL_MODE_HIDDEN: 'eye-slash',
|
||||
Item.UNAVAIL_MODE_INFO: 'info'
|
||||
}
|
||||
)
|
||||
|
||||
self.meta_fields = []
|
||||
meta_defaults = {}
|
||||
if self.instance.pk:
|
||||
@@ -892,7 +943,9 @@ class ItemVariationForm(I18nModelForm):
|
||||
'checkin_attention',
|
||||
'checkin_text',
|
||||
'available_from',
|
||||
'available_from_mode',
|
||||
'available_until',
|
||||
'available_until_mode',
|
||||
'sales_channels',
|
||||
'hide_without_voucher',
|
||||
]
|
||||
|
||||
@@ -100,6 +100,22 @@ class ControlFieldRenderer(FieldRenderer):
|
||||
return '<div class="{klass}"{attrs}>{html}</div>'.format(klass=self.get_form_group_class(), html=html, attrs=attrs)
|
||||
|
||||
|
||||
class ControlFieldWithVisibilityRenderer(ControlFieldRenderer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['layout'] = 'horizontal'
|
||||
kwargs['horizontal_field_class'] = 'col-md-7'
|
||||
self.visibility_field = kwargs['visibility_field']
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def render_visibility_field(self):
|
||||
return self.visibility_field.as_widget(attrs=self.visibility_field.field.widget.attrs)
|
||||
|
||||
def wrap_field(self, html):
|
||||
html = super().wrap_field(html)
|
||||
html += '<div class="col-md-2 text-right">' + self.render_visibility_field() + '</div>'
|
||||
return html
|
||||
|
||||
|
||||
class BulkEditMixin:
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -260,6 +260,8 @@ class SubEventItemForm(SubEventItemOrVariationFormMixin, forms.ModelForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['price'].widget.attrs['placeholder'] = money_filter(self.item.default_price, self.item.event.currency, hide_currency=True)
|
||||
self.fields['price'].label = str(self.item)
|
||||
self.available_from_mode = self.item.available_from_mode
|
||||
self.available_until_mode = self.item.available_until_mode
|
||||
|
||||
class Meta:
|
||||
model = SubEventItem
|
||||
@@ -287,6 +289,8 @@ class SubEventItemVariationForm(SubEventItemOrVariationFormMixin, forms.ModelFor
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['price'].widget.attrs['placeholder'] = money_filter(self.variation.price, self.item.event.currency, hide_currency=True)
|
||||
self.fields['price'].label = '{} – {}'.format(str(self.item), self.variation.value)
|
||||
self.available_from_mode = self.variation.available_from_mode
|
||||
self.available_until_mode = self.variation.available_until_mode
|
||||
|
||||
class Meta:
|
||||
model = SubEventItemVariation
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{% with id=widget.attrs.id %}<div data-toggle="buttons"{% if id %} id="{{ id }}"{% endif %} class="btn-group btn-group-toggle {{ widget.attrs.class }}">{% for group, options, index in widget.optgroups %}
|
||||
{% for option in options %}
|
||||
{% include option.template_name with widget=option %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>{% endwith %}
|
||||
@@ -0,0 +1,2 @@
|
||||
<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %} class="btn btn-primary-if-active form-field-boundary {% if widget.attrs.checked %} active{% endif %}" title="{{ widget.label }}" data-toggle="tooltip">
|
||||
{% include "django/forms/widgets/input.html" %} <span class="fa fa-{{ widget.attrs.icon }}"></span></label>
|
||||
@@ -95,8 +95,8 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% bootstrap_field form.available_from layout="control" %}
|
||||
{% bootstrap_field form.available_until layout="control" %}
|
||||
{% bootstrap_field form.available_from visibility_field=form.available_from_mode layout="control_with_visibility" %}
|
||||
{% bootstrap_field form.available_until visibility_field=form.available_until_mode layout="control_with_visibility" %}
|
||||
{% bootstrap_field form.sales_channels layout="control" %}
|
||||
{% bootstrap_field form.hide_without_voucher layout="control" %}
|
||||
{% bootstrap_field form.require_approval layout="control" %}
|
||||
|
||||
@@ -152,27 +152,28 @@
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Availability" %}</legend>
|
||||
{% bootstrap_field form.sales_channels layout="control" %}
|
||||
{% bootstrap_field form.available_from layout="control" %}
|
||||
{% bootstrap_field form.available_until layout="control" %}
|
||||
{% bootstrap_field form.max_per_order layout="control" %}
|
||||
{% bootstrap_field form.min_per_order layout="control" %}
|
||||
{% bootstrap_field form.require_voucher layout="control" %}
|
||||
{% bootstrap_field form.hide_without_voucher layout="control" %}
|
||||
{% bootstrap_field form.require_bundling layout="control" %}
|
||||
|
||||
|
||||
{% bootstrap_field form.sales_channels layout="control" horizontal_field_class="col-md-7" %}
|
||||
{% bootstrap_field form.available_from visibility_field=form.available_from_mode layout="control_with_visibility" %}
|
||||
{% bootstrap_field form.available_until visibility_field=form.available_until_mode layout="control_with_visibility" %}
|
||||
{% bootstrap_field form.max_per_order layout="control" horizontal_field_class="col-md-7" %}
|
||||
{% bootstrap_field form.min_per_order layout="control" horizontal_field_class="col-md-7" %}
|
||||
{% bootstrap_field form.require_voucher visibility_field=form.hide_without_voucher layout="control_with_visibility" %}
|
||||
{% bootstrap_field form.require_bundling layout="control" horizontal_field_class="col-md-7" %}
|
||||
{% if form.require_membership %}
|
||||
{% bootstrap_field form.require_membership layout="control" %}
|
||||
<div data-display-dependency="#{{ form.require_membership.id_for_label }}">
|
||||
{% bootstrap_field form.require_membership_types layout="control" %}
|
||||
{% bootstrap_field form.require_membership_hidden layout="control" %}
|
||||
{% bootstrap_field form.require_membership_types layout="control" horizontal_field_class="col-md-7" %}
|
||||
{% bootstrap_field form.require_membership_hidden layout="control" horizontal_field_class="col-md-7" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% bootstrap_field form.allow_cancel layout="control" %}
|
||||
{% bootstrap_field form.allow_waitinglist layout="control" %}
|
||||
{% bootstrap_field form.allow_cancel layout="control" horizontal_field_class="col-md-7" %}
|
||||
{% bootstrap_field form.allow_waitinglist layout="control" horizontal_field_class="col-md-7" %}
|
||||
{% if form.hidden_if_available %}
|
||||
{% bootstrap_field form.hidden_if_available layout="control" %}
|
||||
{% bootstrap_field form.hidden_if_available layout="control" horizontal_field_class="col-md-7" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field form.hidden_if_item_available layout="control" %}
|
||||
{% bootstrap_field form.hidden_if_item_available layout="control" horizontal_field_class="col-md-7" %}
|
||||
</fieldset>
|
||||
{% for v in formsets.values %}
|
||||
<fieldset>
|
||||
|
||||
@@ -503,11 +503,13 @@
|
||||
</div>
|
||||
<div class="form-group subevent-itemvar-group">
|
||||
<div class="col-md-4 col-md-offset-3">
|
||||
<label for="{{ f.rel_available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label><br>
|
||||
<label for="{{ f.rel_available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label>
|
||||
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_from_mode %}<br>
|
||||
{% bootstrap_field f.rel_available_from form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="{{ f.rel_available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label><br>
|
||||
<label for="{{ f.rel_available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label>
|
||||
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_until_mode %}<br>
|
||||
{% bootstrap_field f.rel_available_until form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -167,11 +167,13 @@
|
||||
</div>
|
||||
<div class="form-group subevent-itemvar-group">
|
||||
<div class="col-md-4 col-md-offset-3">
|
||||
<label for="{{ f.available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label><br>
|
||||
<label for="{{ f.available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label>
|
||||
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_from_mode %}<br>
|
||||
{% bootstrap_field f.available_from form_group_class="" layout="bulkedit_inline" %}
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label for="{{ f.available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label><br>
|
||||
<label for="{{ f.available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label>
|
||||
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_until_mode %}<br>
|
||||
{% bootstrap_field f.available_until form_group_class="" layout="bulkedit_inline" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -145,11 +145,13 @@
|
||||
</div>
|
||||
<div class="form-group subevent-itemvar-group">
|
||||
<div class="col-md-4 col-md-offset-3">
|
||||
<label for="{{ f.available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label><br>
|
||||
{% bootstrap_field f.available_from form_group_class="" layout="inline" %}
|
||||
<label for="{{ f.available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label>
|
||||
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_from_mode %}<br>
|
||||
{% bootstrap_field f.available_from form_group_class="foo" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="{{ f.available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label><br>
|
||||
<label for="{{ f.available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label>
|
||||
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_until_mode %}<br>
|
||||
{% bootstrap_field f.available_until form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{% load i18n %}
|
||||
{% if mode == "hide" %}
|
||||
<span class="pull-right text-muted unavail-mode-indicator" data-toggle="tooltip" title="{% trans "Hide product if unavailable" %}. {% if f.variation %}{% trans "You can change this option in the variation settings." %}{% else %}{% trans "You can change this option in the product settings." %}{% endif %}"><span class="fa fa-eye-slash"></span></span>
|
||||
{% else %}
|
||||
<span class="pull-right text-muted unavail-mode-indicator" data-toggle="tooltip" title="{% trans "Show info text if unavailable" %}. {% if f.variation %}{% trans "You can change this option in the variation settings." %}{% else %}{% trans "You can change this option in the product settings." %}{% endif %}"><span class="fa fa-info-circle"></span></span>
|
||||
{% endif %}
|
||||
@@ -1493,6 +1493,8 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
|
||||
messages.info(self.request, _("You disabled this item, but it is still part of a product bundle. "
|
||||
"Your participants won't be able to buy the bundle unless you remove this "
|
||||
"item from it."))
|
||||
if ctx['item'].hide_without_voucher:
|
||||
ctx['item'].require_voucher = True
|
||||
|
||||
ctx['sales_channels'] = get_all_sales_channels()
|
||||
return ctx
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% if avail <= 10 %}
|
||||
|
||||
{% if item.current_unavailability_reason == 'require_voucher' %}
|
||||
<div class="col-md-2 col-sm-3 col-xs-6 availability-box unavailable">
|
||||
<p><small><a href="#voucher">{% trans "Enter a voucher code below to buy this product." %}</a></small></p>
|
||||
</div>
|
||||
{% elif item.current_unavailability_reason == 'available_from' or var.current_unavailability_reason == 'available_from' %}
|
||||
<div class="col-md-2 col-sm-3 col-xs-6 availability-box unavailable">
|
||||
<p><small>{% trans "Not available yet." %}</small></p>
|
||||
</div>
|
||||
{% elif item.current_unavailability_reason == 'available_until' or var.current_unavailability_reason == 'available_until' %}
|
||||
<div class="col-md-2 col-sm-3 col-xs-6 availability-box unavailable">
|
||||
<p><small>{% trans "Not available any more." %}</small></p>
|
||||
</div>
|
||||
{% elif avail <= 10 %}
|
||||
<div class="col-md-2 col-sm-3 col-xs-6 availability-box gone">
|
||||
{% if price != None and not price %}
|
||||
<strong>{% trans "FULLY BOOKED" %}</strong>
|
||||
|
||||
@@ -185,11 +185,7 @@
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% if item.require_voucher %}
|
||||
<div class="col-md-2 col-sm-3 col-xs-6 availability-box unavailable">
|
||||
<p><small><a href="#voucher">{% trans "Enter a voucher code below to buy this ticket." %}</a></small></p>
|
||||
</div>
|
||||
{% elif var.cached_availability.0 == 100 %}
|
||||
{% if var.cached_availability.0 == 100 and not item.current_unavailability_reason and not var.current_unavailability_reason %}
|
||||
<div class="col-md-2 col-sm-3 col-xs-6 availability-box available">
|
||||
{% if var.order_max == 1 %}
|
||||
<label class="btn btn-default btn-checkbox">
|
||||
@@ -338,11 +334,7 @@
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% if item.require_voucher %}
|
||||
<div class="col-md-2 col-sm-3 col-xs-6 availability-box unavailable">
|
||||
<p><small><a href="#voucher">{% trans "Enter a voucher code below to buy this ticket." %}</a></small></p>
|
||||
</div>
|
||||
{% elif item.cached_availability.0 == 100 %}
|
||||
{% if item.cached_availability.0 == 100 and not item.current_unavailability_reason %}
|
||||
<div class="col-md-2 col-sm-3 col-xs-6 availability-box available">
|
||||
{% if item.order_max == 1 %}
|
||||
<label class="btn btn-default btn-checkbox">
|
||||
|
||||
@@ -212,7 +212,7 @@
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% if var.cached_availability.0 == 100 %}
|
||||
{% if var.cached_availability.0 == 100 and not item.current_unavailability_reason %}
|
||||
<div class="col-md-2 col-sm-3 col-xs-6 availability-box available radio-box">
|
||||
{% if max_times > 1 %}
|
||||
{% if var.order_max == 1 %}
|
||||
@@ -251,7 +251,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% include "pretixpresale/event/fragment_availability.html" with price=var.display_price.gross avail=var.cached_availability.0 %}
|
||||
{% include "pretixpresale/event/fragment_availability.html" with price=var.display_price.gross avail=var.cached_availability.0 item=item %}
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
</article>
|
||||
@@ -363,7 +363,7 @@
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% if item.cached_availability.0 == 100 %}
|
||||
{% if item.cached_availability.0 == 100 and not item.current_unavailability_reason %}
|
||||
<div class="col-md-2 col-sm-3 col-xs-6 availability-box available radio-box">
|
||||
{% if max_times > 1 %}
|
||||
{% if item.order_max == 1 %}
|
||||
@@ -404,7 +404,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% include "pretixpresale/event/fragment_availability.html" with price=item.display_price.gross avail=item.cached_availability.0 %}
|
||||
{% include "pretixpresale/event/fragment_availability.html" with price=item.display_price.gross avail=item.cached_availability.0 item=item %}
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
</article>
|
||||
|
||||
@@ -48,6 +48,7 @@ from django.core.exceptions import PermissionDenied
|
||||
from django.db.models import (
|
||||
Count, Exists, IntegerField, OuterRef, Prefetch, Q, Value,
|
||||
)
|
||||
from django.db.models.lookups import Exact
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.utils.decorators import method_decorator
|
||||
@@ -118,8 +119,8 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
|
||||
requires_seat = Value(0, output_field=IntegerField())
|
||||
|
||||
variation_q = (
|
||||
Q(Q(available_from__isnull=True) | Q(available_from__lte=now())) &
|
||||
Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
|
||||
Q(Q(available_from__isnull=True) | Q(available_from__lte=now()) | Q(available_from_mode='info')) &
|
||||
Q(Q(available_until__isnull=True) | Q(available_until__gte=now()) | Q(available_until_mode='info'))
|
||||
)
|
||||
if not voucher or not voucher.show_hidden_items:
|
||||
variation_q &= Q(hide_without_voucher=False)
|
||||
@@ -135,7 +136,9 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
|
||||
queryset=ItemVariation.objects.using(settings.DATABASE_REPLICA).annotate(
|
||||
subevent_disabled=Exists(
|
||||
SubEventItemVariation.objects.filter(
|
||||
Q(disabled=True) | Q(available_from__gt=now()) | Q(available_until__lt=now()),
|
||||
Q(disabled=True)
|
||||
| (Exact(OuterRef('available_from_mode'), 'hide') & Q(available_from__gt=now()))
|
||||
| (Exact(OuterRef('available_until_mode'), 'hide') & Q(available_until__lt=now())),
|
||||
variation_id=OuterRef('pk'),
|
||||
subevent=subevent,
|
||||
)
|
||||
@@ -205,7 +208,9 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
|
||||
has_variations=Count('variations'),
|
||||
subevent_disabled=Exists(
|
||||
SubEventItem.objects.filter(
|
||||
Q(disabled=True) | Q(available_from__gt=now()) | Q(available_until__lt=now()),
|
||||
Q(disabled=True)
|
||||
| (Exact(OuterRef('available_from_mode'), 'hide') & Q(available_from__gt=now()))
|
||||
| (Exact(OuterRef('available_until_mode'), 'hide') & Q(available_until__lt=now())),
|
||||
item_id=OuterRef('pk'),
|
||||
subevent=subevent,
|
||||
)
|
||||
@@ -301,6 +306,8 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
|
||||
item._remove = True
|
||||
continue
|
||||
|
||||
item.current_unavailability_reason = item.unavailability_reason(has_voucher=voucher, subevent=subevent)
|
||||
|
||||
item.description = str(item.description)
|
||||
for recv, resp in item_description.send(sender=event, item=item, variation=None, subevent=subevent):
|
||||
if resp:
|
||||
@@ -415,6 +422,8 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
|
||||
if not display_add_to_cart:
|
||||
display_add_to_cart = not item.requires_seat and var.order_max > 0
|
||||
|
||||
var.current_unavailability_reason = var.unavailability_reason(has_voucher=voucher, subevent=subevent)
|
||||
|
||||
item.original_price = (
|
||||
item.tax(item.original_price, currency=event.currency, include_bundled=True,
|
||||
base_price_is='net' if event.settings.display_net_prices else 'gross') # backwards-compat
|
||||
|
||||
@@ -272,7 +272,7 @@ class WidgetAPIProductList(EventListMixin, View):
|
||||
'picture_fullsize': get_picture(self.request.event, item.picture) if item.picture else None,
|
||||
'description': str(rich_text(item.description, safelinks=False)) if item.description else None,
|
||||
'has_variations': item.has_variations,
|
||||
'require_voucher': item.require_voucher,
|
||||
'current_unavailability_reason': item.current_unavailability_reason,
|
||||
'order_min': item.min_per_order,
|
||||
'order_max': item.order_max if not item.has_variations else None,
|
||||
'price': price_dict(item, item.display_price) if not item.has_variations else None,
|
||||
@@ -317,6 +317,7 @@ class WidgetAPIProductList(EventListMixin, View):
|
||||
var.cached_availability[0],
|
||||
var.cached_availability[1] if item.do_show_quota_left else None
|
||||
],
|
||||
'current_unavailability_reason': var.current_unavailability_reason,
|
||||
} for var in item.available_variations if (not variation_filter or var.id in variation_filter)
|
||||
]
|
||||
|
||||
|
||||
@@ -686,6 +686,7 @@ BOOTSTRAP3 = {
|
||||
'default': 'pretix.base.forms.renderers.FieldRenderer',
|
||||
'inline': 'pretix.base.forms.renderers.InlineFieldRenderer',
|
||||
'control': 'pretix.control.forms.renderers.ControlFieldRenderer',
|
||||
'control_with_visibility': 'pretix.control.forms.renderers.ControlFieldWithVisibilityRenderer',
|
||||
'bulkedit': 'pretix.control.forms.renderers.BulkEditFieldRenderer',
|
||||
'bulkedit_inline': 'pretix.control.forms.renderers.InlineBulkEditFieldRenderer',
|
||||
'checkout': 'pretix.presale.forms.renderers.CheckoutFieldRenderer',
|
||||
|
||||
@@ -100,6 +100,16 @@ input[type=number]::-webkit-outer-spin-button {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.btn-primary-if-active {
|
||||
@include button-variant($btn-default-color, $btn-default-bg, $btn-default-border);
|
||||
box-shadow: 0px 0px 0px 1px #cccccc inset;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.active {
|
||||
@include button-variant($btn-primary-color, $btn-primary-bg, $btn-primary-border);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-invisible {
|
||||
background: transparent;
|
||||
border: transparent;
|
||||
|
||||
@@ -908,6 +908,10 @@ tbody th {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.btn-group .btn:not(:first-child) {
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
@import "../../pretixbase/scss/_rtl.scss";
|
||||
@import "../../bootstrap/scss/_rtl.scss";
|
||||
@import "_rtl.scss";
|
||||
|
||||
@@ -31,7 +31,10 @@ var strings = {
|
||||
'tax_incl_mixed': django.pgettext('widget', 'incl. taxes'),
|
||||
'tax_plus_mixed': django.pgettext('widget', 'plus taxes'),
|
||||
'quota_left': django.pgettext('widget', 'currently available: %s'),
|
||||
'voucher_required': django.pgettext('widget', 'Only available with a voucher'),
|
||||
'unavailable_require_voucher': django.pgettext('widget', 'Only available with a voucher'),
|
||||
'unavailable_available_from': django.pgettext('widget', 'Not yet available'),
|
||||
'unavailable_available_until': django.pgettext('widget', 'Not available anymore'),
|
||||
'unavailable_active': django.pgettext('widget', 'Currently not available'),
|
||||
'order_min': django.pgettext('widget', 'minimum amount to order: %s'),
|
||||
'exit': django.pgettext('widget', 'Close ticket shop'),
|
||||
'loading_error': django.pgettext('widget', 'The ticket shop could not be loaded.'),
|
||||
@@ -118,17 +121,8 @@ var getISOWeeks = function (y) {
|
||||
|
||||
/* HTTP API Call helpers */
|
||||
var api = {
|
||||
'_getXHR': function () {
|
||||
try {
|
||||
return new window.XMLHttpRequest();
|
||||
} catch (e) {
|
||||
// explicitly bubble up the exception if not found
|
||||
return new window.ActiveXObject('Microsoft.XMLHTTP');
|
||||
}
|
||||
},
|
||||
|
||||
'_getJSON': function (endpoint, callback, err_callback) {
|
||||
var xhr = api._getXHR();
|
||||
var xhr = new window.XMLHttpRequest();
|
||||
xhr.open("GET", endpoint, true);
|
||||
xhr.onload = function (e) {
|
||||
if (xhr.readyState === 4) {
|
||||
@@ -160,7 +154,7 @@ var api = {
|
||||
return encodeURIComponent(el.name) + '=' + encodeURIComponent(el.value);
|
||||
}).join('&');
|
||||
|
||||
var xhr = api._getXHR();
|
||||
var xhr = new window.XMLHttpRequest();
|
||||
xhr.open("POST", endpoint, true);
|
||||
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||
xhr.onload = function (e) {
|
||||
@@ -199,22 +193,27 @@ var widget_id = makeid(16);
|
||||
/* Vue Components */
|
||||
Vue.component('availbox', {
|
||||
template: ('<div class="pretix-widget-availability-box">'
|
||||
+ '<div class="pretix-widget-availability-unavailable" v-if="require_voucher">'
|
||||
+ '<small><a @click.prevent.stop="focus_voucher_field" role="button">' + strings.voucher_required + '</a></small>'
|
||||
+ '<div class="pretix-widget-availability-unavailable"'
|
||||
+ ' v-if="item.current_unavailability_reason === \'require_voucher\'">'
|
||||
+ '<small><a @click.prevent.stop="focus_voucher_field" role="button">{{unavailability_reason_message}}</a></small>'
|
||||
+ '</div>'
|
||||
+ '<div class="pretix-widget-availability-unavailable"'
|
||||
+ ' v-if="!require_voucher && avail[0] < 100 && avail[0] > 10">'
|
||||
+ ' v-else-if="unavailability_reason_message">'
|
||||
+ '<small>{{unavailability_reason_message}}</small>'
|
||||
+ '</div>'
|
||||
+ '<div class="pretix-widget-availability-unavailable"'
|
||||
+ ' v-else-if="avail[0] < 100 && avail[0] > 10">'
|
||||
+ strings.reserved
|
||||
+ '</div>'
|
||||
+ '<div class="pretix-widget-availability-gone" '
|
||||
+ ' v-if="!require_voucher && avail[0] <= 10">'
|
||||
+ ' v-else-if="avail[0] <= 10">'
|
||||
+ strings.sold_out
|
||||
+ '</div>'
|
||||
+ '<div class="pretix-widget-waiting-list-link"'
|
||||
+ ' v-if="waiting_list_show">'
|
||||
+ '<a :href="waiting_list_url" target="_blank" @click="$root.open_link_in_frame">' + strings.waiting_list + '</a>'
|
||||
+ '</div>'
|
||||
+ '<div class="pretix-widget-availability-available" v-if="!require_voucher && avail[0] === 100">'
|
||||
+ '<div class="pretix-widget-availability-available" v-if="!unavailability_reason_message && avail[0] === 100">'
|
||||
+ '<label class="pretix-widget-item-count-single-label pretix-widget-btn-checkbox" v-if="order_max === 1 && $root.single_item_select == \'button\'">'
|
||||
+ '<input type="checkbox" value="1" :checked="!!amount_selected" @change="amount_selected = $event.target.checked" :name="input_name"'
|
||||
+ ' v-bind:aria-label="label_select_item"'
|
||||
@@ -255,8 +254,12 @@ Vue.component('availbox', {
|
||||
'pretix-widget-item-count-group': !this.$root.use_native_spinners
|
||||
}
|
||||
},
|
||||
require_voucher: function () {
|
||||
return this.item.require_voucher && !this.$root.voucher_code
|
||||
unavailability_reason_message: function () {
|
||||
var reason = this.item.current_unavailability_reason || this.variation.current_unavailability_reason;
|
||||
if (reason) {
|
||||
return strings["unavailable_" + reason] || reason;
|
||||
}
|
||||
return "";
|
||||
},
|
||||
amount_selected: {
|
||||
cache: false,
|
||||
|
||||
@@ -270,6 +270,8 @@ TEST_ITEM_RES = {
|
||||
"picture": None,
|
||||
"available_from": None,
|
||||
"available_until": None,
|
||||
"available_from_mode": "hide",
|
||||
"available_until_mode": "hide",
|
||||
"require_bundling": False,
|
||||
"require_voucher": False,
|
||||
"hide_without_voucher": False,
|
||||
@@ -401,6 +403,8 @@ def test_item_detail_variations(token_client, organizer, event, team, item):
|
||||
"sales_channels": list(get_all_sales_channels().keys()),
|
||||
"available_from": None,
|
||||
"available_until": None,
|
||||
"available_from_mode": "hide",
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": False,
|
||||
"original_price": None,
|
||||
"meta_data": {}
|
||||
@@ -1330,6 +1334,8 @@ TEST_VARIATIONS_RES = {
|
||||
"sales_channels": list(get_all_sales_channels().keys()),
|
||||
"available_from": None,
|
||||
"available_until": None,
|
||||
"available_from_mode": "hide",
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": False,
|
||||
"original_price": None,
|
||||
"free_price_suggestion": None,
|
||||
@@ -1353,6 +1359,8 @@ TEST_VARIATIONS_UPDATE = {
|
||||
"sales_channels": ["web"],
|
||||
"available_from": None,
|
||||
"available_until": None,
|
||||
"available_from_mode": "hide",
|
||||
"available_until_mode": "hide",
|
||||
"hide_without_voucher": False,
|
||||
"original_price": None,
|
||||
"free_price_suggestion": None,
|
||||
|
||||
@@ -186,7 +186,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"require_voucher": False,
|
||||
"current_unavailability_reason": None,
|
||||
"order_min": None,
|
||||
"max_price": None,
|
||||
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
|
||||
@@ -207,7 +207,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
"order_max": 4
|
||||
},
|
||||
{
|
||||
"require_voucher": False,
|
||||
"current_unavailability_reason": None,
|
||||
"order_min": None,
|
||||
"max_price": "14.00",
|
||||
"price": None,
|
||||
@@ -231,7 +231,8 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
"rate": "19.00", "includes_mixed_tax_rate": False},
|
||||
"description": None,
|
||||
"avail": [100, None],
|
||||
"order_max": 2
|
||||
"order_max": 2,
|
||||
"current_unavailability_reason": None,
|
||||
},
|
||||
{
|
||||
"value": "Blue",
|
||||
@@ -243,7 +244,8 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
"rate": "19.00", "includes_mixed_tax_rate": False},
|
||||
"description": None,
|
||||
"avail": [100, None],
|
||||
"order_max": 2
|
||||
"order_max": 2,
|
||||
"current_unavailability_reason": None,
|
||||
}
|
||||
],
|
||||
"id": self.shirt.pk,
|
||||
@@ -273,7 +275,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"require_voucher": False,
|
||||
"current_unavailability_reason": None,
|
||||
"order_min": None,
|
||||
"max_price": None,
|
||||
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00",
|
||||
@@ -322,7 +324,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"require_voucher": False,
|
||||
"current_unavailability_reason": None,
|
||||
"order_min": None,
|
||||
"max_price": "14.00",
|
||||
"price": None,
|
||||
@@ -346,7 +348,8 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
"rate": "19.00", "includes_mixed_tax_rate": False},
|
||||
"description": None,
|
||||
"avail": [100, None],
|
||||
"order_max": 2
|
||||
"order_max": 2,
|
||||
"current_unavailability_reason": None,
|
||||
}
|
||||
],
|
||||
"id": self.shirt.pk,
|
||||
@@ -389,7 +392,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"require_voucher": False,
|
||||
"current_unavailability_reason": None,
|
||||
"order_min": None,
|
||||
"max_price": None,
|
||||
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
|
||||
@@ -457,7 +460,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
'has_variations': 2,
|
||||
"allow_waitinglist": True,
|
||||
"mandatory_priced_addons": False,
|
||||
'require_voucher': False,
|
||||
'current_unavailability_reason': None,
|
||||
'order_min': None,
|
||||
'order_max': None,
|
||||
'price': None,
|
||||
@@ -490,7 +493,8 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
'name': '',
|
||||
'includes_mixed_tax_rate': False
|
||||
},
|
||||
'avail': [100, None]
|
||||
'avail': [100, None],
|
||||
'current_unavailability_reason': None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user