Compare commits

..

24 Commits

Author SHA1 Message Date
Raphael Michel
be4d26cae8 ADjust more tests 2025-10-10 11:38:24 +02:00
Raphael Michel
279becbb8d Stop using ordinal numbers in English date represenation (Z#23210534) 2025-10-10 10:38:28 +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
43 changed files with 475 additions and 227 deletions

View File

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

View File

@@ -764,7 +764,13 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
) and not order.invoices.last() ) and not order.invoices.last()
invoice = None invoice = None
if gen_invoice: 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 # Refresh serializer only after running signals
prefetch_related_objects([order], self._positions_prefetch(request)) 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): def notify_webhooks(logentry_ids: list):
if not isinstance(logentry_ids, list): if not isinstance(logentry_ids, list):
logentry_ids = [logentry_ids] logentry_ids = [logentry_ids]
qs = LogEntry.all.select_related('event', 'event__organizer', 'organizer').filter(id__in=logentry_ids) qs = LogEntry.all.select_related(
_org, _at, webhooks = None, None, None '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: for logentry in qs:
if not logentry.organizer: if not logentry.organizer:
break # We need to know the organizer break # We need to know the organizer
@@ -450,7 +454,7 @@ def notify_webhooks(logentry_ids: list):
if not notification_type: if not notification_type:
break # Ignore, no webhooks for this event 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 _org = logentry.organizer
_at = logentry.action_type _at = logentry.action_type

View File

@@ -391,7 +391,7 @@ class OutboundSyncProvider:
def sync_order(self, order): def sync_order(self, order):
if not self.should_sync_order(order): if not self.should_sync_order(order):
logger.debug("Skipping order %r", order) logger.debug("Skipping order %r", order)
return return {}
logger.debug("Syncing order %r", order) logger.debug("Syncing order %r", order)
positions = list( positions = list(

View File

@@ -105,6 +105,18 @@ class BaseExporter:
""" """
return False 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 @property
def identifier(self) -> str: def identifier(self) -> str:
""" """

View File

@@ -125,6 +125,7 @@ class InvoiceExporter(InvoiceExporterMixin, BaseExporter):
identifier = 'invoices' identifier = 'invoices'
verbose_name = _('All invoices') verbose_name = _('All invoices')
description = _('Download all invoices created by the system as a ZIP file of PDF files.') 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): def render(self, form_data: dict, output_file=None):
qs = self.invoices_queryset(form_data).filter(shredded=False) 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 ' 'includes two sheets, one with a line for every invoice, and one with a line for every position of '
'every invoice.') 'every invoice.')
featured = True featured = True
repeatable_read = False
@property @property
def additional_form_fields(self): 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 ' '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.') 'a line for every additional fee charged in an order.')
featured = True featured = True
repeatable_read = False
@cached_property @cached_property
def providers(self): 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 ' 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 ' 'products, prices or tax rates. The information is only accurate for changes made with '
'pretix versions released after October 2021.') 'pretix versions released after October 2021.')
repeatable_read = False
@cached_property @cached_property
def providers(self): def providers(self):
@@ -1020,6 +1022,7 @@ class PaymentListExporter(ListExporter):
category = pgettext_lazy('export_category', 'Order data') category = pgettext_lazy('export_category', 'Order data')
description = gettext_lazy('Download a spreadsheet of all payments or refunds of every order.') description = gettext_lazy('Download a spreadsheet of all payments or refunds of every order.')
featured = True featured = True
repeatable_read = False
@property @property
def additional_form_fields(self): def additional_form_fields(self):
@@ -1159,7 +1162,7 @@ class QuotaListExporter(ListExporter):
yield headers yield headers
quotas = list(self.event.quotas.select_related('subevent')) 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.queue(*quotas)
qa.compute() qa.compute()
@@ -1200,6 +1203,7 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
organizer_required_permission = 'can_manage_gift_cards' organizer_required_permission = 'can_manage_gift_cards'
category = pgettext_lazy('export_category', 'Gift cards') category = pgettext_lazy('export_category', 'Gift cards')
description = gettext_lazy('Download a spreadsheet of all gift card transactions.') description = gettext_lazy('Download a spreadsheet of all gift card transactions.')
repeatable_read = False
@property @property
def additional_form_fields(self): def additional_form_fields(self):
@@ -1258,6 +1262,7 @@ class GiftcardRedemptionListExporter(ListExporter):
verbose_name = gettext_lazy('Gift card redemptions') verbose_name = gettext_lazy('Gift card redemptions')
category = pgettext_lazy('export_category', 'Order data') category = pgettext_lazy('export_category', 'Order data')
description = gettext_lazy('Download a spreadsheet of all payments or refunds that involve gift cards.') description = gettext_lazy('Download a spreadsheet of all payments or refunds that involve gift cards.')
repeatable_read = False
def iterate_list(self, form_data): def iterate_list(self, form_data):
payments = OrderPayment.objects.filter( payments = OrderPayment.objects.filter(

View File

@@ -34,6 +34,7 @@ class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
verbose_name = _('Reusable media') verbose_name = _('Reusable media')
category = pgettext_lazy('export_category', 'Reusable media') category = pgettext_lazy('export_category', 'Reusable media')
description = _('Download a spread sheet with the data of all reusable medias on your account.') description = _('Download a spread sheet with the data of all reusable medias on your account.')
repeatable_read = False
def iterate_list(self, form_data): def iterate_list(self, form_data):
media = ReusableMedium.objects.filter( media = ReusableMedium.objects.filter(

View File

@@ -41,6 +41,7 @@ class WaitingListExporter(ListExporter):
verbose_name = _('Waiting list') verbose_name = _('Waiting list')
category = pgettext_lazy('export_category', 'Waiting list') category = pgettext_lazy('export_category', 'Waiting list')
description = _('Download a spread sheet with all your waiting list data.') description = _('Download a spread sheet with all your waiting list data.')
repeatable_read = False
# map selected status to label and queryset-filter # map selected status to label and queryset-filter
status_filters = [ status_filters = [

View File

@@ -1840,6 +1840,10 @@ class OrderPayment(models.Model):
)) ))
return False 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): if isinstance(info, str):
locked_instance.info = info locked_instance.info = info
elif info: elif info:
@@ -1855,6 +1859,10 @@ class OrderPayment(models.Model):
'data': log_data, 'data': log_data,
}, user=user, auth=auth) }, 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: if send_mail:
with language(self.order.locale, self.order.event.settings.region): with language(self.order.locale, self.order.event.settings.region):
email_subject = self.order.event.settings.mail_subject_order_payment_failed email_subject = self.order.event.settings.mail_subject_order_payment_failed
@@ -1961,14 +1969,20 @@ class OrderPayment(models.Model):
self.order.invoice_dirty self.order.invoice_dirty
) )
if gen_invoice: if gen_invoice:
if invoices: try:
last_i = self.order.invoices.filter(is_cancellation=False).last() if invoices:
if not last_i.canceled: last_i = self.order.invoices.filter(is_cancellation=False).last()
generate_cancellation(last_i) if not last_i.canceled:
invoice = generate_invoice( generate_cancellation(last_i)
self.order, invoice = generate_invoice(
trigger_pdf=not send_mail or not self.order.event.settings.invoice_email_attachment 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_task = invoice_transmission_separately(invoice)
transmit_invoice_mail = not transmit_invoice_task and self.order.event.settings.invoice_email_attachment and self.order.email 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, periodic_task, register_data_exporters, register_multievent_data_exporters,
) )
from pretix.celery_app import app 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 from pretix.helpers.urls import build_absolute_uri
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -80,7 +80,12 @@ def export(self, event: Event, fileid: str, provider: str, form_data: Dict[str,
continue continue
ex = response(event, event.organizer, set_progress) ex = response(event, event.organizer, set_progress)
if ex.identifier == provider: 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: if d is None:
raise ExportError( raise ExportError(
gettext('Your export did not contain any data.') 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.') 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: if d is None:
raise ExportError( raise ExportError(
gettext('Your export did not contain any data.') gettext('Your export did not contain any data.')
@@ -209,7 +218,11 @@ def _run_scheduled_export(schedule, context: Union[Event, Organizer], exporter,
try: try:
if not exporter: if not exporter:
raise ExportError("Export type not found.") 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: if d is None:
raise ExportEmptyError( raise ExportEmptyError(
gettext('Your export did not contain any data.') gettext('Your export did not contain any data.')

View File

@@ -671,6 +671,7 @@ def send_invoices_to_organizer(sender, **kwargs):
event=i.event, event=i.event,
invoices=[i], invoices=[i],
auto_email=True, auto_email=True,
plain_text_only=True,
) )
i.sent_to_organizer = True i.sent_to_organizer = True
else: 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 # 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/>. # <https://www.gnu.org/licenses/>.
# #
import logging
from decimal import Decimal from decimal import Decimal
from typing import List 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_orders import get_order_import_columns
from pretix.base.modelimport_vouchers import get_voucher_import_columns from pretix.base.modelimport_vouchers import get_voucher_import_columns
from pretix.base.models import ( from pretix.base.models import (
CachedFile, Event, InvoiceAddress, Order, OrderPayment, OrderPosition, CachedFile, Event, InvoiceAddress, LogEntry, Order, OrderPayment,
User, Voucher, OrderPosition, User, Voucher,
) )
from pretix.base.models.orders import Transaction from pretix.base.models.orders import Transaction
from pretix.base.services.invoices import generate_invoice, invoice_qualified 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.base.signals import order_paid, order_placed
from pretix.celery_app import app from pretix.celery_app import app
logger = logging.getLogger(__name__)
def _validate(cf: CachedFile, charset: str, cols: List[ImportColumn], settings: dict): def _validate(cf: CachedFile, charset: str, cols: List[ImportColumn], settings: dict):
try: 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.')) raise DataImportError(_('The seat you selected has already been taken. Please select a different seat.'))
save_transactions = [] save_transactions = []
save_logentries = []
for o in orders: for o in orders:
o.total = sum([c.price for c in o._positions]) # currently no support for fees o.total = sum([c.price for c in o._positions]) # currently no support for fees
if o.total == Decimal('0.00'): 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() o._address.save()
for c in cols: for c in cols:
c.save(o) c.save(o)
o.log_action( save_logentries.append(o.log_action(
'pretix.event.order.placed', 'pretix.event.order.placed',
user=user, user=user,
data={'source': 'import'} data={'source': 'import'},
) save=False,
))
save_transactions += o.create_transactions(is_new=True, fees=[], positions=o._positions, save=False) save_transactions += o.create_transactions(is_new=True, fees=[], positions=o._positions, save=False)
Transaction.objects.bulk_create(save_transactions) Transaction.objects.bulk_create(save_transactions)
LogEntry.bulk_create_and_postprocess(save_logentries)
for o in orders: for o in orders:
with language(o.locale, event.settings.region): 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) (event.settings.get('invoice_generate') == 'paid' and o.status == Order.STATUS_PAID)
) and not o.invoices.last() ) and not o.invoices.last()
if gen_invoice: 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: except DataImportError:
raise ValidationError(_('We were not able to process your request completely as the server was too busy. ' raise ValidationError(_('We were not able to process your request completely as the server was too busy. '
'Please try again.')) 'Please try again.'))
@@ -286,13 +298,16 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
raise DataImportError( raise DataImportError(
_('The seat you selected has already been taken. Please select a different seat.')) _('The seat you selected has already been taken. Please select a different seat.'))
save_logentries = []
for v in vouchers: for v in vouchers:
v.save() v.save()
v.log_action( save_logentries.append(v.log_action(
'pretix.voucher.added', 'pretix.voucher.added',
user=user, user=user,
data={'source': 'import'} data={'source': 'import'},
) save=False,
))
for c in cols: for c in cols:
c.save(v) c.save(v)
LogEntry.bulk_create_and_postprocess(save_logentries)
cf.delete() cf.delete()

View File

@@ -41,7 +41,11 @@ def notify(logentry_ids: list):
if not isinstance(logentry_ids, list): if not isinstance(logentry_ids, list):
logentry_ids = [logentry_ids] 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 _event, _at, notify_specific, notify_global = None, None, None, None
for logentry in qs: 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() 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): 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): 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: if was_expired:
num_invoices = order.invoices.filter(is_cancellation=False).count() 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): 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() order.create_transactions()
with transaction.atomic(): 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 order.event.settings.get('invoice_generate') == 'True' and invoice_qualified(order):
if not invoice: if not invoice:
invoice = generate_invoice( try:
order, invoice = generate_invoice(
# send_mail will trigger PDF generation later order,
trigger_pdf=not transmit_invoice_mail # 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)) 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: if send_mail:
with language(order.locale, order.event.settings.region): 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']) order.save(update_fields=['status', 'cancellation_date', 'total'])
if cancel_invoice and i: 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: else:
order.status = Order.STATUS_CANCELED order.status = Order.STATUS_CANCELED
order.cancellation_date = now() order.cancellation_date = now()
@@ -1306,13 +1330,19 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
) )
) )
if invoice_required: if invoice_required:
invoice = generate_invoice( try:
order, invoice = generate_invoice(
# send_mail will trigger PDF generation later order,
trigger_pdf=not transmit_invoice_mail # 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)) 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.email:
if order.require_approval: 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(): 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) order_split.send(sender=self.order.event, original=self.order, split_order=split_order)
return split_order return split_order
@@ -2812,15 +2848,27 @@ class OrderChangeManager:
if order_now_qualified: if order_now_qualified:
if invoice_should_be_generated_now: if invoice_should_be_generated_now:
if i and not i.canceled: try:
self._invoices.append(generate_cancellation(i)) if i and not i.canceled:
self._invoices.append(generate_invoice(self.order)) 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: elif invoice_should_be_generated_later:
self.order.invoice_dirty = True self.order.invoice_dirty = True
self.order.save(update_fields=["invoice_dirty"]) self.order.save(update_fields=["invoice_dirty"])
else: else:
if i and not i.canceled: try:
self._invoices.append(generate_cancellation(i)) 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): def _check_complete_cancel(self):
current = self.order.positions.count() 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 has_active_invoice = i and not i.canceled
if has_active_invoice and order.total != oldtotal: if has_active_invoice and order.total != oldtotal:
generate_cancellation(i) try:
generate_invoice(order) 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 new_invoice_created = True
elif (not has_active_invoice or order.invoice_dirty) and invoice_qualified(order): 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 order.event.settings.get('invoice_generate') == 'paid' and
new_payment.payment_provider.requires_invoice_immediately new_payment.payment_provider.requires_invoice_immediately
): ):
if has_active_invoice: try:
generate_cancellation(i) if has_active_invoice:
i = generate_invoice(order) generate_cancellation(i)
new_invoice_created = True i = generate_invoice(order)
order.log_action('pretix.event.order.invoice.generated', data={ new_invoice_created = True
'invoice': i.pk 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() order.create_transactions()
return old_fee, new_fee, fee, new_payment, new_invoice_created 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 import django_redis
from django.conf import settings from django.conf import settings
from django.db import models from django.db import connection, models
from django.db.models import ( from django.db.models import (
Case, Count, F, Func, Max, OuterRef, Q, Subquery, Sum, Value, When, Case, Count, F, Func, Max, OuterRef, Q, Subquery, Sum, Value, When,
prefetch_related_objects, prefetch_related_objects,
@@ -64,7 +64,8 @@ class QuotaAvailability:
* count_cart (dict mapping quotas to ints) * 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 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 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 do not care about keeping the cache up to date, you can set this to ``False`` for further
performance improvements. performance improvements.
:param allow_repeatable_read: Allow to run this even in REPEATABLE READ mode, generally not advised.
""" """
self._queue = [] self._queue = []
self._count_waitinglist = count_waitinglist self._count_waitinglist = count_waitinglist
@@ -95,6 +98,7 @@ class QuotaAvailability:
self._var_to_quotas = defaultdict(set) self._var_to_quotas = defaultdict(set)
self._early_out = early_out self._early_out = early_out
self._quota_objects = {} self._quota_objects = {}
self._allow_repeatable_read = allow_repeatable_read
self.results = {} self.results = {}
self.count_paid_orders = defaultdict(int) self.count_paid_orders = defaultdict(int)
self.count_pending_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 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. 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() now_dt = now_dt or now()
quota_ids_set = {q.id for q in self._queue} quota_ids_set = {q.id for q in self._queue}
if not quota_ids_set: if not quota_ids_set:

View File

@@ -21,6 +21,8 @@
# #
from django import forms from django import forms
from django.core.exceptions import ValidationError 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 django.utils.translation import gettext_lazy as _
from pretix.base.modelimport_orders import get_order_import_columns from pretix.base.modelimport_orders import get_order_import_columns
@@ -71,6 +73,9 @@ class ProcessForm(forms.Form):
raise NotImplementedError() # noqa raise NotImplementedError() # noqa
format_html_lazy = lazy(format_html, str)
class OrdersProcessForm(ProcessForm): class OrdersProcessForm(ProcessForm):
orders = forms.ChoiceField( orders = forms.ChoiceField(
label=_('Import mode'), label=_('Import mode'),
@@ -91,7 +96,11 @@ class OrdersProcessForm(ProcessForm):
) )
testmode = forms.BooleanField( testmode = forms.BooleanField(
label=_('Create orders as test mode orders'), 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): def __init__(self, *args, **kwargs):
@@ -100,6 +109,8 @@ class OrdersProcessForm(ProcessForm):
initital['testmode'] = self.event.testmode initital['testmode'] = self.event.testmode
kwargs['initial'] = initital kwargs['initial'] = initital
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if not self.event.testmode:
self.fields["testmode"].help_text = ""
def get_columns(self): def get_columns(self):
return get_order_import_columns(self.event) return get_order_import_columns(self.event)

View File

@@ -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.customer.changed': _('The customer account has been changed.'),
'pretix.event.order.locale.changed': _('The order locale 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.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.regenerated': _('The invoice has been regenerated.'),
'pretix.event.order.invoice.reissued': _('The invoice has been reissued.'), 'pretix.event.order.invoice.reissued': _('The invoice has been reissued.'),
'pretix.event.order.invoice.sent': _('The invoice {full_invoice_no} has been sent.'), '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) prov, meta = datasync_providers.get(active_in=self.request.event, identifier=provider)
if self.request.POST.get("queue_sync") == "true": if self.request.POST.get("queue_sync") == "true":
prov.enqueue_order(self.order, 'user') prov.enqueue_order(self.order, 'user', immediate=True)
messages.success(self.request, _('The sync job has been enqueued and will run in the next minutes.')) messages.success(self.request, _('The sync job has been set to run as soon as possible.'))
elif self.request.POST.get("cancel_job"): elif self.request.POST.get("cancel_job"):
with transaction.atomic(): with transaction.atomic():
try: try:

View File

@@ -21,7 +21,8 @@
# #
import contextlib 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 import connection, transaction
from django.db.models import ( from django.db.models import (
Aggregate, Expression, F, Field, Lookup, OrderBy, Value, Aggregate, Expression, F, Field, Lookup, OrderBy, Value,
@@ -62,6 +63,43 @@ def casual_reads():
yield 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): class GroupConcat(Aggregate):
function = 'group_concat' function = 'group_concat'
template = '%(function)s(%(distinct)s%(field)s, "%(separator)s")' template = '%(function)s(%(distinct)s%(field)s, "%(separator)s")'

View File

@@ -76,11 +76,11 @@ def daterange(df, dt, as_html=False):
return format_html(base_format, _date(df, "j F"), until, _date(dt, "j F Y")) return format_html(base_format, _date(df, "j F"), until, _date(dt, "j F Y"))
elif lng.startswith("en"): elif lng.startswith("en"):
if df.year == dt.year and df.month == dt.month and df.day == dt.day: if df.year == dt.year and df.month == dt.month and df.day == dt.day:
return format_html(base_format, _date(df, "D, N jS, Y")) return format_html(base_format, _date(df, "D, N j, Y"))
elif df.year == dt.year and df.month == dt.month: elif df.year == dt.year and df.month == dt.month:
return format_html(base_format, _date(df, "N jS"), until, _date(dt, "jS, Y")) return format_html(base_format, _date(df, "N j"), until, _date(dt, "j, Y"))
elif df.year == dt.year: elif df.year == dt.year:
return format_html(base_format, _date(df, "N jS"), until, _date(dt, "N jS, Y")) return format_html(base_format, _date(df, "N j"), until, _date(dt, "N j, Y"))
elif lng.startswith("es"): elif lng.startswith("es"):
if df.year == dt.year and df.month == dt.month and df.day == dt.day: if df.year == dt.year and df.month == dt.month and df.day == dt.day:
return format_html(base_format, _date(df, "DATE_FORMAT")) return format_html(base_format, _date(df, "DATE_FORMAT"))

View File

@@ -38,5 +38,5 @@ SHORT_DATE_FORMAT = 'Y-m-d'
SHORT_DATETIME_FORMAT = 'Y-m-d H:i' SHORT_DATETIME_FORMAT = 'Y-m-d H:i'
TIME_FORMAT = 'H:i' TIME_FORMAT = 'H:i'
WEEK_FORMAT = '\\W W, o' WEEK_FORMAT = '\\W W, o'
WEEK_DAY_FORMAT = 'D, M jS' WEEK_DAY_FORMAT = 'D, M j'
SHORT_MONTH_DAY_FORMAT = 'd.m.' SHORT_MONTH_DAY_FORMAT = 'd.m.'

View File

@@ -38,7 +38,7 @@ SHORT_DATE_FORMAT = "Y-m-d"
SHORT_DATETIME_FORMAT = 'Y-m-d P' SHORT_DATETIME_FORMAT = 'Y-m-d P'
TIME_FORMAT = 'P' TIME_FORMAT = 'P'
WEEK_FORMAT = '\\W W, o' WEEK_FORMAT = '\\W W, o'
WEEK_DAY_FORMAT = 'D, M jS' WEEK_DAY_FORMAT = 'D, M j'
SHORT_MONTH_DAY_FORMAT = 'm/d' SHORT_MONTH_DAY_FORMAT = 'm/d'
DATE_INPUT_FORMATS = [ DATE_INPUT_FORMATS = [

View File

@@ -25,7 +25,7 @@ SHORT_DATE_FORMAT = 'm/d/Y'
SHORT_DATETIME_FORMAT = 'm/d/Y P' SHORT_DATETIME_FORMAT = 'm/d/Y P'
TIME_FORMAT = 'P' TIME_FORMAT = 'P'
WEEK_FORMAT = '\\W W, o' WEEK_FORMAT = '\\W W, o'
WEEK_DAY_FORMAT = 'D, M jS' WEEK_DAY_FORMAT = 'D, M j'
SHORT_MONTH_DAY_FORMAT = 'm/d' SHORT_MONTH_DAY_FORMAT = 'm/d'
DATE_INPUT_FORMATS = [ DATE_INPUT_FORMATS = [

View File

@@ -5,8 +5,8 @@ msgstr ""
"Project-Id-Version: 1\n" "Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-26 11:16+0000\n" "POT-Creation-Date: 2025-09-26 11:16+0000\n"
"PO-Revision-Date: 2025-09-26 13:02+0000\n" "PO-Revision-Date: 2025-10-03 01:00+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n" "Last-Translator: Mira <weller@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/de/" "Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/de/"
">\n" ">\n"
"Language: de\n" "Language: de\n"
@@ -28214,7 +28214,7 @@ msgstr ""
#: pretix/control/views/modelimport.py:174 #: pretix/control/views/modelimport.py:174
msgid "The import was successful." msgid "The import was successful."
msgstr "Die Import war erfolgreich." msgstr "Der Import war erfolgreich."
#: pretix/control/views/modelimport.py:186 #: pretix/control/views/modelimport.py:186
msgid "We've been unable to parse the uploaded file as a CSV file." 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" "Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-26 11:16+0000\n" "POT-Creation-Date: 2025-09-26 11:16+0000\n"
"PO-Revision-Date: 2025-09-26 13:02+0000\n" "PO-Revision-Date: 2025-10-03 01:00+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n" "Last-Translator: Mira <weller@rami.io>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/" "Language-Team: German (informal) <https://translate.pretix.eu/projects/"
"pretix/pretix/de_Informal/>\n" "pretix/pretix/de_Informal/>\n"
"Language: de_Informal\n" "Language: de_Informal\n"
@@ -28172,7 +28172,7 @@ msgstr ""
#: pretix/control/views/modelimport.py:174 #: pretix/control/views/modelimport.py:174
msgid "The import was successful." msgid "The import was successful."
msgstr "Die Import war erfolgreich." msgstr "Der Import war erfolgreich."
#: pretix/control/views/modelimport.py:186 #: pretix/control/views/modelimport.py:186
msgid "We've been unable to parse the uploaded file as a CSV file." 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" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-26 11:16+0000\n" "POT-Creation-Date: 2025-09-26 11:16+0000\n"
"PO-Revision-Date: 2025-05-04 16:00+0000\n" "PO-Revision-Date: 2025-10-04 10:10+0000\n"
"Last-Translator: Pekka Sarkola <pekka.sarkola@gispo.fi>\n" "Last-Translator: Sebastian Bożek <sebastian@log-mar.pl>\n"
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix/"
"fi/>\n" "fi/>\n"
"Language: fi\n" "Language: fi\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\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 #: pretix/_base_settings.py:87
msgid "English" msgid "English"
@@ -113,7 +113,7 @@ msgstr "norja (kirjakieli)"
#: pretix/_base_settings.py:110 #: pretix/_base_settings.py:110
msgid "Polish" msgid "Polish"
msgstr "puola" msgstr "Puola"
#: pretix/_base_settings.py:111 #: pretix/_base_settings.py:111
msgid "Portuguese (Portugal)" msgid "Portuguese (Portugal)"

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-08-19 16:35+0000\n" "POT-Creation-Date: 2025-08-19 16:35+0000\n"
"PO-Revision-Date: 2025-09-30 16:00+0000\n" "PO-Revision-Date: 2025-10-03 20:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n" "Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
"ja/>\n" "ja/>\n"
"Language: 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/forms.py:234 pretix/plugins/sendmail/forms.py:386
#: pretix/plugins/sendmail/views.py:267 #: pretix/plugins/sendmail/views.py:267
msgid "payment pending but already confirmed" msgid "payment pending but already confirmed"
msgstr "支払い保留中ですが、すでに確認済み" msgstr "支払い保留中だが確認済み"
#: pretix/plugins/sendmail/forms.py:235 pretix/plugins/sendmail/forms.py:388 #: pretix/plugins/sendmail/forms.py:235 pretix/plugins/sendmail/forms.py:388
#: pretix/plugins/sendmail/views.py:268 #: pretix/plugins/sendmail/views.py:268

View File

@@ -7,7 +7,7 @@ msgstr ""
"Project-Id-Version: 1\n" "Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-26 11:16+0000\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" "Last-Translator: Jan Van Haver <jan.van.haver@gmail.com>\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/>" "Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/>"
"\n" "\n"
@@ -808,6 +808,8 @@ msgid ""
"Field \"{field_name}\" does not exist. Please check your {provider_name} " "Field \"{field_name}\" does not exist. Please check your {provider_name} "
"settings." "settings."
msgstr "" msgstr ""
"Het veld \"{field_name}\" bestaat niet. Controleer uw {provider_name} "
"instellingen."
#: pretix/base/datasync/datasync.py:262 #: pretix/base/datasync/datasync.py:262
#, python-brace-format #, python-brace-format
@@ -815,6 +817,8 @@ msgid ""
"Field \"{field_name}\" requires {required_input}, but only got " "Field \"{field_name}\" requires {required_input}, but only got "
"{available_inputs}. Please check your {provider_name} settings." "{available_inputs}. Please check your {provider_name} settings."
msgstr "" msgstr ""
"Het veld \"{field_name}\" vereist {required_input}, maar heeft alleen "
"{available_inputs}. Controleer uw {provider_name} instellingen."
#: pretix/base/datasync/datasync.py:273 #: pretix/base/datasync/datasync.py:273
#, python-brace-format #, python-brace-format
@@ -822,18 +826,16 @@ msgid ""
"Please update value mapping for field \"{field_name}\" - option \"{val}\" " "Please update value mapping for field \"{field_name}\" - option \"{val}\" "
"not assigned" "not assigned"
msgstr "" msgstr ""
"Werk de mapping van de waarden bij voor veld \"{field_name}\" - optie "
"\"{val}\" is niet toegewezen"
#: pretix/base/datasync/sourcefields.py:128 #: pretix/base/datasync/sourcefields.py:128
#, fuzzy
#| msgid "Order positions"
msgid "Order position details" msgid "Order position details"
msgstr "Bestelde producten" msgstr "Details bestelde producten"
#: pretix/base/datasync/sourcefields.py:129 #: pretix/base/datasync/sourcefields.py:129
#, fuzzy
#| msgid "Attendee email"
msgid "Attendee details" 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/datasync/sourcefields.py:130 pretix/base/exporters/answers.py:66
#: pretix/base/models/items.py:1767 pretix/control/navigation.py:172 #: pretix/base/models/items.py:1767 pretix/control/navigation.py:172
@@ -843,10 +845,8 @@ msgid "Questions"
msgstr "Vragen" msgstr "Vragen"
#: pretix/base/datasync/sourcefields.py:131 #: pretix/base/datasync/sourcefields.py:131
#, fuzzy
#| msgid "Product data"
msgid "Product details" msgid "Product details"
msgstr "Productgegevens" msgstr "Productdetails"
#: pretix/base/datasync/sourcefields.py:132 #: pretix/base/datasync/sourcefields.py:132
#: pretix/control/templates/pretixcontrol/event/settings.html:280 #: pretix/control/templates/pretixcontrol/event/settings.html:280
@@ -1038,16 +1038,12 @@ msgid "Product ID"
msgstr "Product ID" msgstr "Product ID"
#: pretix/base/datasync/sourcefields.py:419 #: pretix/base/datasync/sourcefields.py:419
#, fuzzy
#| msgid "Non-admission product"
msgid "Product is admission product" msgid "Product is admission product"
msgstr "Geen toegangsbewijs" msgstr "Product is een toegangsbewijs"
#: pretix/base/datasync/sourcefields.py:428 #: pretix/base/datasync/sourcefields.py:428
#, fuzzy
#| msgid "Event short name"
msgid "Event short form" 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/datasync/sourcefields.py:437 pretix/base/exporters/events.py:57
#: pretix/base/exporters/orderlist.py:262 #: pretix/base/exporters/orderlist.py:262
@@ -1090,10 +1086,8 @@ msgid "Order code and position number"
msgstr "Bestelcode en plaatsnummer" msgstr "Bestelcode en plaatsnummer"
#: pretix/base/datasync/sourcefields.py:482 #: pretix/base/datasync/sourcefields.py:482
#, fuzzy
#| msgid "Ticket page"
msgid "Ticket price" msgid "Ticket price"
msgstr "Ticketpagina" msgstr "Ticketprijs"
#: pretix/base/datasync/sourcefields.py:491 pretix/base/notifications.py:204 #: pretix/base/datasync/sourcefields.py:491 pretix/base/notifications.py:204
#: pretix/control/forms/filter.py:216 pretix/control/forms/modelimport.py:85 #: pretix/control/forms/filter.py:216 pretix/control/forms/modelimport.py:85
@@ -1101,22 +1095,16 @@ msgid "Order status"
msgstr "Bestelstatus" msgstr "Bestelstatus"
#: pretix/base/datasync/sourcefields.py:500 #: pretix/base/datasync/sourcefields.py:500
#, fuzzy
#| msgid "Device status"
msgid "Ticket status" msgid "Ticket status"
msgstr "Apparaatstatus" msgstr "Ticketstatus"
#: pretix/base/datasync/sourcefields.py:509 #: pretix/base/datasync/sourcefields.py:509
#, fuzzy
#| msgid "Purchase date and time"
msgid "Order date and time" msgid "Order date and time"
msgstr "Aankoopdatum en -tijd" msgstr "Besteldatum en -tijd"
#: pretix/base/datasync/sourcefields.py:518 #: pretix/base/datasync/sourcefields.py:518
#, fuzzy
#| msgid "Printing date and time"
msgid "Payment date and time" msgid "Payment date and time"
msgstr "Printdatum en -tijd" msgstr "Betaaldatum en -tijd"
#: pretix/base/datasync/sourcefields.py:527 #: pretix/base/datasync/sourcefields.py:527
#: pretix/base/exporters/orderlist.py:271 #: pretix/base/exporters/orderlist.py:271
@@ -1127,23 +1115,17 @@ msgid "Order locale"
msgstr "Taal van bestelling" msgstr "Taal van bestelling"
#: pretix/base/datasync/sourcefields.py:536 #: pretix/base/datasync/sourcefields.py:536
#, fuzzy
#| msgid "Order position"
msgid "Order position ID" msgid "Order position ID"
msgstr "Besteld product" msgstr "ID besteld product"
#: pretix/base/datasync/sourcefields.py:545 #: pretix/base/datasync/sourcefields.py:545
#: pretix/base/exporters/orderlist.py:291 #: pretix/base/exporters/orderlist.py:291
#, fuzzy
#| msgid "Order time"
msgid "Order link" msgid "Order link"
msgstr "Besteltijd" msgstr "Bestellink"
#: pretix/base/datasync/sourcefields.py:560 #: pretix/base/datasync/sourcefields.py:560
#, fuzzy
#| msgid "Ticket block"
msgid "Ticket link" msgid "Ticket link"
msgstr "Ticketblok" msgstr "Ticketlink"
#: pretix/base/datasync/sourcefields.py:578 #: pretix/base/datasync/sourcefields.py:578
#, fuzzy, python-brace-format #, fuzzy, python-brace-format

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-26 11:16+0000\n" "POT-Creation-Date: 2025-09-26 11:16+0000\n"
"PO-Revision-Date: 2025-06-02 23:00+0000\n" "PO-Revision-Date: 2025-10-04 19:00+0000\n"
"Last-Translator: Anarion Dunedain <anarion80@gmail.com>\n" "Last-Translator: Sebastian Bożek <sebastian@log-mar.pl>\n"
"Language-Team: Polish <https://translate.pretix.eu/projects/pretix/pretix/pl/" "Language-Team: Polish <https://translate.pretix.eu/projects/pretix/pretix/pl/"
">\n" ">\n"
"Language: pl\n" "Language: pl\n"
@@ -18,7 +18,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " "Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2;\n" "|| 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 #: pretix/_base_settings.py:87
msgid "English" msgid "English"
@@ -90,7 +90,7 @@ msgstr "Grecki"
#: pretix/_base_settings.py:104 #: pretix/_base_settings.py:104
msgid "Hebrew" msgid "Hebrew"
msgstr "" msgstr "Hebrajski"
#: pretix/_base_settings.py:105 #: pretix/_base_settings.py:105
msgid "Indonesian" msgid "Indonesian"
@@ -146,7 +146,7 @@ msgstr "Hiszpański"
#: pretix/_base_settings.py:118 #: pretix/_base_settings.py:118
msgid "Spanish (Latin America)" msgid "Spanish (Latin America)"
msgstr "" msgstr "Hiszpański (Ameryka Łacińska)"
#: pretix/_base_settings.py:119 #: pretix/_base_settings.py:119
msgid "Turkish" msgid "Turkish"
@@ -2328,7 +2328,7 @@ msgstr ""
#: pretix/base/exporters/waitinglist.py:115 pretix/control/forms/event.py:1671 #: pretix/base/exporters/waitinglist.py:115 pretix/control/forms/event.py:1671
#: pretix/control/forms/organizer.py:116 #: pretix/control/forms/organizer.py:116
msgid "Event slug" 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:262
#: pretix/base/exporters/orderlist.py:452 #: pretix/base/exporters/orderlist.py:452
@@ -32680,7 +32680,7 @@ msgid ""
"banks. Please keep your online banking account and login information " "banks. Please keep your online banking account and login information "
"available." "available."
msgstr "" 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." "Prosimy o zachowanie danych logowania i konta bankowego."
#: pretix/plugins/stripe/payment.py:1768 #: pretix/plugins/stripe/payment.py:1768

View File

@@ -476,6 +476,7 @@ class CSVCheckinList(CheckInListMixin, ListExporter):
category = pgettext_lazy('export_category', 'Check-in') category = pgettext_lazy('export_category', 'Check-in')
description = gettext_lazy("Download a spreadsheet with all attendees that are included in a check-in list.") description = gettext_lazy("Download a spreadsheet with all attendees that are included in a check-in list.")
featured = True featured = True
repeatable_read = False
@property @property
def additional_form_fields(self): def additional_form_fields(self):
@@ -673,6 +674,7 @@ class CSVCheckinCodeList(CheckInListMixin, ListExporter):
category = pgettext_lazy('export_category', 'Check-in') 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 " 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.") "different system. Does not included blocked codes or personal data.")
repeatable_read = False
@property @property
def additional_form_fields(self): def additional_form_fields(self):
@@ -743,6 +745,7 @@ class CheckinLogList(ListExporter):
category = pgettext_lazy('export_category', 'Check-in') 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 " description = gettext_lazy("Download a spreadsheet with one line for every scan that happened at your check-in "
"stations.") "stations.")
repeatable_read = False
@property @property
def additional_form_fields(self): def additional_form_fields(self):

View File

@@ -661,6 +661,7 @@ class OrderTaxListReport(MultiSheetListExporter):
verbose_name = gettext_lazy('Tax split list') verbose_name = gettext_lazy('Tax split list')
category = pgettext_lazy('export_category', 'Order data') category = pgettext_lazy('export_category', 'Order data')
description = gettext_lazy("Download a spreadsheet with the tax amounts included in each order.") description = gettext_lazy("Download a spreadsheet with the tax amounts included in each order.")
repeatable_read = False
@property @property
def sheets(self): def sheets(self):

View File

@@ -36,6 +36,7 @@ import copy
import hmac import hmac
import inspect import inspect
import json import json
import logging
import mimetypes import mimetypes
import os import os
import re import re
@@ -98,6 +99,8 @@ from pretix.presale.views import (
from pretix.presale.views.event import get_grouped_items from pretix.presale.views.event import get_grouped_items
from pretix.presale.views.robots import NoSearchIndexViewMixin from pretix.presale.views.robots import NoSearchIndexViewMixin
logger = logging.getLogger(__name__)
class OrderDetailMixin(NoSearchIndexViewMixin): class OrderDetailMixin(NoSearchIndexViewMixin):
@@ -734,11 +737,18 @@ class OrderInvoiceCreate(EventViewMixin, OrderDetailMixin, View):
elif self.order.invoices.exists(): elif self.order.invoices.exists():
messages.error(self.request, _('An invoice for this order already exists.')) messages.error(self.request, _('An invoice for this order already exists.'))
else: else:
i = generate_invoice(self.order) try:
self.order.log_action('pretix.event.order.invoice.generated', data={ i = generate_invoice(self.order)
'invoice': i.pk self.order.log_action('pretix.event.order.invoice.generated', data={
}) 'invoice': i.pk
messages.success(self.request, _('The invoice has been generated.')) })
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()) return redirect(self.get_order_url())
@@ -807,24 +817,37 @@ class OrderModify(EventViewMixin, OrderDetailMixin, OrderQuestionsViewMixin, Tem
elif self.order.invoices.exists(): elif self.order.invoices.exists():
messages.error(self.request, _('An invoice for this order already exists.')) messages.error(self.request, _('An invoice for this order already exists.'))
else: else:
i = generate_invoice(self.order) try:
self.order.log_action('pretix.event.order.invoice.generated', data={ i = generate_invoice(self.order)
'invoice': i.pk self.order.log_action('pretix.event.order.invoice.generated', data={
}) 'invoice': i.pk
messages.success(self.request, _('The invoice has been generated.')) })
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: elif self.request.event.settings.invoice_reissue_after_modify:
if self.invoice_form.changed_data: if self.invoice_form.changed_data:
inv = self.order.invoices.last() try:
if inv and not inv.canceled and not inv.shredded: inv = self.order.invoices.last()
c = generate_cancellation(inv) if inv and not inv.canceled and not inv.shredded:
if self.order.status != Order.STATUS_CANCELED: c = generate_cancellation(inv)
inv = generate_invoice(self.order) if self.order.status != Order.STATUS_CANCELED:
else: inv = generate_invoice(self.order)
inv = c else:
self.order.log_action('pretix.event.order.invoice.reissued', data={ inv = c
'invoice': inv.pk 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}) invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'order': self.order.pk})
CachedTicket.objects.filter(order_position__order=self.order).delete() 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'>" $("#ajaxerr .links").html("<a class='btn btn-default ajaxerr-close'>"
+ gettext("Close message") + "</a>"); + gettext("Close message") + "</a>");
$("body").addClass("ajaxerr has-modal-dialog"); $("body").addClass("ajaxerr has-modal-dialog");
$("#ajaxerr").prop("hidden", false);
}, },
hide: function () { hide: function () {
"use strict"; "use strict";
$("body").removeClass("ajaxerr has-modal-dialog"); $("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) { $(document).ajaxError(function (event, jqXHR, settings, thrownError) {
waitingDialog.hide(); waitingDialog.hide();
var c = $(jqXHR.responseText).filter('.container'); 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) location.href = '/control/login?next=' + encodeURIComponent(location.pathname + location.search + location.hash)
} else if (c.length > 0) { } else if (c.length > 0) {
ajaxErrDialog.show(c.first().html()); ajaxErrDialog.show(c.first().html());
@@ -485,6 +485,7 @@ var form_handlers = function (el) {
theme: "bootstrap", theme: "bootstrap",
language: $("body").attr("data-select2-locale"), language: $("body").attr("data-select2-locale"),
data: JSON.parse($(this.getAttribute('data-select2-src')).text()), data: JSON.parse($(this.getAttribute('data-select2-src')).text()),
width: '100%',
}).val(selectedValue).trigger('change'); }).val(selectedValue).trigger('change');
}); });

