Compare commits

..

23 Commits

Author SHA1 Message Date
Mira Weller
22d8158356 Fix rrules 2025-10-09 17:22:31 +02:00
luelista
bff0f54bf8 Fix formset widths (#5530)
* Remove explicitly specified width for formset-forms
With that style, all formset rows were a fix pixels less wide than surrounding content

* Set select2 width to 100% so they adapt when browser window is resized
2025-10-09 17:02:45 +02:00
luelista
50c1c9c724 Run sync job as soon as possible on clicking "Sync now" (#5526) 2025-10-08 13:15:09 +02:00
Raphael Michel
802268df46 Fix ajax error not being shown 2025-10-08 09:47:09 +02:00
luelista
a823f261f3 Fix unhandled exception in datasync code in case order should not be synced (PRETIXEU-C9H) (#5525)
* Fix unhandled exception in datasync code in case order should not be synced (PRETIXEU-C9H)
* Add test case
2025-10-07 19:58:26 +02:00
luelista
59a754f913 Fix log entry details for datasync logs without external link (Z#23210015) (#5524) 2025-10-07 19:58:13 +02:00
Raphael Michel
82eca01e5c QuotaListExporter: Flip incorrect allow_repeatable_read 2025-10-07 13:20:06 +02:00
Raphael Michel
943f594b6b InvoiceExporter: Set repeatable_read = False 2025-10-07 13:18:03 +02:00
Raphael Michel
15cbb3a416 Do not crash if generate_invoice fails (#5483)
* Do not crash if generate_invoice fails

* Add logging

* Add cancellation to try block

* One last thing…
2025-10-07 11:20:31 +02:00
dependabot[bot]
f447e7b9c4 Update sentry-sdk requirement from ==2.38.* to ==2.40.* (#5521)
Updates the requirements on [sentry-sdk](https://github.com/getsentry/sentry-python) to permit the latest version.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.38.0...2.40.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.40.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-07 10:40:54 +02:00
Raphael Michel
dcf473c543 Send invoice to organizer in plain text (Z#23210026) (#5518) 2025-10-07 10:40:15 +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
33 changed files with 378 additions and 150 deletions

View File

@@ -91,7 +91,7 @@ dependencies = [
"redis==6.4.*",
"reportlab==4.4.*",
"requests==2.32.*",
"sentry-sdk==2.38.*",
"sentry-sdk==2.40.*",
"sepaxml==2.6.*",
"stripe==7.9.*",
"text-unidecode==1.*",

View File

@@ -764,7 +764,13 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
) and not order.invoices.last()
invoice = None
if gen_invoice:
invoice = generate_invoice(order, trigger_pdf=True)
try:
invoice = generate_invoice(order, trigger_pdf=True)
except Exception as e:
logger.exception("Could not generate invoice.")
order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
# Refresh serializer only after running signals
prefetch_related_objects([order], self._positions_prefetch(request))

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

@@ -125,6 +125,7 @@ class InvoiceExporter(InvoiceExporterMixin, BaseExporter):
identifier = 'invoices'
verbose_name = _('All invoices')
description = _('Download all invoices created by the system as a ZIP file of PDF files.')
repeatable_read = False
def render(self, form_data: dict, output_file=None):
qs = self.invoices_queryset(form_data).filter(shredded=False)
@@ -180,6 +181,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=True)
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
@@ -1961,14 +1969,20 @@ class OrderPayment(models.Model):
self.order.invoice_dirty
)
if gen_invoice:
if invoices:
last_i = self.order.invoices.filter(is_cancellation=False).last()
if not last_i.canceled:
generate_cancellation(last_i)
invoice = generate_invoice(
self.order,
trigger_pdf=not send_mail or not self.order.event.settings.invoice_email_attachment
)
try:
if invoices:
last_i = self.order.invoices.filter(is_cancellation=False).last()
if not last_i.canceled:
generate_cancellation(last_i)
invoice = generate_invoice(
self.order,
trigger_pdf=not send_mail or not self.order.event.settings.invoice_email_attachment
)
except Exception as e:
logger.exception("Could not generate invoice.")
self.order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
transmit_invoice_task = invoice_transmission_separately(invoice)
transmit_invoice_mail = not transmit_invoice_task and self.order.event.settings.invoice_email_attachment and self.order.email

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

@@ -671,6 +671,7 @@ def send_invoices_to_organizer(sender, **kwargs):
event=i.event,
invoices=[i],
auto_email=True,
plain_text_only=True,
)
i.sent_to_organizer = True
else:

View File

@@ -19,6 +19,7 @@
# 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 logging
from decimal import Decimal
from typing import List
@@ -33,8 +34,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
@@ -43,6 +44,8 @@ from pretix.base.services.tasks import ProfiledEventTask
from pretix.base.signals import order_paid, order_placed
from pretix.celery_app import app
logger = logging.getLogger(__name__)
def _validate(cf: CachedFile, charset: str, cols: List[ImportColumn], settings: dict):
try:
@@ -175,6 +178,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 +215,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):
@@ -230,7 +236,13 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user,
(event.settings.get('invoice_generate') == 'paid' and o.status == Order.STATUS_PAID)
) and not o.invoices.last()
if gen_invoice:
generate_invoice(o, trigger_pdf=True)
try:
generate_invoice(o, trigger_pdf=True)
except Exception as e:
logger.exception("Could not generate invoice.")
o.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
except DataImportError:
raise ValidationError(_('We were not able to process your request completely as the server was too busy. '
'Please try again.'))
@@ -286,13 +298,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

@@ -264,7 +264,13 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
num_invoices = order.invoices.filter(is_cancellation=False).count()
if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices and invoice_qualified(order):
generate_invoice(order)
try:
generate_invoice(order)
except Exception as e:
logger.exception("Could not generate invoice.")
order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
def extend_order(order: Order, new_date: datetime, force: bool=False, valid_if_pending: bool=None, user: User=None, auth=None):
@@ -312,7 +318,13 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, valid_if_p
if was_expired:
num_invoices = order.invoices.filter(is_cancellation=False).count()
if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices and invoice_qualified(order):
generate_invoice(order)
try:
generate_invoice(order)
except Exception as e:
logger.exception("Could not generate invoice.")
order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
order.create_transactions()
with transaction.atomic():
@@ -397,13 +409,19 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False
if order.event.settings.get('invoice_generate') == 'True' and invoice_qualified(order):
if not invoice:
invoice = generate_invoice(
order,
# send_mail will trigger PDF generation later
trigger_pdf=not transmit_invoice_mail
)
if transmit_invoice_task:
transmit_invoice.apply_async(args=(order.event_id, invoice.pk, False))
try:
invoice = generate_invoice(
order,
# send_mail will trigger PDF generation later
trigger_pdf=not transmit_invoice_mail
)
if transmit_invoice_task:
transmit_invoice.apply_async(args=(order.event_id, invoice.pk, False))
except Exception as e:
logger.exception("Could not generate invoice.")
order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
if send_mail:
with language(order.locale, order.event.settings.region):
@@ -608,7 +626,13 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
order.save(update_fields=['status', 'cancellation_date', 'total'])
if cancel_invoice and i:
invoices.append(generate_invoice(order))
try:
invoices.append(generate_invoice(order))
except Exception as e:
logger.exception("Could not generate invoice.")
order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
else:
order.status = Order.STATUS_CANCELED
order.cancellation_date = now()
@@ -1306,13 +1330,19 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
)
)
if invoice_required:
invoice = generate_invoice(
order,
# send_mail will trigger PDF generation later
trigger_pdf=not transmit_invoice_mail
)
if transmit_invoice_task:
transmit_invoice.apply_async(args=(event.pk, invoice.pk, False))
try:
invoice = generate_invoice(
order,
# send_mail will trigger PDF generation later
trigger_pdf=not transmit_invoice_mail
)
if transmit_invoice_task:
transmit_invoice.apply_async(args=(event.pk, invoice.pk, False))
except Exception as e:
logger.exception("Could not generate invoice.")
order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
if order.email:
if order.require_approval:
@@ -2701,7 +2731,13 @@ class OrderChangeManager:
)
if split_order.total != Decimal('0.00') and self.order.invoices.filter(is_cancellation=False).last():
generate_invoice(split_order)
try:
generate_invoice(split_order)
except Exception as e:
logger.exception("Could not generate invoice.")
split_order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
order_split.send(sender=self.order.event, original=self.order, split_order=split_order)
return split_order
@@ -2812,15 +2848,27 @@ class OrderChangeManager:
if order_now_qualified:
if invoice_should_be_generated_now:
if i and not i.canceled:
self._invoices.append(generate_cancellation(i))
self._invoices.append(generate_invoice(self.order))
try:
if i and not i.canceled:
self._invoices.append(generate_cancellation(i))
self._invoices.append(generate_invoice(self.order))
except Exception as e:
logger.exception("Could not generate invoice.")
self.order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
elif invoice_should_be_generated_later:
self.order.invoice_dirty = True
self.order.save(update_fields=["invoice_dirty"])
else:
if i and not i.canceled:
self._invoices.append(generate_cancellation(i))
try:
if i and not i.canceled:
self._invoices.append(generate_cancellation(i))
except Exception as e:
logger.exception("Could not generate invoice.")
self.order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
def _check_complete_cancel(self):
current = self.order.positions.count()
@@ -3246,8 +3294,14 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
has_active_invoice = i and not i.canceled
if has_active_invoice and order.total != oldtotal:
generate_cancellation(i)
generate_invoice(order)
try:
generate_cancellation(i)
generate_invoice(order)
except Exception as e:
logger.exception("Could not generate invoice.")
order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
new_invoice_created = True
elif (not has_active_invoice or order.invoice_dirty) and invoice_qualified(order):
@@ -3255,13 +3309,19 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
order.event.settings.get('invoice_generate') == 'paid' and
new_payment.payment_provider.requires_invoice_immediately
):
if has_active_invoice:
generate_cancellation(i)
i = generate_invoice(order)
new_invoice_created = True
order.log_action('pretix.event.order.invoice.generated', data={
'invoice': i.pk
})
try:
if has_active_invoice:
generate_cancellation(i)
i = generate_invoice(order)
new_invoice_created = True
order.log_action('pretix.event.order.invoice.generated', data={
'invoice': i.pk
})
except Exception as e:
logger.exception("Could not generate invoice.")
order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
order.create_transactions()
return old_fee, new_fee, fee, new_payment, new_invoice_created

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

