Compare commits

..
51 changed files with 409 additions and 1288 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
FROM python:3.13-trixie FROM python:3.11-bookworm
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
+3 -3
View File
@@ -76,7 +76,7 @@ dependencies = [
"paypal-checkout-serversdk==1.0.*", "paypal-checkout-serversdk==1.0.*",
"PyJWT==2.12.*", "PyJWT==2.12.*",
"phonenumberslite==9.0.*", "phonenumberslite==9.0.*",
"Pillow==12.2.*", "Pillow==12.1.*",
"pretix-plugin-build", "pretix-plugin-build",
"protobuf==7.34.*", "protobuf==7.34.*",
"psycopg2-binary", "psycopg2-binary",
@@ -90,10 +90,10 @@ dependencies = [
"pytz-deprecation-shim==0.1.*", "pytz-deprecation-shim==0.1.*",
"pyuca", "pyuca",
"qrcode==8.2", "qrcode==8.2",
"redis==7.4.*", "redis==7.1.*",
"reportlab==4.4.*", "reportlab==4.4.*",
"requests==2.32.*", "requests==2.32.*",
"sentry-sdk==2.57.*", "sentry-sdk==2.56.*",
"sepaxml==2.7.*", "sepaxml==2.7.*",
"stripe==7.9.*", "stripe==7.9.*",
"text-unidecode==1.*", "text-unidecode==1.*",
+3 -25
View File
@@ -31,9 +31,7 @@ from pretix.api.serializers.order import OrderPositionSerializer
from pretix.api.serializers.organizer import ( from pretix.api.serializers.organizer import (
CustomerSerializer, GiftCardSerializer, CustomerSerializer, GiftCardSerializer,
) )
from pretix.base.models import ( from pretix.base.models import Order, OrderPosition, ReusableMedium
Device, Order, OrderPosition, ReusableMedium, TeamAPIToken,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -82,7 +80,8 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
) )
if 'linked_orderposition' in self.context['request'].query_params.getlist('expand'): if 'linked_orderposition' in self.context['request'].query_params.getlist('expand'):
# Permission Check performed in to_representation # No additional permission check performed, documented limitation of the permission system
# Would get to complex/unusable otherwise since the permission depends on the event
self.fields['linked_orderposition'] = NestedOrderPositionSerializer(read_only=True) self.fields['linked_orderposition'] = NestedOrderPositionSerializer(read_only=True)
else: else:
self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField( self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField(
@@ -118,27 +117,6 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
) )
return data return data
def to_representation(self, instance):
r = super().to_representation(instance)
request = self.context.get('request')
# late permission evaluations for checks that depend on the actual linked events
expand_nested = self.context['request'].query_params.getlist('expand')
perm_holder = request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user
if 'linked_orderposition' in expand_nested:
if instance.linked_orderposition is not None:
event = instance.linked_orderposition.order.event
if not perm_holder.has_event_permission(event.organizer, event, 'event.orders:read', request):
r['linked_orderposition'] = {'id': instance.linked_orderposition.id}
if 'linked_giftcard.owner_ticket' in expand_nested:
gc = instance.linked_giftcard
if gc is not None and gc.owner_ticket is not None:
event = gc.owner_ticket.order.event
if not perm_holder.has_event_permission(event.organizer, event, 'event.orders:read', request):
r['linked_giftcard']['owner_ticket'] = {'id': instance.linked_giftcard.owner_ticket.id}
return r
class Meta: class Meta:
model = ReusableMedium model = ReusableMedium
fields = ( fields = (
+1 -5
View File
@@ -769,11 +769,7 @@ class PaymentDetailsField(serializers.Field):
pp = value.payment_provider pp = value.payment_provider
if not pp: if not pp:
return {} return {}
try: return pp.api_payment_details(value)
return pp.api_payment_details(value)
except Exception:
logger.exception("Failed to retrieve payment_details")
return {}
class OrderPaymentSerializer(I18nAwareModelSerializer): class OrderPaymentSerializer(I18nAwareModelSerializer):
-13
View File
@@ -286,19 +286,6 @@ class GiftCardSerializer(I18nAwareModelSerializer):
) )
return data return data
def to_representation(self, instance):
r = super().to_representation(instance)
request = self.context.get('request')
# late permission evaluations for checks that depend on the actual linked events
if 'owner_ticket' in self.context['request'].query_params.getlist('expand'):
owner_ticket = instance.owner_ticket
if owner_ticket:
event = owner_ticket.order.event
perm_holder = request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user
if not perm_holder.has_event_permission(event.organizer, event, 'event.orders:read', request):
r['owner_ticket'] = {'id': instance.owner_ticket.id}
return r
class Meta: class Meta:
model = GiftCard model = GiftCard
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions', 'owner_ticket', fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions', 'owner_ticket',
+1 -1
View File
@@ -1122,7 +1122,7 @@ class CheckinViewSet(viewsets.ReadOnlyModelViewSet):
permission = 'event.orders:read' permission = 'event.orders:read'
def get_queryset(self): def get_queryset(self):
qs = Checkin.all.filter(list__event=self.request.event).select_related( qs = Checkin.all.filter().select_related(
"position", "position",
"device", "device",
) )
-80
View File
@@ -19,10 +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 ipaddress
import logging import logging
import smtplib
import socket
from itertools import groupby from itertools import groupby
from smtplib import SMTPResponseException from smtplib import SMTPResponseException
from typing import TypeVar from typing import TypeVar
@@ -240,80 +237,3 @@ def base_renderers(sender, **kwargs):
def get_email_context(**kwargs): def get_email_context(**kwargs):
return PlaceholderContext(**kwargs).render_all() return PlaceholderContext(**kwargs).render_all()
def create_connection(address, timeout=socket.getdefaulttimeout(),
source_address=None, *, all_errors=False):
# Taken from the python stdlib, extended with a check for local ips
host, port = address
exceptions = []
for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
af, socktype, proto, canonname, sa = res
if not getattr(settings, "MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS", False):
ip_addr = ipaddress.ip_address(sa[0])
if ip_addr.is_multicast:
raise socket.error(f"Request to multicast address {sa[0]} blocked")
if ip_addr.is_loopback or ip_addr.is_link_local:
raise socket.error(f"Request to local address {sa[0]} blocked")
if ip_addr.is_private:
raise socket.error(f"Request to private address {sa[0]} blocked")
sock = None
try:
sock = socket.socket(af, socktype, proto)
if timeout is not socket.getdefaulttimeout():
sock.settimeout(timeout)
if source_address:
sock.bind(source_address)
sock.connect(sa)
# Break explicitly a reference cycle
exceptions.clear()
return sock
except socket.error as exc:
if not all_errors:
exceptions.clear() # raise only the last error
exceptions.append(exc)
if sock is not None:
sock.close()
if len(exceptions):
try:
if not all_errors:
raise exceptions[0]
raise ExceptionGroup("create_connection failed", exceptions)
finally:
# Break explicitly a reference cycle
exceptions.clear()
else:
raise socket.error("getaddrinfo returns an empty list")
class CheckPrivateNetworkMixin:
# _get_socket taken 1:1 from smtplib, just with a call to our own create_connection
def _get_socket(self, host, port, timeout):
# This makes it simpler for SMTP_SSL to use the SMTP connect code
# and just alter the socket connection bit.
if timeout is not None and not timeout:
raise ValueError('Non-blocking socket (timeout=0) is not supported')
if self.debuglevel > 0:
self._print_debug('connect: to', (host, port), self.source_address)
return create_connection((host, port), timeout, self.source_address)
class SMTP(CheckPrivateNetworkMixin, smtplib.SMTP):
pass
# SMTP used here instead of mixin, because smtp.SMTP_SSL._get_socket calls super()._get_socket and then wraps this socket
# super()._get_socket needs to be our version from the mixin
class SMTP_SSL(smtplib.SMTP_SSL, SMTP): # noqa: N801
pass
class CheckPrivateNetworkSmtpBackend(EmailBackend):
@property
def connection_class(self):
return SMTP_SSL if self.use_ssl else SMTP
+3 -13
View File
@@ -47,7 +47,6 @@ from django.utils.formats import localize
from django.utils.translation import gettext, gettext_lazy as _ from django.utils.translation import gettext, gettext_lazy as _
from pretix.base.models import Event from pretix.base.models import Event
from pretix.base.models.auth import PermissionHolder
from pretix.helpers.safe_openpyxl import ( # NOQA: backwards compatibility for plugins using excel_safe from pretix.helpers.safe_openpyxl import ( # NOQA: backwards compatibility for plugins using excel_safe
SafeWorkbook, remove_invalid_excel_chars as excel_safe, SafeWorkbook, remove_invalid_excel_chars as excel_safe,
) )
@@ -60,20 +59,11 @@ class BaseExporter:
This is the base class for all data exporters This is the base class for all data exporters
""" """
def __init__(self, event, organizer, permission_holder: PermissionHolder=None, progress_callback=lambda v: None): def __init__(self, event, organizer, progress_callback=lambda v: None):
"""
:param event: Event context, can also be a queryset of events for multi-event exports
:param organizer: Organizer context
:param user: The user who triggered the export (or None).
:param token: The API token that triggered the export (or None).
:param device: The device that triggered the export (or None)
:param progress_callback: Callback function with progress
"""
self.event = event self.event = event
self.organizer = organizer self.organizer = organizer
self.progress_callback = progress_callback self.progress_callback = progress_callback
self.is_multievent = isinstance(event, QuerySet) self.is_multievent = isinstance(event, QuerySet)
self.permission_holder = permission_holder
if isinstance(event, QuerySet): if isinstance(event, QuerySet):
self.events = event self.events = event
self.event = None self.event = None
@@ -190,7 +180,7 @@ class BaseExporter:
return True return True
@classmethod @classmethod
def get_required_event_permission(cls) -> Optional[str]: def get_required_event_permission(cls) -> str:
""" """
The permission level required to use this exporter for events. For multi-event-exports, this will be used The permission level required to use this exporter for events. For multi-event-exports, this will be used
to limit the selection of events. Will be ignored if the ``OrganizerLevelExportMixin`` mixin is used. to limit the selection of events. Will be ignored if the ``OrganizerLevelExportMixin`` mixin is used.
@@ -205,7 +195,7 @@ class OrganizerLevelExportMixin:
raise TypeError("required_event_permission may not be called on OrganizerLevelExportMixin") raise TypeError("required_event_permission may not be called on OrganizerLevelExportMixin")
@classmethod @classmethod
def get_required_organizer_permission(cls) -> Optional[str]: def get_required_organizer_permission(cls) -> str:
""" """
The permission level required to use this exporter. Must be set for organizer-level exports. Set to `None` to The permission level required to use this exporter. Must be set for organizer-level exports. Set to `None` to
allow everyone with any access to the organizer. allow everyone with any access to the organizer.
-4
View File
@@ -70,10 +70,6 @@ def parse_csv(file, length=None, mode="strict", charset=None):
except ImportError: except ImportError:
charset = file.charset charset = file.charset
data = data.decode(charset or "utf-8", mode) data = data.decode(charset or "utf-8", mode)
# remove stray linebreaks from the end of the file
data = data.rstrip("\n")
# If the file was modified on a Mac, it only contains \r as line breaks # If the file was modified on a Mac, it only contains \r as line breaks
if '\r' in data and '\n' not in data: if '\r' in data and '\n' not in data:
data = data.replace('\r', '\n') data = data.replace('\r', '\n')
+3 -9
View File
@@ -29,9 +29,7 @@ import inspect
import logging import logging
import os import os
import threading import threading
from pathlib import Path
import django
from django.conf import settings from django.conf import settings
from django.db import transaction from django.db import transaction
@@ -76,14 +74,10 @@ def _transactions_mark_order_dirty(order_id, using=None):
if "PYTEST_CURRENT_TEST" in os.environ: if "PYTEST_CURRENT_TEST" in os.environ:
# We don't care about Order.objects.create() calls in test code so let's try to figure out if this is test code # We don't care about Order.objects.create() calls in test code so let's try to figure out if this is test code
# or not. # or not.
for frame in inspect.stack()[1:]: for frame in inspect.stack():
if ( if 'pretix/base/models/orders' in frame.filename:
'pretix/base/models/orders' in frame.filename
or Path(frame.filename).is_relative_to(Path(django.__file__).parent)
):
# Ignore model- and django-internal code
continue continue
elif 'test_' in frame.filename or 'conftest.py' in frame.filename: elif 'test_' in frame.filename or 'conftest.py in frame.filename':
return return
elif 'pretix/' in frame.filename or 'pretix_' in frame.filename: elif 'pretix/' in frame.filename or 'pretix_' in frame.filename:
# This went through non-test code, let's consider it non-test # This went through non-test code, let's consider it non-test
-21
View File
@@ -38,7 +38,6 @@ import operator
import secrets import secrets
from datetime import timedelta from datetime import timedelta
from functools import reduce from functools import reduce
from typing import Protocol
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import ( from django.contrib.auth.models import (
@@ -68,14 +67,6 @@ class EmailAddressTakenError(IntegrityError):
pass pass
class PermissionHolder(Protocol):
def has_event_permission(self, organizer, event, perm_name=None, request=None, session_key=None) -> bool:
...
def has_organizer_permission(self, organizer, perm_name=None, request=None):
...
class UserManager(BaseUserManager): class UserManager(BaseUserManager):
""" """
This is the user manager for our custom user model. See the User This is the user manager for our custom user model. See the User
@@ -705,18 +696,6 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
return self.teams.exists() return self.teams.exists()
class UserWithStaffSession:
# Wrapper around a User object with a staff session, implementing the PermissionHolder Protocol
def __init__(self, user):
self.user = user
def has_event_permission(self, organizer, event, perm_name=None, request=None, session_key=None) -> bool:
return True
def has_organizer_permission(self, organizer, perm_name=None, request=None):
return True
class UserKnownLoginSource(models.Model): class UserKnownLoginSource(models.Model):
user = models.ForeignKey('User', on_delete=models.CASCADE, related_name="known_login_sources") user = models.ForeignKey('User', on_delete=models.CASCADE, related_name="known_login_sources")
agent_type = models.CharField(max_length=255, null=True, blank=True) agent_type = models.CharField(max_length=255, null=True, blank=True)
+1 -2
View File
@@ -229,7 +229,7 @@ class Device(LoggedModel):
""" """
return self._organizer_permission_set() if self.organizer == organizer else set() return self._organizer_permission_set() if self.organizer == organizer else set()
def has_event_permission(self, organizer, event, perm_name=None, request=None, session_key=None) -> bool: def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
""" """
Checks if this token is part of a team that grants access of type ``perm_name`` Checks if this token is part of a team that grants access of type ``perm_name``
to the event ``event``. to the event ``event``.
@@ -238,7 +238,6 @@ class Device(LoggedModel):
:param event: The event to check :param event: The event to check
:param perm_name: The permission, e.g. ``event.orders:read`` :param perm_name: The permission, e.g. ``event.orders:read``
:param request: This parameter is ignored and only defined for compatibility reasons. :param request: This parameter is ignored and only defined for compatibility reasons.
:param session_key: This parameter is ignored and only defined for compatibility reasons.
:return: bool :return: bool
""" """
has_event_access = (self.all_events and organizer == self.organizer) or ( has_event_access = (self.all_events and organizer == self.organizer) or (
+2 -2
View File
@@ -590,7 +590,7 @@ class Order(LockModel, LoggedModel):
not kwargs.get('force_save_with_deferred_fields', None) and not kwargs.get('force_save_with_deferred_fields', None) and
(not update_fields or ('require_approval' not in update_fields and 'status' not in update_fields)) (not update_fields or ('require_approval' not in update_fields and 'status' not in update_fields))
): ):
_fail("It is unsafe to call save() on an Order with deferred fields since we can't check if you missed " _fail("It is unsafe to call save() on an OrderFee with deferred fields since we can't check if you missed "
"creating a transaction. Call save(force_save_with_deferred_fields=True) if you really want to do " "creating a transaction. Call save(force_save_with_deferred_fields=True) if you really want to do "
"this.") "this.")
@@ -2841,7 +2841,7 @@ class OrderPosition(AbstractPosition):
if Transaction.key(self) != self.__initial_transaction_key or self.canceled != self.__initial_canceled or not self.pk: if Transaction.key(self) != self.__initial_transaction_key or self.canceled != self.__initial_canceled or not self.pk:
_transactions_mark_order_dirty(self.order_id, using=kwargs.get('using', None)) _transactions_mark_order_dirty(self.order_id, using=kwargs.get('using', None))
elif not kwargs.get('force_save_with_deferred_fields', None): elif not kwargs.get('force_save_with_deferred_fields', None):
_fail("It is unsafe to call save() on an OrderPosition with deferred fields since we can't check if you missed " _fail("It is unsafe to call save() on an OrderFee with deferred fields since we can't check if you missed "
"creating a transaction. Call save(force_save_with_deferred_fields=True) if you really want to do " "creating a transaction. Call save(force_save_with_deferred_fields=True) if you really want to do "
"this.") "this.")
+1 -8
View File
@@ -319,9 +319,6 @@ class TeamQuerySet(models.QuerySet):
def event_permission_q(cls, perm_name): def event_permission_q(cls, perm_name):
from ..permissions import assert_valid_event_permission from ..permissions import assert_valid_event_permission
if perm_name is None:
return Q()
if perm_name.startswith('can_') and perm_name in OLD_TO_NEW_EVENT_COMPAT: # legacy if perm_name.startswith('can_') and perm_name in OLD_TO_NEW_EVENT_COMPAT: # legacy
return reduce(operator.and_, [cls.event_permission_q(p) for p in OLD_TO_NEW_EVENT_COMPAT[perm_name]]) return reduce(operator.and_, [cls.event_permission_q(p) for p in OLD_TO_NEW_EVENT_COMPAT[perm_name]])
assert_valid_event_permission(perm_name, allow_legacy=False) assert_valid_event_permission(perm_name, allow_legacy=False)
@@ -334,9 +331,6 @@ class TeamQuerySet(models.QuerySet):
def organizer_permission_q(cls, perm_name): def organizer_permission_q(cls, perm_name):
from ..permissions import assert_valid_organizer_permission from ..permissions import assert_valid_organizer_permission
if perm_name is None:
return Q()
if perm_name.startswith('can_') and perm_name in OLD_TO_NEW_ORGANIZER_COMPAT: # legacy if perm_name.startswith('can_') and perm_name in OLD_TO_NEW_ORGANIZER_COMPAT: # legacy
return reduce(operator.and_, [cls.organizer_permission_q(p) for p in OLD_TO_NEW_ORGANIZER_COMPAT[perm_name]]) return reduce(operator.and_, [cls.organizer_permission_q(p) for p in OLD_TO_NEW_ORGANIZER_COMPAT[perm_name]])
assert_valid_organizer_permission(perm_name, allow_legacy=False) assert_valid_organizer_permission(perm_name, allow_legacy=False)
@@ -556,7 +550,7 @@ class TeamAPIToken(models.Model):
""" """
return self.team.organizer_permission_set() if self.team.organizer == organizer else set() return self.team.organizer_permission_set() if self.team.organizer == organizer else set()
def has_event_permission(self, organizer, event, perm_name=None, request=None, session_key=None) -> bool: def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
""" """
Checks if this token is part of a team that grants access of type ``perm_name`` Checks if this token is part of a team that grants access of type ``perm_name``
to the event ``event``. to the event ``event``.
@@ -565,7 +559,6 @@ class TeamAPIToken(models.Model):
:param event: The event to check :param event: The event to check
:param perm_name: The permission, e.g. ``event.orders:read`` :param perm_name: The permission, e.g. ``event.orders:read``
:param request: This parameter is ignored and only defined for compatibility reasons. :param request: This parameter is ignored and only defined for compatibility reasons.
:param session_key: This parameter is ignored and only defined for compatibility reasons.
:return: bool :return: bool
""" """
has_event_access = (self.team.all_events and organizer == self.team.organizer) or ( has_event_access = (self.team.all_events and organizer == self.team.organizer) or (
+5 -16
View File
@@ -54,7 +54,7 @@ from bidi import get_display
from django.conf import settings from django.conf import settings
from django.contrib.staticfiles import finders from django.contrib.staticfiles import finders
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models import Exists, Max, Min, OuterRef from django.db.models import Max, Min
from django.db.models.fields.files import FieldFile from django.db.models.fields.files import FieldFile
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.deconstruct import deconstructible from django.utils.deconstruct import deconstructible
@@ -76,7 +76,7 @@ from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import Paragraph from reportlab.platypus import Paragraph
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import Checkin, Event, Order, OrderPosition, Question from pretix.base.models import Event, Order, OrderPosition, Question
from pretix.base.settings import PERSON_NAME_SCHEMES from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import layout_image_variables, layout_text_variables from pretix.base.signals import layout_image_variables, layout_text_variables
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
@@ -379,13 +379,6 @@ DEFAULT_VARIABLES = OrderedDict((
str(p) for p in generate_compressed_addon_list(op, order, ev) str(p) for p in generate_compressed_addon_list(op, order, ev)
]) ])
}), }),
("checked_in_addons", {
"label": _("List of Checked-In Add-Ons"),
"editor_sample": _("Add-on 1\n2x Add-on 2"),
"evaluate": lambda op, order, ev: "\n".join([
str(p) for p in generate_compressed_addon_list(op, order, ev, only_checked_in=True)
])
}),
("organizer", { ("organizer", {
"label": _("Organizer name"), "label": _("Organizer name"),
"editor_sample": _("Event organizer company"), "editor_sample": _("Event organizer company"),
@@ -757,16 +750,12 @@ def get_program_times(op: OrderPosition, ev: Event):
]) ])
def generate_compressed_addon_list(op, order, event, only_checked_in=False): def generate_compressed_addon_list(op, order, event):
itemcount = defaultdict(int) itemcount = defaultdict(int)
addon_qs = ( addons = [p for p in (
op.addons.all() if 'addons' in getattr(op, '_prefetched_objects_cache', {}) op.addons.all() if 'addons' in getattr(op, '_prefetched_objects_cache', {})
else op.addons.select_related('item', 'variation') else op.addons.select_related('item', 'variation')
) ) if not p.canceled]
if only_checked_in:
addon_qs = addon_qs.filter(Exists(Checkin.objects.filter(position=OuterRef('pk'))), canceled=False)
addons = [p for p in addon_qs if not p.canceled]
for pos in addons: for pos in addons:
itemcount[pos.item, pos.variation] += 1 itemcount[pos.item, pos.variation] += 1
+3 -19
View File
@@ -40,7 +40,6 @@ from pretix.base.models import (
CachedFile, Device, Event, Organizer, ScheduledEventExport, TeamAPIToken, CachedFile, Device, Event, Organizer, ScheduledEventExport, TeamAPIToken,
User, cachedfile_name, User, cachedfile_name,
) )
from pretix.base.models.auth import UserWithStaffSession
from pretix.base.models.exports import ScheduledOrganizerExport from pretix.base.models.exports import ScheduledOrganizerExport
from pretix.base.services.mail import mail from pretix.base.services.mail import mail
from pretix.base.services.tasks import ( from pretix.base.services.tasks import (
@@ -212,12 +211,7 @@ def init_event_exporters(event, user=None, token=None, device=None, request=None
if not perm_holder.has_event_permission(event.organizer, event, permission_name, request) and not staff_session: if not perm_holder.has_event_permission(event.organizer, event, permission_name, request) and not staff_session:
continue continue
exporter: BaseExporter = response( exporter: BaseExporter = response(event=event, organizer=event.organizer, **kwargs)
event=event,
organizer=event.organizer,
permission_holder=token or device or (UserWithStaffSession(user) if staff_session else user),
**kwargs
)
if not exporter.available_for_user(user if user and user.is_authenticated else None): if not exporter.available_for_user(user if user and user.is_authenticated else None):
continue continue
@@ -249,12 +243,7 @@ def init_organizer_exporters(
continue continue
if issubclass(response, OrganizerLevelExportMixin): if issubclass(response, OrganizerLevelExportMixin):
exporter: BaseExporter = response( exporter: BaseExporter = response(event=Event.objects.none(), organizer=organizer, **kwargs)
event=Event.objects.none(),
organizer=organizer,
permission_holder=token or device or (UserWithStaffSession(user) if staff_session else user),
**kwargs,
)
try: try:
if not perm_holder.has_organizer_permission(organizer, response.get_required_organizer_permission(), request) and not staff_session: if not perm_holder.has_organizer_permission(organizer, response.get_required_organizer_permission(), request) and not staff_session:
@@ -306,12 +295,7 @@ def init_organizer_exporters(
if not _has_permission_on_any_team_cache[permission_name] and not staff_session: if not _has_permission_on_any_team_cache[permission_name] and not staff_session:
continue continue
exporter: BaseExporter = response( exporter: BaseExporter = response(event=_event_list_cache[permission_name], organizer=organizer, **kwargs)
event=_event_list_cache[permission_name],
organizer=organizer,
permission_holder=token or device or (UserWithStaffSession(user) if staff_session else user),
**kwargs,
)
if not exporter.available_for_user(user if user and user.is_authenticated else None): if not exporter.available_for_user(user if user and user.is_authenticated else None):
continue continue
+1 -1
View File
@@ -436,7 +436,7 @@ class OrderPositionAddForm(forms.Form):
d['used_membership'] = [m for m in self.memberships if str(m.pk) == d['used_membership']][0] d['used_membership'] = [m for m in self.memberships if str(m.pk) == d['used_membership']][0]
else: else:
d['used_membership'] = None d['used_membership'] = None
if d.get("count", 1) > 1 and d.get("seat"): if d.get("count", 1) and d.get("seat"):
raise ValidationError({ raise ValidationError({
"seat": _("You can not choose a seat when adding multiple products at once.") "seat": _("You can not choose a seat when adding multiple products at once.")
}) })
+1 -1
View File
@@ -1322,7 +1322,7 @@ class DeviceUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
def form_valid(self, form): def form_valid(self, form):
if form.has_changed(): if form.has_changed():
self.object.log_action('pretix.device.changed', user=self.request.user, data={ self.object.log_action('pretix.device.changed', user=self.request.user, data={
k: form.cleaned_data[k] if k != 'limit_events' else [e.id for e in form.cleaned_data[k]] k: getattr(self.object, k) if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
for k in form.changed_data for k in form.changed_data
}) })
-132
View File
@@ -19,26 +19,12 @@
# 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 ipaddress
import socket
import sys
import types import types
from datetime import datetime from datetime import datetime
from http import cookies from http import cookies
from django.conf import settings
from PIL import Image from PIL import Image
from requests.adapters import HTTPAdapter from requests.adapters import HTTPAdapter
from urllib3.connection import HTTPConnection, HTTPSConnection
from urllib3.connectionpool import HTTPConnectionPool, HTTPSConnectionPool
from urllib3.exceptions import (
ConnectTimeoutError, HTTPError, LocationParseError, NameResolutionError,
NewConnectionError,
)
from urllib3.util.connection import (
_TYPE_SOCKET_OPTIONS, _set_socket_options, allowed_gai_family,
)
from urllib3.util.timeout import _DEFAULT_TIMEOUT
def monkeypatch_vobject_performance(): def monkeypatch_vobject_performance():
@@ -103,123 +89,6 @@ def monkeypatch_requests_timeout():
HTTPAdapter.send = httpadapter_send HTTPAdapter.send = httpadapter_send
def monkeypatch_urllib3_ssrf_protection():
"""
pretix allows HTTP requests to untrusted URLs, e.g. through webhooks or external API URLs. This is dangerous since
it can allow access to private networks that should not be reachable by users ("server-side request forgery", SSRF).
Validating URLs at submission is not sufficient, since with DNS rebinding an attacker can make a domain name pass
validation and then resolve to a private IP address on actual execution. Unfortunately, there seems no clean solution
to this in Python land, so we monkeypatch urllib3's connection management to check the IP address to be external
*after* the DNS resolution.
This does not work when a global http(s) proxy is used, but in that scenario the proxy can perform the validation.
"""
if getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False):
# Settings are not supposed to change during runtime, so we can optimize performance and complexity by skipping
# this if not needed.
return
def create_connection(
address: tuple[str, int],
timeout=_DEFAULT_TIMEOUT,
source_address: tuple[str, int] | None = None,
socket_options: _TYPE_SOCKET_OPTIONS | None = None,
) -> socket.socket:
# This is copied from urllib3.util.connection v2.3.0
host, port = address
if host.startswith("["):
host = host.strip("[]")
err = None
# Using the value from allowed_gai_family() in the context of getaddrinfo lets
# us select whether to work with IPv4 DNS records, IPv6 records, or both.
# The original create_connection function always returns all records.
family = allowed_gai_family()
try:
host.encode("idna")
except UnicodeError:
raise LocationParseError(f"'{host}', label empty or too long") from None
for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM):
af, socktype, proto, canonname, sa = res
if not getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False):
ip_addr = ipaddress.ip_address(sa[0])
if ip_addr.is_multicast:
raise HTTPError(f"Request to multicast address {sa[0]} blocked")
if ip_addr.is_loopback or ip_addr.is_link_local:
raise HTTPError(f"Request to local address {sa[0]} blocked")
if ip_addr.is_private:
raise HTTPError(f"Request to private address {sa[0]} blocked")
sock = None
try:
sock = socket.socket(af, socktype, proto)
# If provided, set socket level options before connecting.
_set_socket_options(sock, socket_options)
if timeout is not _DEFAULT_TIMEOUT:
sock.settimeout(timeout)
if source_address:
sock.bind(source_address)
sock.connect(sa)
# Break explicitly a reference cycle
err = None
return sock
except OSError as _:
err = _
if sock is not None:
sock.close()
if err is not None:
try:
raise err
finally:
# Break explicitly a reference cycle
err = None
else:
raise OSError("getaddrinfo returns an empty list")
class ProtectionMixin:
def _new_conn(self) -> socket.socket:
# This is 1:1 the version from urllib3.connection.HTTPConnection._new_conn v2.3.0
# just with a call to our own create_connection
try:
sock = create_connection(
(self._dns_host, self.port),
self.timeout,
source_address=self.source_address,
socket_options=self.socket_options,
)
except socket.gaierror as e:
raise NameResolutionError(self.host, self, e) from e
except socket.timeout as e:
raise ConnectTimeoutError(
self,
f"Connection to {self.host} timed out. (connect timeout={self.timeout})",
) from e
except OSError as e:
raise NewConnectionError(
self, f"Failed to establish a new connection: {e}"
) from e
sys.audit("http.client.connect", self, self.host, self.port)
return sock
class ProtectedHTTPConnection(ProtectionMixin, HTTPConnection):
pass
class ProtectedHTTPSConnection(ProtectionMixin, HTTPSConnection):
pass
HTTPConnectionPool.ConnectionCls = ProtectedHTTPConnection
HTTPSConnectionPool.ConnectionCls = ProtectedHTTPSConnection
def monkeypatch_cookie_morsel(): def monkeypatch_cookie_morsel():
# See https://code.djangoproject.com/ticket/34613 # See https://code.djangoproject.com/ticket/34613
cookies.Morsel._flags.add("partitioned") cookies.Morsel._flags.add("partitioned")
@@ -230,5 +99,4 @@ def monkeypatch_all_at_ready():
monkeypatch_vobject_performance() monkeypatch_vobject_performance()
monkeypatch_pillow_safer() monkeypatch_pillow_safer()
monkeypatch_requests_timeout() monkeypatch_requests_timeout()
monkeypatch_urllib3_ssrf_protection()
monkeypatch_cookie_morsel() monkeypatch_cookie_morsel()
+8 -10
View File
@@ -8,7 +8,7 @@ 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: 2026-03-30 11:22+0000\n" "POT-Creation-Date: 2026-03-30 11:22+0000\n"
"PO-Revision-Date: 2026-03-31 17:00+0000\n" "PO-Revision-Date: 2026-03-18 12:23+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n" "Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
"es/>\n" "es/>\n"
@@ -13331,7 +13331,7 @@ msgstr ""
#: pretix/base/settings.py:4157 #: pretix/base/settings.py:4157
#, python-brace-format #, python-brace-format
msgid "VAT-ID is not supported for \"{}\"." msgid "VAT-ID is not supported for \"{}\"."
msgstr "El NIF no es compatible con «{}»." msgstr ""
#: pretix/base/settings.py:4164 #: pretix/base/settings.py:4164
msgid "The last payment date cannot be before the end of presale." msgid "The last payment date cannot be before the end of presale."
@@ -27567,30 +27567,28 @@ msgid "Add a two-factor authentication device"
msgstr "Añadir un dispositivo de autenticación de dos factores" msgstr "Añadir un dispositivo de autenticación de dos factores"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:19 #: pretix/control/templates/pretixcontrol/user/2fa_add.html:19
#, fuzzy
#| msgid "Smartphone with the Authenticator application"
msgid "Smartphone with Authenticator app" msgid "Smartphone with Authenticator app"
msgstr "Smartphone con la aplicación Authenticator" msgstr "Celular con aplicación de autenticación"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:21 #: pretix/control/templates/pretixcontrol/user/2fa_add.html:21
msgid "" msgid ""
"Use your smartphone with any Time-based One-Time-Password app like freeOTP, " "Use your smartphone with any Time-based One-Time-Password app like freeOTP, "
"Google Authenticator or Proton Authenticator." "Google Authenticator or Proton Authenticator."
msgstr "" msgstr ""
"Use su smartphone con cualquier aplicación de contraseñas de un solo uso "
"basadas en el tiempo, como freeOTP, Google Authenticator o Proton "
"Authenticator."
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:30 #: pretix/control/templates/pretixcontrol/user/2fa_add.html:30
#, fuzzy
#| msgid "WebAuthn-compatible hardware token (e.g. Yubikey)"
msgid "WebAuthn-compatible hardware token" msgid "WebAuthn-compatible hardware token"
msgstr "Token físico compatible con WebAuthn" msgstr "Hardware compatible con token WebAuthn (p. ej. Yubikey)"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:32 #: pretix/control/templates/pretixcontrol/user/2fa_add.html:32
msgid "" msgid ""
"Use a hardware token like the Yubikey, or other biometric authentication " "Use a hardware token like the Yubikey, or other biometric authentication "
"like fingerprint or face recognition." "like fingerprint or face recognition."
msgstr "" msgstr ""
"Utiliza un dispositivo de seguridad físico, como el Yubikey, u otro método "
"de autenticación biométrica, como el reconocimiento de huellas dactilares o "
"facial."
#: pretix/control/templates/pretixcontrol/user/2fa_confirm_totp.html:8 #: pretix/control/templates/pretixcontrol/user/2fa_confirm_totp.html:8
msgid "To set up this device, please follow the following steps:" msgid "To set up this device, please follow the following steps:"
+10 -12
View File
@@ -4,10 +4,10 @@ msgstr ""
"Project-Id-Version: 1\n" "Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-30 11:22+0000\n" "POT-Creation-Date: 2026-03-30 11:22+0000\n"
"PO-Revision-Date: 2026-03-31 17:00+0000\n" "PO-Revision-Date: 2026-03-18 12:23+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n" "Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
"fr/>\n" ">\n"
"Language: fr\n" "Language: fr\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -13454,7 +13454,7 @@ msgstr ""
#: pretix/base/settings.py:4157 #: pretix/base/settings.py:4157
#, python-brace-format #, python-brace-format
msgid "VAT-ID is not supported for \"{}\"." msgid "VAT-ID is not supported for \"{}\"."
msgstr "Le numéro de TVA n'est pas pris en charge pour « {} »." msgstr ""
#: pretix/base/settings.py:4164 #: pretix/base/settings.py:4164
msgid "The last payment date cannot be before the end of presale." msgid "The last payment date cannot be before the end of presale."
@@ -27774,30 +27774,28 @@ msgid "Add a two-factor authentication device"
msgstr "Ajouter un dispositif d'authentification à deux facteurs" msgstr "Ajouter un dispositif d'authentification à deux facteurs"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:19 #: pretix/control/templates/pretixcontrol/user/2fa_add.html:19
#, fuzzy
#| msgid "Smartphone with the Authenticator application"
msgid "Smartphone with Authenticator app" msgid "Smartphone with Authenticator app"
msgstr "Smartphone équipé de l'application Authenticator" msgstr "Smartphone avec l'application Authenticator"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:21 #: pretix/control/templates/pretixcontrol/user/2fa_add.html:21
msgid "" msgid ""
"Use your smartphone with any Time-based One-Time-Password app like freeOTP, " "Use your smartphone with any Time-based One-Time-Password app like freeOTP, "
"Google Authenticator or Proton Authenticator." "Google Authenticator or Proton Authenticator."
msgstr "" msgstr ""
"Utilisez votre smartphone avec n'importe quelle application de mots de passe "
"à usage unique générés en temps réel, comme freeOTP, Google Authenticator ou "
"Proton Authenticator."
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:30 #: pretix/control/templates/pretixcontrol/user/2fa_add.html:30
#, fuzzy
#| msgid "WebAuthn-compatible hardware token (e.g. Yubikey)"
msgid "WebAuthn-compatible hardware token" msgid "WebAuthn-compatible hardware token"
msgstr "Token matériel compatible WebAuthn" msgstr "Token matériel compatible WebAuthn (par ex. Yubikey)"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:32 #: pretix/control/templates/pretixcontrol/user/2fa_add.html:32
msgid "" msgid ""
"Use a hardware token like the Yubikey, or other biometric authentication " "Use a hardware token like the Yubikey, or other biometric authentication "
"like fingerprint or face recognition." "like fingerprint or face recognition."
msgstr "" msgstr ""
"Utilisez une clé matérielle telle que la Yubikey, ou un autre moyen "
"d'authentification biométrique, comme la reconnaissance d'empreintes "
"digitales ou faciale."
#: pretix/control/templates/pretixcontrol/user/2fa_confirm_totp.html:8 #: pretix/control/templates/pretixcontrol/user/2fa_confirm_totp.html:8
msgid "To set up this device, please follow the following steps:" msgid "To set up this device, please follow the following steps:"
+7 -7
View File
@@ -8,7 +8,7 @@ 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: 2026-03-30 11:22+0000\n" "POT-Creation-Date: 2026-03-30 11:22+0000\n"
"PO-Revision-Date: 2026-04-08 18:00+0000\n" "PO-Revision-Date: 2026-03-23 21:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n" "Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\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"
@@ -12939,7 +12939,7 @@ msgstr "企業名を必須にするには、請求先住所を必須にする必
#: pretix/base/settings.py:4157 #: pretix/base/settings.py:4157
#, python-brace-format #, python-brace-format
msgid "VAT-ID is not supported for \"{}\"." msgid "VAT-ID is not supported for \"{}\"."
msgstr "VAT-IDは「{}」に対してサポートされていません。" msgstr ""
#: pretix/base/settings.py:4164 #: pretix/base/settings.py:4164
msgid "The last payment date cannot be before the end of presale." msgid "The last payment date cannot be before the end of presale."
@@ -26796,6 +26796,8 @@ msgid "Add a two-factor authentication device"
msgstr "2要素認証デバイスを追加してください" msgstr "2要素認証デバイスを追加してください"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:19 #: pretix/control/templates/pretixcontrol/user/2fa_add.html:19
#, fuzzy
#| msgid "Smartphone with the Authenticator application"
msgid "Smartphone with Authenticator app" msgid "Smartphone with Authenticator app"
msgstr "Authenticatorアプリを搭載したスマートフォン" msgstr "Authenticatorアプリを搭載したスマートフォン"
@@ -26804,20 +26806,18 @@ msgid ""
"Use your smartphone with any Time-based One-Time-Password app like freeOTP, " "Use your smartphone with any Time-based One-Time-Password app like freeOTP, "
"Google Authenticator or Proton Authenticator." "Google Authenticator or Proton Authenticator."
msgstr "" msgstr ""
"freeOTP、Google Authenticator、Proton Authenticator などの時間ベースの"
"ワンタイムパスワードアプリをスマートフォンでご利用ください。"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:30 #: pretix/control/templates/pretixcontrol/user/2fa_add.html:30
#, fuzzy
#| msgid "WebAuthn-compatible hardware token (e.g. Yubikey)"
msgid "WebAuthn-compatible hardware token" msgid "WebAuthn-compatible hardware token"
msgstr "WebAuthn対応のハードウェアトークン" msgstr "WebAuthn対応のハードウェアトークン(例:Yubikey)"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:32 #: pretix/control/templates/pretixcontrol/user/2fa_add.html:32
msgid "" msgid ""
"Use a hardware token like the Yubikey, or other biometric authentication " "Use a hardware token like the Yubikey, or other biometric authentication "
"like fingerprint or face recognition." "like fingerprint or face recognition."
msgstr "" msgstr ""
"Yubikey などのハードウェアトークンや、指紋や顔認識などの生体認証を使用してく"
"ださい。"
#: pretix/control/templates/pretixcontrol/user/2fa_confirm_totp.html:8 #: pretix/control/templates/pretixcontrol/user/2fa_confirm_totp.html:8
msgid "To set up this device, please follow the following steps:" msgid "To set up this device, please follow the following steps:"
+10 -10
View File
@@ -7,10 +7,10 @@ msgstr ""
"Project-Id-Version: 1\n" "Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-30 11:22+0000\n" "POT-Creation-Date: 2026-03-30 11:22+0000\n"
"PO-Revision-Date: 2026-03-31 17:00+0000\n" "PO-Revision-Date: 2026-03-18 12:23+0000\n"
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n" "Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\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"
"Language: nl\n" "Language: nl\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -13283,7 +13283,7 @@ msgstr ""
#: pretix/base/settings.py:4157 #: pretix/base/settings.py:4157
#, python-brace-format #, python-brace-format
msgid "VAT-ID is not supported for \"{}\"." msgid "VAT-ID is not supported for \"{}\"."
msgstr "Btw-nummer wordt niet ondersteund voor \"{}\"." msgstr ""
#: pretix/base/settings.py:4164 #: pretix/base/settings.py:4164
msgid "The last payment date cannot be before the end of presale." msgid "The last payment date cannot be before the end of presale."
@@ -27461,28 +27461,28 @@ msgid "Add a two-factor authentication device"
msgstr "Twee-factor-authenticatieapparaat toevoegen" msgstr "Twee-factor-authenticatieapparaat toevoegen"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:19 #: pretix/control/templates/pretixcontrol/user/2fa_add.html:19
#, fuzzy
#| msgid "Smartphone with the Authenticator application"
msgid "Smartphone with Authenticator app" msgid "Smartphone with Authenticator app"
msgstr "Smartphone met Authenticator-app" msgstr "Smartphone met de Authenticator-applicatie"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:21 #: pretix/control/templates/pretixcontrol/user/2fa_add.html:21
msgid "" msgid ""
"Use your smartphone with any Time-based One-Time-Password app like freeOTP, " "Use your smartphone with any Time-based One-Time-Password app like freeOTP, "
"Google Authenticator or Proton Authenticator." "Google Authenticator or Proton Authenticator."
msgstr "" msgstr ""
"Gebruik uw smartphone met een willekeurige app voor tijdgebonden eenmalige "
"wachtwoorden, zoals freeOTP, Google Authenticator of Proton Authenticator."
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:30 #: pretix/control/templates/pretixcontrol/user/2fa_add.html:30
#, fuzzy
#| msgid "WebAuthn-compatible hardware token (e.g. Yubikey)"
msgid "WebAuthn-compatible hardware token" msgid "WebAuthn-compatible hardware token"
msgstr "WebAuthn-compatibel hardwaretoken" msgstr "WebAuthn-compatibel hardware-token (bijvoorbeeld Yubikey)"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:32 #: pretix/control/templates/pretixcontrol/user/2fa_add.html:32
msgid "" msgid ""
"Use a hardware token like the Yubikey, or other biometric authentication " "Use a hardware token like the Yubikey, or other biometric authentication "
"like fingerprint or face recognition." "like fingerprint or face recognition."
msgstr "" msgstr ""
"Gebruik een hardwaretoken zoals de Yubikey, of een andere vorm van "
"biometrische authenticatie, zoals vingerafdruk- of gezichtsherkenning."
#: pretix/control/templates/pretixcontrol/user/2fa_confirm_totp.html:8 #: pretix/control/templates/pretixcontrol/user/2fa_confirm_totp.html:8
msgid "To set up this device, please follow the following steps:" msgid "To set up this device, please follow the following steps:"
File diff suppressed because it is too large Load Diff
@@ -8,7 +8,7 @@ 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: 2026-03-30 11:22+0000\n" "POT-Creation-Date: 2026-03-30 11:22+0000\n"
"PO-Revision-Date: 2026-03-31 17:00+0000\n" "PO-Revision-Date: 2026-03-18 14:50+0000\n"
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n" "Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
"Language-Team: Dutch (informal) <https://translate.pretix.eu/projects/pretix/" "Language-Team: Dutch (informal) <https://translate.pretix.eu/projects/pretix/"
"pretix/nl_Informal/>\n" "pretix/nl_Informal/>\n"
@@ -13314,7 +13314,7 @@ msgstr ""
#: pretix/base/settings.py:4157 #: pretix/base/settings.py:4157
#, python-brace-format #, python-brace-format
msgid "VAT-ID is not supported for \"{}\"." msgid "VAT-ID is not supported for \"{}\"."
msgstr "Btw-nummer wordt niet ondersteund voor \"{}\"." msgstr ""
#: pretix/base/settings.py:4164 #: pretix/base/settings.py:4164
msgid "The last payment date cannot be before the end of presale." msgid "The last payment date cannot be before the end of presale."
@@ -27518,28 +27518,28 @@ msgid "Add a two-factor authentication device"
msgstr "Twee-factor-authenticatieapparaat toevoegen" msgstr "Twee-factor-authenticatieapparaat toevoegen"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:19 #: pretix/control/templates/pretixcontrol/user/2fa_add.html:19
#, fuzzy
#| msgid "Smartphone with the Authenticator application"
msgid "Smartphone with Authenticator app" msgid "Smartphone with Authenticator app"
msgstr "Smartphone met Authenticator-app" msgstr "Smartphone met de Authenticator-applicatie"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:21 #: pretix/control/templates/pretixcontrol/user/2fa_add.html:21
msgid "" msgid ""
"Use your smartphone with any Time-based One-Time-Password app like freeOTP, " "Use your smartphone with any Time-based One-Time-Password app like freeOTP, "
"Google Authenticator or Proton Authenticator." "Google Authenticator or Proton Authenticator."
msgstr "" msgstr ""
"Gebruik je smartphone met een willekeurige app voor tijdgebonden eenmalige "
"wachtwoorden, zoals freeOTP, Google Authenticator of Proton Authenticator."
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:30 #: pretix/control/templates/pretixcontrol/user/2fa_add.html:30
#, fuzzy
#| msgid "WebAuthn-compatible hardware token (e.g. Yubikey)"
msgid "WebAuthn-compatible hardware token" msgid "WebAuthn-compatible hardware token"
msgstr "WebAuthn-compatibel hardwaretoken" msgstr "WebAuthn-compatibel hardware-token (bijvoorbeeld Yubikey)"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:32 #: pretix/control/templates/pretixcontrol/user/2fa_add.html:32
msgid "" msgid ""
"Use a hardware token like the Yubikey, or other biometric authentication " "Use a hardware token like the Yubikey, or other biometric authentication "
"like fingerprint or face recognition." "like fingerprint or face recognition."
msgstr "" msgstr ""
"Gebruik een hardwaretoken zoals de Yubikey, of een andere vorm van "
"biometrische authenticatie, zoals vingerafdruk- of gezichtsherkenning."
#: pretix/control/templates/pretixcontrol/user/2fa_confirm_totp.html:8 #: pretix/control/templates/pretixcontrol/user/2fa_confirm_totp.html:8
msgid "To set up this device, please follow the following steps:" msgid "To set up this device, please follow the following steps:"
@@ -8,7 +8,7 @@ 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: 2026-03-30 11:22+0000\n" "POT-Creation-Date: 2026-03-30 11:22+0000\n"
"PO-Revision-Date: 2026-03-30 21:00+0000\n" "PO-Revision-Date: 2026-03-25 08:00+0000\n"
"Last-Translator: Renne Rocha <renne@rocha.dev.br>\n" "Last-Translator: Renne Rocha <renne@rocha.dev.br>\n"
"Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/" "Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/"
"pretix/pretix/pt_BR/>\n" "pretix/pretix/pt_BR/>\n"
@@ -19613,7 +19613,7 @@ msgstr "Excluir"
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_form.html:91 #: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_form.html:91
#: pretix/presale/templates/pretixpresale/fragment_event_list_filter.html:22 #: pretix/presale/templates/pretixpresale/fragment_event_list_filter.html:22
msgid "Filter" msgid "Filter"
msgstr "Filtrar" msgstr "Filtro"
#: pretix/control/templates/pretixcontrol/checkin/checkins.html:50 #: pretix/control/templates/pretixcontrol/checkin/checkins.html:50
msgid "Your search did not match any check-ins." msgid "Your search did not match any check-ins."
@@ -28003,9 +28003,6 @@ msgid ""
"According to your event settings, sold out products are hidden from " "According to your event settings, sold out products are hidden from "
"customers. This way, customers will not be able to discover the waiting list." "customers. This way, customers will not be able to discover the waiting list."
msgstr "" msgstr ""
"De acordo com as configurações do seu evento, os produtos esgotados ficam "
"ocultos para os clientes. Dessa forma, os clientes não poderão descobrir a "
"lista de espera."
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:38 #: pretix/control/templates/pretixcontrol/waitinglist/index.html:38
msgid "Send vouchers" msgid "Send vouchers"
@@ -28052,9 +28049,6 @@ msgid ""
"waiting list in, you could sell tickets worth an additional " "waiting list in, you could sell tickets worth an additional "
"<strong>%(amount)s</strong>." "<strong>%(amount)s</strong>."
msgstr "" msgstr ""
"Se você conseguir criar espaço suficiente em seu evento para acomodar todas "
"as pessoas na lista de espera, poderá vender ingressos no valor de um "
"adicional de <strong>%(amount)s</strong>."
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:115 #: pretix/control/templates/pretixcontrol/waitinglist/index.html:115
msgid "Successfully redeemed" msgid "Successfully redeemed"
+2 -3
View File
@@ -83,7 +83,7 @@ class AuthenticationForm(forms.Form):
self.request = request self.request = request
self.customer_cache = None self.customer_cache = None
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['password'].help_text = "<a target='_blank' href='{}'>{}</a>".format( self.fields['password'].help_text = "<a href='{}'>{}</a>".format(
build_absolute_uri(False, 'presale:organizer.customer.resetpw', kwargs={ build_absolute_uri(False, 'presale:organizer.customer.resetpw', kwargs={
'organizer': request.organizer.slug, 'organizer': request.organizer.slug,
}), }),
@@ -296,8 +296,7 @@ class SetPasswordForm(forms.Form):
} }
email = forms.EmailField( email = forms.EmailField(
label=_('Email'), label=_('Email'),
widget=forms.EmailInput(attrs={'autocomplete': 'username', 'readonly': 'readonly'}), disabled=True
required=False,
) )
password = forms.CharField( password = forms.CharField(
label=_('Password'), label=_('Password'),
+10 -26
View File
@@ -70,21 +70,18 @@ def cached_invoice_address(request):
# do not create a session, if we don't have a session we also don't have an invoice address ;) # do not create a session, if we don't have a session we also don't have an invoice address ;)
request._checkout_flow_invoice_address = InvoiceAddress() request._checkout_flow_invoice_address = InvoiceAddress()
return request._checkout_flow_invoice_address return request._checkout_flow_invoice_address
cs = cart_session(request, create=False) cs = cart_session(request)
if cs is None: iapk = cs.get('invoice_address')
if not iapk:
request._checkout_flow_invoice_address = InvoiceAddress() request._checkout_flow_invoice_address = InvoiceAddress()
else: else:
iapk = cs.get('invoice_address') try:
if not iapk: with scopes_disabled():
request._checkout_flow_invoice_address = InvoiceAddress.objects.get(
pk=iapk, order__isnull=True
)
except InvoiceAddress.DoesNotExist:
request._checkout_flow_invoice_address = InvoiceAddress() request._checkout_flow_invoice_address = InvoiceAddress()
else:
try:
with scopes_disabled():
request._checkout_flow_invoice_address = InvoiceAddress.objects.get(
pk=iapk, order__isnull=True
)
except InvoiceAddress.DoesNotExist:
request._checkout_flow_invoice_address = InvoiceAddress()
return request._checkout_flow_invoice_address return request._checkout_flow_invoice_address
@@ -114,12 +111,6 @@ class CartMixin:
return cached_invoice_address(self.request) return cached_invoice_address(self.request)
def get_cart(self, answers=False, queryset=None, order=None, downloads=False, payments=None): def get_cart(self, answers=False, queryset=None, order=None, downloads=False, payments=None):
if not self.request.session.session_key and not order:
# The user has not even a session ID yet, so they can't have a cart and we can save a lot of work
return {
'positions': [],
# Other keys are not used on non-checkout pages
}
if queryset is not None: if queryset is not None:
prefetch = [] prefetch = []
if answers: if answers:
@@ -175,8 +166,7 @@ class CartMixin:
else: else:
fees = [] fees = []
if not order and lcp: if not order:
# Do not re-round for empty cart (useless) or confirmed order (incorrect)
apply_rounding(self.request.event.settings.tax_rounding, self.invoice_address, self.request.event.currency, [*lcp, *fees]) apply_rounding(self.request.event.settings.tax_rounding, self.invoice_address, self.request.event.currency, [*lcp, *fees])
total = sum([c.price for c in lcp]) + sum([f.value for f in fees]) total = sum([c.price for c in lcp]) + sum([f.value for f in fees])
@@ -287,12 +277,6 @@ class CartMixin:
} }
def current_selected_payments(self, positions, fees, invoice_address, *, warn=False): def current_selected_payments(self, positions, fees, invoice_address, *, warn=False):
from pretix.presale.views.cart import get_or_create_cart_id
if not get_or_create_cart_id(self.request, create=False):
# No active cart ID, no payments there
return []
raw_payments = copy.deepcopy(self.cart_session.get('payments', [])) raw_payments = copy.deepcopy(self.cart_session.get('payments', []))
fees = [f for f in fees if f.fee_type != OrderFee.FEE_TYPE_PAYMENT] # we re-compute these here fees = [f for f in fees if f.fee_type != OrderFee.FEE_TYPE_PAYMENT] # we re-compute these here
+2 -4
View File
@@ -417,7 +417,7 @@ def get_or_create_cart_id(request, create=True):
return new_id return new_id
def cart_session(request, create=True): def cart_session(request):
""" """
Before pretix 1.8.0, all checkout-related information (like the entered email address) was stored Before pretix 1.8.0, all checkout-related information (like the entered email address) was stored
in the user's regular session dictionary. This led to data interference and leaks for example if a in the user's regular session dictionary. This led to data interference and leaks for example if a
@@ -428,9 +428,7 @@ def cart_session(request, create=True):
active cart session sub-dictionary for read and write access. active cart session sub-dictionary for read and write access.
""" """
request.session.modified = True request.session.modified = True
cart_id = get_or_create_cart_id(request, create=create) cart_id = get_or_create_cart_id(request)
if not cart_id and not create:
return None
return request.session['carts'][cart_id] return request.session['carts'][cart_id]
+2
View File
@@ -681,6 +681,8 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
context = {} context = {}
context['list_type'] = self.request.GET.get("style", self.request.event.settings.event_list_type) context['list_type'] = self.request.GET.get("style", self.request.event.settings.event_list_type)
if context['list_type'] not in ("calendar", "week") and self.request.event.subevents.filter(date_from__gt=time_machine_now()).count() > 50: if context['list_type'] not in ("calendar", "week") and self.request.event.subevents.filter(date_from__gt=time_machine_now()).count() > 50:
if self.request.event.settings.event_list_type not in ("calendar", "week"):
self.request.event.settings.event_list_type = "calendar"
context['list_type'] = "calendar" context['list_type'] = "calendar"
if context['list_type'] == "calendar": if context['list_type'] == "calendar":
+4 -9
View File
@@ -66,27 +66,22 @@ class WaitingView(EventViewMixin, FormView):
if customer else None if customer else None
), ),
) )
groups = {} choices = []
for i in items: for i in items:
if not i.allow_waitinglist: if not i.allow_waitinglist:
continue continue
category_name = str(i.category.name) if i.category else ''
group = groups.setdefault(category_name, [])
if i.has_variations: if i.has_variations:
for v in i.available_variations: for v in i.available_variations:
if v.cached_availability[0] == Quota.AVAILABILITY_OK: if v.cached_availability[0] == Quota.AVAILABILITY_OK:
continue continue
group.append((f'{i.pk}-{v.pk}', f'{i.name} – {v.value}')) choices.append((f'{i.pk}-{v.pk}', f'{i.name} – {v.value}'))
else: else:
if i.cached_availability[0] == Quota.AVAILABILITY_OK: if i.cached_availability[0] == Quota.AVAILABILITY_OK:
continue continue
group.append((f'{i.pk}', f'{i.name}')) choices.append((f'{i.pk}', f'{i.name}'))
return choices
# Remove categories where all items were available (no waiting list choices)
return [(cat, choices) for cat, choices in groups.items() if choices]
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
+3 -1
View File
@@ -530,10 +530,12 @@ class WidgetAPIProductList(EventListMixin, View):
] ]
if hasattr(self.request, 'event') and data['list_type'] not in ("calendar", "week"): if hasattr(self.request, 'event') and data['list_type'] not in ("calendar", "week"):
# only allow list-view of more than 50 subevents if ordering is by date as this can be done in the database # only allow list-view of more than 50 subevents if ordering is by data as this can be done in the database
# ordering by name is currently not supported in database due to I18NField-JSON # ordering by name is currently not supported in database due to I18NField-JSON
ordering = self.request.event.settings.get('frontpage_subevent_ordering', default='date_ascending', as_type=str) ordering = self.request.event.settings.get('frontpage_subevent_ordering', default='date_ascending', as_type=str)
if ordering not in ("date_ascending", "date_descending") and self.request.event.subevents.filter(date_from__gt=now()).count() > 50: if ordering not in ("date_ascending", "date_descending") and self.request.event.subevents.filter(date_from__gt=now()).count() > 50:
if self.request.event.settings.event_list_type not in ("calendar", "week"):
self.request.event.settings.event_list_type = "calendar"
data['list_type'] = list_type = 'calendar' data['list_type'] = list_type = 'calendar'
if hasattr(self.request, 'event'): if hasattr(self.request, 'event'):
+2 -19
View File
@@ -157,7 +157,7 @@ DATABASES = {
'HOST': config.get('database', 'host', fallback=''), 'HOST': config.get('database', 'host', fallback=''),
'PORT': config.get('database', 'port', fallback=''), 'PORT': config.get('database', 'port', fallback=''),
'CONN_MAX_AGE': 0 if db_backend == 'sqlite3' else 120, 'CONN_MAX_AGE': 0 if db_backend == 'sqlite3' else 120,
'CONN_HEALTH_CHECKS': db_backend != 'sqlite3', 'CONN_HEALTH_CHECKS': db_backend != 'sqlite3', # Will only be used from Django 4.1 onwards
'DISABLE_SERVER_SIDE_CURSORS': db_disable_server_side_cursors, 'DISABLE_SERVER_SIDE_CURSORS': db_disable_server_side_cursors,
'OPTIONS': db_options, 'OPTIONS': db_options,
'TEST': {} 'TEST': {}
@@ -179,21 +179,6 @@ if config.has_section('replica'):
} }
DATABASE_ROUTERS = ['pretix.helpers.database.ReplicaRouter'] DATABASE_ROUTERS = ['pretix.helpers.database.ReplicaRouter']
if config.has_section('dbreadonly'):
DATABASES['readonly'] = {
'ENGINE': 'django.db.backends.' + db_backend,
'NAME': config.get('dbreadonly', 'name', fallback=DATABASES['default']['NAME']),
'USER': config.get('dbreadonly', 'user', fallback=DATABASES['default']['USER']),
'PASSWORD': config.get('dbreadonly', 'password', fallback=DATABASES['default']['PASSWORD']),
'HOST': config.get('dbreadonly', 'host', fallback=DATABASES['default']['HOST']),
'PORT': config.get('dbreadonly', 'port', fallback=DATABASES['default']['PORT']),
'CONN_MAX_AGE': 0, # do not spam primary with open connections as long as readonly is only used occasionally
'CONN_HEALTH_CHECKS': db_backend != 'sqlite3',
'DISABLE_SERVER_SIDE_CURSORS': db_disable_server_side_cursors,
'OPTIONS': db_options,
'TEST': {}
}
STATIC_URL = config.get('urls', 'static', fallback='/static/') STATIC_URL = config.get('urls', 'static', fallback='/static/')
MEDIA_URL = config.get('urls', 'media', fallback='/media/') MEDIA_URL = config.get('urls', 'media', fallback='/media/')
@@ -223,7 +208,6 @@ CSRF_TRUSTED_ORIGINS = [urlparse(SITE_URL).scheme + '://' + urlparse(SITE_URL).h
TRUST_X_FORWARDED_FOR = config.getboolean('pretix', 'trust_x_forwarded_for', fallback=False) TRUST_X_FORWARDED_FOR = config.getboolean('pretix', 'trust_x_forwarded_for', fallback=False)
USE_X_FORWARDED_HOST = config.getboolean('pretix', 'trust_x_forwarded_host', fallback=False) USE_X_FORWARDED_HOST = config.getboolean('pretix', 'trust_x_forwarded_host', fallback=False)
ALLOW_HTTP_TO_PRIVATE_NETWORKS = config.getboolean('pretix', 'allow_http_to_private_networks', fallback=False)
REQUEST_ID_HEADER = config.get('pretix', 'request_id_header', fallback=False) REQUEST_ID_HEADER = config.get('pretix', 'request_id_header', fallback=False)
@@ -264,8 +248,7 @@ EMAIL_HOST_PASSWORD = config.get('mail', 'password', fallback='')
EMAIL_USE_TLS = config.getboolean('mail', 'tls', fallback=False) EMAIL_USE_TLS = config.getboolean('mail', 'tls', fallback=False)
EMAIL_USE_SSL = config.getboolean('mail', 'ssl', fallback=False) EMAIL_USE_SSL = config.getboolean('mail', 'ssl', fallback=False)
EMAIL_SUBJECT_PREFIX = '[pretix] ' EMAIL_SUBJECT_PREFIX = '[pretix] '
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_BACKEND = EMAIL_CUSTOM_SMTP_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_CUSTOM_SMTP_BACKEND = 'pretixbase.email.CheckPrivateNetworkSmtpBackend'
EMAIL_TIMEOUT = 60 EMAIL_TIMEOUT = 60
ADMINS = [('Admin', n) for n in config.get('mail', 'admins', fallback='').split(",") if n] ADMINS = [('Admin', n) for n in config.get('mail', 'admins', fallback='').split(",") if n]
+13 -12
View File
@@ -1835,9 +1835,10 @@
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.13", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
@@ -2878,9 +2879,9 @@
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.2", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"engines": { "engines": {
"node": ">=8.6" "node": ">=8.6"
}, },
@@ -4935,9 +4936,9 @@
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.13", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"optional": true, "optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
@@ -5714,9 +5715,9 @@
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
}, },
"picomatch": { "picomatch": {
"version": "2.3.2", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==" "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
}, },
"pify": { "pify": {
"version": "4.0.1", "version": "4.0.1",
-3
View File
@@ -94,9 +94,6 @@ class DisableMigrations(object):
def __getitem__(self, item): def __getitem__(self, item):
return None return None
def setdefault(self, key, default=None):
return
if not os.environ.get("GITHUB_WORKFLOW", ""): if not os.environ.get("GITHUB_WORKFLOW", ""):
MIGRATION_MODULES = DisableMigrations() MIGRATION_MODULES = DisableMigrations()
-29
View File
@@ -171,35 +171,6 @@ def test_giftcard_detail_expand(token_client, organizer, event, giftcard):
} }
@pytest.mark.django_db
def test_giftcard_detail_expand_without_permissions(team, token_client, organizer, event, giftcard):
with scopes_disabled():
o = Order.objects.create(
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10),
sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=14, locale='en'
)
ticket = event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True,
personalized=True)
op = o.positions.create(item=ticket, price=Decimal("14"))
giftcard.owner_ticket = op
giftcard.save()
team.all_event_permissions = False
team.save()
res = dict(TEST_GC_RES)
res["id"] = giftcard.pk
res["issuance"] = giftcard.issuance.isoformat().replace('+00:00', 'Z')
resp = token_client.get('/api/v1/organizers/{}/giftcards/{}/?expand=owner_ticket'.format(organizer.slug, giftcard.pk))
assert resp.status_code == 200
assert resp.data["owner_ticket"] == {
"id": op.pk,
}
TEST_GIFTCARD_CREATE_PAYLOAD = { TEST_GIFTCARD_CREATE_PAYLOAD = {
"secret": "DEFABC", "secret": "DEFABC",
"value": "12.00", "value": "12.00",
+2 -2
View File
@@ -2053,7 +2053,7 @@ def test_pdf_data(token_client, organizer, event, order, django_assert_max_num_q
assert not resp.data['positions'][0].get('pdf_data') assert not resp.data['positions'][0].get('pdf_data')
# order list # order list
with django_assert_max_num_queries(34): with django_assert_max_num_queries(33):
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.format( resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.format(
organizer.slug, event.slug organizer.slug, event.slug
)) ))
@@ -2068,7 +2068,7 @@ def test_pdf_data(token_client, organizer, event, order, django_assert_max_num_q
assert not resp.data['results'][0]['positions'][0].get('pdf_data') assert not resp.data['results'][0]['positions'][0].get('pdf_data')
# position list # position list
with django_assert_max_num_queries(36): with django_assert_max_num_queries(35):
resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/?pdf_data=true'.format( resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/?pdf_data=true'.format(
organizer.slug, event.slug organizer.slug, event.slug
)) ))
-70
View File
@@ -252,76 +252,6 @@ def test_medium_detail(token_client, organizer, event, medium, giftcard, custome
} }
@pytest.mark.django_db
def test_medium_detail_event_permission_missing(token_client, organizer, event, medium, giftcard, customer, team):
team.all_organizer_permissions = False
team.limit_organizer_permissions = {
"organizer.reusablemedia:read": True,
"organizer.customers:read": True,
"organizer.giftcards:read": True,
}
team.all_event_permissions = False
team.save()
with scopes_disabled():
o = Order.objects.create(
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10),
sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=14, locale='en'
)
ticket = event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True,
personalized=True)
op = o.positions.create(item=ticket, price=Decimal("14"))
medium.linked_orderposition = op
medium.linked_giftcard = giftcard
medium.customer = customer
medium.save()
giftcard.owner_ticket = op
giftcard.save()
resp = token_client.get(
'/api/v1/organizers/{}/reusablemedia/{}/?expand=linked_giftcard&expand='
'linked_giftcard.owner_ticket&expand=linked_orderposition&expand=customer'.format(
organizer.slug, medium.pk
)
)
assert resp.status_code == 200
assert resp.data["linked_orderposition"] == {
"id": op.pk,
}
assert resp.data["linked_giftcard"] == {
"id": giftcard.pk,
"secret": "ABCDEF",
"issuance": giftcard.issuance.isoformat().replace("+00:00", "Z"),
"value": "23.00",
"currency": "EUR",
"testmode": False,
"expires": None,
"conditions": None,
"owner_ticket": {"id": op.pk},
"issuer": "dummy",
}
assert resp.data["customer"] == {
"identifier": customer.identifier,
"external_identifier": None,
"email": "foo@example.org",
"phone": None,
"name": "Foo",
"name_parts": {"_legacy": "Foo"},
"is_active": True,
"is_verified": False,
"last_login": None,
"date_joined": customer.date_joined.isoformat().replace("+00:00", "Z"),
"locale": "en",
"last_modified": customer.last_modified.isoformat().replace("+00:00", "Z"),
"notes": None
}
TEST_MEDIUM_CREATE_PAYLOAD = { TEST_MEDIUM_CREATE_PAYLOAD = {
"type": "barcode", "type": "barcode",
"identifier": "FOOBAR", "identifier": "FOOBAR",
-117
View File
@@ -35,11 +35,8 @@
import datetime import datetime
import os import os
import re import re
import socket
from contextlib import contextmanager
from decimal import Decimal from decimal import Decimal
from email.mime.text import MIMEText from email.mime.text import MIMEText
from unittest import mock
import pytest import pytest
from django.conf import settings from django.conf import settings
@@ -594,117 +591,3 @@ def test_attached_ical_localization(env, order):
assert len(djmail.outbox) == 1 assert len(djmail.outbox) == 1
assert len(djmail.outbox[0].attachments) == 1 assert len(djmail.outbox[0].attachments) == 1
assert description in djmail.outbox[0].attachments[0][1] assert description in djmail.outbox[0].attachments[0][1]
PRIVATE_IPS_RES = [
[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('10.0.0.3', 443))],
[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('0.0.0.0', 443))],
[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('127.1.1.1', 443))],
[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('192.168.5.3', 443))],
[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('224.0.0.1', 443))],
[(socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('::1', 443, 0, 0))],
[(socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('fe80::1', 443, 0, 0))],
[(socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('ff00::1', 443, 0, 0))],
[(socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('fc00::1', 443, 0, 0))],
]
@contextmanager
def assert_mail_connection(res, should_connect, use_ssl):
with (
mock.patch('socket.socket') as mock_socket,
mock.patch('socket.getaddrinfo', return_value=res),
mock.patch('smtplib.SMTP.getreply', return_value=(220, "")),
mock.patch('smtplib.SMTP.sendmail'),
mock.patch('ssl.SSLContext.wrap_socket') as mock_ssl
):
yield
if should_connect:
mock_socket.assert_called_once()
mock_socket.return_value.connect.assert_called_once_with(res[0][-1])
if use_ssl:
mock_ssl.assert_called_once()
else:
mock_socket.assert_not_called()
mock_socket.return_value.connect.assert_not_called()
mock_ssl.assert_not_called()
@pytest.mark.parametrize("res", PRIVATE_IPS_RES)
@pytest.mark.parametrize("use_ssl", [
True, False
])
def test_private_smtp_ip(res, use_ssl, settings):
settings.EMAIL_CUSTOM_SMTP_BACKEND = 'pretix.base.email.CheckPrivateNetworkSmtpBackend'
settings.MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = False
with assert_mail_connection(res=res, should_connect=False, use_ssl=use_ssl), pytest.raises(match="Request to .* blocked"):
connection = djmail.get_connection(backend=settings.EMAIL_CUSTOM_SMTP_BACKEND,
host="localhost",
use_ssl=use_ssl)
connection.open()
settings.MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = True
with assert_mail_connection(res=res, should_connect=True, use_ssl=use_ssl):
connection = djmail.get_connection(backend=settings.EMAIL_CUSTOM_SMTP_BACKEND,
host="localhost",
use_ssl=use_ssl)
connection.open()
@pytest.mark.parametrize("use_ssl", [
True, False
])
@pytest.mark.parametrize("allow_private", [
True, False
])
def test_public_smtp_ip(use_ssl, allow_private, settings):
settings.EMAIL_CUSTOM_SMTP_BACKEND = 'pretix.base.email.CheckPrivateNetworkSmtpBackend'
settings.MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = allow_private
with assert_mail_connection(res=[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('8.8.8.8', 443))], should_connect=True, use_ssl=use_ssl):
connection = djmail.get_connection(backend=settings.EMAIL_CUSTOM_SMTP_BACKEND,
host="localhost",
use_ssl=use_ssl)
connection.open()
@pytest.mark.django_db
@pytest.mark.parametrize("use_ssl", [
True, False
])
@pytest.mark.parametrize("allow_private_networks", [
True, False
])
@pytest.mark.parametrize("res", PRIVATE_IPS_RES)
def test_send_mail_private_ip(res, use_ssl, allow_private_networks, env):
settings.EMAIL_CUSTOM_SMTP_BACKEND = 'pretix.base.email.CheckPrivateNetworkSmtpBackend'
settings.MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = allow_private_networks
event, user, organizer = env
event.settings.smtp_use_custom = True
event.settings.smtp_host = "example.com"
event.settings.smtp_use_ssl = use_ssl
event.settings.smtp_use_tls = False
def send_mail():
m = OutgoingMail.objects.create(
to=['recipient@example.com'],
subject='Test',
body_plain='Test',
sender='sender@example.com',
event=event
)
assert m.status == OutgoingMail.STATUS_QUEUED
mail_send_task.apply(kwargs={
'outgoing_mail': m.pk,
}, max_retries=0)
m.refresh_from_db()
return m
with assert_mail_connection(res=res, should_connect=allow_private_networks, use_ssl=use_ssl):
m = send_mail()
if allow_private_networks:
assert m.status == OutgoingMail.STATUS_SENT
else:
assert m.status == OutgoingMail.STATUS_FAILED
-27
View File
@@ -991,30 +991,3 @@ def test_import_mixed_order_size_consistency(user, event, item):
).get() ).get()
assert ('Inconsistent data in row 2: Column Email address contains value "a2@example.com", but for this order, ' assert ('Inconsistent data in row 2: Column Email address contains value "a2@example.com", but for this order, '
'the value has already been set to "a1@example.com".') in str(excinfo.value) 'the value has already been set to "a1@example.com".') in str(excinfo.value)
@pytest.mark.django_db
@scopes_disabled()
def test_import_line_endings_mix(event, item, user):
# Ensures import works with mixed file endings.
# See Ticket#23230806 where a file to import ends with \r\n
settings = dict(DEFAULT_SETTINGS)
settings['item'] = 'static:{}'.format(item.pk)
cf = inputfile_factory()
file = cf.file
file.seek(0)
data = file.read()
data = data.replace(b'\n', b'\r')
data = data.rstrip(b'\r\r')
data = data + b'\r\n'
print(data)
cf.file.save("input.csv", ContentFile(data))
cf.save()
import_orders.apply(
args=(event.pk, cf.id, settings, 'en', user.pk)
)
assert event.orders.count() == 3
assert OrderPosition.objects.count() == 3
+18 -12
View File
@@ -24,6 +24,7 @@ from decimal import Decimal
import pytest import pytest
from django.core import mail as djmail from django.core import mail as djmail
from django.db import transaction
from django.utils.timezone import now from django.utils.timezone import now
from django_scopes import scope from django_scopes import scope
@@ -74,42 +75,47 @@ def user(team):
return user return user
@pytest.fixture
def monkeypatch_on_commit(monkeypatch):
monkeypatch.setattr("django.db.transaction.on_commit", lambda t: t())
@pytest.mark.django_db @pytest.mark.django_db
def test_notification_trigger_event_specific(event, order, user, django_capture_on_commit_callbacks): def test_notification_trigger_event_specific(event, order, user, monkeypatch_on_commit):
djmail.outbox = [] djmail.outbox = []
user.notification_settings.create( user.notification_settings.create(
method='mail', event=event, action_type='pretix.event.order.paid', enabled=True method='mail', event=event, action_type='pretix.event.order.paid', enabled=True
) )
with django_capture_on_commit_callbacks(execute=True): with transaction.atomic():
order.log_action('pretix.event.order.paid', {}) order.log_action('pretix.event.order.paid', {})
assert len(djmail.outbox) == 1 assert len(djmail.outbox) == 1
assert djmail.outbox[0].subject.endswith("DUMMY: Order FOO has been marked as paid.") assert djmail.outbox[0].subject.endswith("DUMMY: Order FOO has been marked as paid.")
@pytest.mark.django_db @pytest.mark.django_db
def test_notification_trigger_global(event, order, user, django_capture_on_commit_callbacks): def test_notification_trigger_global(event, order, user, monkeypatch_on_commit):
djmail.outbox = [] djmail.outbox = []
user.notification_settings.create( user.notification_settings.create(
method='mail', event=None, action_type='pretix.event.order.paid', enabled=True method='mail', event=None, action_type='pretix.event.order.paid', enabled=True
) )
with django_capture_on_commit_callbacks(execute=True): with transaction.atomic():
order.log_action('pretix.event.order.paid', {}) order.log_action('pretix.event.order.paid', {})
assert len(djmail.outbox) == 1 assert len(djmail.outbox) == 1
@pytest.mark.django_db @pytest.mark.django_db
def test_notification_trigger_global_wildcard(event, order, user, django_capture_on_commit_callbacks): def test_notification_trigger_global_wildcard(event, order, user, monkeypatch_on_commit):
djmail.outbox = [] djmail.outbox = []
user.notification_settings.create( user.notification_settings.create(
method='mail', event=None, action_type='pretix.event.order.changed.*', enabled=True method='mail', event=None, action_type='pretix.event.order.changed.*', enabled=True
) )
with django_capture_on_commit_callbacks(execute=True): with transaction.atomic():
order.log_action('pretix.event.order.changed.item', {}) order.log_action('pretix.event.order.changed.item', {})
assert len(djmail.outbox) == 1 assert len(djmail.outbox) == 1
@pytest.mark.django_db @pytest.mark.django_db
def test_notification_enabled_global_ignored_specific(event, order, user, django_capture_on_commit_callbacks): def test_notification_enabled_global_ignored_specific(event, order, user, monkeypatch_on_commit):
djmail.outbox = [] djmail.outbox = []
user.notification_settings.create( user.notification_settings.create(
method='mail', event=None, action_type='pretix.event.order.paid', enabled=True method='mail', event=None, action_type='pretix.event.order.paid', enabled=True
@@ -117,24 +123,24 @@ def test_notification_enabled_global_ignored_specific(event, order, user, django
user.notification_settings.create( user.notification_settings.create(
method='mail', event=event, action_type='pretix.event.order.paid', enabled=False method='mail', event=event, action_type='pretix.event.order.paid', enabled=False
) )
with django_capture_on_commit_callbacks(execute=True): with transaction.atomic():
order.log_action('pretix.event.order.paid', {}) order.log_action('pretix.event.order.paid', {})
assert len(djmail.outbox) == 0 assert len(djmail.outbox) == 0
@pytest.mark.django_db @pytest.mark.django_db
def test_notification_ignore_same_user(event, order, user, django_capture_on_commit_callbacks): def test_notification_ignore_same_user(event, order, user, monkeypatch_on_commit):
djmail.outbox = [] djmail.outbox = []
user.notification_settings.create( user.notification_settings.create(
method='mail', event=event, action_type='pretix.event.order.paid', enabled=True method='mail', event=event, action_type='pretix.event.order.paid', enabled=True
) )
with django_capture_on_commit_callbacks(execute=True): with transaction.atomic():
order.log_action('pretix.event.order.paid', {}, user=user) order.log_action('pretix.event.order.paid', {}, user=user)
assert len(djmail.outbox) == 0 assert len(djmail.outbox) == 0
@pytest.mark.django_db @pytest.mark.django_db
def test_notification_ignore_insufficient_permissions(event, order, user, team, django_capture_on_commit_callbacks): def test_notification_ignore_insufficient_permissions(event, order, user, team, monkeypatch_on_commit):
djmail.outbox = [] djmail.outbox = []
team.all_event_permissions = False team.all_event_permissions = False
team.limit_event_permissions = {"event.vouchers:read": True} team.limit_event_permissions = {"event.vouchers:read": True}
@@ -142,7 +148,7 @@ def test_notification_ignore_insufficient_permissions(event, order, user, team,
user.notification_settings.create( user.notification_settings.create(
method='mail', event=event, action_type='pretix.event.order.paid', enabled=True method='mail', event=event, action_type='pretix.event.order.paid', enabled=True
) )
with django_capture_on_commit_callbacks(execute=True): with transaction.atomic():
order.log_action('pretix.event.order.paid', {}) order.log_action('pretix.event.order.paid', {})
assert len(djmail.outbox) == 0 assert len(djmail.outbox) == 0
+26 -23
View File
@@ -28,9 +28,8 @@ from zoneinfo import ZoneInfo
import pytest import pytest
from django.conf import settings from django.conf import settings
from django.core import mail as djmail from django.core import mail as djmail
from django.db import transaction
from django.db.models import F, Sum from django.db.models import F, Sum
from django.test import TestCase, TransactionTestCase, override_settings from django.test import TestCase, override_settings
from django.utils.timezone import make_aware, now from django.utils.timezone import make_aware, now
from django_countries.fields import Country from django_countries.fields import Country
from django_scopes import scope from django_scopes import scope
@@ -1226,6 +1225,12 @@ class DownloadReminderTests(TestCase):
assert len(djmail.outbox) == 0 assert len(djmail.outbox) == 0
@pytest.fixture
def class_monkeypatch(request, monkeypatch):
request.cls.monkeypatch = monkeypatch
@pytest.mark.usefixtures("class_monkeypatch")
class OrderCancelTests(TestCase): class OrderCancelTests(TestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
@@ -1253,6 +1258,7 @@ class OrderCancelTests(TestCase):
self.order.create_transactions() self.order.create_transactions()
generate_invoice(self.order) generate_invoice(self.order)
djmail.outbox = [] djmail.outbox = []
self.monkeypatch.setattr("django.db.transaction.on_commit", lambda t: t())
@classscope(attr='o') @classscope(attr='o')
def test_cancel_canceled(self): def test_cancel_canceled(self):
@@ -1345,14 +1351,14 @@ class OrderCancelTests(TestCase):
self.order.status = Order.STATUS_PAID self.order.status = Order.STATUS_PAID
self.order.save() self.order.save()
djmail.outbox = [] djmail.outbox = []
with self.captureOnCommitCallbacks(execute=True): cancel_order(self.order.pk, send_mail=True)
cancel_order(self.order.pk, send_mail=True) print([s.subject for s in djmail.outbox])
print([s.to for s in djmail.outbox])
assert len(djmail.outbox) == 2 assert len(djmail.outbox) == 2
assert ["dummy@dummy.test"] == djmail.outbox[0].to assert ["invoice@example.org"] == djmail.outbox[0].to
assert not any(["Invoice_" in a[0] for a in djmail.outbox[0].attachments]) assert any(["Invoice_" in a[0] for a in djmail.outbox[0].attachments])
assert ["invoice@example.org"] == djmail.outbox[1].to assert ["dummy@dummy.test"] == djmail.outbox[1].to
assert any(["Invoice_" in a[0] for a in djmail.outbox[1].attachments]) assert not any(["Invoice_" in a[0] for a in djmail.outbox[1].attachments])
@classscope(attr='o') @classscope(attr='o')
def test_cancel_paid_with_too_high_fee(self): def test_cancel_paid_with_too_high_fee(self):
@@ -1482,7 +1488,8 @@ class OrderCancelTests(TestCase):
assert self.order.all_logentries().filter(action_type='pretix.event.order.refund.requested').exists() assert self.order.all_logentries().filter(action_type='pretix.event.order.refund.requested').exists()
class BaseOrderChangeManagerTestCase: @pytest.mark.usefixtures("class_monkeypatch")
class OrderChangeManagerTests(TestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.o = Organizer.objects.create(name='Dummy', slug='dummy', plugins='pretix.plugins.banktransfer') self.o = Organizer.objects.create(name='Dummy', slug='dummy', plugins='pretix.plugins.banktransfer')
@@ -1545,6 +1552,7 @@ class BaseOrderChangeManagerTestCase:
self.seat_a1 = self.event.seats.create(seat_number="A1", product=self.stalls, seat_guid="A1") self.seat_a1 = self.event.seats.create(seat_number="A1", product=self.stalls, seat_guid="A1")
self.seat_a2 = self.event.seats.create(seat_number="A2", product=self.stalls, seat_guid="A2") self.seat_a2 = self.event.seats.create(seat_number="A2", product=self.stalls, seat_guid="A2")
self.seat_a3 = self.event.seats.create(seat_number="A3", product=self.stalls, seat_guid="A3") self.seat_a3 = self.event.seats.create(seat_number="A3", product=self.stalls, seat_guid="A3")
self.monkeypatch.setattr("django.db.transaction.on_commit", lambda t: t())
def _enable_reverse_charge(self): def _enable_reverse_charge(self):
self.tr7.eu_reverse_charge = True self.tr7.eu_reverse_charge = True
@@ -1558,8 +1566,6 @@ class BaseOrderChangeManagerTestCase:
country=Country('AT') country=Country('AT')
) )
class OrderChangeManagerTests(BaseOrderChangeManagerTestCase, TestCase):
@classscope(attr='o') @classscope(attr='o')
def test_multiple_commits_forbidden(self): def test_multiple_commits_forbidden(self):
self.ocm.change_price(self.op1, Decimal('10.00')) self.ocm.change_price(self.op1, Decimal('10.00'))
@@ -3898,16 +3904,15 @@ class OrderChangeManagerTests(BaseOrderChangeManagerTestCase, TestCase):
@classscope(attr='o') @classscope(attr='o')
def test_set_valid_until(self): def test_set_valid_until(self):
with transaction.atomic(): self.event.settings.ticket_secret_generator = "pretix_sig1"
self.event.settings.ticket_secret_generator = "pretix_sig1" assign_ticket_secret(self.event, self.op1, force_invalidate=True, save=True)
assign_ticket_secret(self.event, self.op1, force_invalidate=True, save=True) old_secret = self.op1.secret
old_secret = self.op1.secret
dt = make_aware(datetime(2022, 9, 20, 15, 0, 0, 0)) dt = make_aware(datetime(2022, 9, 20, 15, 0, 0, 0))
self.ocm.change_valid_until(self.op1, dt) self.ocm.change_valid_until(self.op1, dt)
self.ocm.commit() self.ocm.commit()
self.op1.refresh_from_db() self.op1.refresh_from_db()
assert self.op1.secret != old_secret assert self.op1.secret != old_secret
@classscope(attr='o') @classscope(attr='o')
def test_unset_valid_from_until(self): def test_unset_valid_from_until(self):
@@ -3932,8 +3937,6 @@ class OrderChangeManagerTests(BaseOrderChangeManagerTestCase, TestCase):
assert len(djmail.outbox) == 1 assert len(djmail.outbox) == 1
assert len(["Invoice_" in a[0] for a in djmail.outbox[0].attachments]) == 2 assert len(["Invoice_" in a[0] for a in djmail.outbox[0].attachments]) == 2
class OrderChangeManagerTransactionalTests(BaseOrderChangeManagerTestCase, TransactionTestCase):
@classscope(attr='o') @classscope(attr='o')
def test_new_invoice_send_somewhere_else(self): def test_new_invoice_send_somewhere_else(self):
generate_invoice(self.order) generate_invoice(self.order)
+22 -16
View File
@@ -25,6 +25,7 @@ from decimal import Decimal
import pytest import pytest
import responses import responses
from django.db import transaction
from django.utils.timezone import now from django.utils.timezone import now
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
@@ -81,9 +82,14 @@ def force_str(v):
return v.decode() if isinstance(v, bytes) else str(v) return v.decode() if isinstance(v, bytes) else str(v)
@pytest.fixture
def monkeypatch_on_commit(monkeypatch):
monkeypatch.setattr("django.db.transaction.on_commit", lambda t: t())
@pytest.mark.django_db @pytest.mark.django_db
@responses.activate @responses.activate
def test_webhook_trigger_event_specific(event, order, webhook, django_capture_on_commit_callbacks): def test_webhook_trigger_event_specific(event, order, webhook, monkeypatch_on_commit):
responses.add_callback( responses.add_callback(
responses.POST, 'https://google.com', responses.POST, 'https://google.com',
callback=lambda r: (200, {}, 'ok'), callback=lambda r: (200, {}, 'ok'),
@@ -91,7 +97,7 @@ def test_webhook_trigger_event_specific(event, order, webhook, django_capture_on
match_querystring=None, # https://github.com/getsentry/responses/issues/464 match_querystring=None, # https://github.com/getsentry/responses/issues/464
) )
with django_capture_on_commit_callbacks(execute=True): with transaction.atomic():
le = order.log_action('pretix.event.order.paid', {}) le = order.log_action('pretix.event.order.paid', {})
assert len(responses.calls) == 1 assert len(responses.calls) == 1
assert json.loads(force_str(responses.calls[0].request.body)) == { assert json.loads(force_str(responses.calls[0].request.body)) == {
@@ -113,12 +119,12 @@ def test_webhook_trigger_event_specific(event, order, webhook, django_capture_on
@pytest.mark.django_db @pytest.mark.django_db
@responses.activate @responses.activate
def test_webhook_trigger_global(event, order, webhook, django_capture_on_commit_callbacks): def test_webhook_trigger_global(event, order, webhook, monkeypatch_on_commit):
webhook.limit_events.clear() webhook.limit_events.clear()
webhook.all_events = True webhook.all_events = True
webhook.save() webhook.save()
responses.add(responses.POST, 'https://google.com', status=200) responses.add(responses.POST, 'https://google.com', status=200)
with django_capture_on_commit_callbacks(execute=True): with transaction.atomic():
le = order.log_action('pretix.event.order.paid', {}) le = order.log_action('pretix.event.order.paid', {})
assert len(responses.calls) == 1 assert len(responses.calls) == 1
assert json.loads(force_str(responses.calls[0].request.body)) == { assert json.loads(force_str(responses.calls[0].request.body)) == {
@@ -132,13 +138,13 @@ def test_webhook_trigger_global(event, order, webhook, django_capture_on_commit_
@pytest.mark.django_db @pytest.mark.django_db
@responses.activate @responses.activate
def test_webhook_trigger_global_wildcard(event, order, webhook, django_capture_on_commit_callbacks): def test_webhook_trigger_global_wildcard(event, order, webhook, monkeypatch_on_commit):
webhook.listeners.create(action_type="pretix.event.order.changed.*") webhook.listeners.create(action_type="pretix.event.order.changed.*")
webhook.limit_events.clear() webhook.limit_events.clear()
webhook.all_events = True webhook.all_events = True
webhook.save() webhook.save()
responses.add(responses.POST, 'https://google.com', status=200) responses.add(responses.POST, 'https://google.com', status=200)
with django_capture_on_commit_callbacks(execute=True): with transaction.atomic():
le = order.log_action('pretix.event.order.changed.item', {}) le = order.log_action('pretix.event.order.changed.item', {})
assert len(responses.calls) == 1 assert len(responses.calls) == 1
assert json.loads(force_str(responses.calls[0].request.body)) == { assert json.loads(force_str(responses.calls[0].request.body)) == {
@@ -152,30 +158,30 @@ def test_webhook_trigger_global_wildcard(event, order, webhook, django_capture_o
@pytest.mark.django_db @pytest.mark.django_db
@responses.activate @responses.activate
def test_webhook_ignore_wrong_action_type(event, order, webhook, django_capture_on_commit_callbacks): def test_webhook_ignore_wrong_action_type(event, order, webhook, monkeypatch_on_commit):
responses.add(responses.POST, 'https://google.com', status=200) responses.add(responses.POST, 'https://google.com', status=200)
with django_capture_on_commit_callbacks(execute=True): with transaction.atomic():
order.log_action('pretix.event.order.changed.item', {}) order.log_action('pretix.event.order.changed.item', {})
assert len(responses.calls) == 0 assert len(responses.calls) == 0
@pytest.mark.django_db @pytest.mark.django_db
@responses.activate @responses.activate
def test_webhook_ignore_disabled(event, order, webhook, django_capture_on_commit_callbacks): def test_webhook_ignore_disabled(event, order, webhook, monkeypatch_on_commit):
webhook.enabled = False webhook.enabled = False
webhook.save() webhook.save()
responses.add(responses.POST, 'https://google.com', status=200) responses.add(responses.POST, 'https://google.com', status=200)
with django_capture_on_commit_callbacks(execute=True): with transaction.atomic():
order.log_action('pretix.event.order.changed.item', {}) order.log_action('pretix.event.order.changed.item', {})
assert len(responses.calls) == 0 assert len(responses.calls) == 0
@pytest.mark.django_db @pytest.mark.django_db
@responses.activate @responses.activate
def test_webhook_ignore_wrong_event(event, order, webhook, django_capture_on_commit_callbacks): def test_webhook_ignore_wrong_event(event, order, webhook, monkeypatch_on_commit):
webhook.limit_events.clear() webhook.limit_events.clear()
responses.add(responses.POST, 'https://google.com', status=200) responses.add(responses.POST, 'https://google.com', status=200)
with django_capture_on_commit_callbacks(execute=True): with transaction.atomic():
order.log_action('pretix.event.order.changed.item', {}) order.log_action('pretix.event.order.changed.item', {})
assert len(responses.calls) == 0 assert len(responses.calls) == 0
@@ -183,10 +189,10 @@ def test_webhook_ignore_wrong_event(event, order, webhook, django_capture_on_com
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.xfail(reason="retries can't be tested with celery_always_eager") @pytest.mark.xfail(reason="retries can't be tested with celery_always_eager")
@responses.activate @responses.activate
def test_webhook_retry(event, order, webhook, django_capture_on_commit_callbacks): def test_webhook_retry(event, order, webhook, monkeypatch_on_commit):
responses.add(responses.POST, 'https://google.com', status=500) responses.add(responses.POST, 'https://google.com', status=500)
responses.add(responses.POST, 'https://google.com', status=200) responses.add(responses.POST, 'https://google.com', status=200)
with django_capture_on_commit_callbacks(execute=True): with transaction.atomic():
order.log_action('pretix.event.order.paid', {}) order.log_action('pretix.event.order.paid', {})
assert len(responses.calls) == 2 assert len(responses.calls) == 2
with scopes_disabled(): with scopes_disabled():
@@ -210,9 +216,9 @@ def test_webhook_retry(event, order, webhook, django_capture_on_commit_callbacks
@pytest.mark.django_db @pytest.mark.django_db
@responses.activate @responses.activate
def test_webhook_disable_gone(event, order, webhook, django_capture_on_commit_callbacks): def test_webhook_disable_gone(event, order, webhook, monkeypatch_on_commit):
responses.add(responses.POST, 'https://google.com', status=410) responses.add(responses.POST, 'https://google.com', status=410)
with django_capture_on_commit_callbacks(execute=True): with transaction.atomic():
order.log_action('pretix.event.order.paid', {}) order.log_action('pretix.event.order.paid', {})
assert len(responses.calls) == 1 assert len(responses.calls) == 1
webhook.refresh_from_db() webhook.refresh_from_db()
-5
View File
@@ -131,8 +131,3 @@ def set_lock_namespaces(request):
yield yield
else: else:
yield yield
@pytest.fixture
def class_monkeypatch(request, monkeypatch):
request.cls.monkeypatch = monkeypatch
+5
View File
@@ -385,6 +385,11 @@ class RegistrationFormTest(TestCase):
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
@pytest.fixture
def class_monkeypatch(request, monkeypatch):
request.cls.monkeypatch = monkeypatch
@pytest.mark.usefixtures("class_monkeypatch") @pytest.mark.usefixtures("class_monkeypatch")
class Login2FAFormTest(TestCase): class Login2FAFormTest(TestCase):
+5
View File
@@ -49,6 +49,11 @@ from tests.base import SoupTest, extract_form_fields
from pretix.base.models import Event, LogEntry, Order, Organizer, Team, User from pretix.base.models import Event, LogEntry, Order, Organizer, Team, User
@pytest.fixture
def class_monkeypatch(request, monkeypatch):
request.cls.monkeypatch = monkeypatch
@pytest.mark.usefixtures("class_monkeypatch") @pytest.mark.usefixtures("class_monkeypatch")
class EventsTest(SoupTest): class EventsTest(SoupTest):
@scopes_disabled() @scopes_disabled()
+5
View File
@@ -33,6 +33,11 @@ from tests.base import SoupTest, extract_form_fields
from pretix.base.models import Event, Organizer, OutgoingMail, Team, User from pretix.base.models import Event, Organizer, OutgoingMail, Team, User
@pytest.fixture
def class_monkeypatch(request, monkeypatch):
request.cls.monkeypatch = monkeypatch
@pytest.mark.usefixtures("class_monkeypatch") @pytest.mark.usefixtures("class_monkeypatch")
class OrganizerTest(SoupTest): class OrganizerTest(SoupTest):
@scopes_disabled() @scopes_disabled()
+5
View File
@@ -286,6 +286,11 @@ class UserPasswordChangeTest(SoupTest):
assert self.user.needs_password_change is False assert self.user.needs_password_change is False
@pytest.fixture
def class_monkeypatch(request, monkeypatch):
request.cls.monkeypatch = monkeypatch
@pytest.mark.usefixtures("class_monkeypatch") @pytest.mark.usefixtures("class_monkeypatch")
class UserSettings2FATest(SoupTest): class UserSettings2FATest(SoupTest):
def setUp(self): def setUp(self):
-93
View File
@@ -1,93 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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/>.
#
from socket import AF_INET, SOCK_STREAM
from unittest import mock
import pytest
import requests
from django.test import override_settings
from dns.inet import AF_INET6
from urllib3.exceptions import HTTPError
def test_local_blocked():
with pytest.raises(HTTPError, match="Request to local address.*"):
requests.get("http://localhost", timeout=0.1)
with pytest.raises(HTTPError, match="Request to local address.*"):
requests.get("https://localhost", timeout=0.1)
def test_private_ip_blocked():
with pytest.raises(HTTPError, match="Request to private address.*"):
requests.get("http://10.0.0.1", timeout=0.1)
with pytest.raises(HTTPError, match="Request to private address.*"):
requests.get("https://10.0.0.1", timeout=0.1)
@pytest.mark.django_db
@pytest.mark.parametrize("res", [
[(AF_INET, SOCK_STREAM, 6, '', ('10.0.0.3', 443))],
[(AF_INET, SOCK_STREAM, 6, '', ('0.0.0.0', 443))],
[(AF_INET, SOCK_STREAM, 6, '', ('127.1.1.1', 443))],
[(AF_INET, SOCK_STREAM, 6, '', ('192.168.5.3', 443))],
[(AF_INET, SOCK_STREAM, 6, '', ('224.0.0.1', 443))],
[(AF_INET6, SOCK_STREAM, 6, '', ('::1', 443, 0, 0))],
[(AF_INET6, SOCK_STREAM, 6, '', ('fe80::1', 443, 0, 0))],
[(AF_INET6, SOCK_STREAM, 6, '', ('ff00::1', 443, 0, 0))],
[(AF_INET6, SOCK_STREAM, 6, '', ('fc00::1', 443, 0, 0))],
])
def test_dns_resolving_to_local_blocked(res):
with mock.patch('socket.getaddrinfo') as mock_addr:
mock_addr.return_value = res
with pytest.raises(HTTPError, match="Request to (multicast|private|local) address.*"):
requests.get("https://example.org", timeout=0.1)
with pytest.raises(HTTPError, match="Request to (multicast|private|local) address.*"):
requests.get("http://example.org", timeout=0.1)
def test_dns_remote_allowed():
class SocketOk(Exception):
pass
def side_effect(*args, **kwargs):
raise SocketOk
with mock.patch('socket.getaddrinfo') as mock_addr, mock.patch('socket.socket') as mock_socket:
mock_addr.return_value = [(AF_INET, SOCK_STREAM, 6, '', ('8.8.8.8', 443))]
mock_socket.side_effect = side_effect
with pytest.raises(SocketOk):
requests.get("https://example.org", timeout=0.1)
@override_settings(ALLOW_HTTP_TO_PRIVATE_NETWORKS=True)
def test_local_is_allowed():
class SocketOk(Exception):
pass
def side_effect(*args, **kwargs):
raise SocketOk
with mock.patch('socket.getaddrinfo') as mock_addr, mock.patch('socket.socket') as mock_socket:
mock_addr.return_value = [(AF_INET, SOCK_STREAM, 6, '', ('10.0.0.1', 443))]
mock_socket.side_effect = side_effect
with pytest.raises(SocketOk):
requests.get("https://example.org", timeout=0.1)
+8 -3
View File
@@ -33,7 +33,7 @@ from django.conf import settings
from django.core import mail as djmail from django.core import mail as djmail
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.signing import dumps from django.core.signing import dumps
from django.test import TestCase, TransactionTestCase from django.test import TestCase
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.timezone import now from django.utils.timezone import now
from django_countries.fields import Country from django_countries.fields import Country
@@ -60,6 +60,12 @@ from pretix.testutils.sessions import get_cart_session_key
from .test_timemachine import TimemachineTestMixin from .test_timemachine import TimemachineTestMixin
@pytest.fixture
def class_monkeypatch(request, monkeypatch):
request.cls.monkeypatch = monkeypatch
@pytest.mark.usefixtures("class_monkeypatch")
class BaseCheckoutTestCase: class BaseCheckoutTestCase:
@scopes_disabled() @scopes_disabled()
def setUp(self): def setUp(self):
@@ -98,6 +104,7 @@ class BaseCheckoutTestCase:
self.workshopquota.items.add(self.workshop2) self.workshopquota.items.add(self.workshop2)
self.workshopquota.variations.add(self.workshop2a) self.workshopquota.variations.add(self.workshop2a)
self.workshopquota.variations.add(self.workshop2b) self.workshopquota.variations.add(self.workshop2b)
self.monkeypatch.setattr("django.db.transaction.on_commit", lambda t: t())
def _set_session(self, key, value): def _set_session(self, key, value):
session = self.client.session session = self.client.session
@@ -4413,8 +4420,6 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
assert len(djmail.outbox) == 1 assert len(djmail.outbox) == 1
assert any(["Invoice_" in a[0] for a in djmail.outbox[0].attachments]) assert any(["Invoice_" in a[0] for a in djmail.outbox[0].attachments])
class CheckoutTransactionTestCase(BaseCheckoutTestCase, TransactionTestCase):
def test_order_confirmation_mail_invoice_sent_somewhere_else(self): def test_order_confirmation_mail_invoice_sent_somewhere_else(self):
self.event.settings.invoice_address_asked = True self.event.settings.invoice_address_asked = True
self.event.settings.invoice_address_required = True self.event.settings.invoice_address_required = True
-88
View File
@@ -36,7 +36,6 @@
import datetime import datetime
import re import re
from decimal import Decimal from decimal import Decimal
from importlib import import_module
from json import loads from json import loads
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
@@ -81,34 +80,6 @@ class EventMiddlewareTest(EventTestMixin, SoupTest):
doc = self.get_doc('/%s/%s/' % (self.orga.slug, self.event.slug)) doc = self.get_doc('/%s/%s/' % (self.orga.slug, self.event.slug))
self.assertIn(str(self.event.name), doc.find("h1").text) self.assertIn(str(self.event.name), doc.find("h1").text)
def test_no_session_cookie_set_on_event_index_view(self):
resp = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
self.assertEqual(resp.status_code, 200)
assert settings.SESSION_COOKIE_NAME not in self.client.cookies
def test_no_cart_session_added_on_event_index_view(self):
# Make sure a session is present by doing a cart op on another event
event2 = Event.objects.create(
organizer=self.orga, name='30C3b', slug='30c3b',
date_from=datetime.datetime(now().year + 1, 12, 26, 14, 0, tzinfo=datetime.timezone.utc),
live=True,
)
self.client.post('/%s/%s/cart/add' % (self.orga.slug, event2.slug), {
'item_%d' % 1337: '1', # item does not need to exist
'ajax': 1
})
assert settings.SESSION_COOKIE_NAME in self.client.cookies
# Visit shop, make sure no session is created
resp = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
self.assertEqual(resp.status_code, 200)
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
session = SessionStore(self.client.cookies[settings.SESSION_COOKIE_NAME].value).load()
assert set(session.keys()) == {
f"current_cart_event_{event2.pk}", "carts"
}
def test_not_found(self): def test_not_found(self):
resp = self.client.get('/%s/%s/' % ('foo', 'bar')) resp = self.client.get('/%s/%s/' % ('foo', 'bar'))
self.assertEqual(resp.status_code, 404) self.assertEqual(resp.status_code, 404)
@@ -1162,65 +1133,6 @@ class WaitingListTest(EventTestMixin, SoupTest):
assert wle.voucher is None assert wle.voucher is None
assert wle.locale == 'en' assert wle.locale == 'en'
def test_initial_selection(self):
with scopes_disabled():
cat = ItemCategory.objects.create(event=self.event, name='Tickets')
self.item.category = cat
self.item.save()
item2 = Item.objects.create(
event=self.event, name='VIP ticket',
default_price=Decimal('25.00'),
active=True, category=cat,
)
self.q.items.add(item2)
response = self.client.get(
'/%s/%s/waitinglist/?item=%d' % (
self.orga.slug, self.event.slug, item2.pk
)
)
self.assertEqual(response.status_code, 200)
doc = BeautifulSoup(response.render().content, "lxml")
select = doc.find('select', {'name': 'itemvar'})
optgroup = select.find('optgroup')
self.assertIsNotNone(optgroup, 'Choices should be grouped by category')
self.assertEqual(optgroup['label'], 'Tickets')
selected = select.find_all('option', selected=True)
self.assertEqual(len(selected), 1, 'Exactly one option should be pre-selected')
self.assertEqual(selected[0]['value'], str(item2.pk))
def test_initial_selection_with_variation(self):
with scopes_disabled():
cat = ItemCategory.objects.create(event=self.event, name='Tickets')
self.item.category = cat
self.item.has_variations = True
self.item.save()
var1 = ItemVariation.objects.create(item=self.item, value='Standard')
var2 = ItemVariation.objects.create(item=self.item, value='Premium')
self.q.variations.add(var1, var2)
response = self.client.get(
'/%s/%s/waitinglist/?item=%d&var=%d' % (
self.orga.slug, self.event.slug,
self.item.pk, var2.pk,
)
)
self.assertEqual(response.status_code, 200)
doc = BeautifulSoup(response.render().content, "lxml")
select = doc.find('select', {'name': 'itemvar'})
optgroup = select.find('optgroup')
self.assertIsNotNone(optgroup, 'Choices should be grouped by category')
self.assertEqual(optgroup['label'], 'Tickets')
selected = select.find_all('option', selected=True)
self.assertEqual(len(selected), 1, 'Exactly one option should be pre-selected')
self.assertEqual(selected[0]['value'], '%d-%d' % (self.item.pk, var2.pk))
def test_subevent_valid(self): def test_subevent_valid(self):
with scopes_disabled(): with scopes_disabled():
self.event.has_subevents = True self.event.has_subevents = True