View File

@@ -63,10 +63,6 @@ td > .form-group > .checkbox {
@include box-shadow(none); @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 { .form-plugins .panel-title {
line-height: 34px; line-height: 34px;
} }

View File

@@ -414,7 +414,7 @@ def test_2f1r_discount_cross_selling_eventseries_mixed(eventseries):
Regular Ticket 42.00 42.00 Date1 Regular Ticket 42.00 42.00 Date1
''', ''',
recommendations=f''' Price Discounted Price Max Count Prefix recommendations=f''' Price Discounted Price Max Count Prefix
Tickets (Date1 - Wed, Jan. 1st, 2020 10:00) Reduced Ticket 23.00 11.50 1 {prefix_date1} Tickets (Date1 - Wed, Jan. 1, 2020 10:00) Reduced Ticket 23.00 11.50 1 {prefix_date1}
''' '''
) )
check_cart_behaviour( check_cart_behaviour(
@@ -424,8 +424,8 @@ def test_2f1r_discount_cross_selling_eventseries_mixed(eventseries):
Regular Ticket 42.00 42.00 Date2 Regular Ticket 42.00 42.00 Date2
''', ''',
recommendations=f''' Price Discounted Price Max Count Prefix recommendations=f''' Price Discounted Price Max Count Prefix
Tickets (Date1 - Wed, Jan. 1st, 2020 10:00) Reduced Ticket 23.00 11.50 1 {prefix_date1} Tickets (Date1 - Wed, Jan. 1, 2020 10:00) Reduced Ticket 23.00 11.50 1 {prefix_date1}
Tickets (Date2 - Wed, Jan. 1st, 2020 11:00) Reduced Ticket 23.00 11.50 1 {prefix_date2} Tickets (Date2 - Wed, Jan. 1, 2020 11:00) Reduced Ticket 23.00 11.50 1 {prefix_date2}
''' '''
) )
check_cart_behaviour( check_cart_behaviour(
@@ -460,8 +460,8 @@ def test_2f1r_discount_cross_selling_eventseries_mixed(eventseries):
Regular Ticket 42.00 42.00 Date2 Regular Ticket 42.00 42.00 Date2
''', ''',
recommendations=f''' Price Discounted Price Max Count Prefix recommendations=f''' Price Discounted Price Max Count Prefix
Tickets (Date1 - Wed, Jan. 1st, 2020 10:00) Reduced Ticket 23.00 11.50 2 {prefix_date1} Tickets (Date1 - Wed, Jan. 1, 2020 10:00) Reduced Ticket 23.00 11.50 2 {prefix_date1}
Tickets (Date2 - Wed, Jan. 1st, 2020 11:00) Reduced Ticket 23.00 11.50 2 {prefix_date2} Tickets (Date2 - Wed, Jan. 1, 2020 11:00) Reduced Ticket 23.00 11.50 2 {prefix_date2}
''' '''
) )
check_cart_behaviour( check_cart_behaviour(
@@ -475,7 +475,7 @@ def test_2f1r_discount_cross_selling_eventseries_mixed(eventseries):
Reduced Ticket 23.00 11.50 Date1 Reduced Ticket 23.00 11.50 Date1
''', ''',
recommendations=f''' Price Discounted Price Max Count Prefix recommendations=f''' Price Discounted Price Max Count Prefix
Tickets (Date1 - Wed, Jan. 1st, 2020 10:00) Reduced Ticket 23.00 11.50 1 {prefix_date1} Tickets (Date1 - Wed, Jan. 1, 2020 10:00) Reduced Ticket 23.00 11.50 1 {prefix_date1}
''' '''
) )
check_cart_behaviour( check_cart_behaviour(
@@ -490,7 +490,7 @@ def test_2f1r_discount_cross_selling_eventseries_mixed(eventseries):
Reduced Ticket 23.00 11.50 Date1 Reduced Ticket 23.00 11.50 Date1
''', ''',
recommendations=f''' Price Discounted Price Max Count Prefix recommendations=f''' Price Discounted Price Max Count Prefix
Tickets (Date1 - Wed, Jan. 1st, 2020 10:00) Reduced Ticket 23.00 11.50 1 {prefix_date1} Tickets (Date1 - Wed, Jan. 1, 2020 10:00) Reduced Ticket 23.00 11.50 1 {prefix_date1}
''' '''
) )
check_cart_behaviour( check_cart_behaviour(
@@ -506,7 +506,7 @@ def test_2f1r_discount_cross_selling_eventseries_mixed(eventseries):
Reduced Ticket 23.00 11.50 Date1 Reduced Ticket 23.00 11.50 Date1
''', ''',
recommendations=f''' Price Discounted Price Max Count Prefix recommendations=f''' Price Discounted Price Max Count Prefix
Tickets (Date1 - Wed, Jan. 1st, 2020 10:00) Reduced Ticket 23.00 11.50 2 {prefix_date1} Tickets (Date1 - Wed, Jan. 1, 2020 10:00) Reduced Ticket 23.00 11.50 2 {prefix_date1}
''' '''
) )
@@ -531,8 +531,8 @@ def test_2f1r_discount_cross_selling_eventseries_same(eventseries):
Regular Ticket 42.00 42.00 Date2 Regular Ticket 42.00 42.00 Date2
''', ''',
recommendations=f''' Price Discounted Price Max Count Prefix recommendations=f''' Price Discounted Price Max Count Prefix
Tickets (Date1 - Wed, Jan. 1st, 2020 10:00) Reduced Ticket 23.00 11.50 1 {prefix_date1} Tickets (Date1 - Wed, Jan. 1, 2020 10:00) Reduced Ticket 23.00 11.50 1 {prefix_date1}
Tickets (Date2 - Wed, Jan. 1st, 2020 11:00) Reduced Ticket 23.00 11.50 1 {prefix_date2} Tickets (Date2 - Wed, Jan. 1, 2020 11:00) Reduced Ticket 23.00 11.50 1 {prefix_date2}
''' '''
) )
check_cart_behaviour( check_cart_behaviour(
@@ -546,8 +546,8 @@ def test_2f1r_discount_cross_selling_eventseries_same(eventseries):
Regular Ticket 42.00 42.00 Date2 Regular Ticket 42.00 42.00 Date2
''', ''',
recommendations=f''' Price Discounted Price Max Count Prefix recommendations=f''' Price Discounted Price Max Count Prefix
Tickets (Date1 - Wed, Jan. 1st, 2020 10:00) Reduced Ticket 23.00 11.50 1 {prefix_date1} Tickets (Date1 - Wed, Jan. 1, 2020 10:00) Reduced Ticket 23.00 11.50 1 {prefix_date1}
Tickets (Date2 - Wed, Jan. 1st, 2020 11:00) Reduced Ticket 23.00 11.50 2 {prefix_date2} Tickets (Date2 - Wed, Jan. 1, 2020 11:00) Reduced Ticket 23.00 11.50 2 {prefix_date2}
''' '''
) )
check_cart_behaviour( check_cart_behaviour(
@@ -561,7 +561,7 @@ def test_2f1r_discount_cross_selling_eventseries_same(eventseries):
Reduced Ticket 23.00 11.50 Date1 Reduced Ticket 23.00 11.50 Date1
''', ''',
recommendations=f''' Price Discounted Price Max Count Prefix recommendations=f''' Price Discounted Price Max Count Prefix
Tickets (Date1 - Wed, Jan. 1st, 2020 10:00) Reduced Ticket 23.00 11.50 1 {prefix_date1} Tickets (Date1 - Wed, Jan. 1, 2020 10:00) Reduced Ticket 23.00 11.50 1 {prefix_date1}
''' '''
) )
check_cart_behaviour( check_cart_behaviour(
@@ -589,7 +589,7 @@ def test_2f1r_discount_cross_selling_eventseries_same(eventseries):
Reduced Ticket 23.00 11.50 Date1 Reduced Ticket 23.00 11.50 Date1
''', ''',
recommendations=f''' Price Discounted Price Max Count Prefix recommendations=f''' Price Discounted Price Max Count Prefix
Tickets (Date2 - Wed, Jan. 1st, 2020 11:00) Reduced Ticket 23.00 11.50 1 {prefix_date2} Tickets (Date2 - Wed, Jan. 1, 2020 11:00) Reduced Ticket 23.00 11.50 1 {prefix_date2}
''' '''
) )

