'
+ return html
+
+
class BulkEditMixin:
def __init__(self, *args, **kwargs):
diff --git a/src/pretix/control/forms/subevents.py b/src/pretix/control/forms/subevents.py
index 236ec69c3..f2fe4dbaa 100644
--- a/src/pretix/control/forms/subevents.py
+++ b/src/pretix/control/forms/subevents.py
@@ -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
diff --git a/src/pretix/control/templates/pretixcontrol/button_group_radio.html b/src/pretix/control/templates/pretixcontrol/button_group_radio.html
new file mode 100644
index 000000000..f7de2dcc9
--- /dev/null
+++ b/src/pretix/control/templates/pretixcontrol/button_group_radio.html
@@ -0,0 +1,6 @@
+{% with id=widget.attrs.id %}
{% for group, options, index in widget.optgroups %}
+ {% for option in options %}
+ {% include option.template_name with widget=option %}
+ {% endfor %}
+{% endfor %}
+
{% endwith %}
\ No newline at end of file
diff --git a/src/pretix/control/templates/pretixcontrol/button_group_radio_option.html b/src/pretix/control/templates/pretixcontrol/button_group_radio_option.html
new file mode 100644
index 000000000..85a8c350f
--- /dev/null
+++ b/src/pretix/control/templates/pretixcontrol/button_group_radio_option.html
@@ -0,0 +1,2 @@
+
diff --git a/src/pretix/control/templates/pretixcontrol/item/include_variations.html b/src/pretix/control/templates/pretixcontrol/item/include_variations.html
index 4d538dcaa..319a8b25c 100644
--- a/src/pretix/control/templates/pretixcontrol/item/include_variations.html
+++ b/src/pretix/control/templates/pretixcontrol/item/include_variations.html
@@ -95,8 +95,8 @@
{% 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" %}
diff --git a/src/pretix/control/templates/pretixcontrol/item/index.html b/src/pretix/control/templates/pretixcontrol/item/index.html
index 6594fcd9d..4927c32d9 100644
--- a/src/pretix/control/templates/pretixcontrol/item/index.html
+++ b/src/pretix/control/templates/pretixcontrol/item/index.html
@@ -152,27 +152,28 @@
{% for v in formsets.values %}
- {% if item.cached_availability.0 == 100 %}
+ {% if item.cached_availability.0 == 100 and not item.current_unavailability_reason %}
{% if max_times > 1 %}
{% if item.order_max == 1 %}
@@ -404,7 +404,7 @@
{% endif %}
{% 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 %}
diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py
index d567b398d..7af403ba5 100644
--- a/src/pretix/presale/views/event.py
+++ b/src/pretix/presale/views/event.py
@@ -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
diff --git a/src/pretix/presale/views/widget.py b/src/pretix/presale/views/widget.py
index 3804e8537..0f463c34a 100644
--- a/src/pretix/presale/views/widget.py
+++ b/src/pretix/presale/views/widget.py
@@ -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)
]
diff --git a/src/pretix/settings.py b/src/pretix/settings.py
index 823d34558..29e14be83 100644
--- a/src/pretix/settings.py
+++ b/src/pretix/settings.py
@@ -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',
diff --git a/src/pretix/static/pretixbase/scss/_theme.scss b/src/pretix/static/pretixbase/scss/_theme.scss
index 7768948f3..882fd7de3 100644
--- a/src/pretix/static/pretixbase/scss/_theme.scss
+++ b/src/pretix/static/pretixbase/scss/_theme.scss
@@ -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;
diff --git a/src/pretix/static/pretixcontrol/scss/main.scss b/src/pretix/static/pretixcontrol/scss/main.scss
index b6d6f8ac5..8e587ccc6 100644
--- a/src/pretix/static/pretixcontrol/scss/main.scss
+++ b/src/pretix/static/pretixcontrol/scss/main.scss
@@ -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";
diff --git a/src/pretix/static/pretixpresale/js/widget/widget.js b/src/pretix/static/pretixpresale/js/widget/widget.js
index c1a91f235..e3928988f 100644
--- a/src/pretix/static/pretixpresale/js/widget/widget.js
+++ b/src/pretix/static/pretixpresale/js/widget/widget.js
@@ -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: ('