Compare commits

..

3 Commits

Author SHA1 Message Date
Mira Weller
12ba49e263 remove stray word 2026-03-06 11:09:18 +01:00
Mira Weller
e37b032d4e Always check voucher validity against real time, not time machine time 2026-03-06 10:56:42 +01:00
Mira Weller
3c3b5529bf Add note about limits of the time machine feature 2026-03-05 13:46:13 +01:00
22 changed files with 133 additions and 402 deletions

View File

@@ -1719,56 +1719,6 @@ List of all order positions
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/orderpositions/
Returns a list of all order positions within all events of a given organizer (with sufficient access permissions).
The supported query parameters and output format of this endpoint are almost identical to those of the list endpoint
within an event.
The only changes are that responses also contain the ``event`` attribute in each result and that the 'pdf_data'
parameter is not supported.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/orderpositions/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
X-Page-Generated: 2017-12-01T10:00:00Z
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id:": 23442
"event": "sampleconf",
"order": "ABC12",
"positionid": 1,
"canceled": false,
"item": 1345,
...
}
]
}
:param organizer: The ``slug`` field of the organizer to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
Fetching individual positions
-----------------------------

View File

@@ -77,7 +77,7 @@ dependencies = [
"phonenumberslite==9.0.*",
"Pillow==12.1.*",
"pretix-plugin-build",
"protobuf==7.34.*",
"protobuf==6.33.*",
"psycopg2-binary",
"pycountry",
"pycparser==3.0",
@@ -92,7 +92,7 @@ dependencies = [
"redis==7.1.*",
"reportlab==4.4.*",
"requests==2.32.*",
"sentry-sdk==2.54.*",
"sentry-sdk==2.53.*",
"sepaxml==2.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",

View File

@@ -637,14 +637,6 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
return entry
class OrganizerOrderPositionSerializer(OrderPositionSerializer):
event = SlugRelatedField(slug_field='slug', read_only=True)
class Meta(OrderPositionSerializer.Meta):
fields = OrderPositionSerializer.Meta.fields + ('event',)
read_only_fields = OrderPositionSerializer.Meta.read_only_fields + ('event',)
class RequireAttentionField(serializers.Field):
def to_representation(self, instance: OrderPosition):
return instance.require_checkin_attention

View File

@@ -67,7 +67,6 @@ orga_router.register(r'invoices', order.InvoiceViewSet)
orga_router.register(r'scheduled_exports', exporters.ScheduledOrganizerExportViewSet)
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')
orga_router.register(r'transactions', order.OrganizerTransactionViewSet)
orga_router.register(r'orderpositions', order.OrganizerOrderPositionViewSet, basename='orderpositions')
team_router = routers.DefaultRouter()
team_router.register(r'members', organizer.TeamMemberViewSet)
@@ -84,7 +83,7 @@ event_router.register(r'discounts', discount.DiscountViewSet)
event_router.register(r'quotas', item.QuotaViewSet)
event_router.register(r'vouchers', voucher.VoucherViewSet)
event_router.register(r'orders', order.EventOrderViewSet)
event_router.register(r'orderpositions', order.EventOrderPositionViewSet)
event_router.register(r'orderpositions', order.OrderPositionViewSet)
event_router.register(r'transactions', order.TransactionViewSet)
event_router.register(r'invoices', order.InvoiceViewSet)
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')

View File

@@ -57,10 +57,9 @@ from pretix.api.serializers.order import (
BlockedTicketSecretSerializer, InvoiceSerializer, OrderCreateSerializer,
OrderPaymentCreateSerializer, OrderPaymentSerializer,
OrderPositionSerializer, OrderRefundCreateSerializer,
OrderRefundSerializer, OrderSerializer, OrganizerOrderPositionSerializer,
OrganizerTransactionSerializer, PriceCalcSerializer, PrintLogSerializer,
RevokedTicketSecretSerializer, SimulatedOrderSerializer,
TransactionSerializer,
OrderRefundSerializer, OrderSerializer, OrganizerTransactionSerializer,
PriceCalcSerializer, PrintLogSerializer, RevokedTicketSecretSerializer,
SimulatedOrderSerializer, TransactionSerializer,
)
from pretix.api.serializers.orderchange import (
BlockNameSerializer, OrderChangeOperationSerializer,
@@ -1066,7 +1065,8 @@ with scopes_disabled():
}
class OrderPositionViewSetMixin:
class OrderPositionViewSet(viewsets.ModelViewSet):
serializer_class = OrderPositionSerializer
queryset = OrderPosition.all.none()
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
ordering = ('order__datetime', 'positionid')
@@ -1087,7 +1087,8 @@ class OrderPositionViewSetMixin:
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['pdf_data'] = False
ctx['event'] = self.request.event
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
ctx['check_quotas'] = self.request.query_params.get('check_quotas', 'true').lower() == 'true'
return ctx
@@ -1096,8 +1097,9 @@ class OrderPositionViewSetMixin:
qs = OrderPosition.all
else:
qs = OrderPosition.objects
qs = qs.filter(order__event__organizer=self.request.organizer)
if self.request.query_params.get('pdf_data', 'false').lower() == 'true' and getattr(self.request, 'event', None):
qs = qs.filter(order__event=self.request.event)
if self.request.query_params.get('pdf_data', 'false').lower() == 'true':
prefetch_related_objects([self.request.organizer], 'meta_properties')
prefetch_related_objects(
[self.request.event],
@@ -1152,9 +1154,9 @@ class OrderPositionViewSetMixin:
qs = qs.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'answers', 'answers__options', 'answers__question', 'order__event', 'order__event__organizer'
'answers', 'answers__options', 'answers__question',
).select_related(
'item', 'order', 'seat'
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
)
return qs
@@ -1166,45 +1168,6 @@ class OrderPositionViewSetMixin:
return prov
raise NotFound('Unknown output provider.')
class OrganizerOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = OrganizerOrderPositionSerializer
def get_queryset(self):
qs = super().get_queryset()
perm = self.permission if self.request.method in SAFE_METHODS else self.write_permission
if isinstance(self.request.auth, (TeamAPIToken, Device)):
auth_obj = self.request.auth
elif self.request.user.is_authenticated:
auth_obj = self.request.user
else:
raise PermissionDenied("Unknown authentication scheme")
qs = qs.filter(
order__event__in=auth_obj.get_events_with_permission(perm, request=self.request).filter(
organizer=self.request.organizer
)
)
return qs
class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet):
serializer_class = OrderPositionSerializer
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
return ctx
def get_queryset(self):
qs = super().get_queryset()
qs = qs.filter(order__event=self.request.event)
return qs
@action(detail=True, methods=['POST'], url_name='price_calc')
def price_calc(self, request, *args, **kwargs):
"""

View File

@@ -148,10 +148,6 @@ class NumberedCanvas(Canvas):
self.restoreState()
class InvoiceNotReadyException(Exception):
pass
class BaseInvoiceRenderer:
"""
This is the base class for all invoice renderers.

View File

@@ -181,11 +181,10 @@ class WaitingListEntry(LoggedModel):
block_quota=True,
item_id=self.item_id,
subevent_id=self.subevent_id,
waitinglistentries__isnull=False,
seat__isnull=True
waitinglistentries__isnull=False
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
free_seats = num_free_seats_for_product - num_valid_vouchers_for_product
if free_seats < 1:
if not free_seats:
raise WaitingListException(_('No seat with this product is currently available.'))
if '@' not in self.email:

View File

@@ -334,7 +334,8 @@ def _check_position_constraints(
raise CartPositionError(error_messages['voucher_invalid_subevent'])
# Voucher expired
if voucher and voucher.valid_until and voucher.valid_until < time_machine_now_dt:
# (checked using real_now_dt as vouchers influence quota calculations)
if voucher and voucher.valid_until and voucher.valid_until < real_now_dt:
raise CartPositionError(error_messages['voucher_expired'])
# Subevent has been disabled

View File

@@ -51,7 +51,6 @@ from django_scopes import scope, scopes_disabled
from i18nfield.strings import LazyI18nString
from pretix.base.i18n import language
from pretix.base.invoicing.pdf import InvoiceNotReadyException
from pretix.base.invoicing.transmission import (
get_transmission_types, transmission_providers,
)
@@ -505,7 +504,7 @@ def generate_invoice(order: Order, trigger_pdf=True):
return invoice
@app.task(base=TransactionAwareTask, throws=(InvoiceNotReadyException,))
@app.task(base=TransactionAwareTask)
def invoice_pdf_task(invoice: int):
with scopes_disabled():
i = Invoice.objects.get(pk=invoice)

View File

@@ -409,18 +409,6 @@ def mail_send_task(self, **kwargs) -> bool:
outgoing_mail.inflight_since = now()
outgoing_mail.save(update_fields=["status", "inflight_since"])
# Performance optimization, saves database queries later on if we resolve the known relationships
if outgoing_mail.event_id:
assert outgoing_mail.event.organizer_id == outgoing_mail.organizer.pk
outgoing_mail.event.organizer = outgoing_mail.organizer
if outgoing_mail.order_id:
assert outgoing_mail.order.event_id == outgoing_mail.event_id
outgoing_mail.order.event = outgoing_mail.event
outgoing_mail.order.organizer = outgoing_mail.organizer
if outgoing_mail.orderposition_id:
assert outgoing_mail.orderposition.order_id == outgoing_mail.order_id
outgoing_mail.orderposition.order = outgoing_mail.order
headers = dict(outgoing_mail.headers)
headers.setdefault('X-PX-Correlation', str(outgoing_mail.guid))
email = CustomEmail(

View File

@@ -24,7 +24,6 @@ import logging
from datetime import timedelta
from decimal import Decimal
from django.db.models import Prefetch, prefetch_related_objects
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.html import escape, mark_safe
@@ -36,7 +35,6 @@ from pretix.base.forms.widgets import format_placeholders_help_text
from pretix.base.i18n import (
LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber,
)
from pretix.base.models import EventMetaValue
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
from pretix.base.signals import (
@@ -754,11 +752,6 @@ def base_placeholders(sender, **kwargs):
name_scheme['sample'][f]
))
prefetch_related_objects(
[sender],
Prefetch('meta_values', queryset=EventMetaValue.objects.select_related("property"), to_attr="meta_values_cached")
)
prefetch_related_objects([sender.organizer], Prefetch('meta_properties'))
for k, v in sender.meta_data.items():
ph.append(MarkdownTextPlaceholder(
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],

View File

@@ -12,9 +12,6 @@
<meta charset="utf-8">
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
{% block custom_header %}{% endblock %}
{% if css_theme %}
<link rel="stylesheet" type="text/css" href="{{ css_theme }}" />
{% endif %}
</head>
<body>
<div class="container">

View File

@@ -423,7 +423,7 @@ def resolve_timeframe_to_dates_inclusive(ref_dt, frame, timezone) -> Tuple[Optio
raise ValueError(f"Invalid timeframe '{frame}'")
def resolve_timeframe_to_datetime_start_inclusive_end_exclusive(ref_dt, frame, timezone) -> Tuple[Optional[datetime], Optional[datetime]]:
def resolve_timeframe_to_datetime_start_inclusive_end_exclusive(ref_dt, frame, timezone) -> Tuple[Optional[date], Optional[date]]:
"""
Given a serialized timeframe, evaluate it relative to `ref_dt` and return a tuple of datetimes
where the first element ist the first possible datetime within the timeframe and the second

View File

@@ -280,12 +280,11 @@ class WaitingListView(EventPermissionRequiredMixin, WaitingListQuerySetMixin, Pa
block_quota=True,
item_id=wle.item_id,
subevent=wle.subevent_id,
waitinglistentries__isnull=False,
seat__isnull=True
waitinglistentries__isnull=False
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
free_seats = num_free_seats_for_product - num_valid_vouchers_for_product
wle.availability = (
Quota.AVAILABILITY_GONE if free_seats < 1 else wle.availability[0],
Quota.AVAILABILITY_GONE if free_seats == 0 else wle.availability[0],
min(free_seats, wle.availability[1]) if wle.availability[1] is not None else free_seats,
)

View File

@@ -4,16 +4,16 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-24 11:50+0000\n"
"PO-Revision-Date: 2026-03-05 20:00+0000\n"
"PO-Revision-Date: 2026-02-19 22:00+0000\n"
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix/"
"da/>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix/da/"
">\n"
"Language: da\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.16.1\n"
"X-Generator: Weblate 5.16\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -34285,7 +34285,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/voucher.html:293
#, python-format
msgid "minimum amount to order: %(num)s"
msgstr "Minimumsbestilling: %(num)s"
msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:76
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:160

View File

@@ -5,8 +5,8 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-24 11:50+0000\n"
"PO-Revision-Date: 2026-03-07 23:00+0000\n"
"Last-Translator: argonimos <jonas@pfeiffer-wagner.de>\n"
"PO-Revision-Date: 2026-02-24 12:07+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/"
"de/>\n"
"Language: de\n"
@@ -14,7 +14,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.16.2\n"
"X-Generator: Weblate 5.16\n"
"X-Poedit-Bookmarks: -1,-1,904,-1,-1,-1,-1,-1,-1,-1\n"
#: pretix/_base_settings.py:87
@@ -3845,7 +3845,7 @@ msgstr "Restbetrag"
#, python-brace-format
msgctxt "invoice"
msgid "Invoice period: {daterange}"
msgstr "Rechnungsperiode: {daterange}"
msgstr "Rechungsperiode: {daterange}"
#: pretix/base/invoicing/pdf.py:1039
msgctxt "invoice"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-24 11:50+0000\n"
"PO-Revision-Date: 2026-03-09 12:52+0000\n"
"PO-Revision-Date: 2026-03-02 10:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
"ja/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.16.2\n"
"X-Generator: Weblate 5.16.1\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -11448,7 +11448,7 @@ msgstr ""
"{event}のご注文が完了しました。無料製品のみのご注文のため、\n"
"お支払いは不要です。\n"
"\n"
"注文の詳細の変更やステータス確認は、以下のURLから行えます\n"
"注文の詳細の変更やステータス確認は、以下のURLから行えます\n"
"{url}\n"
"\n"
"よろしくお願いいたします。\n"
@@ -11750,7 +11750,7 @@ msgstr ""
"{event}のご注文のお支払いを受け取りました。\n"
"\n"
"残念ながら、受け取った金額は必要な全額よりも少ないです。\n"
"したがって、追加の **{pending_sum}** の支払いが不足しているため、\n"
"したがって、追加の**{pending_sum}**の支払いが不足しているため、\n"
"ご注文は未払いと見なされます。\n"
"\n"
"お支払い情報やご注文の状況は、以下のURLでご確認いただけます。\n"
@@ -18428,7 +18428,7 @@ msgid ""
"Do you really want to grant the application <strong>%(application)s</strong> "
"access to your pretix account?"
msgstr ""
"本当にアプリケーション<strong>%(application)s</strong>にpretixアカウントへの"
"本当にアプリケーション<strong>%(application)s</strong>にPretixアカウントへの"
"アクセスを許可しますか?"
#: pretix/control/templates/pretixcontrol/auth/oauth_authorization.html:24
@@ -24692,7 +24692,7 @@ msgstr "顧客履歴"
#: pretix/control/templates/pretixcontrol/organizers/customer_anonymize.html:11
#, python-format
msgid "Anonymize customer #%(id)s"
msgstr "顧客 #%(id)s を匿名化"
msgstr "顧客のID #%(id)s を匿名化"
#: pretix/control/templates/pretixcontrol/organizers/customer_anonymize.html:16
msgid "Are you sure you want to anonymize this customer account?"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-24 11:50+0000\n"
"PO-Revision-Date: 2026-03-09 12:52+0000\n"
"PO-Revision-Date: 2026-03-04 16:57+0000\n"
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
"Language-Team: Dutch (Belgium) <https://translate.pretix.eu/projects/pretix/"
"pretix/nl_BE/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.16.2\n"
"X-Generator: Weblate 5.16.1\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -26981,10 +26981,6 @@ msgid ""
"the affected data in your legislation, e.g. for reasons of taxation. In many "
"countries, you need to keep some data in the live system in case of an audit."
msgstr ""
"Het is uw eigen verantwoordelijkheid om te controleren of u de gegevens "
"volgens uw wetgeving mag verwijderen, bijvoorbeeld om fiscale redenen. In "
"veel landen moet u bepaalde gegevens in het livesysteem bewaren voor het "
"geval er een audit plaatsvindt."
#: pretix/control/templates/pretixcontrol/shredder/index.html:32
msgid ""
@@ -26992,87 +26988,81 @@ msgid ""
"to store it offline. Some kinds of data (such as some payment information) "
"as well as historical log data cannot be downloaded at the moment."
msgstr ""
"U kunt voor de meeste categorieën de gegevens gedeeltelijk downloaden om ze "
"offline op te slaan. Sommige soorten gegevens (bijvoorbeeld sommige "
"betalingsinformatie) en historische loggegevens kunnen momenteel niet worden "
"gedownload."
#: pretix/control/templates/pretixcontrol/shredder/index.html:46
msgid "Data selection"
msgstr "Gegevensselectie"
msgstr ""
#: pretix/control/templates/pretixcontrol/shredder/index.html:63
msgid ""
"We recommend not to remove this data because you might need it in case of a "
"tax audit."
msgstr ""
"We raden aan om deze gegevens niet te verwijderen, omdat u ze mogelijk nodig "
"hebt bij een belastingaudit."
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:10
msgctxt "subevent"
msgid "Create multiple dates"
msgstr "Meerdere datums aanmaken"
msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:35
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:146
msgid "Repetition rule"
msgstr "Regel voor herhaling"
msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:81
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:192
#, python-format
msgid "Repeat every %(interval)s %(freq)s, starting at %(start)s."
msgstr "Herhaal ieder(e) %(interval)s %(freq)s, beginnend op %(start)s."
msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:258
msgctxt "subevent"
msgid "Preview"
msgstr "Voorbeeldweergave"
msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:265
msgctxt "subevent"
msgid "Times"
msgstr "Tijden"
msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:339
msgid "Start of first slot"
msgstr "Begin van eerste tijdsslot"
msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:345
msgid "End of time slots"
msgstr "Einde van tijdsslots"
msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:351
msgid "Length of slots"
msgstr "Lengte van tijdsslots"
msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:360
msgid "Break between slots"
msgstr "Pauze tussen tijdsslots"
msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:370
msgid "Create"
msgstr "Aanmaken"
msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:377
msgid "Add a single time slot"
msgstr "Eén tijdsslot toevoegen"
msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:379
msgid "Add many time slots"
msgstr "Meerdere tijdsslots toevoegen"
msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:481
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:266
#: pretix/control/templates/pretixcontrol/subevents/detail.html:124
msgid "Add a new quota"
msgstr "Nieuw quotum toevoegen"
msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:485
#: pretix/control/templates/pretixcontrol/subevents/detail.html:128
msgid "Product settings"
msgstr "Productinstellingen"
msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:487
#: pretix/control/templates/pretixcontrol/subevents/detail.html:130
@@ -27080,8 +27070,6 @@ msgid ""
"These settings are optional, if you leave them empty, the default values "
"from the product settings will be used."
msgstr ""
"Deze instellingen zijn optioneel. Als u deze instellingen leeg laat, zullen "
"de standaardwaarden uit de productinstellingen worden gebruikt."
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:523
#: pretix/control/templates/pretixcontrol/subevents/detail.html:166

View File

@@ -20,6 +20,7 @@
{% bootstrap_form_errors timemachine_form "all" %}
<p>{% trans "Test your shop as if it were a different date and time." %}</p>
<p>{% trans "Please note that the changed time is not taken into account for aspects of the shop that affect quotas, such as the validity period of carts and vouchers." %}</p>
<div class="row">
<div class="col-md-6">
@@ -44,4 +45,4 @@
<div class="clear"></div>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@@ -2776,9 +2776,9 @@
}
},
"node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"optional": true,
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -5642,9 +5642,9 @@
"optional": true
},
"minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"

View File

@@ -34,7 +34,7 @@ from stripe import error
from tests.plugins.stripe.test_checkout import apple_domain_create
from tests.plugins.stripe.test_provider import MockedCharge
from pretix.base.models import InvoiceAddress, Order, OrderPosition, Team
from pretix.base.models import InvoiceAddress, Order, OrderPosition
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
@@ -180,41 +180,6 @@ def order2(event2, item2):
return o
@pytest.fixture
@scopes_disabled()
def team2(organizer, event2):
team2 = Team.objects.create(
organizer=organizer,
name="Test-Team 2",
can_change_teams=True,
can_manage_gift_cards=True,
can_change_items=True,
can_create_events=True,
can_change_event_settings=True,
can_change_vouchers=True,
can_view_vouchers=True,
can_change_orders=True,
can_manage_customers=True,
can_manage_reusable_media=True,
can_change_organizer_settings=True,
)
team2.limit_events.add(event2)
team2.save()
return team2
@pytest.fixture
@scopes_disabled()
def limited_token_client(client, team2):
team2.can_view_orders = True
team2.can_view_vouchers = True
team2.save()
t = team2.tokens.create(name='Foo')
client.credentials(HTTP_AUTHORIZATION='Token ' + t.token)
return client
TEST_ORDERPOSITION_RES = {
"id": 1,
"order": "FOO",
@@ -1022,64 +987,8 @@ def test_refund_cancel(token_client, organizer, event, order):
assert resp.status_code == 400
@pytest.mark.parametrize(
"endpoint_template, response_code",
[('/api/v1/organizers/{}/events/{}/orderpositions/', 403), ('/api/v1/organizers/{}/orderpositions/', 200)]
)
@pytest.mark.django_db
def test_orderposition_list_limited_read(
endpoint_template, response_code, limited_token_client, organizer, device, event, order, item, subevent, subevent2, question
):
endpoint = endpoint_template.format(organizer.slug, event.slug)
i2 = copy.copy(item)
i2.pk = None
i2.save()
with scopes_disabled():
var = item.variations.create(value="Children")
res = copy.copy(TEST_ORDERPOSITION_RES)
op = order.positions.first()
op.variation = var
op.save()
res["id"] = op.pk
res["item"] = item.pk
res["variation"] = var.pk
res["answers"][0]["question"] = question.pk
res["print_logs"][0]["id"] = op.print_logs.first().pk
res["print_logs"][0]["device_id"] = device.device_id
resp = limited_token_client.get(endpoint)
assert resp.status_code == response_code
if response_code == 200:
assert resp.json() == {'count': 0, 'next': None, 'previous': None, 'results': []}
else:
assert resp.json() == {'detail': 'You do not have permission to perform this action.'}
@pytest.mark.parametrize(
("endpoint_template", "endpoint_type"),
[
('/api/v1/organizers/{}/events/{}/orderpositions/', "event"),
('/api/v1/organizers/{}/orderpositions/', "organizer")
],
)
@pytest.mark.django_db
def test_orderposition_list(
endpoint_template,
endpoint_type,
token_client,
organizer,
device,
event,
order,
item,
subevent,
subevent2,
question,
django_assert_num_queries
):
endpoint = endpoint_template.format(organizer.slug, event.slug)
def test_orderposition_list(token_client, organizer, device, event, order, item, subevent, subevent2, question, django_assert_num_queries):
i2 = copy.copy(item)
i2.pk = None
i2.save()
@@ -1096,64 +1005,88 @@ def test_orderposition_list(
res["answers"][0]["question"] = question.pk
res["print_logs"][0]["id"] = op.print_logs.first().pk
res["print_logs"][0]["device_id"] = device.device_id
if endpoint_type == "organizer":
res["event"] = event.slug
resp = token_client.get(endpoint)
resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [res] == resp.data['results']
resp = token_client.get(endpoint + '?order__status=n')
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?order__status=n'.format(organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get(endpoint + '?order__status=p')
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?order__status=p'.format(organizer.slug, event.slug))
assert [] == resp.data['results']
resp = token_client.get(endpoint + '?item={}'.format(item.pk))
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?item={}'.format(organizer.slug, event.slug, item.pk))
assert [res] == resp.data['results']
resp = token_client.get(endpoint + '?item__in={},{}'.format(item.pk, i2.pk))
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?item__in={},{}'.format(
organizer.slug, event.slug, item.pk, i2.pk
))
assert [res] == resp.data['results']
resp = token_client.get(endpoint + '?item={}'.format(i2.pk))
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?item={}'.format(organizer.slug, event.slug, i2.pk))
assert [] == resp.data['results']
resp = token_client.get(endpoint + '?variation={}'.format(var.pk))
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?variation={}'.format(organizer.slug, event.slug, var.pk))
assert [res] == resp.data['results']
resp = token_client.get(endpoint + '?variation={}'.format(var2.pk))
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?variation={}'.format(organizer.slug, event.slug, var2.pk))
assert [] == resp.data['results']
resp = token_client.get(endpoint + '?attendee_name=Peter')
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?attendee_name=Peter'.format(organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get(endpoint + '?attendee_name=peter')
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?attendee_name=peter'.format(organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get(endpoint + '?attendee_name=Mark')
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?attendee_name=Mark'.format(organizer.slug, event.slug))
assert [] == resp.data['results']
resp = token_client.get(endpoint + '?secret=z3fsn8jyufm5kpk768q69gkbyr5f4h6w')
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?secret=z3fsn8jyufm5kpk768q69gkbyr5f4h6w'.format(
organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get(endpoint + '?secret=abc123')
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?secret=abc123'.format(organizer.slug, event.slug))
assert [] == resp.data['results']
resp = token_client.get(endpoint + '?pseudonymization_id=ABCDEFGHKL')
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?pseudonymization_id=ABCDEFGHKL'.format(
organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get(endpoint + '?pseudonymization_id=FOO')
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?pseudonymization_id=FOO'.format(organizer.slug, event.slug))
assert [] == resp.data['results']
resp = token_client.get(endpoint + '?search=FO')
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?search=FO'.format(organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get(endpoint + '?search=z3fsn8j')
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?search=z3fsn8j'.format(organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get(endpoint + '?search=Peter')
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?search=Peter'.format(organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get(endpoint + '?search=5f4h6w')
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?search=5f4h6w'.format(organizer.slug, event.slug))
assert [] == resp.data['results']
resp = token_client.get(endpoint + '?order=FOO')
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?order=FOO'.format(organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get(endpoint + '?order=BAR')
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?order=BAR'.format(organizer.slug, event.slug))
assert [] == resp.data['results']
resp = token_client.get(endpoint + '?has_checkin=false')
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?has_checkin=false'.format(organizer.slug, event.slug))
assert [res] == resp.data['results']
resp = token_client.get(endpoint + '?has_checkin=true')
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?has_checkin=true'.format(organizer.slug, event.slug))
assert [] == resp.data['results']
with scopes_disabled():
@@ -1170,28 +1103,33 @@ def test_orderposition_list(
'gate': None,
'type': 'entry'
}]
if '/events/' in endpoint:
with django_assert_num_queries(18):
resp = token_client.get(endpoint + '?has_checkin=true')
else:
with django_assert_num_queries(17):
resp = token_client.get(endpoint + '?has_checkin=true')
with django_assert_num_queries(16):
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?has_checkin=true'.format(organizer.slug, event.slug)
)
assert [res] == resp.data['results']
op.subevent = subevent
op.save()
res['subevent'] = subevent.pk
resp = token_client.get(endpoint + '?subevent={}'.format(subevent.pk))
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?subevent={}'.format(organizer.slug, event.slug, subevent.pk))
assert [res] == resp.data['results']
resp = token_client.get(endpoint + '?subevent__in={},{}'.format(subevent.pk, subevent2.pk))
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?subevent__in={},{}'.format(organizer.slug, event.slug,
subevent.pk, subevent2.pk))
assert [res] == resp.data['results']
resp = token_client.get(endpoint + '?subevent={}'.format(subevent.pk + 1))
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?subevent={}'.format(organizer.slug, event.slug,
subevent.pk + 1))
assert [] == resp.data['results']
resp = token_client.get(endpoint + '?include_canceled_positions=false')
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?include_canceled_positions=false'.format(organizer.slug, event.slug))
assert len(resp.data['results']) == 1
resp = token_client.get(endpoint + '?include_canceled_positions=true')
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?include_canceled_positions=true'.format(organizer.slug, event.slug))
assert len(resp.data['results']) == 2

View File

@@ -29,8 +29,6 @@ from pretix.base.models import (
Event, Item, ItemVariation, Organizer, Quota, Team, User, Voucher,
WaitingListEntry,
)
from pretix.base.models.seating import Seat, SeatingPlan
from pretix.base.models.waitinglist import WaitingListException
from pretix.control.views.dashboards import waitinglist_widgets
@@ -57,11 +55,11 @@ def env():
WaitingListEntry.objects.create(
event=event, item=item1, email='success@example.org', voucher=v
)
v = Voucher.objects.create(item=item2, event=event, block_quota=True, redeemed=0, valid_until=now() - timedelta(days=5))
v = Voucher.objects.create(item=item1, event=event, block_quota=True, redeemed=0, valid_until=now() - timedelta(days=5))
WaitingListEntry.objects.create(
event=event, item=item2, email='expired@example.org', voucher=v
)
v = Voucher.objects.create(item=item2, event=event, block_quota=True, redeemed=0, valid_until=now() + timedelta(days=5))
v = Voucher.objects.create(item=item1, event=event, block_quota=True, redeemed=0, valid_until=now() + timedelta(days=5))
WaitingListEntry.objects.create(
event=event, item=item2, email='valid@example.org', voucher=v
)
@@ -347,75 +345,5 @@ def test_dashboard(client, env):
quota.items.add(env['item1'])
w = waitinglist_widgets(env['event'])
assert '2' in w[0]['content']
assert '1' in w[0]['content']
assert '5' in w[1]['content']
@pytest.mark.django_db
def test_waitinglist_seat_calc(client, env):
item = env['item1']
event = env['event']
wle = env['wle']
SeatingPlan.objects.create(
name="Plan", organizer=event.organizer, layout="{}"
)
event.seat_category_mappings.create(
layout_category='Stalls', product=item
)
for i in range(2):
event.seats.create(seat_number=f"A{i}", product=item, seat_guid=f"A{i}")
quota = Quota.objects.create(event=event, size=10)
quota.items.add(item)
client.login(email='dummy@dummy.dummy', password='dummy')
# Calculated availability should not be more than number of available seats
response = client.get('/control/event/dummy/dummy/waitinglist/')
assert len(response.context['entries']) == 5
for entry in response.context['entries']:
assert entry.availability == (Quota.AVAILABILITY_OK, 2)
# Sending out a voucher reduces availability by 1
with scopes_disabled():
wle.send_voucher()
voucher = wle.voucher
assert voucher
response = client.get('/control/event/dummy/dummy/waitinglist/')
assert len(response.context['entries']) == 4
for entry in response.context['entries']:
assert entry.availability == (Quota.AVAILABILITY_OK, 1)
# Assigning a seat to a voucher does not decrease availability further
with scopes_disabled():
voucher.seat = Seat.objects.get(seat_guid="A0")
voucher.save()
response = client.get('/control/event/dummy/dummy/waitinglist/')
assert len(response.context['entries']) == 4
for entry in response.context['entries']:
assert entry.availability == (Quota.AVAILABILITY_OK, 1)
with scopes_disabled():
wle2 = WaitingListEntry.objects.filter(item=item, voucher__isnull=True).first()
wle2.send_voucher()
# Overbooking is handled correctly
# Regression test for calculation that used `not free_seats` instead of `free_seats < 1`
with scopes_disabled():
# Block seat
seat = Seat.objects.get(seat_guid="A1")
seat.blocked = True
seat.save()
response = client.get('/control/event/dummy/dummy/waitinglist/')
assert len(response.context['entries']) == 3
for entry in response.context['entries']:
assert entry.availability == (Quota.AVAILABILITY_GONE, -1)
with scopes_disabled(), pytest.raises(WaitingListException):
wle3 = WaitingListEntry.objects.filter(item=item, voucher__isnull=True).first()
wle3.send_voucher()