View File

@@ -327,6 +327,26 @@ def test_enqueue_order_twice(event):
SimpleOrderSync.enqueue_order(order, 'testcase_2nd') SimpleOrderSync.enqueue_order(order, 'testcase_2nd')
class DoNothingSync(SimpleOrderSync):
def should_sync_order(self, order):
return False
@pytest.mark.django_db
def test_should_not_sync(event):
_register_with_fake_plugin_name(datasync_providers, DoNothingSync, 'testplugin')
DoNothingSync.fake_api_client = FakeSyncAPI()
for order in event.orders.order_by("code").all():
DoNothingSync.enqueue_order(order, 'testcase')
sync_all()
assert DoNothingSync.fake_api_client.fake_database == {}
StaticMappingWithAssociations = namedtuple('StaticMappingWithAssociations', ( StaticMappingWithAssociations = namedtuple('StaticMappingWithAssociations', (
'id', 'pretix_model', 'external_object_type', 'pretix_id_field', 'external_id_field', 'property_mappings', 'association_mappings' 'id', 'pretix_model', 'external_object_type', 'pretix_id_field', 'external_id_field', 'property_mappings', 'association_mappings'
)) ))

View File

@@ -64,6 +64,7 @@ from pretix.base.models.items import (
from pretix.base.reldate import RelativeDate, RelativeDateWrapper from pretix.base.reldate import RelativeDate, RelativeDateWrapper
from pretix.base.services.orders import OrderError, cancel_order, perform_order from pretix.base.services.orders import OrderError, cancel_order, perform_order
from pretix.base.services.quotas import QuotaAvailability from pretix.base.services.quotas import QuotaAvailability
from pretix.helpers import repeatable_reads_transaction
from pretix.testutils.scope import classscope from pretix.testutils.scope import classscope
@@ -99,6 +100,29 @@ class BaseQuotaTestCase(TestCase):
self.var3 = ItemVariation.objects.create(item=self.item3, value='Fancy') 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") @pytest.mark.usefixtures("fakeredis_client")
class QuotaTestCase(BaseQuotaTestCase): class QuotaTestCase(BaseQuotaTestCase):
@classscope(attr='o') @classscope(attr='o')
@@ -2465,44 +2489,44 @@ class EventTest(TestCase):
( (
datetime.datetime(2025, 3, 9, 21, 0, 0, tzinfo=tz), datetime.datetime(2025, 3, 9, 21, 0, 0, tzinfo=tz),
datetime.datetime(2025, 3, 9, 22, 0, 0, tzinfo=tz), datetime.datetime(2025, 3, 9, 22, 0, 0, tzinfo=tz),
'Sun, March 9th, 2025', 'Sun, March 9, 2025',
'<time datetime="2025-03-09">Sun, March 9th, 2025</time>', '<time datetime="2025-03-09">Sun, March 9, 2025</time>',
'Sun, March 9th, 2025 20:0021:00', 'Sun, March 9, 2025 20:0021:00',
'<time datetime="2025-03-09">Sun, March 9th, 2025</time> ' '<time datetime="2025-03-09">Sun, March 9, 2025</time> '
'<time datetime="2025-03-09T21:00:00+01:00" data-timezone="UTC" data-time-short>20:0021:00</time>' '<time datetime="2025-03-09T21:00:00+01:00" data-timezone="UTC" data-time-short>20:0021:00</time>'
), ),
( (
datetime.datetime(2025, 3, 9, 21, 0, 0, tzinfo=tz), datetime.datetime(2025, 3, 9, 21, 0, 0, tzinfo=tz),
datetime.datetime(2025, 3, 10, 3, 0, 0, tzinfo=tz), datetime.datetime(2025, 3, 10, 3, 0, 0, tzinfo=tz),
'March 9th 10th, 2025', 'March 9 10, 2025',
'<time datetime="2025-03-09">March 9th</time> ' '<time datetime="2025-03-09">March 9</time> '
'<span aria-hidden="true"></span><span class="sr-only"> until </span> ' '<span aria-hidden="true"></span><span class="sr-only"> until </span> '
'<time datetime="2025-03-10">10th, 2025</time>', '<time datetime="2025-03-10">10, 2025</time>',
'March 9th 10th, 2025 20:0002:00', 'March 9 10, 2025 20:0002:00',
'<time datetime="2025-03-09">March 9th</time> ' '<time datetime="2025-03-09">March 9</time> '
'<span aria-hidden="true"></span><span class="sr-only"> until </span> ' '<span aria-hidden="true"></span><span class="sr-only"> until </span> '
'<time datetime="2025-03-10">10th, 2025</time> ' '<time datetime="2025-03-10">10, 2025</time> '
'<time datetime="2025-03-09T21:00:00+01:00" data-timezone="UTC" data-time-short>20:0002:00</time>' '<time datetime="2025-03-09T21:00:00+01:00" data-timezone="UTC" data-time-short>20:0002:00</time>'
), ),
( (
datetime.datetime(2025, 3, 9, 21, 0, 0, tzinfo=tz), datetime.datetime(2025, 3, 9, 21, 0, 0, tzinfo=tz),
datetime.datetime(2025, 3, 12, 14, 0, 0, tzinfo=tz), datetime.datetime(2025, 3, 12, 14, 0, 0, tzinfo=tz),
'March 9th 12th, 2025', 'March 9 12, 2025',
'<time datetime="2025-03-09">March 9th</time> ' '<time datetime="2025-03-09">March 9</time> '
'<span aria-hidden="true"></span><span class="sr-only"> until </span> ' '<span aria-hidden="true"></span><span class="sr-only"> until </span> '
'<time datetime="2025-03-12">12th, 2025</time>', '<time datetime="2025-03-12">12, 2025</time>',
'March 9th 12th, 2025', 'March 9 12, 2025',
'<time datetime="2025-03-09">March 9th</time> ' '<time datetime="2025-03-09">March 9</time> '
'<span aria-hidden="true"></span><span class="sr-only"> until </span> ' '<span aria-hidden="true"></span><span class="sr-only"> until </span> '
'<time datetime="2025-03-12">12th, 2025</time>', '<time datetime="2025-03-12">12, 2025</time>',
), ),
( (
datetime.datetime(2025, 3, 9, 21, 0, 0, tzinfo=tz), datetime.datetime(2025, 3, 9, 21, 0, 0, tzinfo=tz),
None, None,
'Sun, March 9th, 2025', 'Sun, March 9, 2025',
'<time datetime="2025-03-09">Sun, March 9th, 2025</time>', '<time datetime="2025-03-09">Sun, March 9, 2025</time>',
'Sun, March 9th, 2025 20:00', 'Sun, March 9, 2025 20:00',
'<time datetime="2025-03-09">Sun, March 9th, 2025</time> ' '<time datetime="2025-03-09">Sun, March 9, 2025</time> '
'<time datetime="2025-03-09T21:00:00+01:00" data-timezone="UTC" data-time-short>20:00</time>' '<time datetime="2025-03-09T21:00:00+01:00" data-timezone="UTC" data-time-short>20:00</time>'
), ),
) )

