mirror of
https://github.com/pretix/pretix.git
synced 2025-12-11 01:22:28 +00:00
Compare commits
15 Commits
fix-datasy
...
subevent-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2237af4c0 | ||
|
|
43c2f15994 | ||
|
|
571724b1f7 | ||
|
|
85a9a3caa6 | ||
|
|
42b1010c36 | ||
|
|
5b851e270b | ||
|
|
2b796aa45e | ||
|
|
11460d878b | ||
|
|
1ce4c11572 | ||
|
|
11269c277b | ||
|
|
2650bf6f4f | ||
|
|
301191e4bd | ||
|
|
867cd8c59e | ||
|
|
7e8da3cef6 | ||
|
|
25f57f89b0 |
@@ -439,8 +439,12 @@ def register_default_webhook_events(sender, **kwargs):
|
||||
def notify_webhooks(logentry_ids: list):
|
||||
if not isinstance(logentry_ids, list):
|
||||
logentry_ids = [logentry_ids]
|
||||
qs = LogEntry.all.select_related('event', 'event__organizer', 'organizer').filter(id__in=logentry_ids)
|
||||
_org, _at, webhooks = None, None, None
|
||||
qs = LogEntry.all.select_related(
|
||||
'event', 'event__organizer', 'organizer'
|
||||
).order_by(
|
||||
'action_type', 'organizer_id', 'event_id',
|
||||
).filter(id__in=logentry_ids)
|
||||
_org, _at, _ev, webhooks = None, None, None, None
|
||||
for logentry in qs:
|
||||
if not logentry.organizer:
|
||||
break # We need to know the organizer
|
||||
@@ -450,7 +454,7 @@ def notify_webhooks(logentry_ids: list):
|
||||
if not notification_type:
|
||||
break # Ignore, no webhooks for this event type
|
||||
|
||||
if _org != logentry.organizer or _at != logentry.action_type or webhooks is None:
|
||||
if _org != logentry.organizer or _at != logentry.action_type or _ev != logentry.event_id or webhooks is None:
|
||||
_org = logentry.organizer
|
||||
_at = logentry.action_type
|
||||
|
||||
|
||||
@@ -105,6 +105,18 @@ class BaseExporter:
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def repeatable_read(self) -> bool:
|
||||
"""
|
||||
If ``True``, this exporter will be run in a REPEATABLE READ transaction. This ensures consistent results for
|
||||
all queries performed by the exporter, but creates a performance burden on the database server. We recommend to
|
||||
disable this for exporters that take very long to run and do not rely on this behavior, such as export of lists
|
||||
to CSV files.
|
||||
|
||||
Defaults to ``True`` for now, but default may change in future versions.
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
"""
|
||||
|
||||
@@ -180,6 +180,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
|
||||
'includes two sheets, one with a line for every invoice, and one with a line for every position of '
|
||||
'every invoice.')
|
||||
featured = True
|
||||
repeatable_read = False
|
||||
|
||||
@property
|
||||
def additional_form_fields(self):
|
||||
|
||||
@@ -90,6 +90,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
'with a line for every order, one with a line for every order position, and one with '
|
||||
'a line for every additional fee charged in an order.')
|
||||
featured = True
|
||||
repeatable_read = False
|
||||
|
||||
@cached_property
|
||||
def providers(self):
|
||||
@@ -842,6 +843,7 @@ class TransactionListExporter(ListExporter):
|
||||
description = gettext_lazy('Download a spreadsheet of all substantial changes to orders, i.e. all changes to '
|
||||
'products, prices or tax rates. The information is only accurate for changes made with '
|
||||
'pretix versions released after October 2021.')
|
||||
repeatable_read = False
|
||||
|
||||
@cached_property
|
||||
def providers(self):
|
||||
@@ -1020,6 +1022,7 @@ class PaymentListExporter(ListExporter):
|
||||
category = pgettext_lazy('export_category', 'Order data')
|
||||
description = gettext_lazy('Download a spreadsheet of all payments or refunds of every order.')
|
||||
featured = True
|
||||
repeatable_read = False
|
||||
|
||||
@property
|
||||
def additional_form_fields(self):
|
||||
@@ -1159,7 +1162,7 @@ class QuotaListExporter(ListExporter):
|
||||
yield headers
|
||||
|
||||
quotas = list(self.event.quotas.select_related('subevent'))
|
||||
qa = QuotaAvailability(full_results=True)
|
||||
qa = QuotaAvailability(full_results=True, allow_repeatable_read=False)
|
||||
qa.queue(*quotas)
|
||||
qa.compute()
|
||||
|
||||
@@ -1200,6 +1203,7 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
organizer_required_permission = 'can_manage_gift_cards'
|
||||
category = pgettext_lazy('export_category', 'Gift cards')
|
||||
description = gettext_lazy('Download a spreadsheet of all gift card transactions.')
|
||||
repeatable_read = False
|
||||
|
||||
@property
|
||||
def additional_form_fields(self):
|
||||
@@ -1258,6 +1262,7 @@ class GiftcardRedemptionListExporter(ListExporter):
|
||||
verbose_name = gettext_lazy('Gift card redemptions')
|
||||
category = pgettext_lazy('export_category', 'Order data')
|
||||
description = gettext_lazy('Download a spreadsheet of all payments or refunds that involve gift cards.')
|
||||
repeatable_read = False
|
||||
|
||||
def iterate_list(self, form_data):
|
||||
payments = OrderPayment.objects.filter(
|
||||
|
||||
@@ -34,6 +34,7 @@ class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
verbose_name = _('Reusable media')
|
||||
category = pgettext_lazy('export_category', 'Reusable media')
|
||||
description = _('Download a spread sheet with the data of all reusable medias on your account.')
|
||||
repeatable_read = False
|
||||
|
||||
def iterate_list(self, form_data):
|
||||
media = ReusableMedium.objects.filter(
|
||||
|
||||
@@ -41,6 +41,7 @@ class WaitingListExporter(ListExporter):
|
||||
verbose_name = _('Waiting list')
|
||||
category = pgettext_lazy('export_category', 'Waiting list')
|
||||
description = _('Download a spread sheet with all your waiting list data.')
|
||||
repeatable_read = False
|
||||
|
||||
# map selected status to label and queryset-filter
|
||||
status_filters = [
|
||||
|
||||
@@ -1840,6 +1840,10 @@ class OrderPayment(models.Model):
|
||||
))
|
||||
return False
|
||||
|
||||
if locked_instance.state == OrderPayment.PAYMENT_STATE_CANCELED:
|
||||
# Never send mails when the payment was already canceled intentionally
|
||||
send_mail = False
|
||||
|
||||
if isinstance(info, str):
|
||||
locked_instance.info = info
|
||||
elif info:
|
||||
@@ -1855,6 +1859,10 @@ class OrderPayment(models.Model):
|
||||
'data': log_data,
|
||||
}, user=user, auth=auth)
|
||||
|
||||
if self.order.status in (Order.STATUS_PAID, Order.STATUS_CANCELED, Order.STATUS_EXPIRED):
|
||||
# No reason to send mail, as the payment is no longer really expected
|
||||
send_mail = False
|
||||
|
||||
if send_mail:
|
||||
with language(self.order.locale, self.order.event.settings.region):
|
||||
email_subject = self.order.event.settings.mail_subject_order_payment_failed
|
||||
|
||||
@@ -49,7 +49,7 @@ from pretix.base.signals import (
|
||||
periodic_task, register_data_exporters, register_multievent_data_exporters,
|
||||
)
|
||||
from pretix.celery_app import app
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers import OF_SELF, repeatable_reads_transaction
|
||||
from pretix.helpers.urls import build_absolute_uri
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -80,7 +80,12 @@ def export(self, event: Event, fileid: str, provider: str, form_data: Dict[str,
|
||||
continue
|
||||
ex = response(event, event.organizer, set_progress)
|
||||
if ex.identifier == provider:
|
||||
d = ex.render(form_data)
|
||||
if ex.repeatable_read:
|
||||
with repeatable_reads_transaction():
|
||||
d = ex.render(form_data)
|
||||
else:
|
||||
d = ex.render(form_data)
|
||||
|
||||
if d is None:
|
||||
raise ExportError(
|
||||
gettext('Your export did not contain any data.')
|
||||
@@ -151,7 +156,11 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
|
||||
gettext('You do not have sufficient permission to perform this export.')
|
||||
)
|
||||
|
||||
d = ex.render(form_data)
|
||||
if ex.repeatable_read:
|
||||
with repeatable_reads_transaction():
|
||||
d = ex.render(form_data)
|
||||
else:
|
||||
d = ex.render(form_data)
|
||||
if d is None:
|
||||
raise ExportError(
|
||||
gettext('Your export did not contain any data.')
|
||||
@@ -209,7 +218,11 @@ def _run_scheduled_export(schedule, context: Union[Event, Organizer], exporter,
|
||||
try:
|
||||
if not exporter:
|
||||
raise ExportError("Export type not found.")
|
||||
d = exporter.render(schedule.export_form_data)
|
||||
if exporter.repeatable_read:
|
||||
with repeatable_reads_transaction():
|
||||
d = exporter.render(schedule.export_form_data)
|
||||
else:
|
||||
d = exporter.render(schedule.export_form_data)
|
||||
if d is None:
|
||||
raise ExportEmptyError(
|
||||
gettext('Your export did not contain any data.')
|
||||
|
||||
@@ -33,8 +33,8 @@ from pretix.base.modelimport import DataImportError, ImportColumn, parse_csv
|
||||
from pretix.base.modelimport_orders import get_order_import_columns
|
||||
from pretix.base.modelimport_vouchers import get_voucher_import_columns
|
||||
from pretix.base.models import (
|
||||
CachedFile, Event, InvoiceAddress, Order, OrderPayment, OrderPosition,
|
||||
User, Voucher,
|
||||
CachedFile, Event, InvoiceAddress, LogEntry, Order, OrderPayment,
|
||||
OrderPosition, User, Voucher,
|
||||
)
|
||||
from pretix.base.models.orders import Transaction
|
||||
from pretix.base.services.invoices import generate_invoice, invoice_qualified
|
||||
@@ -175,6 +175,7 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user,
|
||||
raise DataImportError(_('The seat you selected has already been taken. Please select a different seat.'))
|
||||
|
||||
save_transactions = []
|
||||
save_logentries = []
|
||||
for o in orders:
|
||||
o.total = sum([c.price for c in o._positions]) # currently no support for fees
|
||||
if o.total == Decimal('0.00'):
|
||||
@@ -211,13 +212,15 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user,
|
||||
o._address.save()
|
||||
for c in cols:
|
||||
c.save(o)
|
||||
o.log_action(
|
||||
save_logentries.append(o.log_action(
|
||||
'pretix.event.order.placed',
|
||||
user=user,
|
||||
data={'source': 'import'}
|
||||
)
|
||||
data={'source': 'import'},
|
||||
save=False,
|
||||
))
|
||||
save_transactions += o.create_transactions(is_new=True, fees=[], positions=o._positions, save=False)
|
||||
Transaction.objects.bulk_create(save_transactions)
|
||||
LogEntry.bulk_create_and_postprocess(save_logentries)
|
||||
|
||||
for o in orders:
|
||||
with language(o.locale, event.settings.region):
|
||||
@@ -286,13 +289,16 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
|
||||
raise DataImportError(
|
||||
_('The seat you selected has already been taken. Please select a different seat.'))
|
||||
|
||||
save_logentries = []
|
||||
for v in vouchers:
|
||||
v.save()
|
||||
v.log_action(
|
||||
save_logentries.append(v.log_action(
|
||||
'pretix.voucher.added',
|
||||
user=user,
|
||||
data={'source': 'import'}
|
||||
)
|
||||
data={'source': 'import'},
|
||||
save=False,
|
||||
))
|
||||
for c in cols:
|
||||
c.save(v)
|
||||
LogEntry.bulk_create_and_postprocess(save_logentries)
|
||||
cf.delete()
|
||||
|
||||
@@ -41,7 +41,11 @@ def notify(logentry_ids: list):
|
||||
if not isinstance(logentry_ids, list):
|
||||
logentry_ids = [logentry_ids]
|
||||
|
||||
qs = LogEntry.all.select_related('event', 'event__organizer').filter(id__in=logentry_ids)
|
||||
qs = LogEntry.all.select_related(
|
||||
'event', 'event__organizer'
|
||||
).order_by(
|
||||
'action_type', 'event_id',
|
||||
).filter(id__in=logentry_ids)
|
||||
|
||||
_event, _at, notify_specific, notify_global = None, None, None, None
|
||||
for logentry in qs:
|
||||
|
||||
@@ -26,7 +26,7 @@ from itertools import zip_longest
|
||||
|
||||
import django_redis
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.db import connection, models
|
||||
from django.db.models import (
|
||||
Case, Count, F, Func, Max, OuterRef, Q, Subquery, Sum, Value, When,
|
||||
prefetch_related_objects,
|
||||
@@ -64,7 +64,8 @@ class QuotaAvailability:
|
||||
* count_cart (dict mapping quotas to ints)
|
||||
"""
|
||||
|
||||
def __init__(self, count_waitinglist=True, ignore_closed=False, full_results=False, early_out=True):
|
||||
def __init__(self, count_waitinglist=True, ignore_closed=False, full_results=False, early_out=True,
|
||||
allow_repeatable_read=False):
|
||||
"""
|
||||
Initialize a new quota availability calculator
|
||||
|
||||
@@ -86,6 +87,8 @@ class QuotaAvailability:
|
||||
keep the database-level quota cache up to date so backend overviews render quickly. If you
|
||||
do not care about keeping the cache up to date, you can set this to ``False`` for further
|
||||
performance improvements.
|
||||
|
||||
:param allow_repeatable_read: Allow to run this even in REPEATABLE READ mode, generally not advised.
|
||||
"""
|
||||
self._queue = []
|
||||
self._count_waitinglist = count_waitinglist
|
||||
@@ -95,6 +98,7 @@ class QuotaAvailability:
|
||||
self._var_to_quotas = defaultdict(set)
|
||||
self._early_out = early_out
|
||||
self._quota_objects = {}
|
||||
self._allow_repeatable_read = allow_repeatable_read
|
||||
self.results = {}
|
||||
self.count_paid_orders = defaultdict(int)
|
||||
self.count_pending_orders = defaultdict(int)
|
||||
@@ -119,6 +123,10 @@ class QuotaAvailability:
|
||||
Compute the queued quotas. If ``allow_cache`` is set, results may also be taken from a cache that might
|
||||
be a few minutes outdated. In this case, you may not rely on the results in the ``count_*`` properties.
|
||||
"""
|
||||
if not self._allow_repeatable_read and getattr(connection, "tx_in_repeatable_read", False):
|
||||
raise ValueError("You cannot compute quotas in REPEATABLE READ mode unless you explicitly opted in to "
|
||||
"do so.")
|
||||
|
||||
now_dt = now_dt or now()
|
||||
quota_ids_set = {q.id for q in self._queue}
|
||||
if not quota_ids_set:
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.functional import lazy
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.modelimport_orders import get_order_import_columns
|
||||
@@ -71,6 +73,9 @@ class ProcessForm(forms.Form):
|
||||
raise NotImplementedError() # noqa
|
||||
|
||||
|
||||
format_html_lazy = lazy(format_html, str)
|
||||
|
||||
|
||||
class OrdersProcessForm(ProcessForm):
|
||||
orders = forms.ChoiceField(
|
||||
label=_('Import mode'),
|
||||
@@ -91,7 +96,11 @@ class OrdersProcessForm(ProcessForm):
|
||||
)
|
||||
testmode = forms.BooleanField(
|
||||
label=_('Create orders as test mode orders'),
|
||||
required=False
|
||||
required=False,
|
||||
help_text=format_html_lazy(
|
||||
'<div class="alert alert-warning" data-display-dependency="#id_testmode" data-inverse>{}</div>',
|
||||
_('Orders not created in test mode cannot be deleted again after import.')
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -100,6 +109,8 @@ class OrdersProcessForm(ProcessForm):
|
||||
initital['testmode'] = self.event.testmode
|
||||
kwargs['initial'] = initital
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.event.testmode:
|
||||
self.fields["testmode"].help_text = ""
|
||||
|
||||
def get_columns(self):
|
||||
return get_order_import_columns(self.event)
|
||||
|
||||
@@ -390,7 +390,8 @@ class QuotaFormSet(I18nInlineFormSet):
|
||||
use_required_attribute=False,
|
||||
locales=self.locales,
|
||||
event=self.event,
|
||||
items=self.items
|
||||
items=self.items,
|
||||
searchable_selection=self.searchable_selection,
|
||||
)
|
||||
self.add_fields(form, None)
|
||||
return form
|
||||
|
||||
@@ -487,30 +487,32 @@
|
||||
{% trans "These settings are optional, if you leave them empty, the default values from the product settings will be used." %}
|
||||
</p>
|
||||
{% for f in itemvar_forms %}
|
||||
{% bootstrap_form_errors f %}
|
||||
<div class="form-group subevent-itemvar-group">
|
||||
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
|
||||
{% if f.variation %}{{ f.item }} – {{ f.variation }}{% else %}{{ f.item }}{% endif %}
|
||||
</label>
|
||||
<div class="col-md-4">
|
||||
<label for="{{ f.price.id_for_label }}" class="text-muted">{% trans "Price" %}</label><br>
|
||||
{% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="inline" %}
|
||||
<div data-itemvar="{{ f.item.id }}{% if f.variation %}-{{ f.variation.id }}{% endif %}">
|
||||
{% bootstrap_form_errors f %}
|
||||
<div class="form-group subevent-itemvar-group">
|
||||
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
|
||||
{% if f.variation %}{{ f.item }} – {{ f.variation }}{% else %}{{ f.item }}{% endif %}
|
||||
</label>
|
||||
<div class="col-md-4">
|
||||
<label for="{{ f.price.id_for_label }}" class="text-muted">{% trans "Price" %}</label><br>
|
||||
{% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<br>
|
||||
{% bootstrap_field f.disabled layout="inline" form_group_class="" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<br>
|
||||
{% bootstrap_field f.disabled layout="inline" form_group_class="" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group subevent-itemvar-group">
|
||||
<div class="col-md-4 col-md-offset-3">
|
||||
<label for="{{ f.rel_available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label>
|
||||
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_from_mode %}<br>
|
||||
{% bootstrap_field f.rel_available_from form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="{{ f.rel_available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label>
|
||||
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_until_mode %}<br>
|
||||
{% bootstrap_field f.rel_available_until form_group_class="" layout="inline" %}
|
||||
<div class="form-group subevent-itemvar-group">
|
||||
<div class="col-md-4 col-md-offset-3">
|
||||
<label for="{{ f.rel_available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label>
|
||||
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_from_mode %}<br>
|
||||
{% bootstrap_field f.rel_available_from form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="{{ f.rel_available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label>
|
||||
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_until_mode %}<br>
|
||||
{% bootstrap_field f.rel_available_until form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -625,7 +627,7 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<div class="form-group submit-group submit-group-sticky">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
|
||||
@@ -363,7 +363,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<div class="form-group submit-group submit-group-sticky">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
|
||||
@@ -130,30 +130,32 @@
|
||||
{% trans "These settings are optional, if you leave them empty, the default values from the product settings will be used." %}
|
||||
</p>
|
||||
{% for f in itemvar_forms %}
|
||||
{% bootstrap_form_errors f %}
|
||||
<div class="form-group subevent-itemvar-group">
|
||||
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
|
||||
{% if f.variation %}{{ f.item }} – {{ f.variation }}{% else %}{{ f.item }}{% endif %}
|
||||
</label>
|
||||
<div class="col-md-4">
|
||||
<label for="{{ f.price.id_for_label }}" class="text-muted">{% trans "Price" %}</label><br>
|
||||
{% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="inline" %}
|
||||
<div data-itemvar="{{ f.item.id }}{% if f.variation %}-{{ f.variation.id }}{% endif %}">
|
||||
{% bootstrap_form_errors f %}
|
||||
<div class="form-group subevent-itemvar-group">
|
||||
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
|
||||
{% if f.variation %}{{ f.item }} – {{ f.variation }}{% else %}{{ f.item }}{% endif %}
|
||||
</label>
|
||||
<div class="col-md-4">
|
||||
<label for="{{ f.price.id_for_label }}" class="text-muted">{% trans "Price" %}</label><br>
|
||||
{% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<br>
|
||||
{% bootstrap_field f.disabled layout="inline" form_group_class="" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<br>
|
||||
{% bootstrap_field f.disabled layout="inline" form_group_class="" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group subevent-itemvar-group">
|
||||
<div class="col-md-4 col-md-offset-3">
|
||||
<label for="{{ f.available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label>
|
||||
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_from_mode %}<br>
|
||||
{% bootstrap_field f.available_from form_group_class="foo" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="{{ f.available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label>
|
||||
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_until_mode %}<br>
|
||||
{% bootstrap_field f.available_until form_group_class="" layout="inline" %}
|
||||
<div class="form-group subevent-itemvar-group">
|
||||
<div class="col-md-4 col-md-offset-3">
|
||||
<label for="{{ f.available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label>
|
||||
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_from_mode %}<br>
|
||||
{% bootstrap_field f.available_from form_group_class="foo" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="{{ f.available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label>
|
||||
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_until_mode %}<br>
|
||||
{% bootstrap_field f.available_until form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -282,7 +284,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group submit-group">
|
||||
<div class="form-group submit-group submit-group-sticky">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
#
|
||||
import contextlib
|
||||
|
||||
from django.core.exceptions import FieldDoesNotExist
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
|
||||
from django.db import connection, transaction
|
||||
from django.db.models import (
|
||||
Aggregate, Expression, F, Field, Lookup, OrderBy, Value,
|
||||
@@ -62,6 +63,43 @@ def casual_reads():
|
||||
yield
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def repeatable_reads_transaction():
|
||||
"""
|
||||
pretix, and Django, operate in the transaction isolation level READ COMMITTED by default. This is not a strong level
|
||||
of isolation, but we NEED to use it: Otherwise e.g. our quota logic breaks, because we need to be able to get the
|
||||
*current* number of tickets sold at any time in a transaction, not the number of tickets sold *before* our transaction
|
||||
started.
|
||||
|
||||
However, this isolation mode has drawbacks, for example during reporting. When a user retrieves a report from the
|
||||
system, it should return numbers that are consistent with each other. However, if the report makes multiple SQL
|
||||
queries in READ COMMITTED mode, the results might be different for each query, causing numbers to be inconsistent
|
||||
with each other.
|
||||
|
||||
This context manager creates a transaction that is running in REPEATABLE READ mode to avoid this problem.
|
||||
|
||||
**You should only make read-only queries during this transaction and not rely on quota calculations.**
|
||||
"""
|
||||
is_under_test = 'tests.testdummy' in settings.INSTALLED_APPS
|
||||
try:
|
||||
with transaction.atomic(durable=not is_under_test):
|
||||
if not is_under_test:
|
||||
# We're not running this in tests, where we can basically not use this since the test runner does its
|
||||
# own transaction logic for efficiency
|
||||
with connection.cursor() as cursor:
|
||||
if 'postgresql' in settings.DATABASES['default']['ENGINE']:
|
||||
cursor.execute('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;')
|
||||
elif 'sqlite' in settings.DATABASES['default']['ENGINE']:
|
||||
pass # noop
|
||||
else:
|
||||
raise ImproperlyConfigured("Cannot set transaction isolation mode on this database backend")
|
||||
|
||||
connection.tx_in_repeatable_read = True
|
||||
yield
|
||||
finally:
|
||||
connection.tx_in_repeatable_read = False
|
||||
|
||||
|
||||
class GroupConcat(Aggregate):
|
||||
function = 'group_concat'
|
||||
template = '%(function)s(%(distinct)s%(field)s, "%(separator)s")'
|
||||
|
||||
@@ -5,8 +5,8 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-26 11:16+0000\n"
|
||||
"PO-Revision-Date: 2025-09-26 13:02+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"PO-Revision-Date: 2025-10-03 01:00+0000\n"
|
||||
"Last-Translator: Mira <weller@rami.io>\n"
|
||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/de/"
|
||||
">\n"
|
||||
"Language: de\n"
|
||||
@@ -28214,7 +28214,7 @@ msgstr ""
|
||||
|
||||
#: pretix/control/views/modelimport.py:174
|
||||
msgid "The import was successful."
|
||||
msgstr "Die Import war erfolgreich."
|
||||
msgstr "Der Import war erfolgreich."
|
||||
|
||||
#: pretix/control/views/modelimport.py:186
|
||||
msgid "We've been unable to parse the uploaded file as a CSV file."
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-26 11:16+0000\n"
|
||||
"PO-Revision-Date: 2025-09-26 13:02+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"PO-Revision-Date: 2025-10-03 01:00+0000\n"
|
||||
"Last-Translator: Mira <weller@rami.io>\n"
|
||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||
"pretix/pretix/de_Informal/>\n"
|
||||
"Language: de_Informal\n"
|
||||
@@ -28172,7 +28172,7 @@ msgstr ""
|
||||
|
||||
#: pretix/control/views/modelimport.py:174
|
||||
msgid "The import was successful."
|
||||
msgstr "Die Import war erfolgreich."
|
||||
msgstr "Der Import war erfolgreich."
|
||||
|
||||
#: pretix/control/views/modelimport.py:186
|
||||
msgid "We've been unable to parse the uploaded file as a CSV file."
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-26 11:16+0000\n"
|
||||
"PO-Revision-Date: 2025-05-04 16:00+0000\n"
|
||||
"Last-Translator: Pekka Sarkola <pekka.sarkola@gispo.fi>\n"
|
||||
"PO-Revision-Date: 2025-10-04 10:10+0000\n"
|
||||
"Last-Translator: Sebastian Bożek <sebastian@log-mar.pl>\n"
|
||||
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"fi/>\n"
|
||||
"Language: fi\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.11.1\n"
|
||||
"X-Generator: Weblate 5.13.3\n"
|
||||
|
||||
#: pretix/_base_settings.py:87
|
||||
msgid "English"
|
||||
@@ -113,7 +113,7 @@ msgstr "norja (kirjakieli)"
|
||||
|
||||
#: pretix/_base_settings.py:110
|
||||
msgid "Polish"
|
||||
msgstr "puola"
|
||||
msgstr "Puola"
|
||||
|
||||
#: pretix/_base_settings.py:111
|
||||
msgid "Portuguese (Portugal)"
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-08-19 16:35+0000\n"
|
||||
"PO-Revision-Date: 2025-09-30 16:00+0000\n"
|
||||
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
|
||||
"PO-Revision-Date: 2025-10-03 20:00+0000\n"
|
||||
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n"
|
||||
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"ja/>\n"
|
||||
"Language: ja\n"
|
||||
@@ -30760,7 +30760,7 @@ msgstr "このイベントのメール設定でチケットの添付が無効に
|
||||
#: pretix/plugins/sendmail/forms.py:234 pretix/plugins/sendmail/forms.py:386
|
||||
#: pretix/plugins/sendmail/views.py:267
|
||||
msgid "payment pending but already confirmed"
|
||||
msgstr "支払い保留中ですが、すでに確認済み"
|
||||
msgstr "支払い保留中だが確認済み"
|
||||
|
||||
#: pretix/plugins/sendmail/forms.py:235 pretix/plugins/sendmail/forms.py:388
|
||||
#: pretix/plugins/sendmail/views.py:268
|
||||
|
||||
@@ -7,7 +7,7 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-26 11:16+0000\n"
|
||||
"PO-Revision-Date: 2025-09-30 01:00+0000\n"
|
||||
"PO-Revision-Date: 2025-10-04 19:00+0000\n"
|
||||
"Last-Translator: Jan Van Haver <jan.van.haver@gmail.com>\n"
|
||||
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/>"
|
||||
"\n"
|
||||
@@ -808,6 +808,8 @@ msgid ""
|
||||
"Field \"{field_name}\" does not exist. Please check your {provider_name} "
|
||||
"settings."
|
||||
msgstr ""
|
||||
"Het veld \"{field_name}\" bestaat niet. Controleer uw {provider_name} "
|
||||
"instellingen."
|
||||
|
||||
#: pretix/base/datasync/datasync.py:262
|
||||
#, python-brace-format
|
||||
@@ -815,6 +817,8 @@ msgid ""
|
||||
"Field \"{field_name}\" requires {required_input}, but only got "
|
||||
"{available_inputs}. Please check your {provider_name} settings."
|
||||
msgstr ""
|
||||
"Het veld \"{field_name}\" vereist {required_input}, maar heeft alleen "
|
||||
"{available_inputs}. Controleer uw {provider_name} instellingen."
|
||||
|
||||
#: pretix/base/datasync/datasync.py:273
|
||||
#, python-brace-format
|
||||
@@ -822,18 +826,16 @@ msgid ""
|
||||
"Please update value mapping for field \"{field_name}\" - option \"{val}\" "
|
||||
"not assigned"
|
||||
msgstr ""
|
||||
"Werk de mapping van de waarden bij voor veld \"{field_name}\" - optie "
|
||||
"\"{val}\" is niet toegewezen"
|
||||
|
||||
#: pretix/base/datasync/sourcefields.py:128
|
||||
#, fuzzy
|
||||
#| msgid "Order positions"
|
||||
msgid "Order position details"
|
||||
msgstr "Bestelde producten"
|
||||
msgstr "Details bestelde producten"
|
||||
|
||||
#: pretix/base/datasync/sourcefields.py:129
|
||||
#, fuzzy
|
||||
#| msgid "Attendee email"
|
||||
msgid "Attendee details"
|
||||
msgstr "E-mailadres van aanwezige"
|
||||
msgstr "Details van aanwezige"
|
||||
|
||||
#: pretix/base/datasync/sourcefields.py:130 pretix/base/exporters/answers.py:66
|
||||
#: pretix/base/models/items.py:1767 pretix/control/navigation.py:172
|
||||
@@ -843,10 +845,8 @@ msgid "Questions"
|
||||
msgstr "Vragen"
|
||||
|
||||
#: pretix/base/datasync/sourcefields.py:131
|
||||
#, fuzzy
|
||||
#| msgid "Product data"
|
||||
msgid "Product details"
|
||||
msgstr "Productgegevens"
|
||||
msgstr "Productdetails"
|
||||
|
||||
#: pretix/base/datasync/sourcefields.py:132
|
||||
#: pretix/control/templates/pretixcontrol/event/settings.html:280
|
||||
@@ -1038,16 +1038,12 @@ msgid "Product ID"
|
||||
msgstr "Product ID"
|
||||
|
||||
#: pretix/base/datasync/sourcefields.py:419
|
||||
#, fuzzy
|
||||
#| msgid "Non-admission product"
|
||||
msgid "Product is admission product"
|
||||
msgstr "Geen toegangsbewijs"
|
||||
msgstr "Product is een toegangsbewijs"
|
||||
|
||||
#: pretix/base/datasync/sourcefields.py:428
|
||||
#, fuzzy
|
||||
#| msgid "Event short name"
|
||||
msgid "Event short form"
|
||||
msgstr "Korte naam evenement"
|
||||
msgstr "Kort formulier evenement"
|
||||
|
||||
#: pretix/base/datasync/sourcefields.py:437 pretix/base/exporters/events.py:57
|
||||
#: pretix/base/exporters/orderlist.py:262
|
||||
@@ -1090,10 +1086,8 @@ msgid "Order code and position number"
|
||||
msgstr "Bestelcode en plaatsnummer"
|
||||
|
||||
#: pretix/base/datasync/sourcefields.py:482
|
||||
#, fuzzy
|
||||
#| msgid "Ticket page"
|
||||
msgid "Ticket price"
|
||||
msgstr "Ticketpagina"
|
||||
msgstr "Ticketprijs"
|
||||
|
||||
#: pretix/base/datasync/sourcefields.py:491 pretix/base/notifications.py:204
|
||||
#: pretix/control/forms/filter.py:216 pretix/control/forms/modelimport.py:85
|
||||
@@ -1101,22 +1095,16 @@ msgid "Order status"
|
||||
msgstr "Bestelstatus"
|
||||
|
||||
#: pretix/base/datasync/sourcefields.py:500
|
||||
#, fuzzy
|
||||
#| msgid "Device status"
|
||||
msgid "Ticket status"
|
||||
msgstr "Apparaatstatus"
|
||||
msgstr "Ticketstatus"
|
||||
|
||||
#: pretix/base/datasync/sourcefields.py:509
|
||||
#, fuzzy
|
||||
#| msgid "Purchase date and time"
|
||||
msgid "Order date and time"
|
||||
msgstr "Aankoopdatum en -tijd"
|
||||
msgstr "Besteldatum en -tijd"
|
||||
|
||||
#: pretix/base/datasync/sourcefields.py:518
|
||||
#, fuzzy
|
||||
#| msgid "Printing date and time"
|
||||
msgid "Payment date and time"
|
||||
msgstr "Printdatum en -tijd"
|
||||
msgstr "Betaaldatum en -tijd"
|
||||
|
||||
#: pretix/base/datasync/sourcefields.py:527
|
||||
#: pretix/base/exporters/orderlist.py:271
|
||||
@@ -1127,23 +1115,17 @@ msgid "Order locale"
|
||||
msgstr "Taal van bestelling"
|
||||
|
||||
#: pretix/base/datasync/sourcefields.py:536
|
||||
#, fuzzy
|
||||
#| msgid "Order position"
|
||||
msgid "Order position ID"
|
||||
msgstr "Besteld product"
|
||||
msgstr "ID besteld product"
|
||||
|
||||
#: pretix/base/datasync/sourcefields.py:545
|
||||
#: pretix/base/exporters/orderlist.py:291
|
||||
#, fuzzy
|
||||
#| msgid "Order time"
|
||||
msgid "Order link"
|
||||
msgstr "Besteltijd"
|
||||
msgstr "Bestellink"
|
||||
|
||||
#: pretix/base/datasync/sourcefields.py:560
|
||||
#, fuzzy
|
||||
#| msgid "Ticket block"
|
||||
msgid "Ticket link"
|
||||
msgstr "Ticketblok"
|
||||
msgstr "Ticketlink"
|
||||
|
||||
#: pretix/base/datasync/sourcefields.py:578
|
||||
#, fuzzy, python-brace-format
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-26 11:16+0000\n"
|
||||
"PO-Revision-Date: 2025-06-02 23:00+0000\n"
|
||||
"Last-Translator: Anarion Dunedain <anarion80@gmail.com>\n"
|
||||
"PO-Revision-Date: 2025-10-04 19:00+0000\n"
|
||||
"Last-Translator: Sebastian Bożek <sebastian@log-mar.pl>\n"
|
||||
"Language-Team: Polish <https://translate.pretix.eu/projects/pretix/pretix/pl/"
|
||||
">\n"
|
||||
"Language: pl\n"
|
||||
@@ -18,7 +18,7 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
|
||||
"|| n%100>=20) ? 1 : 2;\n"
|
||||
"X-Generator: Weblate 5.11.4\n"
|
||||
"X-Generator: Weblate 5.13.3\n"
|
||||
|
||||
#: pretix/_base_settings.py:87
|
||||
msgid "English"
|
||||
@@ -90,7 +90,7 @@ msgstr "Grecki"
|
||||
|
||||
#: pretix/_base_settings.py:104
|
||||
msgid "Hebrew"
|
||||
msgstr ""
|
||||
msgstr "Hebrajski"
|
||||
|
||||
#: pretix/_base_settings.py:105
|
||||
msgid "Indonesian"
|
||||
@@ -146,7 +146,7 @@ msgstr "Hiszpański"
|
||||
|
||||
#: pretix/_base_settings.py:118
|
||||
msgid "Spanish (Latin America)"
|
||||
msgstr ""
|
||||
msgstr "Hiszpański (Ameryka Łacińska)"
|
||||
|
||||
#: pretix/_base_settings.py:119
|
||||
msgid "Turkish"
|
||||
@@ -2328,7 +2328,7 @@ msgstr ""
|
||||
#: pretix/base/exporters/waitinglist.py:115 pretix/control/forms/event.py:1671
|
||||
#: pretix/control/forms/organizer.py:116
|
||||
msgid "Event slug"
|
||||
msgstr "Kod wydarzenia"
|
||||
msgstr "Fragment adresu URL, który pojawia się po nazwie domeny."
|
||||
|
||||
#: pretix/base/exporters/orderlist.py:262
|
||||
#: pretix/base/exporters/orderlist.py:452
|
||||
@@ -32680,7 +32680,7 @@ msgid ""
|
||||
"banks. Please keep your online banking account and login information "
|
||||
"available."
|
||||
msgstr ""
|
||||
"Przelewy24 to metoda płatności online dostępna dla klientów polskich banków. "
|
||||
"Przelewy24 to metoda płatności online dostępna dla klientów Polskich banków. "
|
||||
"Prosimy o zachowanie danych logowania i konta bankowego."
|
||||
|
||||
#: pretix/plugins/stripe/payment.py:1768
|
||||
|
||||
@@ -476,6 +476,7 @@ class CSVCheckinList(CheckInListMixin, ListExporter):
|
||||
category = pgettext_lazy('export_category', 'Check-in')
|
||||
description = gettext_lazy("Download a spreadsheet with all attendees that are included in a check-in list.")
|
||||
featured = True
|
||||
repeatable_read = False
|
||||
|
||||
@property
|
||||
def additional_form_fields(self):
|
||||
@@ -673,6 +674,7 @@ class CSVCheckinCodeList(CheckInListMixin, ListExporter):
|
||||
category = pgettext_lazy('export_category', 'Check-in')
|
||||
description = gettext_lazy("Download a spreadsheet with all valid check-in barcodes e.g. for import into a "
|
||||
"different system. Does not included blocked codes or personal data.")
|
||||
repeatable_read = False
|
||||
|
||||
@property
|
||||
def additional_form_fields(self):
|
||||
@@ -743,6 +745,7 @@ class CheckinLogList(ListExporter):
|
||||
category = pgettext_lazy('export_category', 'Check-in')
|
||||
description = gettext_lazy("Download a spreadsheet with one line for every scan that happened at your check-in "
|
||||
"stations.")
|
||||
repeatable_read = False
|
||||
|
||||
@property
|
||||
def additional_form_fields(self):
|
||||
|
||||
@@ -661,6 +661,7 @@ class OrderTaxListReport(MultiSheetListExporter):
|
||||
verbose_name = gettext_lazy('Tax split list')
|
||||
category = pgettext_lazy('export_category', 'Order data')
|
||||
description = gettext_lazy("Download a spreadsheet with the tax amounts included in each order.")
|
||||
repeatable_read = False
|
||||
|
||||
@property
|
||||
def sheets(self):
|
||||
|
||||
@@ -26,6 +26,7 @@ $(document).on("pretix:bind-forms", function () {
|
||||
}
|
||||
}
|
||||
|
||||
// RRule editor
|
||||
function rrule_preview() {
|
||||
var ruleset = new rrule.RRuleSet();
|
||||
|
||||
@@ -121,7 +122,14 @@ $(document).on("pretix:bind-forms", function () {
|
||||
});
|
||||
}
|
||||
}
|
||||
$("#rrule-formset").on("change keydown keyup keypress dp.change", "input, select", function () {
|
||||
rrule_preview();
|
||||
});
|
||||
rrule_preview();
|
||||
|
||||
$("#rrule-formset").on("formAdded", "div", function (event) {rrule_bind_form($(event.target)); });
|
||||
|
||||
// Timeslot editor
|
||||
$("#subevent_add_many_slots_go").on("click", function () {
|
||||
$("#time-formset [data-formset-form]").each(function () {
|
||||
var tf = $(this).find("[name$=time_from]").val()
|
||||
@@ -167,13 +175,45 @@ $(document).on("pretix:bind-forms", function () {
|
||||
$(this).addClass("hidden");
|
||||
});
|
||||
|
||||
$("#rrule-formset").on("change keydown keyup keypress dp.change", "input, select", function () {
|
||||
rrule_preview();
|
||||
});
|
||||
rrule_preview();
|
||||
// Hide config for products that are not for sale
|
||||
function quota_form_handlers(el) {
|
||||
// searchable_selection = True
|
||||
el.find('[id^="id_quotas-"]').on("select2:select select2:unselect", () => {
|
||||
update_item_visibility();
|
||||
});
|
||||
// searchable_selection = False
|
||||
el.find('input[id^="id_quotas-"][id*=itemvars_]').on("change", () => {
|
||||
update_item_visibility();
|
||||
});
|
||||
}
|
||||
function update_item_visibility() {
|
||||
const itemvars = [];
|
||||
|
||||
$("#rrule-formset").on("formAdded", "div", function (event) { rrule_bind_form($(event.target)); });
|
||||
// searchable_selection = True
|
||||
$("select[id^=id_quotas-][id$=-itemvars]").filter((idx, el) => {
|
||||
return !$(el).closest('[data-formset-form]').is('[data-formset-form-deleted]');
|
||||
}).each((_, e) => itemvars.push(...$(e).val()));
|
||||
// searchable_selection = False
|
||||
$("input[id^=id_quotas-][id*=itemvars_]:checked").filter((idx, el) => {
|
||||
return !$(el).closest('[data-formset-form]').is('[data-formset-form-deleted]');
|
||||
}).each((_, e) => itemvars.push($(e).val()));
|
||||
|
||||
$("div[data-itemvar]").each(function (idx, e) {
|
||||
const el = $(e);
|
||||
el.prop("hidden", !itemvars.includes(el.attr("data-itemvar")) && !el.find(".has-error, .alert-danger").length);
|
||||
});
|
||||
}
|
||||
|
||||
$('[data-formset-prefix="quotas"]').on("formDeleted", "div", () => {
|
||||
update_item_visibility();
|
||||
}).on("formAdded", "div", (event) => {
|
||||
quota_form_handlers($(event.target));
|
||||
update_item_visibility();
|
||||
})
|
||||
quota_form_handlers($("body"));
|
||||
update_item_visibility();
|
||||
|
||||
// Auto-set name of check-in list
|
||||
var $namef = $("input[id^=id_name]").first();
|
||||
var lastValue = $namef.val();
|
||||
$namef.change(function () {
|
||||
|
||||
@@ -95,6 +95,12 @@ div[data-formset-body], div[data-formset-form], div[data-nested-formset-form], d
|
||||
}
|
||||
}
|
||||
|
||||
.submit-group-sticky {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.panel .form-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ from pretix.base.models.items import (
|
||||
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
|
||||
from pretix.base.services.orders import OrderError, cancel_order, perform_order
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.helpers import repeatable_reads_transaction
|
||||
from pretix.testutils.scope import classscope
|
||||
|
||||
|
||||
@@ -99,6 +100,29 @@ class BaseQuotaTestCase(TestCase):
|
||||
self.var3 = ItemVariation.objects.create(item=self.item3, value='Fancy')
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@scopes_disabled()
|
||||
def test_verify_repeatable_read_check():
|
||||
if 'sqlite' in settings.DATABASES['default']['ENGINE']:
|
||||
pytest.skip('Not supported on SQLite')
|
||||
|
||||
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
||||
event = Event.objects.create(
|
||||
organizer=o, name='Dummy', slug='dummy',
|
||||
date_from=now(), plugins='tests.testdummy'
|
||||
)
|
||||
quota = Quota.objects.create(name="Test", size=2, event=event)
|
||||
|
||||
with repeatable_reads_transaction():
|
||||
with pytest.raises(ValueError):
|
||||
qa = QuotaAvailability(full_results=True)
|
||||
qa.queue(quota)
|
||||
qa.compute()
|
||||
qa = QuotaAvailability(full_results=True, allow_repeatable_read=True)
|
||||
qa.queue(quota)
|
||||
qa.compute()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("fakeredis_client")
|
||||
class QuotaTestCase(BaseQuotaTestCase):
|
||||
@classscope(attr='o')
|
||||
|
||||
Reference in New Issue
Block a user