mirror of
https://github.com/pretix/pretix.git
synced 2026-08-27 13:24:41 +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:
co-authored by
Richard Schreiber
Raphael Michel
parent
43facd1e43
commit
22f91f7aa2
@@ -45,8 +45,16 @@ sales_channels list of strings Sales channels
|
|||||||
available.
|
available.
|
||||||
available_from datetime The first date time at which this variation can be bought
|
available_from datetime The first date time at which this variation can be bought
|
||||||
(or ``null``).
|
(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
|
available_until datetime The last date time at which this variation can be bought
|
||||||
(or ``null``).
|
(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
|
hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
|
||||||
redemption process, but not in the normal shop
|
redemption process, but not in the normal shop
|
||||||
frontend.
|
frontend.
|
||||||
@@ -105,7 +113,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": {
|
"description": {
|
||||||
"en": "Test2"
|
"en": "Test2"
|
||||||
@@ -131,7 +141,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": {},
|
"description": {},
|
||||||
"position": 1,
|
"position": 1,
|
||||||
@@ -192,7 +204,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"position": 0,
|
"position": 0,
|
||||||
@@ -232,7 +246,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"position": 0,
|
"position": 0,
|
||||||
@@ -263,7 +279,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"position": 0,
|
"position": 0,
|
||||||
@@ -325,7 +343,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"position": 1,
|
"position": 1,
|
||||||
|
|||||||
@@ -50,8 +50,16 @@ sales_channels list of strings Sales channel
|
|||||||
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
|
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
|
||||||
available_from datetime The first date time at which this item can be bought
|
available_from datetime The first date time at which this item can be bought
|
||||||
(or ``null``).
|
(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
|
available_until datetime The last date time at which this item can be bought
|
||||||
(or ``null``).
|
(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
|
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
|
set, this item won't be shown publicly as long as this
|
||||||
quota is available.
|
quota is available.
|
||||||
@@ -156,8 +164,16 @@ variations list of objects A list with o
|
|||||||
available.
|
available.
|
||||||
├ available_from datetime The first date time at which this variation can be bought
|
├ available_from datetime The first date time at which this variation can be bought
|
||||||
(or ``null``).
|
(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
|
├ available_until datetime The last date time at which this variation can be bought
|
||||||
(or ``null``).
|
(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
|
├ hide_without_voucher boolean If ``true``, this variation is only shown during the voucher
|
||||||
redemption process, but not in the normal shop
|
redemption process, but not in the normal shop
|
||||||
frontend.
|
frontend.
|
||||||
@@ -279,7 +295,9 @@ Endpoints
|
|||||||
"position": 0,
|
"position": 0,
|
||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hidden_if_available": null,
|
"hidden_if_available": null,
|
||||||
"hidden_if_item_available": null,
|
"hidden_if_item_available": null,
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
@@ -324,7 +342,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
@@ -344,7 +364,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
@@ -417,7 +439,9 @@ Endpoints
|
|||||||
"position": 0,
|
"position": 0,
|
||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hidden_if_available": null,
|
"hidden_if_available": null,
|
||||||
"hidden_if_item_available": null,
|
"hidden_if_item_available": null,
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
@@ -463,7 +487,9 @@ Endpoints
|
|||||||
"description": null,
|
"description": null,
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
"position": 0
|
"position": 0
|
||||||
@@ -482,7 +508,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
@@ -536,7 +564,9 @@ Endpoints
|
|||||||
"position": 0,
|
"position": 0,
|
||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hidden_if_available": null,
|
"hidden_if_available": null,
|
||||||
"hidden_if_item_available": null,
|
"hidden_if_item_available": null,
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
@@ -580,7 +610,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
@@ -600,7 +632,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
@@ -642,7 +676,9 @@ Endpoints
|
|||||||
"position": 0,
|
"position": 0,
|
||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hidden_if_available": null,
|
"hidden_if_available": null,
|
||||||
"hidden_if_item_available": null,
|
"hidden_if_item_available": null,
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
@@ -687,7 +723,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
@@ -707,7 +745,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
@@ -780,7 +820,9 @@ Endpoints
|
|||||||
"position": 0,
|
"position": 0,
|
||||||
"picture": null,
|
"picture": null,
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hidden_if_available": null,
|
"hidden_if_available": null,
|
||||||
"hidden_if_item_available": null,
|
"hidden_if_item_available": null,
|
||||||
"require_voucher": false,
|
"require_voucher": false,
|
||||||
@@ -825,7 +867,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
@@ -845,7 +889,9 @@ Endpoints
|
|||||||
"require_membership_types": [],
|
"require_membership_types": [],
|
||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": null,
|
"available_from": null,
|
||||||
|
"available_from_mode": "hide",
|
||||||
"available_until": null,
|
"available_until": null,
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": false,
|
"hide_without_voucher": false,
|
||||||
"description": null,
|
"description": null,
|
||||||
"meta_data": {},
|
"meta_data": {},
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('id', 'value', 'active', 'description',
|
fields = ('id', 'value', 'active', 'description',
|
||||||
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
|
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
|
||||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
'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')
|
'sales_channels', 'hide_without_voucher', 'meta_data')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -85,7 +86,8 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('id', 'value', 'active', 'description',
|
fields = ('id', 'value', 'active', 'description',
|
||||||
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
|
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
|
||||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
'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')
|
'sales_channels', 'hide_without_voucher', 'meta_data')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -235,7 +237,8 @@ class ItemSerializer(I18nAwareModelSerializer):
|
|||||||
model = Item
|
model = Item
|
||||||
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
|
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
|
||||||
'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
|
'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',
|
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
||||||
'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations',
|
'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations',
|
||||||
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
||||||
|
|||||||
@@ -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
|
# IMPORTANT: If this is updated, also update the ItemVariation query
|
||||||
# in models/event.py: EventMixin.annotated()
|
# in models/event.py: EventMixin.annotated()
|
||||||
Q(active=True)
|
Q(active=True)
|
||||||
& Q(Q(available_from__isnull=True) | Q(available_from__lte=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(Q(available_until__isnull=True) | Q(available_until__gte=now()) | Q(available_until_mode='info'))
|
||||||
& Q(sales_channels__contains=channel) & Q(require_bundling=False)
|
& Q(sales_channels__contains=channel) & Q(require_bundling=False)
|
||||||
)
|
)
|
||||||
if not allow_addons:
|
if not allow_addons:
|
||||||
@@ -374,6 +374,13 @@ class Item(LoggedModel):
|
|||||||
(VALIDITY_MODE_DYNAMIC, _('Dynamic validity')),
|
(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_REUSE = 'reuse'
|
||||||
MEDIA_POLICY_NEW = 'new'
|
MEDIA_POLICY_NEW = 'new'
|
||||||
MEDIA_POLICY_REUSE_OR_NEW = 'reuse_or_new'
|
MEDIA_POLICY_REUSE_OR_NEW = 'reuse_or_new'
|
||||||
@@ -487,11 +494,21 @@ class Item(LoggedModel):
|
|||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
help_text=_('This product will not be sold before the given date.')
|
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(
|
available_until = models.DateTimeField(
|
||||||
verbose_name=_("Available until"),
|
verbose_name=_("Available until"),
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
help_text=_('This product will not be sold after the given date.')
|
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(
|
hidden_if_available = models.ForeignKey(
|
||||||
'Quota',
|
'Quota',
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
@@ -703,6 +720,8 @@ class Item(LoggedModel):
|
|||||||
return str(self.internal_name or self.name)
|
return str(self.internal_name or self.name)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
if self.hide_without_voucher:
|
||||||
|
self.require_voucher = True
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
if self.event:
|
if self.event:
|
||||||
self.event.cache.clear()
|
self.event.cache.clear()
|
||||||
@@ -780,6 +799,24 @@ class Item(LoggedModel):
|
|||||||
return False
|
return False
|
||||||
return True
|
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):
|
def _get_quotas(self, ignored_quotas=None, subevent=None):
|
||||||
check_quotas = set(getattr(
|
check_quotas = set(getattr(
|
||||||
self, '_subevent_quotas', # Utilize cache in product list
|
self, '_subevent_quotas', # Utilize cache in product list
|
||||||
@@ -1078,11 +1115,21 @@ class ItemVariation(models.Model):
|
|||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
help_text=_('This variation will not be sold before the given date.')
|
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(
|
available_until = models.DateTimeField(
|
||||||
verbose_name=_("Available until"),
|
verbose_name=_("Available until"),
|
||||||
null=True, blank=True,
|
null=True, blank=True,
|
||||||
help_text=_('This variation will not be sold after the given date.')
|
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(
|
sales_channels = fields.MultiStringField(
|
||||||
verbose_name=_('Sales channels'),
|
verbose_name=_('Sales channels'),
|
||||||
default=_all_sales_channels_identifiers,
|
default=_all_sales_channels_identifiers,
|
||||||
@@ -1260,6 +1307,22 @@ class ItemVariation(models.Model):
|
|||||||
return False
|
return False
|
||||||
return True
|
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
|
@property
|
||||||
def meta_data(self):
|
def meta_data(self):
|
||||||
data = self.item.meta_data
|
data = self.item.meta_data
|
||||||
|
|||||||
@@ -415,3 +415,17 @@ class FontSelect(forms.RadioSelect):
|
|||||||
class ItemMultipleChoiceField(SafeModelMultipleChoiceField):
|
class ItemMultipleChoiceField(SafeModelMultipleChoiceField):
|
||||||
def label_from_instance(self, obj):
|
def label_from_instance(self, obj):
|
||||||
return str(obj) if obj.active else mark_safe(f'<strike class="text-muted">{escape(obj)}</strike>')
|
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.models.items import ItemAddOn, ItemBundle, ItemMetaValue
|
||||||
from pretix.base.signals import item_copy_data
|
from pretix.base.signals import item_copy_data
|
||||||
from pretix.control.forms import (
|
from pretix.control.forms import (
|
||||||
ItemMultipleChoiceField, SizeValidationMixin, SplitDateTimeField,
|
ButtonGroupRadioSelect, ItemMultipleChoiceField, SizeValidationMixin,
|
||||||
SplitDateTimePickerWidget,
|
SplitDateTimeField, SplitDateTimePickerWidget,
|
||||||
)
|
)
|
||||||
from pretix.control.forms.widgets import Select2
|
from pretix.control.forms.widgets import Select2
|
||||||
from pretix.helpers.models import modelcopy
|
from pretix.helpers.models import modelcopy
|
||||||
@@ -380,7 +380,9 @@ class ItemCreateForm(I18nModelForm):
|
|||||||
'description',
|
'description',
|
||||||
'active',
|
'active',
|
||||||
'available_from',
|
'available_from',
|
||||||
|
'available_from_mode',
|
||||||
'available_until',
|
'available_until',
|
||||||
|
'available_until_mode',
|
||||||
'require_voucher',
|
'require_voucher',
|
||||||
'hide_without_voucher',
|
'hide_without_voucher',
|
||||||
'allow_cancel',
|
'allow_cancel',
|
||||||
@@ -562,6 +564,34 @@ class ItemUpdateForm(I18nModelForm):
|
|||||||
)
|
)
|
||||||
change_decimal_field(self.fields['default_price'], self.event.currency)
|
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:
|
if self.instance.hidden_if_available_id:
|
||||||
self.fields['hidden_if_available'].queryset = self.event.quotas.all()
|
self.fields['hidden_if_available'].queryset = self.event.quotas.all()
|
||||||
self.fields['hidden_if_available'].help_text = format_html(
|
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'):
|
if d.get('require_membership') and not d.get('require_membership_types'):
|
||||||
self.add_error(
|
self.add_error(
|
||||||
'require_membership_types',
|
'require_membership_types',
|
||||||
@@ -713,7 +746,9 @@ class ItemUpdateForm(I18nModelForm):
|
|||||||
'free_price_suggestion',
|
'free_price_suggestion',
|
||||||
'tax_rule',
|
'tax_rule',
|
||||||
'available_from',
|
'available_from',
|
||||||
|
'available_from_mode',
|
||||||
'available_until',
|
'available_until',
|
||||||
|
'available_until_mode',
|
||||||
'require_voucher',
|
'require_voucher',
|
||||||
'require_approval',
|
'require_approval',
|
||||||
'hide_without_voucher',
|
'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['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 = []
|
self.meta_fields = []
|
||||||
meta_defaults = {}
|
meta_defaults = {}
|
||||||
if self.instance.pk:
|
if self.instance.pk:
|
||||||
@@ -892,7 +943,9 @@ class ItemVariationForm(I18nModelForm):
|
|||||||
'checkin_attention',
|
'checkin_attention',
|
||||||
'checkin_text',
|
'checkin_text',
|
||||||
'available_from',
|
'available_from',
|
||||||
|
'available_from_mode',
|
||||||
'available_until',
|
'available_until',
|
||||||
|
'available_until_mode',
|
||||||
'sales_channels',
|
'sales_channels',
|
||||||
'hide_without_voucher',
|
'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)
|
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:
|
class BulkEditMixin:
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -260,6 +260,8 @@ class SubEventItemForm(SubEventItemOrVariationFormMixin, forms.ModelForm):
|
|||||||
super().__init__(*args, **kwargs)
|
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'].widget.attrs['placeholder'] = money_filter(self.item.default_price, self.item.event.currency, hide_currency=True)
|
||||||
self.fields['price'].label = str(self.item)
|
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:
|
class Meta:
|
||||||
model = SubEventItem
|
model = SubEventItem
|
||||||
@@ -287,6 +289,8 @@ class SubEventItemVariationForm(SubEventItemOrVariationFormMixin, forms.ModelFor
|
|||||||
super().__init__(*args, **kwargs)
|
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'].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.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:
|
class Meta:
|
||||||
model = SubEventItemVariation
|
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>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% bootstrap_field form.available_from layout="control" %}
|
{% bootstrap_field form.available_from visibility_field=form.available_from_mode layout="control_with_visibility" %}
|
||||||
{% bootstrap_field form.available_until layout="control" %}
|
{% 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.sales_channels layout="control" %}
|
||||||
{% bootstrap_field form.hide_without_voucher layout="control" %}
|
{% bootstrap_field form.hide_without_voucher layout="control" %}
|
||||||
{% bootstrap_field form.require_approval layout="control" %}
|
{% bootstrap_field form.require_approval layout="control" %}
|
||||||
|
|||||||
@@ -152,27 +152,28 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Availability" %}</legend>
|
<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.sales_channels layout="control" horizontal_field_class="col-md-7" %}
|
||||||
{% bootstrap_field form.max_per_order layout="control" %}
|
{% bootstrap_field form.available_from visibility_field=form.available_from_mode layout="control_with_visibility" %}
|
||||||
{% bootstrap_field form.min_per_order layout="control" %}
|
{% bootstrap_field form.available_until visibility_field=form.available_until_mode layout="control_with_visibility" %}
|
||||||
{% bootstrap_field form.require_voucher layout="control" %}
|
{% bootstrap_field form.max_per_order layout="control" horizontal_field_class="col-md-7" %}
|
||||||
{% bootstrap_field form.hide_without_voucher layout="control" %}
|
{% bootstrap_field form.min_per_order layout="control" horizontal_field_class="col-md-7" %}
|
||||||
{% bootstrap_field form.require_bundling layout="control" %}
|
{% 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 %}
|
{% if form.require_membership %}
|
||||||
{% bootstrap_field form.require_membership layout="control" %}
|
{% bootstrap_field form.require_membership layout="control" %}
|
||||||
<div data-display-dependency="#{{ form.require_membership.id_for_label }}">
|
<div data-display-dependency="#{{ form.require_membership.id_for_label }}">
|
||||||
{% bootstrap_field form.require_membership_types layout="control" %}
|
{% bootstrap_field form.require_membership_types layout="control" horizontal_field_class="col-md-7" %}
|
||||||
{% bootstrap_field form.require_membership_hidden layout="control" %}
|
{% bootstrap_field form.require_membership_hidden layout="control" horizontal_field_class="col-md-7" %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% bootstrap_field form.allow_cancel layout="control" %}
|
{% bootstrap_field form.allow_cancel layout="control" horizontal_field_class="col-md-7" %}
|
||||||
{% bootstrap_field form.allow_waitinglist layout="control" %}
|
{% bootstrap_field form.allow_waitinglist layout="control" horizontal_field_class="col-md-7" %}
|
||||||
{% if form.hidden_if_available %}
|
{% 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 %}
|
{% 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>
|
</fieldset>
|
||||||
{% for v in formsets.values %}
|
{% for v in formsets.values %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|||||||
@@ -503,11 +503,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group subevent-itemvar-group">
|
<div class="form-group subevent-itemvar-group">
|
||||||
<div class="col-md-4 col-md-offset-3">
|
<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" %}
|
{% bootstrap_field f.rel_available_from form_group_class="" layout="inline" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<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" %}
|
{% bootstrap_field f.rel_available_until form_group_class="" layout="inline" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -167,11 +167,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group subevent-itemvar-group">
|
<div class="form-group subevent-itemvar-group">
|
||||||
<div class="col-md-4 col-md-offset-3">
|
<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" %}
|
{% bootstrap_field f.available_from form_group_class="" layout="bulkedit_inline" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-5">
|
<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" %}
|
{% bootstrap_field f.available_until form_group_class="" layout="bulkedit_inline" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -145,11 +145,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group subevent-itemvar-group">
|
<div class="form-group subevent-itemvar-group">
|
||||||
<div class="col-md-4 col-md-offset-3">
|
<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>
|
||||||
{% bootstrap_field f.available_from form_group_class="" layout="inline" %}
|
{% 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>
|
||||||
<div class="col-md-4">
|
<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" %}
|
{% bootstrap_field f.available_until form_group_class="" layout="inline" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+6
@@ -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. "
|
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 "
|
"Your participants won't be able to buy the bundle unless you remove this "
|
||||||
"item from it."))
|
"item from it."))
|
||||||
|
if ctx['item'].hide_without_voucher:
|
||||||
|
ctx['item'].require_voucher = True
|
||||||
|
|
||||||
ctx['sales_channels'] = get_all_sales_channels()
|
ctx['sales_channels'] = get_all_sales_channels()
|
||||||
return ctx
|
return ctx
|
||||||
|
|||||||
@@ -1,6 +1,19 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load eventurl %}
|
{% 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">
|
<div class="col-md-2 col-sm-3 col-xs-6 availability-box gone">
|
||||||
{% if price != None and not price %}
|
{% if price != None and not price %}
|
||||||
<strong>{% trans "FULLY BOOKED" %}</strong>
|
<strong>{% trans "FULLY BOOKED" %}</strong>
|
||||||
|
|||||||
@@ -185,11 +185,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{% if item.require_voucher %}
|
{% 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 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 %}
|
|
||||||
<div class="col-md-2 col-sm-3 col-xs-6 availability-box available">
|
<div class="col-md-2 col-sm-3 col-xs-6 availability-box available">
|
||||||
{% if var.order_max == 1 %}
|
{% if var.order_max == 1 %}
|
||||||
<label class="btn btn-default btn-checkbox">
|
<label class="btn btn-default btn-checkbox">
|
||||||
@@ -338,11 +334,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{% if item.require_voucher %}
|
{% 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 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 %}
|
|
||||||
<div class="col-md-2 col-sm-3 col-xs-6 availability-box available">
|
<div class="col-md-2 col-sm-3 col-xs-6 availability-box available">
|
||||||
{% if item.order_max == 1 %}
|
{% if item.order_max == 1 %}
|
||||||
<label class="btn btn-default btn-checkbox">
|
<label class="btn btn-default btn-checkbox">
|
||||||
|
|||||||
@@ -212,7 +212,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
<div class="col-md-2 col-sm-3 col-xs-6 availability-box available radio-box">
|
||||||
{% if max_times > 1 %}
|
{% if max_times > 1 %}
|
||||||
{% if var.order_max == 1 %}
|
{% if var.order_max == 1 %}
|
||||||
@@ -251,7 +251,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% 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 %}
|
{% endif %}
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
</article>
|
</article>
|
||||||
@@ -363,7 +363,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
<div class="col-md-2 col-sm-3 col-xs-6 availability-box available radio-box">
|
||||||
{% if max_times > 1 %}
|
{% if max_times > 1 %}
|
||||||
{% if item.order_max == 1 %}
|
{% if item.order_max == 1 %}
|
||||||
@@ -404,7 +404,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% 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 %}
|
{% endif %}
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ from django.core.exceptions import PermissionDenied
|
|||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Count, Exists, IntegerField, OuterRef, Prefetch, Q, Value,
|
Count, Exists, IntegerField, OuterRef, Prefetch, Q, Value,
|
||||||
)
|
)
|
||||||
|
from django.db.models.lookups import Exact
|
||||||
from django.http import Http404, HttpResponse
|
from django.http import Http404, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
from django.utils.decorators import method_decorator
|
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())
|
requires_seat = Value(0, output_field=IntegerField())
|
||||||
|
|
||||||
variation_q = (
|
variation_q = (
|
||||||
Q(Q(available_from__isnull=True) | Q(available_from__lte=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(Q(available_until__isnull=True) | Q(available_until__gte=now()) | Q(available_until_mode='info'))
|
||||||
)
|
)
|
||||||
if not voucher or not voucher.show_hidden_items:
|
if not voucher or not voucher.show_hidden_items:
|
||||||
variation_q &= Q(hide_without_voucher=False)
|
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(
|
queryset=ItemVariation.objects.using(settings.DATABASE_REPLICA).annotate(
|
||||||
subevent_disabled=Exists(
|
subevent_disabled=Exists(
|
||||||
SubEventItemVariation.objects.filter(
|
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'),
|
variation_id=OuterRef('pk'),
|
||||||
subevent=subevent,
|
subevent=subevent,
|
||||||
)
|
)
|
||||||
@@ -205,7 +208,9 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
|
|||||||
has_variations=Count('variations'),
|
has_variations=Count('variations'),
|
||||||
subevent_disabled=Exists(
|
subevent_disabled=Exists(
|
||||||
SubEventItem.objects.filter(
|
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'),
|
item_id=OuterRef('pk'),
|
||||||
subevent=subevent,
|
subevent=subevent,
|
||||||
)
|
)
|
||||||
@@ -301,6 +306,8 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
|
|||||||
item._remove = True
|
item._remove = True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
item.current_unavailability_reason = item.unavailability_reason(has_voucher=voucher, subevent=subevent)
|
||||||
|
|
||||||
item.description = str(item.description)
|
item.description = str(item.description)
|
||||||
for recv, resp in item_description.send(sender=event, item=item, variation=None, subevent=subevent):
|
for recv, resp in item_description.send(sender=event, item=item, variation=None, subevent=subevent):
|
||||||
if resp:
|
if resp:
|
||||||
@@ -415,6 +422,8 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
|
|||||||
if not display_add_to_cart:
|
if not display_add_to_cart:
|
||||||
display_add_to_cart = not item.requires_seat and var.order_max > 0
|
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.original_price = (
|
||||||
item.tax(item.original_price, currency=event.currency, include_bundled=True,
|
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
|
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,
|
'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,
|
'description': str(rich_text(item.description, safelinks=False)) if item.description else None,
|
||||||
'has_variations': item.has_variations,
|
'has_variations': item.has_variations,
|
||||||
'require_voucher': item.require_voucher,
|
'current_unavailability_reason': item.current_unavailability_reason,
|
||||||
'order_min': item.min_per_order,
|
'order_min': item.min_per_order,
|
||||||
'order_max': item.order_max if not item.has_variations else None,
|
'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,
|
'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[0],
|
||||||
var.cached_availability[1] if item.do_show_quota_left else None
|
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)
|
} 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',
|
'default': 'pretix.base.forms.renderers.FieldRenderer',
|
||||||
'inline': 'pretix.base.forms.renderers.InlineFieldRenderer',
|
'inline': 'pretix.base.forms.renderers.InlineFieldRenderer',
|
||||||
'control': 'pretix.control.forms.renderers.ControlFieldRenderer',
|
'control': 'pretix.control.forms.renderers.ControlFieldRenderer',
|
||||||
|
'control_with_visibility': 'pretix.control.forms.renderers.ControlFieldWithVisibilityRenderer',
|
||||||
'bulkedit': 'pretix.control.forms.renderers.BulkEditFieldRenderer',
|
'bulkedit': 'pretix.control.forms.renderers.BulkEditFieldRenderer',
|
||||||
'bulkedit_inline': 'pretix.control.forms.renderers.InlineBulkEditFieldRenderer',
|
'bulkedit_inline': 'pretix.control.forms.renderers.InlineBulkEditFieldRenderer',
|
||||||
'checkout': 'pretix.presale.forms.renderers.CheckoutFieldRenderer',
|
'checkout': 'pretix.presale.forms.renderers.CheckoutFieldRenderer',
|
||||||
|
|||||||
@@ -100,6 +100,16 @@ input[type=number]::-webkit-outer-spin-button {
|
|||||||
outline: 0;
|
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 {
|
.btn-invisible {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: transparent;
|
border: transparent;
|
||||||
|
|||||||
@@ -908,6 +908,10 @@ tbody th {
|
|||||||
display: block !important;
|
display: block !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-group .btn:not(:first-child) {
|
||||||
|
margin-left: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
@import "../../pretixbase/scss/_rtl.scss";
|
@import "../../pretixbase/scss/_rtl.scss";
|
||||||
@import "../../bootstrap/scss/_rtl.scss";
|
@import "../../bootstrap/scss/_rtl.scss";
|
||||||
@import "_rtl.scss";
|
@import "_rtl.scss";
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ var strings = {
|
|||||||
'tax_incl_mixed': django.pgettext('widget', 'incl. taxes'),
|
'tax_incl_mixed': django.pgettext('widget', 'incl. taxes'),
|
||||||
'tax_plus_mixed': django.pgettext('widget', 'plus taxes'),
|
'tax_plus_mixed': django.pgettext('widget', 'plus taxes'),
|
||||||
'quota_left': django.pgettext('widget', 'currently available: %s'),
|
'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'),
|
'order_min': django.pgettext('widget', 'minimum amount to order: %s'),
|
||||||
'exit': django.pgettext('widget', 'Close ticket shop'),
|
'exit': django.pgettext('widget', 'Close ticket shop'),
|
||||||
'loading_error': django.pgettext('widget', 'The ticket shop could not be loaded.'),
|
'loading_error': django.pgettext('widget', 'The ticket shop could not be loaded.'),
|
||||||
@@ -118,17 +121,8 @@ var getISOWeeks = function (y) {
|
|||||||
|
|
||||||
/* HTTP API Call helpers */
|
/* HTTP API Call helpers */
|
||||||
var api = {
|
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) {
|
'_getJSON': function (endpoint, callback, err_callback) {
|
||||||
var xhr = api._getXHR();
|
var xhr = new window.XMLHttpRequest();
|
||||||
xhr.open("GET", endpoint, true);
|
xhr.open("GET", endpoint, true);
|
||||||
xhr.onload = function (e) {
|
xhr.onload = function (e) {
|
||||||
if (xhr.readyState === 4) {
|
if (xhr.readyState === 4) {
|
||||||
@@ -160,7 +154,7 @@ var api = {
|
|||||||
return encodeURIComponent(el.name) + '=' + encodeURIComponent(el.value);
|
return encodeURIComponent(el.name) + '=' + encodeURIComponent(el.value);
|
||||||
}).join('&');
|
}).join('&');
|
||||||
|
|
||||||
var xhr = api._getXHR();
|
var xhr = new window.XMLHttpRequest();
|
||||||
xhr.open("POST", endpoint, true);
|
xhr.open("POST", endpoint, true);
|
||||||
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||||
xhr.onload = function (e) {
|
xhr.onload = function (e) {
|
||||||
@@ -199,22 +193,27 @@ var widget_id = makeid(16);
|
|||||||
/* Vue Components */
|
/* Vue Components */
|
||||||
Vue.component('availbox', {
|
Vue.component('availbox', {
|
||||||
template: ('<div class="pretix-widget-availability-box">'
|
template: ('<div class="pretix-widget-availability-box">'
|
||||||
+ '<div class="pretix-widget-availability-unavailable" v-if="require_voucher">'
|
+ '<div class="pretix-widget-availability-unavailable"'
|
||||||
+ '<small><a @click.prevent.stop="focus_voucher_field" role="button">' + strings.voucher_required + '</a></small>'
|
+ ' 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>'
|
||||||
+ '<div class="pretix-widget-availability-unavailable"'
|
+ '<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
|
+ strings.reserved
|
||||||
+ '</div>'
|
+ '</div>'
|
||||||
+ '<div class="pretix-widget-availability-gone" '
|
+ '<div class="pretix-widget-availability-gone" '
|
||||||
+ ' v-if="!require_voucher && avail[0] <= 10">'
|
+ ' v-else-if="avail[0] <= 10">'
|
||||||
+ strings.sold_out
|
+ strings.sold_out
|
||||||
+ '</div>'
|
+ '</div>'
|
||||||
+ '<div class="pretix-widget-waiting-list-link"'
|
+ '<div class="pretix-widget-waiting-list-link"'
|
||||||
+ ' v-if="waiting_list_show">'
|
+ ' v-if="waiting_list_show">'
|
||||||
+ '<a :href="waiting_list_url" target="_blank" @click="$root.open_link_in_frame">' + strings.waiting_list + '</a>'
|
+ '<a :href="waiting_list_url" target="_blank" @click="$root.open_link_in_frame">' + strings.waiting_list + '</a>'
|
||||||
+ '</div>'
|
+ '</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\'">'
|
+ '<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"'
|
+ '<input type="checkbox" value="1" :checked="!!amount_selected" @change="amount_selected = $event.target.checked" :name="input_name"'
|
||||||
+ ' v-bind:aria-label="label_select_item"'
|
+ ' v-bind:aria-label="label_select_item"'
|
||||||
@@ -255,8 +254,12 @@ Vue.component('availbox', {
|
|||||||
'pretix-widget-item-count-group': !this.$root.use_native_spinners
|
'pretix-widget-item-count-group': !this.$root.use_native_spinners
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
require_voucher: function () {
|
unavailability_reason_message: function () {
|
||||||
return this.item.require_voucher && !this.$root.voucher_code
|
var reason = this.item.current_unavailability_reason || this.variation.current_unavailability_reason;
|
||||||
|
if (reason) {
|
||||||
|
return strings["unavailable_" + reason] || reason;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
},
|
},
|
||||||
amount_selected: {
|
amount_selected: {
|
||||||
cache: false,
|
cache: false,
|
||||||
|
|||||||
@@ -270,6 +270,8 @@ TEST_ITEM_RES = {
|
|||||||
"picture": None,
|
"picture": None,
|
||||||
"available_from": None,
|
"available_from": None,
|
||||||
"available_until": None,
|
"available_until": None,
|
||||||
|
"available_from_mode": "hide",
|
||||||
|
"available_until_mode": "hide",
|
||||||
"require_bundling": False,
|
"require_bundling": False,
|
||||||
"require_voucher": False,
|
"require_voucher": False,
|
||||||
"hide_without_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()),
|
"sales_channels": list(get_all_sales_channels().keys()),
|
||||||
"available_from": None,
|
"available_from": None,
|
||||||
"available_until": None,
|
"available_until": None,
|
||||||
|
"available_from_mode": "hide",
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": False,
|
"hide_without_voucher": False,
|
||||||
"original_price": None,
|
"original_price": None,
|
||||||
"meta_data": {}
|
"meta_data": {}
|
||||||
@@ -1330,6 +1334,8 @@ TEST_VARIATIONS_RES = {
|
|||||||
"sales_channels": list(get_all_sales_channels().keys()),
|
"sales_channels": list(get_all_sales_channels().keys()),
|
||||||
"available_from": None,
|
"available_from": None,
|
||||||
"available_until": None,
|
"available_until": None,
|
||||||
|
"available_from_mode": "hide",
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": False,
|
"hide_without_voucher": False,
|
||||||
"original_price": None,
|
"original_price": None,
|
||||||
"free_price_suggestion": None,
|
"free_price_suggestion": None,
|
||||||
@@ -1353,6 +1359,8 @@ TEST_VARIATIONS_UPDATE = {
|
|||||||
"sales_channels": ["web"],
|
"sales_channels": ["web"],
|
||||||
"available_from": None,
|
"available_from": None,
|
||||||
"available_until": None,
|
"available_until": None,
|
||||||
|
"available_from_mode": "hide",
|
||||||
|
"available_until_mode": "hide",
|
||||||
"hide_without_voucher": False,
|
"hide_without_voucher": False,
|
||||||
"original_price": None,
|
"original_price": None,
|
||||||
"free_price_suggestion": None,
|
"free_price_suggestion": None,
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
|||||||
{
|
{
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"require_voucher": False,
|
"current_unavailability_reason": None,
|
||||||
"order_min": None,
|
"order_min": None,
|
||||||
"max_price": None,
|
"max_price": None,
|
||||||
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
|
"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
|
"order_max": 4
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"require_voucher": False,
|
"current_unavailability_reason": None,
|
||||||
"order_min": None,
|
"order_min": None,
|
||||||
"max_price": "14.00",
|
"max_price": "14.00",
|
||||||
"price": None,
|
"price": None,
|
||||||
@@ -231,7 +231,8 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
|||||||
"rate": "19.00", "includes_mixed_tax_rate": False},
|
"rate": "19.00", "includes_mixed_tax_rate": False},
|
||||||
"description": None,
|
"description": None,
|
||||||
"avail": [100, None],
|
"avail": [100, None],
|
||||||
"order_max": 2
|
"order_max": 2,
|
||||||
|
"current_unavailability_reason": None,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"value": "Blue",
|
"value": "Blue",
|
||||||
@@ -243,7 +244,8 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
|||||||
"rate": "19.00", "includes_mixed_tax_rate": False},
|
"rate": "19.00", "includes_mixed_tax_rate": False},
|
||||||
"description": None,
|
"description": None,
|
||||||
"avail": [100, None],
|
"avail": [100, None],
|
||||||
"order_max": 2
|
"order_max": 2,
|
||||||
|
"current_unavailability_reason": None,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"id": self.shirt.pk,
|
"id": self.shirt.pk,
|
||||||
@@ -273,7 +275,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
|||||||
{
|
{
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"require_voucher": False,
|
"current_unavailability_reason": None,
|
||||||
"order_min": None,
|
"order_min": None,
|
||||||
"max_price": None,
|
"max_price": None,
|
||||||
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00",
|
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00",
|
||||||
@@ -322,7 +324,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
|||||||
{
|
{
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"require_voucher": False,
|
"current_unavailability_reason": None,
|
||||||
"order_min": None,
|
"order_min": None,
|
||||||
"max_price": "14.00",
|
"max_price": "14.00",
|
||||||
"price": None,
|
"price": None,
|
||||||
@@ -346,7 +348,8 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
|||||||
"rate": "19.00", "includes_mixed_tax_rate": False},
|
"rate": "19.00", "includes_mixed_tax_rate": False},
|
||||||
"description": None,
|
"description": None,
|
||||||
"avail": [100, None],
|
"avail": [100, None],
|
||||||
"order_max": 2
|
"order_max": 2,
|
||||||
|
"current_unavailability_reason": None,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"id": self.shirt.pk,
|
"id": self.shirt.pk,
|
||||||
@@ -389,7 +392,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
|||||||
{
|
{
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"require_voucher": False,
|
"current_unavailability_reason": None,
|
||||||
"order_min": None,
|
"order_min": None,
|
||||||
"max_price": None,
|
"max_price": None,
|
||||||
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
|
"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,
|
'has_variations': 2,
|
||||||
"allow_waitinglist": True,
|
"allow_waitinglist": True,
|
||||||
"mandatory_priced_addons": False,
|
"mandatory_priced_addons": False,
|
||||||
'require_voucher': False,
|
'current_unavailability_reason': None,
|
||||||
'order_min': None,
|
'order_min': None,
|
||||||
'order_max': None,
|
'order_max': None,
|
||||||
'price': None,
|
'price': None,
|
||||||
@@ -490,7 +493,8 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
|||||||
'name': '',
|
'name': '',
|
||||||
'includes_mixed_tax_rate': False
|
'includes_mixed_tax_rate': False
|
||||||
},
|
},
|
||||||
'avail': [100, None]
|
'avail': [100, None],
|
||||||
|
'current_unavailability_reason': None,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user