View File

@@ -48,10 +48,10 @@ ref_date = date(2023, 3, 28)
(ref_date, 'days_tomorrow', date(2023, 3, 29), date(2023, 3, 29), None), (ref_date, 'days_tomorrow', date(2023, 3, 29), date(2023, 3, 29), None),
(ref_date, 'days_next7', date(2023, 3, 29), date(2023, 4, 4), None), (ref_date, 'days_next7', date(2023, 3, 29), date(2023, 4, 4), None),
(ref_date, 'days_next14', date(2023, 3, 29), date(2023, 4, 11), None), (ref_date, 'days_next14', date(2023, 3, 29), date(2023, 4, 11), None),
(ref_date, 'week_this', date(2023, 3, 27), date(2023, 4, 2), 'W 13, 2023 - March 27th April 2nd, 2023'), (ref_date, 'week_this', date(2023, 3, 27), date(2023, 4, 2), 'W 13, 2023 - March 27 April 2, 2023'),
(ref_date, 'week_to_date', date(2023, 3, 27), date(2023, 3, 28), 'W 13, 2023 - March 27th 28th, 2023'), (ref_date, 'week_to_date', date(2023, 3, 27), date(2023, 3, 28), 'W 13, 2023 - March 27 28, 2023'),
(ref_date, 'week_previous', date(2023, 3, 20), date(2023, 3, 26), 'W 12, 2023 - March 20th 26th, 2023'), (ref_date, 'week_previous', date(2023, 3, 20), date(2023, 3, 26), 'W 12, 2023 - March 20 26, 2023'),
(ref_date, 'week_next', date(2023, 4, 3), date(2023, 4, 9), 'W 14, 2023 - April 3rd 9th, 2023'), (ref_date, 'week_next', date(2023, 4, 3), date(2023, 4, 9), 'W 14, 2023 - April 3 9, 2023'),
(ref_date, 'month_this', date(2023, 3, 1), date(2023, 3, 31), 'March 2023'), (ref_date, 'month_this', date(2023, 3, 1), date(2023, 3, 31), 'March 2023'),
(ref_date, 'month_to_date', date(2023, 3, 1), date(2023, 3, 28), 'March 2023'), (ref_date, 'month_to_date', date(2023, 3, 1), date(2023, 3, 28), 'March 2023'),
(ref_date, 'month_previous', date(2023, 2, 1), date(2023, 2, 28), 'February 2023'), (ref_date, 'month_previous', date(2023, 2, 1), date(2023, 2, 28), 'February 2023'),

