mirror of
https://github.com/pretix/pretix.git
synced 2026-08-15 11:36:27 +00:00
Merge branch 'master' into order-change-nplus1
This commit is contained in:
@@ -864,6 +864,9 @@ Generating new secrets
|
||||
|
||||
Triggers generation of new ``secret`` and ``web_secret`` attributes for both the order and all order positions.
|
||||
|
||||
Ticket secrets of order positions that have been used to issue a gift card can not
|
||||
be changed. Only the link (``web_secret``) will be changed in this case.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@@ -895,6 +898,9 @@ Generating new secrets
|
||||
|
||||
Triggers generation of a new ``secret`` and ``web_secret`` attribute for a single order position.
|
||||
|
||||
Ticket secrets of order positions that have been used to issue a gift card can not
|
||||
be changed. Only the link (``web_secret``) will be changed in this case.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# Generated by Django 5.2.12 on 2026-04-28 11:34
|
||||
import logging
|
||||
|
||||
from django.db import IntegrityError, migrations, transaction
|
||||
from django.db.models import Count, F
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def fix_cross_organizer_eventmetavalues(apps, schema_editor):
|
||||
EventMetaProperty = apps.get_model("pretixbase", "EventMetaProperty")
|
||||
EventMetaValue = apps.get_model("pretixbase", "EventMetaValue")
|
||||
|
||||
cross_org_values = EventMetaValue.objects.filter(
|
||||
event__organizer__pk__ne=F('property__organizer__pk')
|
||||
).order_by('event__organizer__slug', 'event__slug')
|
||||
for emv in cross_org_values:
|
||||
logger.warning("%s", f"Fixing cross-organizer EventMetaValue: {emv.event.organizer.slug}/{emv.event.slug}")
|
||||
logger.warning(" %s", f"{emv.property.name}({emv.property.id}@{emv.property.organizer.slug}) = {repr(emv.value)}")
|
||||
try:
|
||||
emv.property = emv.event.organizer.meta_properties.get(name=emv.property.name)
|
||||
if EventMetaValue.objects.filter(event=emv.event, property=emv.property).exists():
|
||||
correct = EventMetaValue.objects.get(event=emv.event, property=emv.property)
|
||||
if correct.value != emv.value:
|
||||
logger.warning(" %s", f"WARN: conflicting EventMetaValue with property in correct organizer already exists, deleting the cross-organizer one")
|
||||
else:
|
||||
logger.warning(" %s", f"OK: same-value EventMetaValue with property in correct organizer already exists, deleting the cross-organizer one")
|
||||
logger.warning(" %s", f"keeping: {correct.property.name}({correct.property.id}@{correct.property.organizer.slug}) = {repr(correct.value)}")
|
||||
emv.delete()
|
||||
else:
|
||||
logger.warning(" %s", f"OK: found existing EventMetaProperty in {emv.event.organizer.slug}, updating reference")
|
||||
logger.warning(" %s", f"after: {emv.property.name}({emv.property.id}@{emv.property.organizer.slug}) = {repr(emv.value)}")
|
||||
emv.save(update_fields=["property"])
|
||||
except EventMetaProperty.DoesNotExist:
|
||||
meta_prop = emv.property
|
||||
meta_prop.pk = None
|
||||
meta_prop.organizer = emv.event.organizer
|
||||
meta_prop.filter_public = False
|
||||
meta_prop.save(force_insert=True)
|
||||
logger.warning(" %s", f"WARN: found no matching EventMetaProperty, creating")
|
||||
logger.warning(" %s", f"after: {emv.property.name}({emv.property.id}@{emv.property.organizer.slug}) = {repr(emv.value)}")
|
||||
emv.save(update_fields=["property"])
|
||||
|
||||
|
||||
def make_eventmetaproperties_unique(apps, schema_editor):
|
||||
EventMetaProperty = apps.get_model("pretixbase", "EventMetaProperty")
|
||||
EventMetaValue = apps.get_model("pretixbase", "EventMetaValue")
|
||||
|
||||
duplicates = EventMetaProperty.objects.values('organizer', 'organizer__slug', 'name').annotate(count=Count('id')).filter(count__gt=1)
|
||||
for dup in duplicates:
|
||||
logger.warning("%s", f"Fixup duplicate property {dup['organizer__slug']} {dup['name']}")
|
||||
props = list(EventMetaProperty.objects.filter(organizer=dup['organizer'], name=dup['name']))
|
||||
|
||||
target = props[0]
|
||||
invalid = props[1:]
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
affected = EventMetaValue.objects.filter(
|
||||
event__organizer=dup['organizer'], property__in=invalid
|
||||
).update(
|
||||
property=target
|
||||
)
|
||||
logger.warning("%s", f" Switching {affected} value(s) over to {target.name}({target.id}@{target.organizer.slug})")
|
||||
|
||||
except IntegrityError as e:
|
||||
logger.warning("%s", f" Failed to switch all value(s) over to {target.name}({target.id}@{target.organizer.slug})")
|
||||
logger.warning("%s", f" {e}")
|
||||
for prop in invalid:
|
||||
newname = f'{prop.name}_DUPLICATE_{prop.id}'
|
||||
logger.warning("%s", f" Renaming {prop.name}({prop.id}@{prop.organizer.slug}) to {newname}({prop.id}@{prop.organizer.slug})")
|
||||
prop.name = newname
|
||||
prop.filter_public = False
|
||||
prop.save()
|
||||
|
||||
else:
|
||||
for prop in invalid:
|
||||
logger.warning("%s", f" Deleting {prop.name}({prop.id}@{prop.organizer.slug})")
|
||||
prop.delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0301_reusablemedium_remove_orderposition"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(fix_cross_organizer_eventmetavalues, migrations.RunPython.noop),
|
||||
migrations.RunPython(make_eventmetaproperties_unique, migrations.RunPython.noop),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.2.12 on 2026-04-28 11:34
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0302_fixup_eventmetaproperties"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name="eventmetaproperty",
|
||||
unique_together={("organizer", "name")},
|
||||
),
|
||||
]
|
||||
@@ -1851,6 +1851,7 @@ class EventMetaProperty(LoggedModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ("position", "name",)
|
||||
unique_together = ('organizer', 'name')
|
||||
|
||||
@property
|
||||
def choice_keys(self):
|
||||
|
||||
@@ -885,26 +885,6 @@ 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 time_machine_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'
|
||||
elif self.hidden_if_item_available and self._dependency_available:
|
||||
return 'hidden_if_item_available'
|
||||
else:
|
||||
return None
|
||||
|
||||
def _get_quotas(self, ignored_quotas=None, subevent=None):
|
||||
check_quotas = set(getattr(
|
||||
self, '_subevent_quotas', # Utilize cache in product list
|
||||
@@ -1413,22 +1393,6 @@ 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 time_machine_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
|
||||
|
||||
@@ -936,7 +936,7 @@ class BasePaymentProvider:
|
||||
"""
|
||||
Will be called if the *event administrator* views the details of a payment.
|
||||
|
||||
It should return HTML code containing information regarding the current payment
|
||||
It should return a SafeString containing HTML code, with information regarding the current payment
|
||||
status and, if applicable, next steps.
|
||||
|
||||
The default implementation returns an empty string.
|
||||
@@ -961,7 +961,7 @@ class BasePaymentProvider:
|
||||
"""
|
||||
Will be called if the *event administrator* views the details of a refund.
|
||||
|
||||
It should return HTML code containing information regarding the current refund
|
||||
It should return a SafeString containing HTML code, with information regarding the current refund
|
||||
status and, if applicable, next steps.
|
||||
|
||||
The default implementation returns an empty string.
|
||||
|
||||
@@ -245,6 +245,9 @@ def recv_classic(sender, **kwargs):
|
||||
|
||||
|
||||
def assign_ticket_secret(event, position, force_invalidate_if_revokation_list_used=False, force_invalidate=False, save=True):
|
||||
if position.pk and position.issued_gift_cards.exists():
|
||||
return
|
||||
|
||||
gen = event.ticket_secret_generator
|
||||
if gen.use_revocation_list and force_invalidate_if_revokation_list_used:
|
||||
force_invalidate = True
|
||||
|
||||
@@ -29,7 +29,7 @@ from typing import List
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from pretix.base.models import CartPosition, ItemCategory, SalesChannel
|
||||
from pretix.presale.views.event import get_grouped_items
|
||||
from pretix.presale.productlist import prepare_item_list_for_shop
|
||||
|
||||
|
||||
class DummyCategory:
|
||||
@@ -162,7 +162,7 @@ class CrossSellingService:
|
||||
]
|
||||
|
||||
def _prepare_items(self, subevent, items_qs, discount_info):
|
||||
items, _btn = get_grouped_items(
|
||||
items, _btn = prepare_item_list_for_shop(
|
||||
self.event,
|
||||
subevent=subevent,
|
||||
voucher=None,
|
||||
|
||||
@@ -110,6 +110,7 @@ from pretix.celery_app import app
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.models import modelcopy
|
||||
from pretix.helpers.periodic import minimum_interval
|
||||
from pretix.presale.productlist import prepare_item_list_for_shop
|
||||
from pretix.testutils.middleware import debugflags_var
|
||||
|
||||
|
||||
@@ -1599,6 +1600,7 @@ class OrderChangeManager:
|
||||
'seat_forbidden': gettext_lazy('The selected product does not allow to select a seat.'),
|
||||
'tax_rule_country_blocked': gettext_lazy('The selected country is blocked by your tax rule.'),
|
||||
'gift_card_change': gettext_lazy('You cannot change the price of a position that has been used to issue a gift card.'),
|
||||
'gift_card_secret': gettext_lazy('You cannot change the ticket secret of a position that has been used to issue a gift card.'),
|
||||
'max_items_per_product': ngettext_lazy(
|
||||
"You cannot select more than %(max)s item of the product %(product)s.",
|
||||
"You cannot select more than %(max)s items of the product %(product)s.",
|
||||
@@ -1756,6 +1758,9 @@ class OrderChangeManager:
|
||||
self._operations.append(self.RegenerateSecretOperation(position))
|
||||
|
||||
def change_ticket_secret(self, position: OrderPosition, new_secret: str):
|
||||
if position.issued_gift_cards.exists():
|
||||
raise OrderError(self.error_messages['gift_card_secret'])
|
||||
|
||||
self._operations.append(self.ChangeSecretOperation(position, new_secret))
|
||||
|
||||
def change_valid_from(self, position: OrderPosition, new_value: datetime):
|
||||
@@ -1943,13 +1948,18 @@ class OrderChangeManager:
|
||||
|
||||
:param addons: A list of dictionaries with the keys ``"addon_to"``, ``"item"``, ``"variation"`` (all ID values),
|
||||
``"count"``, and ``"price"``.
|
||||
:param limit_main_positions: By default, the method works on all methods of the order. If you set this to a
|
||||
:param limit_main_positions: By default, the method works on all positions of the order. If you set this to a
|
||||
queryset or a list of positions, all other positions and their add-ons will be kept
|
||||
untouched.
|
||||
"""
|
||||
if self._operations:
|
||||
raise ValueError("Setting addons should be the first/only operation")
|
||||
|
||||
def _allowed_on_order_sales_channel(item_or_var, order):
|
||||
return item_or_var.all_sales_channels or (
|
||||
order.sales_channel.identifier in (s.identifier for s in item_or_var.limit_sales_channels.all())
|
||||
)
|
||||
|
||||
# Prepare containers for min/max check of products
|
||||
item_counts = Counter()
|
||||
for p in self.order.positions.all():
|
||||
@@ -2043,13 +2053,11 @@ class OrderChangeManager:
|
||||
if not item.is_available() or (variation and not variation.is_available()):
|
||||
raise OrderError(error_messages['unavailable'])
|
||||
|
||||
if not item.all_sales_channels:
|
||||
if self.order.sales_channel.identifier not in (s.identifier for s in item.limit_sales_channels.all()):
|
||||
raise OrderError(error_messages['unavailable'])
|
||||
if not _allowed_on_order_sales_channel(item, self.order):
|
||||
raise OrderError(error_messages['unavailable'])
|
||||
|
||||
if variation and not variation.all_sales_channels:
|
||||
if self.order.sales_channel.identifier not in (s.identifier for s in variation.limit_sales_channels.all()):
|
||||
raise OrderError(error_messages['unavailable'])
|
||||
if variation and not _allowed_on_order_sales_channel(variation, self.order):
|
||||
raise OrderError(error_messages['unavailable'])
|
||||
|
||||
if subevent and item.pk in subevent.item_overrides and not subevent.item_overrides[item.pk].is_available():
|
||||
raise OrderError(error_messages['not_for_sale'])
|
||||
@@ -2097,6 +2105,36 @@ class OrderChangeManager:
|
||||
)
|
||||
item_counts[item] += 1
|
||||
|
||||
def _addon_is_available(a):
|
||||
# If an item is no longer available due to time, it should usually also be no longer
|
||||
# user-removable, because e.g. the stock has already been ordered.
|
||||
# We always set voucher=None because that's what's done when generating the form in
|
||||
# OrderChangeMixin (vouchers for addons are not supported).
|
||||
# This also prevents accidental removal through the UI because a hidden product will no longer
|
||||
# be part of the input.
|
||||
if not _allowed_on_order_sales_channel(a.item, self.order) or (
|
||||
a.variation and not _allowed_on_order_sales_channel(a.variation, self.order)
|
||||
):
|
||||
return False
|
||||
|
||||
items, _ = prepare_item_list_for_shop(
|
||||
self.order.event,
|
||||
channel=self.order.sales_channel,
|
||||
subevent=a.subevent,
|
||||
voucher=None,
|
||||
base_qs=Item.objects.filter(pk=a.item.pk),
|
||||
allow_addons=True
|
||||
)
|
||||
if (not items) or items[0].current_unavailability_reason:
|
||||
return False
|
||||
|
||||
if a.variation:
|
||||
variations = [var for var in items[0].available_variations if var.pk == a.variation.pk]
|
||||
if (not variations) or variations[0].current_unavailability_reason:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# Detect removed add-ons and create RemoveOperations
|
||||
for cp, al in list(current_addons.items()):
|
||||
for k, v in al.items():
|
||||
@@ -2106,22 +2144,7 @@ class OrderChangeManager:
|
||||
for a in current_addons[cp][k][:current_num - input_num]:
|
||||
if a.canceled:
|
||||
continue
|
||||
is_unavailable = (
|
||||
# If an item is no longer available due to time, it should usually also be no longer
|
||||
# user-removable, because e.g. the stock has already been ordered.
|
||||
# We always pass has_voucher=True because if a product now requires a voucher, it usually does
|
||||
# not mean it should be unremovable for others.
|
||||
# This also prevents accidental removal through the UI because a hidden product will no longer
|
||||
# be part of the input.
|
||||
(a.variation and a.variation.unavailability_reason(has_voucher=True, subevent=a.subevent))
|
||||
or (a.variation and not a.variation.all_sales_channels and not a.variation.limit_sales_channels.contains(self.order.sales_channel))
|
||||
or a.item.unavailability_reason(has_voucher=True, subevent=a.subevent)
|
||||
or (
|
||||
not a.item.all_sales_channels and
|
||||
not a.item.limit_sales_channels.contains(self.order.sales_channel)
|
||||
)
|
||||
)
|
||||
if is_unavailable:
|
||||
if not _addon_is_available(a):
|
||||
# "Re-select" add-on
|
||||
selected_addons[cp.id, a.item.category_id][a.item_id, a.variation_id] += 1
|
||||
continue
|
||||
|
||||
@@ -141,7 +141,7 @@ event_dashboard_widgets = EventPluginSignal()
|
||||
This signal is sent out to include widgets in the event dashboard. Receivers
|
||||
should return a list of dictionaries, where each dictionary can have the keys:
|
||||
|
||||
* content (str, containing HTML)
|
||||
* content (SafeString, containing HTML)
|
||||
* display_size (str, one of "full" (whole row), "big" (half a row) or "small"
|
||||
(quarter of a row). May be ignored on small displays, default is "small")
|
||||
* priority (int, used for ordering, higher comes first, default is 1)
|
||||
@@ -158,7 +158,7 @@ Arguments: 'user'
|
||||
This signal is sent out to include widgets in the personal user dashboard. Receivers
|
||||
should return a list of dictionaries, where each dictionary can have the keys:
|
||||
|
||||
* content (str, containing HTML)
|
||||
* content (SafeString, containing HTML)
|
||||
* display_size (str, one of "full" (whole row), "big" (half a row) or "small"
|
||||
(quarter of a row). May be ignored on small displays, default is "small")
|
||||
* priority (int, used for ordering, higher comes first, default is 1)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load compress %}
|
||||
{% load escapejson %}
|
||||
{% block content %}
|
||||
<form class="form-signin" action="" method="post" id="webauthn-form">
|
||||
{% csrf_token %}
|
||||
@@ -30,8 +31,7 @@
|
||||
</form>
|
||||
{% if jsondata %}
|
||||
<script type="text/json" id="webauthn-login">
|
||||
{{ jsondata|safe }}
|
||||
|
||||
{{ jsondata|escapejson }}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% compress js %}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{% if w.lazy %}
|
||||
<span class="fa fa-cog fa-4x"></span>
|
||||
{% else %}
|
||||
{{ w.content|safe }}
|
||||
{{ w.content }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,7 +51,7 @@
|
||||
{% if w.lazy %}
|
||||
<span class="fa fa-cog fa-4x"></span>
|
||||
{% else %}
|
||||
{{ w.content|safe }}
|
||||
{{ w.content }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,7 +72,7 @@
|
||||
{% if w.lazy %}
|
||||
<span class="fa fa-cog fa-4x"></span>
|
||||
{% else %}
|
||||
{{ w.content|safe }}
|
||||
{{ w.content }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -94,7 +94,7 @@
|
||||
{% if w.lazy %}
|
||||
<span class="fa fa-cog fa-4x"></span>
|
||||
{% else %}
|
||||
{{ w.content|safe }}
|
||||
{{ w.content }}
|
||||
{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
@@ -102,7 +102,7 @@
|
||||
{% if w.lazy %}
|
||||
<span class="fa fa-cog fa-4x"></span>
|
||||
{% else %}
|
||||
{{ w.content|safe }}
|
||||
{{ w.content }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
{% if w.lazy %}
|
||||
<span class="fa fa-cog fa-4x"></span>
|
||||
{% else %}
|
||||
{{ w.content|safe }}
|
||||
{{ w.content }}
|
||||
{% endif %}
|
||||
</a>
|
||||
{% elif w.link %}
|
||||
@@ -114,7 +114,7 @@
|
||||
{% if w.lazy %}
|
||||
<span class="fa fa-cog fa-4x´"></span>
|
||||
{% else %}
|
||||
{{ w.content|safe }}
|
||||
{{ w.content }}
|
||||
{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
@@ -122,7 +122,7 @@
|
||||
{% if w.lazy %}
|
||||
<span class="fa fa-cog fa-4x"></span>
|
||||
{% else %}
|
||||
{{ w.content|safe }}
|
||||
{{ w.content }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<table class="table table-payment-providers">
|
||||
<tbody>
|
||||
{% for provider in providers %}
|
||||
<tr>
|
||||
<tr{% if provider.highlight %} class="success-left"{% endif %}>
|
||||
<td>
|
||||
<strong>{{ provider.verbose_name }}</strong>
|
||||
</td>
|
||||
@@ -56,7 +56,7 @@
|
||||
<td colspan="4">
|
||||
<br>
|
||||
{% url "control:event.settings.plugins" event=request.event.slug organizer=request.organizer.slug as plugin_settings_url %}
|
||||
<a href="{{ plugin_settings_url }}#tab-0-1-open" class="btn btn-default">
|
||||
<a href="{{ plugin_settings_url }}?go=payment#tab-0-1-open" class="btn btn-default">
|
||||
<i class="fa fa-plus"></i> {% trans "Enable additional payment plugins" %}
|
||||
</a>
|
||||
</td>
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
</div>
|
||||
<form action="" method="post" class="form-horizontal form-plugins">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="go" value="{{ request.GET.go }}">
|
||||
<div id="plugin_search_results" class="panel panel-default collapse">
|
||||
<div class="panel-heading">
|
||||
<button type="button" class="close" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load money %}
|
||||
{% load wrap_in %}
|
||||
{% block title %}
|
||||
{% trans "Cancel order" %}
|
||||
{% endblock %}
|
||||
@@ -26,7 +27,7 @@
|
||||
{% if form.cancellation_fee %}
|
||||
{% if fee %}
|
||||
{% with fee|money:request.event.currency as f %}
|
||||
<p>{% blocktrans trimmed with fee="<strong>"|add:f|add:"</strong>"|safe %}
|
||||
<p>{% blocktrans trimmed with fee=f|wrap_in:"strong" %}
|
||||
The configured cancellation fee for a self-service cancellation would be {{ fee }} for this
|
||||
order, but for a cancellation performed by you, you need to set the cancellation fee here:
|
||||
{% endblocktrans %}</p>
|
||||
|
||||
@@ -284,6 +284,14 @@
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
{% bootstrap_field position.form.operation_secret layout='inline' %}
|
||||
{% if position.issued_gift_cards.exists %}
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed %}
|
||||
Ticket secrets of order positions that have been used to issue a gift card can not
|
||||
be changed. Only the link will be changed in this case.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -903,7 +903,7 @@
|
||||
<tr>
|
||||
<td colspan="1"></td>
|
||||
<td colspan="5">
|
||||
{{ p.html_info|safe }}
|
||||
{{ p.html_info }}
|
||||
{% if staff_session %}
|
||||
<p>
|
||||
<a href="" class="btn btn-default btn-xs admin-only" data-expandpayment data-id="{{ p.pk }}">
|
||||
@@ -1018,7 +1018,7 @@
|
||||
</dl>
|
||||
{% endif %}
|
||||
{% if r.html_info %}
|
||||
{{ r.html_info|safe }}
|
||||
{{ r.html_info }}
|
||||
{% endif %}
|
||||
{% if staff_session %}
|
||||
<p>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% load escapejson %}
|
||||
{% block inner %}
|
||||
<h1>{% trans "Connect to device:" %} {{ device.name }}</h1>
|
||||
|
||||
@@ -18,7 +19,7 @@
|
||||
{% trans "Open the app that you want to connect and optionally reset it to the original state." %}
|
||||
</li>
|
||||
<li>{% trans "Scan the following configuration code:" %}<br><br>
|
||||
<script type="text/json" data-replace-with-qr>{{ qrdata|safe }}</script><br>
|
||||
<script type="application/json" data-replace-with-qr>{{ qrdata|escapejson_dumps }}</script><br>
|
||||
{% trans "If your app/device does not support scanning a QR code, you can also enter the following information:" %}
|
||||
<br>
|
||||
<strong>{% trans "System URL:" %}</strong> <code id="system_url">{{ settings.SITE_URL }}</code>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load escapejson %}
|
||||
{% block title %}{% trans "Add a two-factor authentication device" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Add a two-factor authentication device" %}</h1>
|
||||
@@ -32,7 +33,7 @@
|
||||
</li>
|
||||
<li>
|
||||
{% trans "Add a new account to the app by scanning the following barcode:" %}
|
||||
<div class="qrcode-canvas" data-qrdata="#qrdata"></div>
|
||||
<script type="application/json" data-replace-with-qr>{{ qrdata|escapejson_dumps }}</script>
|
||||
<p>
|
||||
<a data-toggle="collapse" href="#no_scan">
|
||||
{% trans "Can't scan the barcode?" %}
|
||||
@@ -81,9 +82,4 @@
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<script type="text/json" id="qrdata">
|
||||
{{ qrdata|safe }}
|
||||
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
{% load bootstrap3 %}
|
||||
{% load static %}
|
||||
{% load compress %}
|
||||
{% load escapejson %}
|
||||
{% block title %}{% trans "Add a two-factor authentication device" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Add a two-factor authentication device" %}</h1>
|
||||
@@ -26,9 +27,7 @@
|
||||
{% trans "Device registration failed." %}
|
||||
</div>
|
||||
<script type="text/json" id="webauthn-enroll">
|
||||
{{ jsondata|safe }}
|
||||
|
||||
|
||||
{{ jsondata|escapejson }}
|
||||
</script>
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/base64js.js" %}"></script>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
{% load bootstrap3 %}
|
||||
{% load compress %}
|
||||
{% load static %}
|
||||
{% load escapejson %}
|
||||
{% block content %}
|
||||
<form class="form-signin" id="webauthn-form" action="" method="post">
|
||||
{% csrf_token %}
|
||||
@@ -43,7 +44,7 @@
|
||||
|
||||
{% if jsondata %}
|
||||
<script type="text/json" id="webauthn-login">
|
||||
{{ jsondata|safe }}
|
||||
{{ jsondata|escapejson }}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% compress js %}
|
||||
|
||||
@@ -115,34 +115,34 @@
|
||||
{% endif %}
|
||||
<th>
|
||||
{% trans "Voucher code" %}
|
||||
<a href="?{% url_replace request 'ordering' '-code' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'code' %}"><i class="fa fa-caret-up"></i></a>
|
||||
<a href="?{% url_replace request 'filter-ordering' '-code' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'filter-ordering' 'code' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Redemptions" %}
|
||||
<a href="?{% url_replace request 'ordering' '-redeemed' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'redeemed' %}"><i class="fa fa-caret-up"></i></a>
|
||||
<a href="?{% url_replace request 'filter-ordering' '-redeemed' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'filter-ordering' 'redeemed' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Expiry" %}
|
||||
<a href="?{% url_replace request 'ordering' '-valid_until' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'valid_until' %}"><i class="fa fa-caret-up"></i></a>
|
||||
<a href="?{% url_replace request 'filter-ordering' '-valid_until' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'filter-ordering' 'valid_until' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Tag" %}
|
||||
<a href="?{% url_replace request 'ordering' '-tag' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'tag' %}"><i class="fa fa-caret-up"></i></a>
|
||||
<a href="?{% url_replace request 'filter-ordering' '-tag' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'filter-ordering' 'tag' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Product" %}
|
||||
<a href="?{% url_replace request 'ordering' '-item' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'item' %}"><i class="fa fa-caret-up"></i></a>
|
||||
<a href="?{% url_replace request 'filter-ordering' '-item' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'filter-ordering' 'item' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
{% if request.event.has_subevents %}
|
||||
<th>
|
||||
{% trans "Date" context "subevent" %}
|
||||
<a href="?{% url_replace request 'ordering' '-subevent' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'subevent' %}"><i class="fa fa-caret-up"></i></a>
|
||||
<a href="?{% url_replace request 'filter-ordering' '-subevent' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'filter-ordering' 'subevent' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
{% endif %}
|
||||
<th></th>
|
||||
|
||||
@@ -49,7 +49,7 @@ from django.shortcuts import render
|
||||
from django.template.loader import get_template
|
||||
from django.urls import reverse
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.html import escape
|
||||
from django.utils.html import conditional_escape, escape, format_html
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _, ngettext, pgettext
|
||||
|
||||
@@ -112,7 +112,7 @@ def base_widgets(sender, subevent=None, lazy=False, **kwargs):
|
||||
|
||||
return [
|
||||
{
|
||||
'content': None if lazy else NUM_WIDGET.format(num=intcomma(tickc), text=_('Attendees (ordered)')),
|
||||
'content': None if lazy else format_html(NUM_WIDGET, num=intcomma(tickc), text=_('Attendees (ordered)')),
|
||||
'lazy': 'attendees-ordered',
|
||||
'display_size': 'small',
|
||||
'priority': 100,
|
||||
@@ -122,7 +122,7 @@ def base_widgets(sender, subevent=None, lazy=False, **kwargs):
|
||||
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
|
||||
},
|
||||
{
|
||||
'content': None if lazy else NUM_WIDGET.format(num=intcomma(paidc), text=_('Attendees (paid)')),
|
||||
'content': None if lazy else format_html(NUM_WIDGET, num=intcomma(paidc), text=_('Attendees (paid)')),
|
||||
'lazy': 'attendees-paid',
|
||||
'display_size': 'small',
|
||||
'priority': 100,
|
||||
@@ -132,7 +132,8 @@ def base_widgets(sender, subevent=None, lazy=False, **kwargs):
|
||||
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
|
||||
},
|
||||
{
|
||||
'content': None if lazy else NUM_WIDGET.format(
|
||||
'content': None if lazy else format_html(
|
||||
NUM_WIDGET,
|
||||
num=money_filter(round_decimal(rev, sender.currency), sender.currency, hide_currency=True),
|
||||
text=_('Total revenue ({currency})').format(currency=sender.currency)
|
||||
),
|
||||
@@ -145,7 +146,7 @@ def base_widgets(sender, subevent=None, lazy=False, **kwargs):
|
||||
}) + ('?subevent={}'.format(subevent.pk) if subevent else '')
|
||||
},
|
||||
{
|
||||
'content': None if lazy else NUM_WIDGET.format(num=prodc, text=_('Active products')),
|
||||
'content': None if lazy else format_html(NUM_WIDGET, num=prodc, text=_('Active products')),
|
||||
'lazy': 'active-products',
|
||||
'display_size': 'small',
|
||||
'priority': 100,
|
||||
@@ -209,8 +210,8 @@ def waitinglist_widgets(sender, subevent=None, lazy=False, **kwargs):
|
||||
quota_cache[q.pk] = (quota_cache[q.pk][0], quota_cache[q.pk][1] - min(wlt['cnt'], row[1]))
|
||||
|
||||
widgets.append({
|
||||
'content': None if lazy else NUM_WIDGET.format(
|
||||
num=intcomma(happy), text=_('available to give to people on waiting list')
|
||||
'content': None if lazy else format_html(
|
||||
NUM_WIDGET, num=intcomma(happy), text=_('available to give to people on waiting list')
|
||||
),
|
||||
'lazy': 'waitinglist-avail',
|
||||
'priority': 50,
|
||||
@@ -220,7 +221,9 @@ def waitinglist_widgets(sender, subevent=None, lazy=False, **kwargs):
|
||||
})
|
||||
})
|
||||
widgets.append({
|
||||
'content': None if lazy else NUM_WIDGET.format(num=intcomma(wles.count()), text=_('total waiting list length')),
|
||||
'content': None if lazy else format_html(
|
||||
NUM_WIDGET, num=intcomma(wles.count()), text=_('total waiting list length')
|
||||
),
|
||||
'lazy': 'waitinglist-length',
|
||||
'display_size': 'small',
|
||||
'priority': 50,
|
||||
@@ -247,7 +250,8 @@ def quota_widgets(sender, subevent=None, lazy=False, **kwargs):
|
||||
if not lazy:
|
||||
status, left = qa.results[q] if q in qa.results else q.availability(allow_cache=True)
|
||||
widgets.append({
|
||||
'content': None if lazy else NUM_WIDGET.format(
|
||||
'content': None if lazy else format_html(
|
||||
NUM_WIDGET,
|
||||
num='{}/{}'.format(intcomma(left), intcomma(q.size)) if q.size is not None else '\u221e',
|
||||
text=_('{quota} left').format(quota=escape(q.name))
|
||||
),
|
||||
@@ -268,7 +272,8 @@ def shop_state_widget(sender, **kwargs):
|
||||
return [{
|
||||
'display_size': 'small',
|
||||
'priority': 1000,
|
||||
'content': '<div class="shopstate">{t1}<br><span class="{cls}"><span class="fa {icon}"></span> {state}</span>{t2}</div>'.format(
|
||||
'content': format_html(
|
||||
'<div class="shopstate">{t1}<br><span class="{cls}"><span class="fa {icon}"></span> {state}</span>{t2}</div>',
|
||||
t1=_('Your ticket shop is'), t2=_('Click here to change'),
|
||||
state=_('live') if sender.live and not sender.testmode else (
|
||||
_('live and in test mode') if sender.live else (
|
||||
@@ -299,7 +304,8 @@ def checkin_widget(sender, subevent=None, lazy=False, **kwargs):
|
||||
qs = sender.checkin_lists.filter(subevent=subevent)
|
||||
for cl in qs:
|
||||
widgets.append({
|
||||
'content': None if lazy else NUM_WIDGET.format(
|
||||
'content': None if lazy else format_html(
|
||||
NUM_WIDGET,
|
||||
num='{}/{}'.format(intcomma(cl.inside_count), intcomma(cl.position_count)),
|
||||
text=_('Present – {list}').format(list=escape(cl.name))
|
||||
),
|
||||
@@ -340,6 +346,12 @@ def welcome_wizard_widget(sender, **kwargs):
|
||||
}]
|
||||
|
||||
|
||||
def build_json_response(widgets):
|
||||
for widget in widgets:
|
||||
widget['content'] = conditional_escape(widget['content'])
|
||||
return JsonResponse({'widgets': widgets})
|
||||
|
||||
|
||||
def event_index(request, organizer, event):
|
||||
from pretix.control.forms.event import CommentForm
|
||||
|
||||
@@ -418,7 +430,7 @@ def event_index_widgets_lazy(request, organizer, event):
|
||||
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=False):
|
||||
widgets.extend(result)
|
||||
|
||||
return JsonResponse({'widgets': widgets})
|
||||
return build_json_response(widgets)
|
||||
|
||||
|
||||
def event_index_log_lazy(request, organizer, event):
|
||||
@@ -505,7 +517,7 @@ def widgets_for_event_qs(request, qs, user, nmax, lazy=False):
|
||||
<a href="{url}" class="event">
|
||||
<div class="name">{event}</div>
|
||||
<div class="daterange">{daterange}</div>
|
||||
<div class="times">{times}</div>
|
||||
<div class="times">{times}{timezone}</div>
|
||||
</a>
|
||||
<div class="bottomrow">
|
||||
{orders}
|
||||
@@ -549,14 +561,16 @@ def widgets_for_event_qs(request, qs, user, nmax, lazy=False):
|
||||
status = ('success', _('On sale'))
|
||||
|
||||
widgets.append({
|
||||
'content': tpl.format(
|
||||
'content': format_html(
|
||||
tpl,
|
||||
event=escape(event.name),
|
||||
times=_('Event series') if event.has_subevents else (
|
||||
((date_format(event.date_admission.astimezone(tz), 'TIME_FORMAT') + ' / ')
|
||||
if event.date_admission and event.date_admission != event.date_from else '')
|
||||
+ (date_format(event.date_from.astimezone(tz), 'TIME_FORMAT') if event.date_from else '')
|
||||
) + (
|
||||
' <span class="fa fa-globe text-muted" data-toggle="tooltip" title="{}"></span>'.format(tzname)
|
||||
),
|
||||
timezone=(
|
||||
format_html(' <span class="fa fa-globe text-muted" data-toggle="tooltip" title="{}"></span>', tzname)
|
||||
if tzname != request.timezone and not event.has_subevents else ''
|
||||
),
|
||||
url=reverse('control:event.index', kwargs={
|
||||
@@ -564,7 +578,8 @@ def widgets_for_event_qs(request, qs, user, nmax, lazy=False):
|
||||
'organizer': event.organizer.slug
|
||||
}),
|
||||
orders=(
|
||||
'<a href="{orders_url}" class="orders">{orders_text}</a>'.format(
|
||||
format_html(
|
||||
'<a href="{orders_url}" class="orders">{orders_text}</a>',
|
||||
orders_url=reverse('control:event.orders', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug
|
||||
@@ -631,7 +646,7 @@ def user_index_widgets_lazy(request):
|
||||
request.user,
|
||||
8
|
||||
)
|
||||
return JsonResponse({'widgets': widgets})
|
||||
return build_json_response(widgets)
|
||||
|
||||
|
||||
def user_index(request):
|
||||
|
||||
@@ -41,7 +41,7 @@ from collections import OrderedDict, defaultdict
|
||||
from decimal import Decimal
|
||||
from io import BytesIO
|
||||
from itertools import groupby
|
||||
from urllib.parse import urlsplit
|
||||
from urllib.parse import quote, urlsplit
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import bleach
|
||||
@@ -82,7 +82,7 @@ from pretix.base.models import Event, LogEntry, Order, TaxRule, Voucher
|
||||
from pretix.base.models.event import EventMetaValue
|
||||
from pretix.base.services import tickets
|
||||
from pretix.base.services.invoices import build_preview_invoice_pdf
|
||||
from pretix.base.signals import register_ticket_outputs
|
||||
from pretix.base.signals import get_defining_app, register_ticket_outputs
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
from pretix.control.forms.event import (
|
||||
CancelSettingsForm, CommentForm, ConfirmTextFormset, EventDeleteForm,
|
||||
@@ -441,6 +441,7 @@ class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, Templat
|
||||
plugins_available = {
|
||||
p.module: p for p in self.available_plugins(self.object)
|
||||
}
|
||||
plugin_enabled = None
|
||||
|
||||
with transaction.atomic():
|
||||
save_organizer = False
|
||||
@@ -490,6 +491,7 @@ class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, Templat
|
||||
format_html(_('The plugin {} is now active.'),
|
||||
format_html("<strong>{}</strong>", pluginmeta.name)),
|
||||
]
|
||||
plugin_enabled = module
|
||||
messages.success(self.request, mark_safe("".join(info)))
|
||||
else:
|
||||
self.request.event.log_action('pretix.event.plugins.disabled', user=self.request.user,
|
||||
@@ -499,13 +501,19 @@ class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, Templat
|
||||
self.object.save()
|
||||
if save_organizer:
|
||||
self.object.organizer.save()
|
||||
return redirect(self.get_success_url())
|
||||
return redirect(self.get_success_url(plugin_enabled))
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.settings.plugins', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
})
|
||||
def get_success_url(self, plugin_enabled) -> str:
|
||||
if plugin_enabled and self.request.POST.get('go') == 'payment':
|
||||
return reverse('control:event.settings.payment', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
}) + '?highlight=' + quote(plugin_enabled) + '#'
|
||||
else:
|
||||
return reverse('control:event.settings.plugins', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
})
|
||||
|
||||
|
||||
class PaymentProviderSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
|
||||
@@ -671,6 +679,8 @@ class PaymentSettings(WritePermissionMixin, EventSettingsViewMixin, EventSetting
|
||||
p.sales_channels = [sales_channels[channel] for channel in p.settings.get('_restrict_to_sales_channels', as_type=list, default=['web'])]
|
||||
if p.is_meta:
|
||||
p.show_enabled = p.settings._enabled in (True, 'True')
|
||||
if self.request.GET.get('highlight') and getattr(get_defining_app(p), 'name', None) == self.request.GET.get('highlight'):
|
||||
p.highlight = True
|
||||
return context
|
||||
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ from django.urls import reverse
|
||||
from django.utils import formats
|
||||
from django.utils.formats import date_format, get_format
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.html import conditional_escape, escape
|
||||
from django.utils.html import conditional_escape, escape, format_html
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.timezone import make_aware, now
|
||||
@@ -79,7 +79,7 @@ from pretix.base.email import get_email_context
|
||||
from pretix.base.exporter import MultiSheetListExporter
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedFile, CachedTicket, Checkin, Invoice, InvoiceAddress, Item,
|
||||
CachedFile, CachedTicket, Checkin, GiftCard, Invoice, InvoiceAddress, Item,
|
||||
ItemVariation, LogEntry, Order, QuestionAnswer, Quota,
|
||||
ScheduledEventExport, generate_secret, SeatCategoryMapping,
|
||||
)
|
||||
@@ -551,10 +551,10 @@ class OrderDetail(OrderView):
|
||||
ctx['refunds'] = self.order.refunds.select_related('payment').order_by('-created')
|
||||
for p in ctx['payments']:
|
||||
if p.payment_provider:
|
||||
p.html_info = (p.payment_provider.payment_control_render(self.request, p) or "").strip()
|
||||
p.html_info = p.payment_provider.payment_control_render(self.request, p) or ""
|
||||
for r in ctx['refunds']:
|
||||
if r.payment_provider:
|
||||
r.html_info = (r.payment_provider.refund_control_render(self.request, r) or "").strip()
|
||||
r.html_info = r.payment_provider.refund_control_render(self.request, r) or ""
|
||||
ctx['invoices'] = list(self.order.invoices.all().select_related('event'))
|
||||
ctx['comment_form'] = CommentForm(initial={
|
||||
'comment': self.order.comment,
|
||||
@@ -2285,6 +2285,12 @@ class OrderContactChange(OrderView):
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
ctx['form'] = self.form
|
||||
if self.order.all_positions.filter(Exists(GiftCard.objects.filter(issued_in=OuterRef('pk')))).exists():
|
||||
self.form.fields['regenerate_secrets'].help_text = format_html(
|
||||
'{}<br><br><strong><span class="fa fa-warning"></span> {}</strong>',
|
||||
self.form.fields['regenerate_secrets'].help_text,
|
||||
_("Ticket secrets of order positions that have been used to issue a gift card can not be changed. Only the link will be changed in this case."),
|
||||
)
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
|
||||
@@ -47,5 +47,5 @@ def escapejson(value):
|
||||
|
||||
@keep_lazy(str, SafeText)
|
||||
def escapejson_attr(value):
|
||||
"""Hex encodes characters for use in a html attributw script."""
|
||||
"""Hex encodes characters for use in a html attribute."""
|
||||
return mark_safe(force_str(value).translate(_json_escapes_attr))
|
||||
|
||||
@@ -323,7 +323,7 @@ $(function () {
|
||||
}
|
||||
}
|
||||
} else if ($("#stripe_payment_intent_next_action_redirect_url").length) {
|
||||
let payment_intent_next_action_redirect_url = $.trim($("#stripe_payment_intent_next_action_redirect_url").html());
|
||||
let payment_intent_next_action_redirect_url = JSON.parse($("#stripe_payment_intent_next_action_redirect_url").html());
|
||||
pretixstripe.handlePaymentRedirectAction(payment_intent_next_action_redirect_url);
|
||||
} else if ($.trim($("#stripe_payment_intent_action_type").html()) === "promptpay_display_qr_code") {
|
||||
waitingDialog.hide();
|
||||
@@ -432,4 +432,4 @@ $(function () {
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<script type="text/plain" id="stripe_payment_intent_action_type">{{ payment_intent_action_type }}</script>
|
||||
<script type="text/plain" id="stripe_payment_intent_client_secret">{{ payment_intent_client_secret }}</script>
|
||||
{% if payment_intent_next_action_redirect_url %}
|
||||
<script type="text/plain" id="stripe_payment_intent_next_action_redirect_url">{{ payment_intent_next_action_redirect_url|safe }}</script>
|
||||
{{ payment_intent_next_action_redirect_url|json_script:"stripe_payment_intent_next_action_redirect_url" }}
|
||||
{% endif %}
|
||||
{% if payment_intent_redirect_action_handling %}
|
||||
<script type="text/plain" id="stripe_payment_intent_redirect_action_handling">{{ payment_intent_redirect_action_handling }}</script>
|
||||
|
||||
@@ -87,6 +87,7 @@ from pretix.presale.forms.checkout import (
|
||||
ContactForm, InvoiceAddressForm, InvoiceNameForm, MembershipForm,
|
||||
)
|
||||
from pretix.presale.forms.customer import AuthenticationForm, RegistrationForm
|
||||
from pretix.presale.productlist import prepare_item_list_for_shop
|
||||
from pretix.presale.signals import (
|
||||
checkout_all_optional, checkout_confirm_messages, checkout_flow_steps,
|
||||
contact_form_fields, contact_form_fields_overrides,
|
||||
@@ -99,7 +100,6 @@ from pretix.presale.views.cart import (
|
||||
_items_from_post_data, cart_session, create_empty_cart_id,
|
||||
get_or_create_cart_id,
|
||||
)
|
||||
from pretix.presale.views.event import get_grouped_items
|
||||
from pretix.presale.views.questions import QuestionsViewMixin
|
||||
|
||||
|
||||
@@ -603,7 +603,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
|
||||
if ckey not in item_cache:
|
||||
# Get all items to possibly show
|
||||
items, _btn = get_grouped_items(
|
||||
items, _btn = prepare_item_list_for_shop(
|
||||
self.request.event,
|
||||
subevent=cartpos.subevent,
|
||||
voucher=None,
|
||||
|
||||
@@ -0,0 +1,460 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import (
|
||||
Count, Exists, IntegerField, OuterRef, Prefetch, Q, Value,
|
||||
)
|
||||
from django.db.models.lookups import Exact
|
||||
|
||||
from pretix.base.models import (
|
||||
ItemVariation, Quota, SalesChannel, SeatCategoryMapping,
|
||||
)
|
||||
from pretix.base.models.items import (
|
||||
Item, ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation,
|
||||
)
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.base.timemachine import time_machine_now
|
||||
from pretix.presale.signals import item_description
|
||||
|
||||
|
||||
def item_group_by_category(items):
|
||||
return sorted(
|
||||
[
|
||||
# a group is a tuple of a category and a list of items
|
||||
(cat, [i for i in items if i.category == cat])
|
||||
for cat in set([i.category for i in items])
|
||||
# insert categories into a set for uniqueness
|
||||
# a set is unsorted, so sort again by category
|
||||
],
|
||||
key=lambda group: (group[0].position, group[0].id) if (
|
||||
group[0] is not None and group[0].id is not None) else (0, 0)
|
||||
)
|
||||
|
||||
|
||||
def prepare_item_list_for_shop(event, *, channel: SalesChannel, subevent=None, voucher=None, require_seat=0, base_qs=None,
|
||||
allow_addons=False, allow_cross_sell=False,
|
||||
quota_cache=None, filter_items=None, filter_categories=None, memberships=None,
|
||||
ignore_hide_sold_out_for_item_ids=None):
|
||||
base_qs_set = base_qs is not None
|
||||
base_qs = base_qs if base_qs is not None else event.items
|
||||
|
||||
requires_seat = Exists(
|
||||
SeatCategoryMapping.objects.filter(
|
||||
product_id=OuterRef('pk'),
|
||||
subevent=subevent
|
||||
)
|
||||
)
|
||||
if not event.settings.seating_choice:
|
||||
requires_seat = Value(0, output_field=IntegerField())
|
||||
|
||||
variation_q = (
|
||||
Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()) | Q(available_from_mode='info')) &
|
||||
Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()) | Q(available_until_mode='info'))
|
||||
)
|
||||
if not voucher or not voucher.show_hidden_items:
|
||||
variation_q &= Q(hide_without_voucher=False)
|
||||
|
||||
if memberships is not None:
|
||||
prefetch_membership_types = ['require_membership_types']
|
||||
else:
|
||||
prefetch_membership_types = []
|
||||
|
||||
prefetch_var = Prefetch(
|
||||
'variations',
|
||||
to_attr='available_variations',
|
||||
queryset=ItemVariation.objects.using(settings.DATABASE_REPLICA).annotate(
|
||||
subevent_disabled=Exists(
|
||||
SubEventItemVariation.objects.filter(
|
||||
Q(disabled=True)
|
||||
| (Exact(OuterRef('available_from_mode'), 'hide') & Q(available_from__gt=time_machine_now()))
|
||||
| (Exact(OuterRef('available_until_mode'), 'hide') & Q(available_until__lt=time_machine_now())),
|
||||
variation_id=OuterRef('pk'),
|
||||
subevent=subevent,
|
||||
)
|
||||
),
|
||||
).filter(
|
||||
variation_q,
|
||||
Q(all_sales_channels=True) | Q(limit_sales_channels=channel),
|
||||
Exists(Quota.variations.through.objects.filter(quota__subevent_id=subevent, itemvariation_id=OuterRef("pk"))),
|
||||
active=True,
|
||||
subevent_disabled=False
|
||||
).prefetch_related(
|
||||
*prefetch_membership_types,
|
||||
Prefetch('quotas',
|
||||
to_attr='_subevent_quotas',
|
||||
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(
|
||||
subevent=subevent).select_related("subevent"))
|
||||
).distinct()
|
||||
)
|
||||
prefetch_quotas = Prefetch(
|
||||
'quotas',
|
||||
to_attr='_subevent_quotas',
|
||||
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(subevent=subevent).select_related("subevent")
|
||||
)
|
||||
prefetch_bundles = Prefetch(
|
||||
'bundles',
|
||||
queryset=ItemBundle.objects.using(settings.DATABASE_REPLICA).prefetch_related(
|
||||
Prefetch('bundled_item',
|
||||
queryset=event.items.using(settings.DATABASE_REPLICA).select_related(
|
||||
'tax_rule').prefetch_related(
|
||||
Prefetch('quotas',
|
||||
to_attr='_subevent_quotas',
|
||||
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(
|
||||
subevent=subevent)),
|
||||
)),
|
||||
Prefetch('bundled_variation',
|
||||
queryset=ItemVariation.objects.using(
|
||||
settings.DATABASE_REPLICA
|
||||
).select_related('item', 'item__tax_rule').filter(item__event=event).prefetch_related(
|
||||
Prefetch('quotas',
|
||||
to_attr='_subevent_quotas',
|
||||
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(
|
||||
subevent=subevent)),
|
||||
)),
|
||||
)
|
||||
)
|
||||
|
||||
items = base_qs.using(settings.DATABASE_REPLICA).filter_available(
|
||||
channel=channel.identifier, voucher=voucher, allow_addons=allow_addons, allow_cross_sell=allow_cross_sell
|
||||
).select_related(
|
||||
'category', 'tax_rule', # for re-grouping
|
||||
'hidden_if_available',
|
||||
).prefetch_related(
|
||||
*prefetch_membership_types,
|
||||
Prefetch(
|
||||
'hidden_if_item_available',
|
||||
queryset=event.items.annotate(
|
||||
has_variations=Count('variations'),
|
||||
).prefetch_related(
|
||||
prefetch_var,
|
||||
prefetch_quotas,
|
||||
prefetch_bundles,
|
||||
)
|
||||
),
|
||||
prefetch_quotas,
|
||||
prefetch_var,
|
||||
prefetch_bundles,
|
||||
).annotate(
|
||||
has_variations=Count('variations'),
|
||||
subevent_disabled=Exists(
|
||||
SubEventItem.objects.filter(
|
||||
Q(disabled=True)
|
||||
| (Exact(OuterRef('available_from_mode'), 'hide') & Q(available_from__gt=time_machine_now()))
|
||||
| (Exact(OuterRef('available_until_mode'), 'hide') & Q(available_until__lt=time_machine_now())),
|
||||
item_id=OuterRef('pk'),
|
||||
subevent=subevent,
|
||||
)
|
||||
),
|
||||
mandatory_priced_addons=Exists(
|
||||
ItemAddOn.objects.filter(
|
||||
base_item_id=OuterRef('pk'),
|
||||
min_count__gte=1,
|
||||
price_included=False
|
||||
)
|
||||
),
|
||||
requires_seat=requires_seat,
|
||||
).filter(
|
||||
Exists(Quota.items.through.objects.filter(quota__subevent_id=subevent, item_id=OuterRef("pk"))),
|
||||
subevent_disabled=False,
|
||||
).order_by('category__position', 'category_id', 'position', 'name')
|
||||
if require_seat:
|
||||
items = items.filter(requires_seat__gt=0)
|
||||
elif require_seat is not None:
|
||||
items = items.filter(requires_seat=0)
|
||||
|
||||
if filter_items:
|
||||
items = items.filter(pk__in=[a for a in filter_items if a.isdigit()])
|
||||
if filter_categories:
|
||||
items = items.filter(category_id__in=[a for a in filter_categories if a.isdigit()])
|
||||
|
||||
display_add_to_cart = False
|
||||
quota_cache_key = f'item_quota_cache:{subevent.id if subevent else 0}:{channel.identifier}:{bool(require_seat)}'
|
||||
quota_cache = quota_cache or event.cache.get(quota_cache_key) or {}
|
||||
quota_cache_existed = bool(quota_cache)
|
||||
|
||||
if subevent:
|
||||
item_price_override = subevent.item_price_overrides
|
||||
var_price_override = subevent.var_price_overrides
|
||||
else:
|
||||
item_price_override = {}
|
||||
var_price_override = {}
|
||||
|
||||
restrict_vars = set()
|
||||
if voucher and voucher.quota_id:
|
||||
# If a voucher is set to a specific quota, we need to filter out on that level
|
||||
restrict_vars = set(voucher.quota.variations.all())
|
||||
|
||||
quotas_to_compute = []
|
||||
for item in items:
|
||||
assert item.event_id == event.pk
|
||||
item.event = event # save a database query if this is looked up
|
||||
if item.has_variations:
|
||||
for v in item.available_variations:
|
||||
for q in v._subevent_quotas:
|
||||
if q.pk not in quota_cache:
|
||||
quotas_to_compute.append(q)
|
||||
else:
|
||||
for q in item._subevent_quotas:
|
||||
if q.pk not in quota_cache:
|
||||
quotas_to_compute.append(q)
|
||||
|
||||
if quotas_to_compute:
|
||||
qa = QuotaAvailability()
|
||||
qa.queue(*quotas_to_compute)
|
||||
qa.compute()
|
||||
quota_cache.update({q.pk: r for q, r in qa.results.items()})
|
||||
|
||||
for item in items:
|
||||
if voucher and voucher.item_id and voucher.variation_id:
|
||||
# Restrict variations if the voucher only allows one
|
||||
item.available_variations = [v for v in item.available_variations
|
||||
if v.pk == voucher.variation_id]
|
||||
|
||||
if channel.type_instance.unlimited_items_per_order:
|
||||
max_per_order = sys.maxsize
|
||||
else:
|
||||
max_per_order = item.max_per_order or int(event.settings.max_items_per_order)
|
||||
if voucher:
|
||||
max_per_order = min(max_per_order, voucher.max_usages - voucher.redeemed)
|
||||
|
||||
if item.hidden_if_available:
|
||||
q = item.hidden_if_available.availability(_cache=quota_cache)
|
||||
if q[0] == Quota.AVAILABILITY_OK:
|
||||
item._remove = True
|
||||
continue
|
||||
|
||||
if item.hidden_if_item_available:
|
||||
if item.hidden_if_item_available.has_variations:
|
||||
item._dependency_available = any(
|
||||
var.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)[0] == Quota.AVAILABILITY_OK
|
||||
for var in item.hidden_if_item_available.available_variations
|
||||
)
|
||||
else:
|
||||
q = item.hidden_if_item_available.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
|
||||
time_available = item.hidden_if_item_available.is_available()
|
||||
item._dependency_available = (q[0] == Quota.AVAILABILITY_OK) and time_available
|
||||
if item._dependency_available and item.hidden_if_item_available_mode == Item.UNAVAIL_MODE_HIDDEN:
|
||||
item._remove = True
|
||||
continue
|
||||
|
||||
if item.require_membership and item.require_membership_hidden:
|
||||
if not memberships or not any([m.membership_type in item.require_membership_types.all() for m in memberships]):
|
||||
item._remove = True
|
||||
continue
|
||||
|
||||
item.current_unavailability_reason = _get_item_unavailability_reason(item, 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:
|
||||
item.description += ("<br/>" if item.description else "") + resp
|
||||
|
||||
if not item.has_variations:
|
||||
item._remove = False
|
||||
if not bool(item._subevent_quotas):
|
||||
item._remove = True
|
||||
continue
|
||||
|
||||
if voucher and (voucher.allow_ignore_quota or voucher.block_quota):
|
||||
item.cached_availability = (
|
||||
Quota.AVAILABILITY_OK, voucher.max_usages - voucher.redeemed
|
||||
)
|
||||
else:
|
||||
item.cached_availability = list(
|
||||
item.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
|
||||
)
|
||||
|
||||
if not (
|
||||
ignore_hide_sold_out_for_item_ids and item.pk in ignore_hide_sold_out_for_item_ids
|
||||
) and event.settings.hide_sold_out and item.cached_availability[0] < Quota.AVAILABILITY_RESERVED:
|
||||
item._remove = True
|
||||
continue
|
||||
|
||||
item.order_max = min(
|
||||
item.cached_availability[1]
|
||||
if item.cached_availability[1] is not None else sys.maxsize,
|
||||
max_per_order
|
||||
)
|
||||
|
||||
original_price = item_price_override.get(item.pk, item.default_price)
|
||||
voucher_reduced = False
|
||||
if voucher:
|
||||
price = voucher.calculate_price(original_price)
|
||||
voucher_reduced = price < original_price
|
||||
include_bundled = not voucher.all_bundles_included
|
||||
else:
|
||||
price = original_price
|
||||
include_bundled = True
|
||||
|
||||
item.display_price = item.tax(price, currency=event.currency, include_bundled=include_bundled)
|
||||
if item.free_price and item.free_price_suggestion is not None and not voucher_reduced:
|
||||
item.suggested_price = item.tax(max(price, item.free_price_suggestion), currency=event.currency, include_bundled=include_bundled)
|
||||
else:
|
||||
item.suggested_price = item.display_price
|
||||
|
||||
if price != original_price:
|
||||
item.original_price = item.tax(original_price, currency=event.currency, include_bundled=True)
|
||||
else:
|
||||
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
|
||||
if item.original_price else None
|
||||
)
|
||||
if not display_add_to_cart:
|
||||
display_add_to_cart = not item.requires_seat and item.order_max > 0
|
||||
else:
|
||||
for var in item.available_variations:
|
||||
if var.require_membership and var.require_membership_hidden:
|
||||
if not memberships or not any([m.membership_type in var.require_membership_types.all() for m in memberships]):
|
||||
var._remove = True
|
||||
continue
|
||||
|
||||
var.description = str(var.description)
|
||||
for recv, resp in item_description.send(sender=event, item=item, variation=var, subevent=subevent):
|
||||
if resp:
|
||||
var.description += ("<br/>" if var.description else "") + resp
|
||||
|
||||
if voucher and (voucher.allow_ignore_quota or voucher.block_quota):
|
||||
var.cached_availability = (
|
||||
Quota.AVAILABILITY_OK, voucher.max_usages - voucher.redeemed
|
||||
)
|
||||
else:
|
||||
var.cached_availability = list(
|
||||
var.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
|
||||
)
|
||||
|
||||
var.order_max = min(
|
||||
var.cached_availability[1]
|
||||
if var.cached_availability[1] is not None else sys.maxsize,
|
||||
max_per_order
|
||||
)
|
||||
|
||||
original_price = var_price_override.get(var.pk, var.price)
|
||||
voucher_reduced = False
|
||||
if voucher:
|
||||
price = voucher.calculate_price(original_price)
|
||||
voucher_reduced = price < original_price
|
||||
include_bundled = not voucher.all_bundles_included
|
||||
else:
|
||||
price = original_price
|
||||
include_bundled = True
|
||||
|
||||
var.display_price = var.tax(price, currency=event.currency, include_bundled=include_bundled)
|
||||
|
||||
if item.free_price and var.free_price_suggestion is not None and not voucher_reduced:
|
||||
var.suggested_price = item.tax(max(price, var.free_price_suggestion), currency=event.currency,
|
||||
include_bundled=include_bundled)
|
||||
elif item.free_price and item.free_price_suggestion is not None and not voucher_reduced:
|
||||
var.suggested_price = item.tax(max(price, item.free_price_suggestion), currency=event.currency,
|
||||
include_bundled=include_bundled)
|
||||
else:
|
||||
var.suggested_price = var.display_price
|
||||
|
||||
if price != original_price:
|
||||
var.original_price = var.tax(original_price, currency=event.currency, include_bundled=True)
|
||||
else:
|
||||
var.original_price = (
|
||||
var.tax(var.original_price or item.original_price, currency=event.currency,
|
||||
include_bundled=True,
|
||||
base_price_is='net' if event.settings.display_net_prices else 'gross') # backwards-compat
|
||||
) if var.original_price or item.original_price else None
|
||||
|
||||
var.current_unavailability_reason = _get_variant_unavailability_reason(var, 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
|
||||
if item.original_price else None
|
||||
)
|
||||
|
||||
item.available_variations = [
|
||||
v for v in item.available_variations if v._subevent_quotas and (
|
||||
not voucher or not voucher.quota_id or v in restrict_vars
|
||||
) and not getattr(v, '_remove', False)
|
||||
]
|
||||
|
||||
if not (ignore_hide_sold_out_for_item_ids and item.pk in ignore_hide_sold_out_for_item_ids) and event.settings.hide_sold_out:
|
||||
item.available_variations = [v for v in item.available_variations
|
||||
if v.cached_availability[0] >= Quota.AVAILABILITY_RESERVED]
|
||||
|
||||
if voucher and voucher.variation_id:
|
||||
item.available_variations = [v for v in item.available_variations
|
||||
if v.pk == voucher.variation_id]
|
||||
|
||||
if len(item.available_variations) > 0:
|
||||
item.min_price = min([v.display_price.net if event.settings.display_net_prices else
|
||||
v.display_price.gross for v in item.available_variations])
|
||||
item.max_price = max([v.display_price.net if event.settings.display_net_prices else
|
||||
v.display_price.gross for v in item.available_variations])
|
||||
item.best_variation_availability = max([v.cached_availability[0] for v in item.available_variations])
|
||||
|
||||
item._remove = not bool(item.available_variations)
|
||||
if not item._remove and not display_add_to_cart:
|
||||
display_add_to_cart = not item.requires_seat and any(v.order_max > 0 for v in item.available_variations)
|
||||
|
||||
if not quota_cache_existed and not voucher and not allow_addons and not base_qs_set and not filter_items and not filter_categories:
|
||||
event.cache.set(quota_cache_key, quota_cache, 5)
|
||||
items = [item for item in items
|
||||
if (len(item.available_variations) > 0 or not item.has_variations) and not item._remove]
|
||||
return items, display_add_to_cart
|
||||
|
||||
|
||||
def _get_item_unavailability_reason(item, now_dt: Optional[datetime]=None, has_voucher=False, subevent=None) -> Optional[str]:
|
||||
now_dt = now_dt or time_machine_now()
|
||||
subevent_item = subevent and subevent.item_overrides.get(item.pk)
|
||||
if not item.active:
|
||||
return 'active'
|
||||
elif item.available_from and item.available_from > now_dt:
|
||||
return 'available_from'
|
||||
elif item.available_until and item.available_until < now_dt:
|
||||
return 'available_until'
|
||||
elif (item.require_voucher or item.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'
|
||||
elif item.hidden_if_item_available and item._dependency_available:
|
||||
return 'hidden_if_item_available'
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _get_variant_unavailability_reason(variant, now_dt: Optional[datetime]=None, has_voucher=False, subevent=None) -> Optional[str]:
|
||||
now_dt = now_dt or time_machine_now()
|
||||
subevent_var = subevent and subevent.var_overrides.get(variant.pk)
|
||||
if not variant.active:
|
||||
return 'active'
|
||||
elif variant.available_from and variant.available_from > now_dt:
|
||||
return 'available_from'
|
||||
elif variant.available_until and variant.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
|
||||
@@ -68,13 +68,13 @@ from pretix.base.timemachine import time_machine_now
|
||||
from pretix.base.views.tasks import AsyncAction
|
||||
from pretix.helpers.http import redirect_to_url
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.presale.productlist import (
|
||||
item_group_by_category, prepare_item_list_for_shop,
|
||||
)
|
||||
from pretix.presale.views import (
|
||||
CartMixin, EventViewMixin, allow_cors_if_namespaced,
|
||||
allow_frame_if_namespaced, get_cart, iframe_entry_view_wrapper,
|
||||
)
|
||||
from pretix.presale.views.event import (
|
||||
get_grouped_items, item_group_by_category,
|
||||
)
|
||||
from pretix.presale.views.robots import NoSearchIndexViewMixin
|
||||
|
||||
try:
|
||||
@@ -669,7 +669,7 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, CartMixin, TemplateView
|
||||
context['max_times'] = self.voucher.max_usages - self.voucher.redeemed
|
||||
|
||||
# Fetch all items
|
||||
items, display_add_to_cart = get_grouped_items(
|
||||
items, display_add_to_cart = prepare_item_list_for_shop(
|
||||
self.request.event,
|
||||
subevent=self.subevent,
|
||||
voucher=self.voucher,
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
|
||||
import calendar
|
||||
import hashlib
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from datetime import date, datetime, timedelta
|
||||
from decimal import Decimal
|
||||
@@ -47,10 +46,7 @@ from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
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.db.models import Count
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.decorators import method_decorator
|
||||
@@ -65,15 +61,9 @@ from django.views.generic import TemplateView
|
||||
|
||||
from pretix.base.auth import has_event_access_permission
|
||||
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
||||
from pretix.base.models import (
|
||||
ItemVariation, Quota, SalesChannel, SeatCategoryMapping, Voucher,
|
||||
)
|
||||
from pretix.base.models import Quota, Voucher
|
||||
from pretix.base.models.event import Event, SubEvent
|
||||
from pretix.base.models.items import (
|
||||
Item, ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation,
|
||||
)
|
||||
from pretix.base.services.placeholders import PlaceholderContext
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.base.timemachine import time_machine_now
|
||||
from pretix.helpers.compat import date_fromisocalendar
|
||||
from pretix.helpers.formats.en.formats import (
|
||||
@@ -82,7 +72,10 @@ from pretix.helpers.formats.en.formats import (
|
||||
from pretix.helpers.http import redirect_to_url
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.presale.ical import get_public_ical
|
||||
from pretix.presale.signals import item_description, seatingframe_html_head
|
||||
from pretix.presale.productlist import (
|
||||
item_group_by_category, prepare_item_list_for_shop,
|
||||
)
|
||||
from pretix.presale.signals import seatingframe_html_head
|
||||
from pretix.presale.views.organizer import (
|
||||
EventListMixin, add_subevents_for_days, days_for_template,
|
||||
filter_qs_by_attr, filter_subevents_with_plugins, has_before_after,
|
||||
@@ -94,391 +87,12 @@ from . import (
|
||||
iframe_entry_view_wrapper,
|
||||
)
|
||||
|
||||
from pretix.presale.productlist import prepare_item_list_for_shop as get_grouped_items # noqa
|
||||
|
||||
|
||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
|
||||
def item_group_by_category(items):
|
||||
return sorted(
|
||||
[
|
||||
# a group is a tuple of a category and a list of items
|
||||
(cat, [i for i in items if i.category == cat])
|
||||
for cat in set([i.category for i in items])
|
||||
# insert categories into a set for uniqueness
|
||||
# a set is unsorted, so sort again by category
|
||||
],
|
||||
key=lambda group: (group[0].position, group[0].id) if (
|
||||
group[0] is not None and group[0].id is not None) else (0, 0)
|
||||
)
|
||||
|
||||
|
||||
def get_grouped_items(event, *, channel: SalesChannel, subevent=None, voucher=None, require_seat=0, base_qs=None,
|
||||
allow_addons=False, allow_cross_sell=False,
|
||||
quota_cache=None, filter_items=None, filter_categories=None, memberships=None,
|
||||
ignore_hide_sold_out_for_item_ids=None):
|
||||
base_qs_set = base_qs is not None
|
||||
base_qs = base_qs if base_qs is not None else event.items
|
||||
|
||||
requires_seat = Exists(
|
||||
SeatCategoryMapping.objects.filter(
|
||||
product_id=OuterRef('pk'),
|
||||
subevent=subevent
|
||||
)
|
||||
)
|
||||
if not event.settings.seating_choice:
|
||||
requires_seat = Value(0, output_field=IntegerField())
|
||||
|
||||
variation_q = (
|
||||
Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()) | Q(available_from_mode='info')) &
|
||||
Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()) | Q(available_until_mode='info'))
|
||||
)
|
||||
if not voucher or not voucher.show_hidden_items:
|
||||
variation_q &= Q(hide_without_voucher=False)
|
||||
|
||||
if memberships is not None:
|
||||
prefetch_membership_types = ['require_membership_types']
|
||||
else:
|
||||
prefetch_membership_types = []
|
||||
|
||||
prefetch_var = Prefetch(
|
||||
'variations',
|
||||
to_attr='available_variations',
|
||||
queryset=ItemVariation.objects.using(settings.DATABASE_REPLICA).annotate(
|
||||
subevent_disabled=Exists(
|
||||
SubEventItemVariation.objects.filter(
|
||||
Q(disabled=True)
|
||||
| (Exact(OuterRef('available_from_mode'), 'hide') & Q(available_from__gt=time_machine_now()))
|
||||
| (Exact(OuterRef('available_until_mode'), 'hide') & Q(available_until__lt=time_machine_now())),
|
||||
variation_id=OuterRef('pk'),
|
||||
subevent=subevent,
|
||||
)
|
||||
),
|
||||
).filter(
|
||||
variation_q,
|
||||
Q(all_sales_channels=True) | Q(limit_sales_channels=channel),
|
||||
Exists(Quota.variations.through.objects.filter(quota__subevent_id=subevent, itemvariation_id=OuterRef("pk"))),
|
||||
active=True,
|
||||
subevent_disabled=False
|
||||
).prefetch_related(
|
||||
*prefetch_membership_types,
|
||||
Prefetch('quotas',
|
||||
to_attr='_subevent_quotas',
|
||||
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(
|
||||
subevent=subevent).select_related("subevent"))
|
||||
).distinct()
|
||||
)
|
||||
prefetch_quotas = Prefetch(
|
||||
'quotas',
|
||||
to_attr='_subevent_quotas',
|
||||
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(subevent=subevent).select_related("subevent")
|
||||
)
|
||||
prefetch_bundles = Prefetch(
|
||||
'bundles',
|
||||
queryset=ItemBundle.objects.using(settings.DATABASE_REPLICA).prefetch_related(
|
||||
Prefetch('bundled_item',
|
||||
queryset=event.items.using(settings.DATABASE_REPLICA).select_related(
|
||||
'tax_rule').prefetch_related(
|
||||
Prefetch('quotas',
|
||||
to_attr='_subevent_quotas',
|
||||
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(
|
||||
subevent=subevent)),
|
||||
)),
|
||||
Prefetch('bundled_variation',
|
||||
queryset=ItemVariation.objects.using(
|
||||
settings.DATABASE_REPLICA
|
||||
).select_related('item', 'item__tax_rule').filter(item__event=event).prefetch_related(
|
||||
Prefetch('quotas',
|
||||
to_attr='_subevent_quotas',
|
||||
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(
|
||||
subevent=subevent)),
|
||||
)),
|
||||
)
|
||||
)
|
||||
|
||||
items = base_qs.using(settings.DATABASE_REPLICA).filter_available(
|
||||
channel=channel.identifier, voucher=voucher, allow_addons=allow_addons, allow_cross_sell=allow_cross_sell
|
||||
).select_related(
|
||||
'category', 'tax_rule', # for re-grouping
|
||||
'hidden_if_available',
|
||||
).prefetch_related(
|
||||
*prefetch_membership_types,
|
||||
Prefetch(
|
||||
'hidden_if_item_available',
|
||||
queryset=event.items.annotate(
|
||||
has_variations=Count('variations'),
|
||||
).prefetch_related(
|
||||
prefetch_var,
|
||||
prefetch_quotas,
|
||||
prefetch_bundles,
|
||||
)
|
||||
),
|
||||
prefetch_quotas,
|
||||
prefetch_var,
|
||||
prefetch_bundles,
|
||||
).annotate(
|
||||
has_variations=Count('variations'),
|
||||
subevent_disabled=Exists(
|
||||
SubEventItem.objects.filter(
|
||||
Q(disabled=True)
|
||||
| (Exact(OuterRef('available_from_mode'), 'hide') & Q(available_from__gt=time_machine_now()))
|
||||
| (Exact(OuterRef('available_until_mode'), 'hide') & Q(available_until__lt=time_machine_now())),
|
||||
item_id=OuterRef('pk'),
|
||||
subevent=subevent,
|
||||
)
|
||||
),
|
||||
mandatory_priced_addons=Exists(
|
||||
ItemAddOn.objects.filter(
|
||||
base_item_id=OuterRef('pk'),
|
||||
min_count__gte=1,
|
||||
price_included=False
|
||||
)
|
||||
),
|
||||
requires_seat=requires_seat,
|
||||
).filter(
|
||||
Exists(Quota.items.through.objects.filter(quota__subevent_id=subevent, item_id=OuterRef("pk"))),
|
||||
subevent_disabled=False,
|
||||
).order_by('category__position', 'category_id', 'position', 'name')
|
||||
if require_seat:
|
||||
items = items.filter(requires_seat__gt=0)
|
||||
elif require_seat is not None:
|
||||
items = items.filter(requires_seat=0)
|
||||
|
||||
if filter_items:
|
||||
items = items.filter(pk__in=[a for a in filter_items if a.isdigit()])
|
||||
if filter_categories:
|
||||
items = items.filter(category_id__in=[a for a in filter_categories if a.isdigit()])
|
||||
|
||||
display_add_to_cart = False
|
||||
quota_cache_key = f'item_quota_cache:{subevent.id if subevent else 0}:{channel.identifier}:{bool(require_seat)}'
|
||||
quota_cache = quota_cache or event.cache.get(quota_cache_key) or {}
|
||||
quota_cache_existed = bool(quota_cache)
|
||||
|
||||
if subevent:
|
||||
item_price_override = subevent.item_price_overrides
|
||||
var_price_override = subevent.var_price_overrides
|
||||
else:
|
||||
item_price_override = {}
|
||||
var_price_override = {}
|
||||
|
||||
restrict_vars = set()
|
||||
if voucher and voucher.quota_id:
|
||||
# If a voucher is set to a specific quota, we need to filter out on that level
|
||||
restrict_vars = set(voucher.quota.variations.all())
|
||||
|
||||
quotas_to_compute = []
|
||||
for item in items:
|
||||
assert item.event_id == event.pk
|
||||
item.event = event # save a database query if this is looked up
|
||||
if item.has_variations:
|
||||
for v in item.available_variations:
|
||||
for q in v._subevent_quotas:
|
||||
if q.pk not in quota_cache:
|
||||
quotas_to_compute.append(q)
|
||||
else:
|
||||
for q in item._subevent_quotas:
|
||||
if q.pk not in quota_cache:
|
||||
quotas_to_compute.append(q)
|
||||
|
||||
if quotas_to_compute:
|
||||
qa = QuotaAvailability()
|
||||
qa.queue(*quotas_to_compute)
|
||||
qa.compute()
|
||||
quota_cache.update({q.pk: r for q, r in qa.results.items()})
|
||||
|
||||
for item in items:
|
||||
if voucher and voucher.item_id and voucher.variation_id:
|
||||
# Restrict variations if the voucher only allows one
|
||||
item.available_variations = [v for v in item.available_variations
|
||||
if v.pk == voucher.variation_id]
|
||||
|
||||
if channel.type_instance.unlimited_items_per_order:
|
||||
max_per_order = sys.maxsize
|
||||
else:
|
||||
max_per_order = item.max_per_order or int(event.settings.max_items_per_order)
|
||||
if voucher:
|
||||
max_per_order = min(max_per_order, voucher.max_usages - voucher.redeemed)
|
||||
|
||||
if item.hidden_if_available:
|
||||
q = item.hidden_if_available.availability(_cache=quota_cache)
|
||||
if q[0] == Quota.AVAILABILITY_OK:
|
||||
item._remove = True
|
||||
continue
|
||||
|
||||
if item.hidden_if_item_available:
|
||||
if item.hidden_if_item_available.has_variations:
|
||||
item._dependency_available = any(
|
||||
var.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)[0] == Quota.AVAILABILITY_OK
|
||||
for var in item.hidden_if_item_available.available_variations
|
||||
)
|
||||
else:
|
||||
q = item.hidden_if_item_available.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
|
||||
time_available = item.hidden_if_item_available.is_available()
|
||||
item._dependency_available = (q[0] == Quota.AVAILABILITY_OK) and time_available
|
||||
if item._dependency_available and item.hidden_if_item_available_mode == Item.UNAVAIL_MODE_HIDDEN:
|
||||
item._remove = True
|
||||
continue
|
||||
|
||||
if item.require_membership and item.require_membership_hidden:
|
||||
if not memberships or not any([m.membership_type in item.require_membership_types.all() for m in memberships]):
|
||||
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:
|
||||
item.description += ("<br/>" if item.description else "") + resp
|
||||
|
||||
if not item.has_variations:
|
||||
item._remove = False
|
||||
if not bool(item._subevent_quotas):
|
||||
item._remove = True
|
||||
continue
|
||||
|
||||
if voucher and (voucher.allow_ignore_quota or voucher.block_quota):
|
||||
item.cached_availability = (
|
||||
Quota.AVAILABILITY_OK, voucher.max_usages - voucher.redeemed
|
||||
)
|
||||
else:
|
||||
item.cached_availability = list(
|
||||
item.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
|
||||
)
|
||||
|
||||
if not (
|
||||
ignore_hide_sold_out_for_item_ids and item.pk in ignore_hide_sold_out_for_item_ids
|
||||
) and event.settings.hide_sold_out and item.cached_availability[0] < Quota.AVAILABILITY_RESERVED:
|
||||
item._remove = True
|
||||
continue
|
||||
|
||||
item.order_max = min(
|
||||
item.cached_availability[1]
|
||||
if item.cached_availability[1] is not None else sys.maxsize,
|
||||
max_per_order
|
||||
)
|
||||
|
||||
original_price = item_price_override.get(item.pk, item.default_price)
|
||||
voucher_reduced = False
|
||||
if voucher:
|
||||
price = voucher.calculate_price(original_price)
|
||||
voucher_reduced = price < original_price
|
||||
include_bundled = not voucher.all_bundles_included
|
||||
else:
|
||||
price = original_price
|
||||
include_bundled = True
|
||||
|
||||
item.display_price = item.tax(price, currency=event.currency, include_bundled=include_bundled)
|
||||
if item.free_price and item.free_price_suggestion is not None and not voucher_reduced:
|
||||
item.suggested_price = item.tax(max(price, item.free_price_suggestion), currency=event.currency, include_bundled=include_bundled)
|
||||
else:
|
||||
item.suggested_price = item.display_price
|
||||
|
||||
if price != original_price:
|
||||
item.original_price = item.tax(original_price, currency=event.currency, include_bundled=True)
|
||||
else:
|
||||
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
|
||||
if item.original_price else None
|
||||
)
|
||||
if not display_add_to_cart:
|
||||
display_add_to_cart = not item.requires_seat and item.order_max > 0
|
||||
else:
|
||||
for var in item.available_variations:
|
||||
if var.require_membership and var.require_membership_hidden:
|
||||
if not memberships or not any([m.membership_type in var.require_membership_types.all() for m in memberships]):
|
||||
var._remove = True
|
||||
continue
|
||||
|
||||
var.description = str(var.description)
|
||||
for recv, resp in item_description.send(sender=event, item=item, variation=var, subevent=subevent):
|
||||
if resp:
|
||||
var.description += ("<br/>" if var.description else "") + resp
|
||||
|
||||
if voucher and (voucher.allow_ignore_quota or voucher.block_quota):
|
||||
var.cached_availability = (
|
||||
Quota.AVAILABILITY_OK, voucher.max_usages - voucher.redeemed
|
||||
)
|
||||
else:
|
||||
var.cached_availability = list(
|
||||
var.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
|
||||
)
|
||||
|
||||
var.order_max = min(
|
||||
var.cached_availability[1]
|
||||
if var.cached_availability[1] is not None else sys.maxsize,
|
||||
max_per_order
|
||||
)
|
||||
|
||||
original_price = var_price_override.get(var.pk, var.price)
|
||||
voucher_reduced = False
|
||||
if voucher:
|
||||
price = voucher.calculate_price(original_price)
|
||||
voucher_reduced = price < original_price
|
||||
include_bundled = not voucher.all_bundles_included
|
||||
else:
|
||||
price = original_price
|
||||
include_bundled = True
|
||||
|
||||
var.display_price = var.tax(price, currency=event.currency, include_bundled=include_bundled)
|
||||
|
||||
if item.free_price and var.free_price_suggestion is not None and not voucher_reduced:
|
||||
var.suggested_price = item.tax(max(price, var.free_price_suggestion), currency=event.currency,
|
||||
include_bundled=include_bundled)
|
||||
elif item.free_price and item.free_price_suggestion is not None and not voucher_reduced:
|
||||
var.suggested_price = item.tax(max(price, item.free_price_suggestion), currency=event.currency,
|
||||
include_bundled=include_bundled)
|
||||
else:
|
||||
var.suggested_price = var.display_price
|
||||
|
||||
if price != original_price:
|
||||
var.original_price = var.tax(original_price, currency=event.currency, include_bundled=True)
|
||||
else:
|
||||
var.original_price = (
|
||||
var.tax(var.original_price or item.original_price, currency=event.currency,
|
||||
include_bundled=True,
|
||||
base_price_is='net' if event.settings.display_net_prices else 'gross') # backwards-compat
|
||||
) if var.original_price or item.original_price else None
|
||||
|
||||
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
|
||||
if item.original_price else None
|
||||
)
|
||||
|
||||
item.available_variations = [
|
||||
v for v in item.available_variations if v._subevent_quotas and (
|
||||
not voucher or not voucher.quota_id or v in restrict_vars
|
||||
) and not getattr(v, '_remove', False)
|
||||
]
|
||||
|
||||
if not (ignore_hide_sold_out_for_item_ids and item.pk in ignore_hide_sold_out_for_item_ids) and event.settings.hide_sold_out:
|
||||
item.available_variations = [v for v in item.available_variations
|
||||
if v.cached_availability[0] >= Quota.AVAILABILITY_RESERVED]
|
||||
|
||||
if voucher and voucher.variation_id:
|
||||
item.available_variations = [v for v in item.available_variations
|
||||
if v.pk == voucher.variation_id]
|
||||
|
||||
if len(item.available_variations) > 0:
|
||||
item.min_price = min([v.display_price.net if event.settings.display_net_prices else
|
||||
v.display_price.gross for v in item.available_variations])
|
||||
item.max_price = max([v.display_price.net if event.settings.display_net_prices else
|
||||
v.display_price.gross for v in item.available_variations])
|
||||
item.best_variation_availability = max([v.cached_availability[0] for v in item.available_variations])
|
||||
|
||||
item._remove = not bool(item.available_variations)
|
||||
if not item._remove and not display_add_to_cart:
|
||||
display_add_to_cart = not item.requires_seat and any(v.order_max > 0 for v in item.available_variations)
|
||||
|
||||
if not quota_cache_existed and not voucher and not allow_addons and not base_qs_set and not filter_items and not filter_categories:
|
||||
event.cache.set(quota_cache_key, quota_cache, 5)
|
||||
items = [item for item in items
|
||||
if (len(item.available_variations) > 0 or not item.has_variations) and not item._remove]
|
||||
return items, display_add_to_cart
|
||||
|
||||
|
||||
@method_decorator(allow_frame_if_namespaced, 'dispatch')
|
||||
@method_decorator(iframe_entry_view_wrapper, 'dispatch')
|
||||
class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
|
||||
@@ -582,7 +196,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
|
||||
|
||||
if not self.request.event.has_subevents or self.subevent:
|
||||
# Fetch all items
|
||||
items, display_add_to_cart = get_grouped_items(
|
||||
items, display_add_to_cart = prepare_item_list_for_shop(
|
||||
self.request.event,
|
||||
subevent=self.subevent,
|
||||
filter_items=self.request.GET.getlist('item'),
|
||||
@@ -634,10 +248,6 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
|
||||
context['has_addon_choices'] = any(cp.has_addon_choices for cp in get_cart(self.request))
|
||||
|
||||
templating_context = PlaceholderContext(event_or_subevent=self.subevent or self.request.event, event=self.request.event)
|
||||
|
||||
for field in ('presale_has_ended_text',):
|
||||
context[field] = templating_context.format(str(self.request.event.settings[field]))
|
||||
|
||||
if self.subevent:
|
||||
context['frontpage_text'] = templating_context.format(str(self.subevent.frontpage_text))
|
||||
else:
|
||||
|
||||
@@ -79,7 +79,7 @@ from pretix.base.services.invoices import (
|
||||
)
|
||||
from pretix.base.services.orders import (
|
||||
OrderChangeManager, OrderError, _try_auto_refund, cancel_order,
|
||||
change_payment_provider, error_messages,
|
||||
change_payment_provider,
|
||||
)
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.base.services.tickets import generate, invalidate_cache
|
||||
@@ -92,11 +92,11 @@ from pretix.helpers.safedownload import check_token
|
||||
from pretix.multidomain.urlreverse import eventreverse, eventreverse_absolute
|
||||
from pretix.presale.forms.checkout import InvoiceAddressForm, QuestionsForm
|
||||
from pretix.presale.forms.order import OrderPositionChangeForm
|
||||
from pretix.presale.productlist import prepare_item_list_for_shop
|
||||
from pretix.presale.signals import question_form_fields_overrides
|
||||
from pretix.presale.views import (
|
||||
CartMixin, EventViewMixin, iframe_entry_view_wrapper,
|
||||
)
|
||||
from pretix.presale.views.event import get_grouped_items
|
||||
from pretix.presale.views.robots import NoSearchIndexViewMixin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -1452,7 +1452,7 @@ class OrderChangeMixin:
|
||||
|
||||
if ckey not in item_cache:
|
||||
# Get all items to possibly show
|
||||
items, _btn = get_grouped_items(
|
||||
items, _btn = prepare_item_list_for_shop(
|
||||
self.request.event,
|
||||
subevent=p.subevent,
|
||||
voucher=None,
|
||||
@@ -1592,30 +1592,6 @@ class OrderChangeMixin:
|
||||
if val:
|
||||
selected[i, None] = val, price
|
||||
|
||||
if sum(a[0] for a in selected.values()) > category['max_count']:
|
||||
raise ValidationError(
|
||||
error_messages['addon_max_count'] % {
|
||||
'base': str(form['pos'].item.name),
|
||||
'max': category['max_count'],
|
||||
'cat': str(category['category'].name),
|
||||
}
|
||||
)
|
||||
elif sum(a[0] for a in selected.values()) < category['min_count']:
|
||||
raise ValidationError(
|
||||
error_messages['addon_min_count'] % {
|
||||
'base': str(form['pos'].item.name),
|
||||
'min': category['min_count'],
|
||||
'cat': str(category['category'].name),
|
||||
}
|
||||
)
|
||||
elif any(sum(v[0] for k, v in selected.items() if k[0] == i) > 1 for i in category['items']) and not category['multi_allowed']:
|
||||
raise ValidationError(
|
||||
error_messages['addon_no_multi'] % {
|
||||
'base': str(form['pos'].item.name),
|
||||
'cat': str(category['category'].name),
|
||||
}
|
||||
)
|
||||
|
||||
return selected
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
@@ -40,8 +40,8 @@ from pretix.presale.views import EventViewMixin, iframe_entry_view_wrapper
|
||||
from ...base.i18n import get_language_without_region
|
||||
from ...base.models import Voucher, WaitingListEntry
|
||||
from ..forms.waitinglist import WaitingListForm
|
||||
from ..productlist import prepare_item_list_for_shop
|
||||
from . import allow_frame_if_namespaced
|
||||
from .event import get_grouped_items
|
||||
|
||||
|
||||
@method_decorator(allow_frame_if_namespaced, 'dispatch')
|
||||
@@ -53,7 +53,7 @@ class WaitingView(EventViewMixin, FormView):
|
||||
@cached_property
|
||||
def itemvars(self):
|
||||
customer = getattr(self.request, 'customer', None)
|
||||
items, display_add_to_cart = get_grouped_items(
|
||||
items, display_add_to_cart = prepare_item_list_for_shop(
|
||||
self.request.event,
|
||||
subevent=self.subevent,
|
||||
require_seat=None,
|
||||
|
||||
@@ -67,11 +67,11 @@ from pretix.helpers.daterange import daterange
|
||||
from pretix.helpers.thumb import get_thumbnail
|
||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
||||
from pretix.presale.forms.organizer import meta_filtersets
|
||||
from pretix.presale.productlist import (
|
||||
item_group_by_category, prepare_item_list_for_shop,
|
||||
)
|
||||
from pretix.presale.style import get_theme_vars_css
|
||||
from pretix.presale.views.cart import get_or_create_cart_id
|
||||
from pretix.presale.views.event import (
|
||||
get_grouped_items, item_group_by_category,
|
||||
)
|
||||
from pretix.presale.views.organizer import (
|
||||
EventListMixin, add_events_for_days, add_subevents_for_days,
|
||||
days_for_template, filter_qs_by_attr, filter_subevents_with_plugins,
|
||||
@@ -321,7 +321,7 @@ class WidgetAPIProductList(EventListMixin, View):
|
||||
).values_list('item_id', flat=True)
|
||||
)
|
||||
|
||||
items, display_add_to_cart = get_grouped_items(
|
||||
items, display_add_to_cart = prepare_item_list_for_shop(
|
||||
self.request.event,
|
||||
subevent=self.subevent,
|
||||
voucher=self.voucher,
|
||||
|
||||
@@ -874,14 +874,6 @@ function setup_basics(el) {
|
||||
});
|
||||
});
|
||||
|
||||
el.find(".qrcode-canvas").each(function () {
|
||||
$(this).qrcode(
|
||||
{
|
||||
text: $.trim($($(this).attr("data-qrdata")).html())
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
el.find(".propagated-settings-box").find("input, textarea, select").not("[readonly]")
|
||||
.attr("data-propagated-locked", "true").prop("readonly", true);
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/*global $, gettext*/
|
||||
$(function () {
|
||||
// Question view
|
||||
if (!$(".form-order-change").length) {
|
||||
return;
|
||||
}
|
||||
@@ -57,51 +56,4 @@ $(function () {
|
||||
}
|
||||
});
|
||||
});
|
||||
$('[data-model-select2=seat]').each(function () {
|
||||
var $s = $(this);
|
||||
$s.select2({
|
||||
theme: "bootstrap",
|
||||
delay: 100,
|
||||
allowClear: !$s.prop("required"),
|
||||
width: '100%',
|
||||
language: $("body").attr("data-select2-locale"),
|
||||
placeholder: $(this).attr("data-placeholder"),
|
||||
ajax: {
|
||||
url: function() {
|
||||
var se = $(this).closest(".form-order-change, .form-horizontal").attr("data-subevent");
|
||||
var url = $(this).attr('data-select2-url');
|
||||
var changed = $(this).closest(".form-order-change, .form-horizontal").find("[id$=subevent]").val();
|
||||
if (changed) {
|
||||
return url + '?subevent=' + changed;
|
||||
} else if (se) {
|
||||
return url + '?subevent=' + se;
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
},
|
||||
data: function (params) {
|
||||
return {
|
||||
query: params.term,
|
||||
page: params.page || 1
|
||||
}
|
||||
}
|
||||
},
|
||||
templateResult: function (res) {
|
||||
if (!res.id) {
|
||||
return res.text;
|
||||
}
|
||||
var $ret = $("<span>").append(
|
||||
$("<span>").addClass("primary").append($("<div>").text(res.text).html())
|
||||
);
|
||||
if (res.event) {
|
||||
$ret.append(
|
||||
$("<span>").addClass("secondary").append(
|
||||
$("<span>").addClass("fa fa-calendar fa-fw")
|
||||
).append(" ").append($("<div>").text(res.event).html())
|
||||
);
|
||||
}
|
||||
return $ret;
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -646,7 +646,10 @@ ul.pagination {
|
||||
.table-payment-providers > tbody > tr > td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.success-left {
|
||||
border-left: 3px solid $brand-success;
|
||||
background: var(--pretix-brand-success-lighten-50);
|
||||
}
|
||||
|
||||
details {
|
||||
list-style: none;
|
||||
|
||||
@@ -994,7 +994,7 @@ class OrderChangeAddonsTest(BaseOrdersTest):
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.total == Decimal('23.00')
|
||||
|
||||
def test_do_not_remove_unavailable_on_adding(self):
|
||||
def test_do_not_remove_timeunavailable_on_adding(self):
|
||||
self.iao.max_count = 2
|
||||
self.iao.save()
|
||||
self.workshop1.available_until = now() - datetime.timedelta(days=1)
|
||||
@@ -1036,6 +1036,48 @@ class OrderChangeAddonsTest(BaseOrdersTest):
|
||||
with scopes_disabled():
|
||||
assert self.ticket_pos.addons.count() == 2
|
||||
|
||||
def test_do_not_remove_dependencyunavailable_on_adding(self):
|
||||
self.iao.max_count = 2
|
||||
self.iao.save()
|
||||
self.workshop1.hidden_if_item_available = self.workshop2
|
||||
self.workshop1.save()
|
||||
with scopes_disabled():
|
||||
OrderPosition.objects.create(
|
||||
order=self.order,
|
||||
item=self.workshop1,
|
||||
variation=None,
|
||||
price=Decimal("12"),
|
||||
addon_to=self.ticket_pos,
|
||||
attendee_name_parts={'full_name': "Peter"}
|
||||
)
|
||||
self.order.total += Decimal("12")
|
||||
self.order.save()
|
||||
|
||||
response = self.client.get(
|
||||
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert '<li>1x Workshop 1</li>' in response.content.decode()
|
||||
assert f'cp_{self.ticket_pos.pk}_item_{self.workshop1.pk}' not in response.content.decode()
|
||||
|
||||
response = self.client.post(
|
||||
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
|
||||
{
|
||||
f'cp_{self.ticket_pos.pk}_variation_{self.workshop2.pk}_{self.workshop2a.pk}': '1'
|
||||
},
|
||||
follow=True
|
||||
)
|
||||
doc = BeautifulSoup(response.content.decode(), "lxml")
|
||||
form_data = extract_form_fields(doc.select('.main-box form')[0])
|
||||
form_data['confirm'] = 'true'
|
||||
response = self.client.post(
|
||||
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), form_data, follow=True
|
||||
)
|
||||
assert 'alert-success' in response.content.decode()
|
||||
|
||||
with scopes_disabled():
|
||||
assert self.ticket_pos.addons.count() == 2
|
||||
|
||||
def test_do_not_overbook_unavailable_on_adding(self):
|
||||
self.iao.max_count = 1
|
||||
self.iao.save()
|
||||
|
||||
Reference in New Issue
Block a user