Compare commits

...

15 Commits

Author SHA1 Message Date
Raphael Michel
b2237af4c0 Only show product settings that match quotas 2025-10-06 14:07:38 +02:00
Raphael Michel
43c2f15994 Make submit row sticky on really long forms 2025-10-06 12:50:41 +02:00
Raphael Michel
571724b1f7 Use select2 item input in newly added quotas 2025-10-06 12:45:25 +02:00
Raphael Michel
85a9a3caa6 Run exporters in repeatable read by default (Z#23173095) (#5500)
* Run exporters in repeatable read by default (Z#23173095)

* Update src/pretix/helpers/database.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Rename parameter, add test

* Do not run during tests

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2025-10-06 10:38:19 +02:00
Sebastian Bożek
42b1010c36 Translations: Update Polish
Currently translated at 95.5% (5803 of 6076 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pl/

powered by weblate
2025-10-06 09:53:18 +02:00
Jan Van Haver
5b851e270b Translations: Update Dutch
Currently translated at 97.2% (5907 of 6076 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl/

powered by weblate
2025-10-06 09:53:18 +02:00
Sebastian Bożek
2b796aa45e Translations: Update Finnish
Currently translated at 64.4% (3917 of 6076 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fi/

powered by weblate
2025-10-06 09:53:18 +02:00
Sebastian Bożek
11460d878b Translations: Update Polish
Currently translated at 95.5% (5803 of 6076 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/pl/

powered by weblate
2025-10-06 09:53:18 +02:00
Yasunobu YesNo Kawaguchi
1ce4c11572 Translations: Update Japanese
Currently translated at 100.0% (6076 of 6076 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ja/

powered by weblate
2025-10-06 09:53:18 +02:00
Mira
11269c277b Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (6076 of 6076 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2025-10-06 09:53:18 +02:00
Mira
2650bf6f4f Translations: Update German
Currently translated at 100.0% (6076 of 6076 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de/

powered by weblate
2025-10-06 09:53:18 +02:00
Raphael Michel
301191e4bd Notification queues: Optimize order for less queries (#5512)
* Notification queues: Optimize order for less queries

* Update src/pretix/api/webhooks.py

Co-authored-by: luelista <weller@rami.io>

---------

Co-authored-by: luelista <weller@rami.io>
2025-10-06 09:24:51 +02:00
Raphael Michel
867cd8c59e Model import: Create logentries in bulk (#5511)
* Model import: Create logentries in bulk

* Update src/pretix/base/services/modelimport.py

Co-authored-by: luelista <weller@rami.io>

---------

Co-authored-by: luelista <weller@rami.io>
2025-10-06 09:24:39 +02:00
Raphael Michel
7e8da3cef6 Do not sent "payment failed" email if payment is no longer expected (Z#23202699) (#5509) 2025-10-06 09:24:20 +02:00
Raphael Michel
25f57f89b0 Order import: Additional warning for disabling test mode (Z#23208360) (#5494) 2025-10-02 19:14:29 +02:00
28 changed files with 306 additions and 133 deletions

View File

@@ -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

View File

@@ -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:
"""

View File

@@ -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):

View File

@@ -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(

View File

@@ -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(

View File

@@ -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 = [

View File

@@ -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

View File

@@ -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.')

View File

@@ -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()

View File

@@ -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:

View File

@@ -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:

View File

@@ -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)

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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")'

View File

@@ -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."

View 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."

View 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)"

View File

@@ -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

View File

@@ -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

View 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-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

View File

@@ -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):

View File

@@ -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):

View File

@@ -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 () {

View File

@@ -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;
}

View File

@@ -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')