View File

@@ -50,8 +50,8 @@ def test_same_day_german():
def test_same_day_english(): def test_same_day_english():
with translation.override('en'): with translation.override('en'):
df = date(2003, 2, 1) df = date(2003, 2, 1)
assert daterange(df, df) == "Sat, Feb. 1st, 2003" assert daterange(df, df) == "Sat, Feb. 1, 2003"
assert daterange(df, df, as_html=True) == '<time datetime="2003-02-01">Sat, Feb. 1st, 2003</time>' assert daterange(df, df, as_html=True) == '<time datetime="2003-02-01">Sat, Feb. 1, 2003</time>'
def test_same_day_spanish(): def test_same_day_spanish():
@@ -82,10 +82,10 @@ def test_same_month_english():
with translation.override('en'): with translation.override('en'):
df = date(2003, 2, 1) df = date(2003, 2, 1)
dt = date(2003, 2, 3) dt = date(2003, 2, 3)
assert daterange(df, dt) == "Feb. 1st 3rd, 2003" assert daterange(df, dt) == "Feb. 1 3, 2003"
assert daterange(df, dt, as_html=True) == '<time datetime="2003-02-01">Feb. 1st</time> ' \ assert daterange(df, dt, as_html=True) == '<time datetime="2003-02-01">Feb. 1</time> ' \
'<span aria-hidden="true"></span><span class="sr-only"> until </span> ' \ '<span aria-hidden="true"></span><span class="sr-only"> until </span> ' \
'<time datetime="2003-02-03">3rd, 2003</time>' '<time datetime="2003-02-03">3, 2003</time>'
def test_same_month_spanish(): def test_same_month_spanish():
@@ -112,10 +112,10 @@ def test_same_year_english():
with translation.override('en'): with translation.override('en'):
df = date(2003, 2, 1) df = date(2003, 2, 1)
dt = date(2003, 4, 3) dt = date(2003, 4, 3)
assert daterange(df, dt) == "Feb. 1st April 3rd, 2003" assert daterange(df, dt) == "Feb. 1 April 3, 2003"
assert daterange(df, dt, as_html=True) == '<time datetime="2003-02-01">Feb. 1st</time> ' \ assert daterange(df, dt, as_html=True) == '<time datetime="2003-02-01">Feb. 1</time> ' \
'<span aria-hidden="true"></span><span class="sr-only"> until </span> ' \ '<span aria-hidden="true"></span><span class="sr-only"> until </span> ' \
'<time datetime="2003-04-03">April 3rd, 2003</time>' '<time datetime="2003-04-03">April 3, 2003</time>'
def test_same_year_spanish(): def test_same_year_spanish():