@@ -455,7 +455,7 @@ class OrderDataSyncSuccessLogEntryType(OrderDataSyncLogEntryType):
links.append(", ".join(
prov.get_external_link_html(logentry.event, obj['external_link_href'], obj['external_link_display_name'])
for obj in objs
if obj and 'external_link_href' in obj and 'external_link_display_name' in obj
if obj and obj.get('external_link_href') and obj.get('external_link_display_name')
))
return mark_safe(escape(super().display(logentry, data)) + "".join("<p>" + link + "</p>" for link in links))
@@ -522,6 +522,7 @@ def pretixcontrol_orderposition_blocked_display(sender: Event, orderposition, bl
'pretix.event.order.customer.changed': _('The customer account has been changed.'),
'pretix.event.order.locale.changed': _('The order locale has been changed.'),
'pretix.event.order.invoice.generated': _('The invoice has been generated.'),
'pretix.event.order.invoice.failed': _('The invoice could not be generated.'),
'pretix.event.order.invoice.regenerated': _('The invoice has been regenerated.'),
'pretix.event.order.invoice.reissued': _('The invoice has been reissued.'),
'pretix.event.order.invoice.sent': _('The invoice {full_invoice_no} has been sent.'),

View File

@@ -78,8 +78,8 @@ class ControlSyncJob(OrderView):
prov, meta = datasync_providers.get(active_in=self.request.event, identifier=provider)
if self.request.POST.get("queue_sync") == "true":
prov.enqueue_order(self.order, 'user')
messages.success(self.request, _('The sync job has been enqueued and will run in the next minutes.'))
prov.enqueue_order(self.order, 'user', immediate=True)
messages.success(self.request, _('The sync job has been set to run as soon as possible.'))
elif self.request.POST.get("cancel_job"):
with transaction.atomic():
try:

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

@@ -36,6 +36,7 @@ import copy
import hmac
import inspect
import json
import logging
import mimetypes
import os
import re
@@ -98,6 +99,8 @@ from pretix.presale.views import (
from pretix.presale.views.event import get_grouped_items
from pretix.presale.views.robots import NoSearchIndexViewMixin
logger = logging.getLogger(__name__)
class OrderDetailMixin(NoSearchIndexViewMixin):
@@ -734,11 +737,18 @@ class OrderInvoiceCreate(EventViewMixin, OrderDetailMixin, View):
elif self.order.invoices.exists():
messages.error(self.request, _('An invoice for this order already exists.'))
else:
i = generate_invoice(self.order)
self.order.log_action('pretix.event.order.invoice.generated', data={
'invoice': i.pk
})
messages.success(self.request, _('The invoice has been generated.'))
try:
i = generate_invoice(self.order)
self.order.log_action('pretix.event.order.invoice.generated', data={
'invoice': i.pk
})
messages.success(self.request, _('The invoice has been generated.'))
except Exception as e:
logger.exception("Could not generate invoice.")
self.order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
messages.error(self.request, _('Invoice generation has failed, please reach out to the organizer.'))
return redirect(self.get_order_url())
@@ -807,24 +817,37 @@ class OrderModify(EventViewMixin, OrderDetailMixin, OrderQuestionsViewMixin, Tem
elif self.order.invoices.exists():
messages.error(self.request, _('An invoice for this order already exists.'))
else:
i = generate_invoice(self.order)
self.order.log_action('pretix.event.order.invoice.generated', data={
'invoice': i.pk
})
messages.success(self.request, _('The invoice has been generated.'))
try:
i = generate_invoice(self.order)
self.order.log_action('pretix.event.order.invoice.generated', data={
'invoice': i.pk
})
messages.success(self.request, _('The invoice has been generated.'))
except Exception as e:
logger.exception("Could not generate invoice.")
self.order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
messages.error(self.request, _('Invoice generation has failed, please reach out to the organizer.'))
elif self.request.event.settings.invoice_reissue_after_modify:
if self.invoice_form.changed_data:
inv = self.order.invoices.last()
if inv and not inv.canceled and not inv.shredded:
c = generate_cancellation(inv)
if self.order.status != Order.STATUS_CANCELED:
inv = generate_invoice(self.order)
else:
inv = c
self.order.log_action('pretix.event.order.invoice.reissued', data={
'invoice': inv.pk
try:
inv = self.order.invoices.last()
if inv and not inv.canceled and not inv.shredded:
c = generate_cancellation(inv)
if self.order.status != Order.STATUS_CANCELED:
inv = generate_invoice(self.order)
else:
inv = c
self.order.log_action('pretix.event.order.invoice.reissued', data={
'invoice': inv.pk
})
messages.success(self.request, _('The invoice has been reissued.'))
except Exception as e:
self.order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
messages.success(self.request, _('The invoice has been reissued.'))
logger.exception("Could not generate invoice.")
invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'order': self.order.pk})
CachedTicket.objects.filter(order_position__order=self.order).delete()

View File

@@ -330,9 +330,11 @@ var ajaxErrDialog = {
$("#ajaxerr .links").html("<a class='btn btn-default ajaxerr-close'>"
+ gettext("Close message") + "</a>");
$("body").addClass("ajaxerr has-modal-dialog");
$("#ajaxerr").prop("hidden", false);
},
hide: function () {
"use strict";
$("body").removeClass("ajaxerr has-modal-dialog");
$("#ajaxerr").prop("hidden", true);
},
};

View File

@@ -60,7 +60,7 @@ var i18nToString = function (i18nstring) {
$(document).ajaxError(function (event, jqXHR, settings, thrownError) {
waitingDialog.hide();
var c = $(jqXHR.responseText).filter('.container');
if (jqXHR.responseText.indexOf("<!-- pretix-login-marker -->") !== -1) {
if (jqXHR.responseText && jqXHR.responseText.indexOf("<!-- pretix-login-marker -->") !== -1) {
location.href = '/control/login?next=' + encodeURIComponent(location.pathname + location.search + location.hash)
} else if (c.length > 0) {
ajaxErrDialog.show(c.first().html());
@@ -485,6 +485,7 @@ var form_handlers = function (el) {
theme: "bootstrap",
language: $("body").attr("data-select2-locale"),
data: JSON.parse($(this.getAttribute('data-select2-src')).text()),
width: '100%',
}).val(selectedValue).trigger('change');
});

View File

@@ -63,10 +63,6 @@ td > .form-group > .checkbox {
@include box-shadow(none);
}
div[data-formset-body], div[data-formset-form], div[data-nested-formset-form], div[data-nested-formset-body], details[data-formset-form] {
width: 100%;
}
.form-plugins .panel-title {
line-height: 34px;
}

View File

@@ -1934,7 +1934,7 @@ function buildPoslist(bysetpos, timeset, start, end, ii, dayset) {
function iter(iterResult, options) {
var dtstart = options.dtstart, freq = options.freq, interval = options.interval, until = options.until, bysetpos = options.bysetpos;
var count = options.count;
if (count <= 0 || interval <= 0) {
if (count === 0 || count < 0 || interval === 0 || interval < 0) {
return emitResult(iterResult);
}
var counterDate = datetime_DateTime.fromDate(dtstart);

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