View File

@@ -1621,7 +1621,7 @@ class EventLocaleTest(EventTestMixin, SoupTest):
'/%s/%s/' % (self.orga.slug, self.event.slug) '/%s/%s/' % (self.orga.slug, self.event.slug)
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIn('Thu, Dec. 26th,', response.rendered_content) self.assertIn('Thu, Dec. 26,', response.rendered_content)
self.assertIn('14:00', response.rendered_content) self.assertIn('14:00', response.rendered_content)
def test_english_region_US(self): def test_english_region_US(self):
@@ -1631,7 +1631,7 @@ class EventLocaleTest(EventTestMixin, SoupTest):
'/%s/%s/' % (self.orga.slug, self.event.slug) '/%s/%s/' % (self.orga.slug, self.event.slug)
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIn('Thu, Dec. 26th,', response.rendered_content) self.assertIn('Thu, Dec. 26,', response.rendered_content)
self.assertIn('2 p.m.', response.rendered_content) self.assertIn('2 p.m.', response.rendered_content)
def test_german_region_US(self): def test_german_region_US(self):

View File

@@ -169,7 +169,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
"target_url": "http://example.com/ccc/30c3/", "target_url": "http://example.com/ccc/30c3/",
"subevent": None, "subevent": None,
"name": "30C3", "name": "30C3",
"date_range": f"{self.event.date_from.strftime('%a')}, Dec. 26th, {self.event.date_from.year} 00:00", "date_range": f"{self.event.date_from.strftime('%a')}, Dec. 26, {self.event.date_from.year} 00:00",
"frontpage_text": "", "frontpage_text": "",
"location": "", "location": "",
"currency": "EUR", "currency": "EUR",
@@ -375,7 +375,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
"target_url": "http://example.com/ccc/30c3/", "target_url": "http://example.com/ccc/30c3/",
"subevent": None, "subevent": None,
"name": "30C3", "name": "30C3",
"date_range": f"{self.event.date_from.strftime('%a')}, Dec. 26th, {self.event.date_from.year} 00:00", "date_range": f"{self.event.date_from.strftime('%a')}, Dec. 26, {self.event.date_from.year} 00:00",
"frontpage_text": "", "frontpage_text": "",
"location": "", "location": "",
"currency": "EUR", "currency": "EUR",
@@ -435,7 +435,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
"target_url": "http://example.com/ccc/30c3/", "target_url": "http://example.com/ccc/30c3/",
"subevent": None, "subevent": None,
"name": "30C3", "name": "30C3",
"date_range": f"{self.event.date_from.strftime('%a')}, Dec. 26th, {self.event.date_from.year} 00:00", "date_range": f"{self.event.date_from.strftime('%a')}, Dec. 26, {self.event.date_from.year} 00:00",
"frontpage_text": "", "frontpage_text": "",
"location": "", "location": "",
"currency": "EUR", "currency": "EUR",
@@ -520,7 +520,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
"target_url": "http://example.com/ccc/30c3/", "target_url": "http://example.com/ccc/30c3/",
"subevent": None, "subevent": None,
"name": "30C3", "name": "30C3",
"date_range": f"{self.event.date_from.strftime('%a')}, Dec. 26th, {self.event.date_from.year} 00:00", "date_range": f"{self.event.date_from.strftime('%a')}, Dec. 26, {self.event.date_from.year} 00:00",
"frontpage_text": "", "frontpage_text": "",
"location": "", "location": "",
"currency": "EUR", "currency": "EUR",
@@ -627,9 +627,9 @@ class WidgetCartTest(CartTestMixin, TestCase):
'poweredby': '<a href="https://pretix.eu" target="_blank" rel="noopener">ticketing powered by pretix</a>', 'poweredby': '<a href="https://pretix.eu" target="_blank" rel="noopener">ticketing powered by pretix</a>',
'has_more_events': False, 'has_more_events': False,
'events': [ 'events': [
{'name': 'Present', 'date_range': 'Tue, Jan. 1st, 2019 11:00', 'availability': {'color': 'none', 'text': 'More info', 'reason': 'unknown'}, {'name': 'Present', 'date_range': 'Tue, Jan. 1, 2019 11:00', 'availability': {'color': 'none', 'text': 'More info', 'reason': 'unknown'},
'event_url': 'http://example.com/ccc/30c3/', 'subevent': se1.pk, 'location': ''}, 'event_url': 'http://example.com/ccc/30c3/', 'subevent': se1.pk, 'location': ''},
{'name': 'Future', 'date_range': 'Fri, Jan. 4th, 2019 11:00', 'availability': {'color': 'none', 'text': 'More info', 'reason': 'unknown'}, {'name': 'Future', 'date_range': 'Fri, Jan. 4, 2019 11:00', 'availability': {'color': 'none', 'text': 'More info', 'reason': 'unknown'},
'event_url': 'http://example.com/ccc/30c3/', 'subevent': se2.pk, 'location': ''} 'event_url': 'http://example.com/ccc/30c3/', 'subevent': se2.pk, 'location': ''}
] ]
} }
@@ -659,14 +659,14 @@ class WidgetCartTest(CartTestMixin, TestCase):
[ [
None, None,
{'day': 1, 'date': '2019-01-01', 'events': [ {'day': 1, 'date': '2019-01-01', 'events': [
{'name': 'Present', 'time': '11:00', 'continued': False, 'date_range': 'Tue, Jan. 1st, 2019 11:00', {'name': 'Present', 'time': '11:00', 'continued': False, 'date_range': 'Tue, Jan. 1, 2019 11:00',
'location': '', 'location': '',
'availability': {'color': 'none', 'text': 'More info', 'reason': 'unknown'}, 'availability': {'color': 'none', 'text': 'More info', 'reason': 'unknown'},
'event_url': 'http://example.com/ccc/30c3/', 'subevent': se1.pk}]}, 'event_url': 'http://example.com/ccc/30c3/', 'subevent': se1.pk}]},
{'day': 2, 'date': '2019-01-02', 'events': []}, {'day': 2, 'date': '2019-01-02', 'events': []},
{'day': 3, 'date': '2019-01-03', 'events': []}, {'day': 3, 'date': '2019-01-03', 'events': []},
{'day': 4, 'date': '2019-01-04', 'events': [ {'day': 4, 'date': '2019-01-04', 'events': [
{'name': 'Future', 'time': '11:00', 'continued': False, 'date_range': 'Fri, Jan. 4th, 2019 11:00', {'name': 'Future', 'time': '11:00', 'continued': False, 'date_range': 'Fri, Jan. 4, 2019 11:00',
'location': '', 'location': '',
'availability': {'color': 'none', 'text': 'More info', 'reason': 'unknown'}, 'availability': {'color': 'none', 'text': 'More info', 'reason': 'unknown'},
'event_url': 'http://example.com/ccc/30c3/', 'subevent': se2.pk}]}, 'event_url': 'http://example.com/ccc/30c3/', 'subevent': se2.pk}]},
@@ -732,21 +732,21 @@ class WidgetCartTest(CartTestMixin, TestCase):
'week': [2019, 1], 'week': [2019, 1],
'poweredby': '<a href="https://pretix.eu" target="_blank" rel="noopener">ticketing powered by pretix</a>', 'poweredby': '<a href="https://pretix.eu" target="_blank" rel="noopener">ticketing powered by pretix</a>',
'days': [ 'days': [
{'day_formatted': 'Mon, Dec 31st', 'date': '2018-12-31', 'events': [], 'today': False}, {'day_formatted': 'Mon, Dec 31', 'date': '2018-12-31', 'events': [], 'today': False},
{'day_formatted': 'Tue, Jan 1st', 'date': '2019-01-01', 'events': [ {'day_formatted': 'Tue, Jan 1', 'date': '2019-01-01', 'events': [
{'name': 'Present', 'time': '11:00', 'continued': False, 'date_range': 'Tue, Jan. 1st, 2019 11:00', {'name': 'Present', 'time': '11:00', 'continued': False, 'date_range': 'Tue, Jan. 1, 2019 11:00',
'location': '', 'location': '',
'availability': {'color': 'none', 'text': 'More info', 'reason': 'unknown'}, 'availability': {'color': 'none', 'text': 'More info', 'reason': 'unknown'},
'event_url': 'http://example.com/ccc/30c3/', 'subevent': se1.pk}], 'today': True}, 'event_url': 'http://example.com/ccc/30c3/', 'subevent': se1.pk}], 'today': True},
{'day_formatted': 'Wed, Jan 2nd', 'date': '2019-01-02', 'events': [], 'today': False}, {'day_formatted': 'Wed, Jan 2', 'date': '2019-01-02', 'events': [], 'today': False},
{'day_formatted': 'Thu, Jan 3rd', 'date': '2019-01-03', 'events': [], 'today': False}, {'day_formatted': 'Thu, Jan 3', 'date': '2019-01-03', 'events': [], 'today': False},
{'day_formatted': 'Fri, Jan 4th', 'date': '2019-01-04', 'events': [ {'day_formatted': 'Fri, Jan 4', 'date': '2019-01-04', 'events': [
{'name': 'Future', 'time': '11:00', 'continued': False, 'date_range': 'Fri, Jan. 4th, 2019 11:00', {'name': 'Future', 'time': '11:00', 'continued': False, 'date_range': 'Fri, Jan. 4, 2019 11:00',
'location': '', 'location': '',
'availability': {'color': 'none', 'text': 'More info', 'reason': 'unknown'}, 'availability': {'color': 'none', 'text': 'More info', 'reason': 'unknown'},
'event_url': 'http://example.com/ccc/30c3/', 'subevent': se2.pk}], 'today': False}, 'event_url': 'http://example.com/ccc/30c3/', 'subevent': se2.pk}], 'today': False},
{'day_formatted': 'Sat, Jan 5th', 'date': '2019-01-05', 'events': [], 'today': False}, {'day_formatted': 'Sat, Jan 5', 'date': '2019-01-05', 'events': [], 'today': False},
{'day_formatted': 'Sun, Jan 6th', 'date': '2019-01-06', 'events': [], 'today': False} {'day_formatted': 'Sun, Jan 6', 'date': '2019-01-06', 'events': [], 'today': False}
], ],
} }
@@ -773,17 +773,17 @@ class WidgetCartTest(CartTestMixin, TestCase):
'poweredby': '<a href="https://pretix.eu" target="_blank" rel="noopener">ticketing powered by pretix</a>', 'poweredby': '<a href="https://pretix.eu" target="_blank" rel="noopener">ticketing powered by pretix</a>',
'events': [ 'events': [
{'availability': {'color': 'none', 'text': 'Event series'}, {'availability': {'color': 'none', 'text': 'Event series'},
'date_range': 'Jan. 1st 4th, 2019', 'date_range': 'Jan. 1 4, 2019',
'event_url': 'http://example.com/ccc/30c3/', 'event_url': 'http://example.com/ccc/30c3/',
'location': '', 'location': '',
'name': '30C3'}, 'name': '30C3'},
{'availability': {'color': 'none', 'text': 'More info', 'reason': 'unknown'}, {'availability': {'color': 'none', 'text': 'More info', 'reason': 'unknown'},
'date_range': 'Tue, Jan. 1st, 2019 10:00', 'date_range': 'Tue, Jan. 1, 2019 10:00',
'location': '', 'location': '',
'event_url': 'http://example.com/ccc/present/', 'event_url': 'http://example.com/ccc/present/',
'name': 'Present'}, 'name': 'Present'},
{'availability': {'color': 'none', 'text': 'More info', 'reason': 'unknown'}, {'availability': {'color': 'none', 'text': 'More info', 'reason': 'unknown'},
'date_range': 'Fri, Jan. 4th, 2019 10:00', 'date_range': 'Fri, Jan. 4, 2019 10:00',
'location': '', 'location': '',
'event_url': 'http://example.com/ccc/future/', 'event_url': 'http://example.com/ccc/future/',
'name': 'Future'} 'name': 'Future'}
@@ -884,7 +884,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
'day': 1, 'day': 1,
'events': [{'availability': {'color': 'none', 'text': 'More info', 'reason': 'unknown'}, 'events': [{'availability': {'color': 'none', 'text': 'More info', 'reason': 'unknown'},
'continued': False, 'continued': False,
'date_range': 'Tue, Jan. 1st, 2019 10:00', 'date_range': 'Tue, Jan. 1, 2019 10:00',
'event_url': 'http://example.com/ccc/present/', 'event_url': 'http://example.com/ccc/present/',
'name': 'Present', 'name': 'Present',
'location': '', 'location': '',
@@ -892,7 +892,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
'time': '10:00'}, 'time': '10:00'},
{'availability': {'color': 'none', 'text': 'More info', 'reason': 'unknown'}, {'availability': {'color': 'none', 'text': 'More info', 'reason': 'unknown'},
'continued': False, 'continued': False,
'date_range': 'Tue, Jan. 1st, 2019 11:00', 'date_range': 'Tue, Jan. 1, 2019 11:00',
'event_url': 'http://example.com/ccc/30c3/', 'event_url': 'http://example.com/ccc/30c3/',
'name': 'Present', 'name': 'Present',
'location': '', 'location': '',
@@ -904,7 +904,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
'day': 4, 'day': 4,
'events': [{'availability': {'color': 'none', 'text': 'More info', 'reason': 'unknown'}, 'events': [{'availability': {'color': 'none', 'text': 'More info', 'reason': 'unknown'},
'continued': False, 'continued': False,
'date_range': 'Fri, Jan. 4th, 2019 10:00', 'date_range': 'Fri, Jan. 4, 2019 10:00',
'event_url': 'http://example.com/ccc/future/', 'event_url': 'http://example.com/ccc/future/',
'name': 'Future', 'name': 'Future',
'location': '', 'location': '',
@@ -912,7 +912,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
'time': '10:00'}, 'time': '10:00'},
{'availability': {'color': 'none', 'text': 'More info', 'reason': 'unknown'}, {'availability': {'color': 'none', 'text': 'More info', 'reason': 'unknown'},
'continued': False, 'continued': False,
'date_range': 'Fri, Jan. 4th, 2019 11:00', 'date_range': 'Fri, Jan. 4, 2019 11:00',
'event_url': 'http://example.com/ccc/30c3/', 'event_url': 'http://example.com/ccc/30c3/',
'name': 'Future', 'name': 'Future',
'location': '', 'location': '',