mirror of
https://github.com/pretix/pretix.git
synced 2026-08-13 11:17:01 +00:00
Compare commits
29
Commits
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.11-bookworm
|
FROM python:3.13-trixie
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
|
|||||||
+3
-3
@@ -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.1.*",
|
"Pillow==12.2.*",
|
||||||
"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.1.*",
|
"redis==7.4.*",
|
||||||
"reportlab==4.4.*",
|
"reportlab==4.4.*",
|
||||||
"requests==2.32.*",
|
"requests==2.32.*",
|
||||||
"sentry-sdk==2.56.*",
|
"sentry-sdk==2.57.*",
|
||||||
"sepaxml==2.7.*",
|
"sepaxml==2.7.*",
|
||||||
"stripe==7.9.*",
|
"stripe==7.9.*",
|
||||||
"text-unidecode==1.*",
|
"text-unidecode==1.*",
|
||||||
|
|||||||
@@ -19,4 +19,4 @@
|
|||||||
# 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/>.
|
||||||
#
|
#
|
||||||
__version__ = "2026.3.0"
|
__version__ = "2026.4.0.dev0"
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ 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,
|
||||||
)
|
)
|
||||||
@@ -59,11 +60,20 @@ 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, progress_callback=lambda v: None):
|
def __init__(self, event, organizer, permission_holder: PermissionHolder=None, 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
|
||||||
@@ -180,7 +190,7 @@ class BaseExporter:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_required_event_permission(cls) -> str:
|
def get_required_event_permission(cls) -> Optional[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.
|
||||||
@@ -195,7 +205,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) -> str:
|
def get_required_organizer_permission(cls) -> Optional[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.
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ 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')
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ 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
|
||||||
|
|
||||||
@@ -74,10 +76,14 @@ 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():
|
for frame in inspect.stack()[1:]:
|
||||||
if 'pretix/base/models/orders' in frame.filename:
|
if (
|
||||||
|
'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
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ 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 (
|
||||||
@@ -67,6 +68,14 @@ 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
|
||||||
@@ -696,6 +705,18 @@ 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)
|
||||||
|
|||||||
@@ -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) -> bool:
|
def has_event_permission(self, organizer, event, perm_name=None, request=None, session_key=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,6 +238,7 @@ 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 (
|
||||||
|
|||||||
@@ -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 OrderFee with deferred fields since we can't check if you missed "
|
_fail("It is unsafe to call save() on an Order 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 OrderFee with deferred fields since we can't check if you missed "
|
_fail("It is unsafe to call save() on an OrderPosition 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.")
|
||||||
|
|
||||||
|
|||||||
@@ -319,6 +319,9 @@ 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)
|
||||||
@@ -331,6 +334,9 @@ 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)
|
||||||
@@ -550,7 +556,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) -> bool:
|
def has_event_permission(self, organizer, event, perm_name=None, request=None, session_key=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``.
|
||||||
@@ -559,6 +565,7 @@ 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 (
|
||||||
|
|||||||
+16
-5
@@ -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 Max, Min
|
from django.db.models import Exists, Max, Min, OuterRef
|
||||||
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 Event, Order, OrderPosition, Question
|
from pretix.base.models import Checkin, 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,6 +379,13 @@ 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"),
|
||||||
@@ -750,12 +757,16 @@ def get_program_times(op: OrderPosition, ev: Event):
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
def generate_compressed_addon_list(op, order, event):
|
def generate_compressed_addon_list(op, order, event, only_checked_in=False):
|
||||||
itemcount = defaultdict(int)
|
itemcount = defaultdict(int)
|
||||||
addons = [p for p in (
|
addon_qs = (
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ 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 (
|
||||||
@@ -211,7 +212,12 @@ 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(event=event, organizer=event.organizer, **kwargs)
|
exporter: BaseExporter = response(
|
||||||
|
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
|
||||||
@@ -243,7 +249,12 @@ def init_organizer_exporters(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if issubclass(response, OrganizerLevelExportMixin):
|
if issubclass(response, OrganizerLevelExportMixin):
|
||||||
exporter: BaseExporter = response(event=Event.objects.none(), organizer=organizer, **kwargs)
|
exporter: BaseExporter = response(
|
||||||
|
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:
|
||||||
@@ -295,7 +306,12 @@ 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(event=_event_list_cache[permission_name], organizer=organizer, **kwargs)
|
exporter: BaseExporter = response(
|
||||||
|
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
|
||||||
|
|||||||
@@ -67,9 +67,9 @@ from pretix.base.email import get_email_context
|
|||||||
from pretix.base.i18n import get_language_without_region, language
|
from pretix.base.i18n import get_language_without_region, language
|
||||||
from pretix.base.media import MEDIA_TYPES
|
from pretix.base.media import MEDIA_TYPES
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CartPosition, Device, Event, GiftCard, Item, ItemVariation, Membership,
|
CartPosition, Device, Event, GiftCard, Item, ItemVariation, LogEntry,
|
||||||
Order, OrderPayment, OrderPosition, Quota, Seat, SeatCategoryMapping, User,
|
Membership, Order, OrderPayment, OrderPosition, Quota, Seat,
|
||||||
Voucher,
|
SeatCategoryMapping, User, Voucher,
|
||||||
)
|
)
|
||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
from pretix.base.models.orders import (
|
from pretix.base.models.orders import (
|
||||||
@@ -1618,7 +1618,7 @@ class OrderChangeManager:
|
|||||||
MembershipOperation = namedtuple('MembershipOperation', ('position', 'membership'))
|
MembershipOperation = namedtuple('MembershipOperation', ('position', 'membership'))
|
||||||
CancelOperation = namedtuple('CancelOperation', ('position', 'price_diff'))
|
CancelOperation = namedtuple('CancelOperation', ('position', 'price_diff'))
|
||||||
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent', 'seat', 'membership',
|
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent', 'seat', 'membership',
|
||||||
'valid_from', 'valid_until', 'is_bundled', 'result'))
|
'valid_from', 'valid_until', 'is_bundled', 'result', 'count'))
|
||||||
SplitOperation = namedtuple('SplitOperation', ('position',))
|
SplitOperation = namedtuple('SplitOperation', ('position',))
|
||||||
FeeValueOperation = namedtuple('FeeValueOperation', ('fee', 'value', 'price_diff'))
|
FeeValueOperation = namedtuple('FeeValueOperation', ('fee', 'value', 'price_diff'))
|
||||||
AddFeeOperation = namedtuple('AddFeeOperation', ('fee', 'price_diff'))
|
AddFeeOperation = namedtuple('AddFeeOperation', ('fee', 'price_diff'))
|
||||||
@@ -1632,16 +1632,24 @@ class OrderChangeManager:
|
|||||||
ForceRecomputeOperation = namedtuple('ForceRecomputeOperation', tuple())
|
ForceRecomputeOperation = namedtuple('ForceRecomputeOperation', tuple())
|
||||||
|
|
||||||
class AddPositionResult:
|
class AddPositionResult:
|
||||||
_position: Optional[OrderPosition]
|
_positions: Optional[List[OrderPosition]]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._position = None
|
self._positions = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def position(self) -> OrderPosition:
|
def position(self) -> OrderPosition:
|
||||||
if self._position is None:
|
if self._positions is None:
|
||||||
raise RuntimeError("Order position has not been created yet. Call commit() first on OrderChangeManager.")
|
raise RuntimeError("Order position has not been created yet. Call commit() first on OrderChangeManager.")
|
||||||
return self._position
|
if len(self._positions) != 1:
|
||||||
|
raise RuntimeError("More than one position created.")
|
||||||
|
return self._positions[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def positions(self) -> List[OrderPosition]:
|
||||||
|
if self._positions is None:
|
||||||
|
raise RuntimeError("Order position has not been created yet. Call commit() first on OrderChangeManager.")
|
||||||
|
return self._positions
|
||||||
|
|
||||||
def __init__(self, order: Order, user=None, auth=None, notify=True, reissue_invoice=True, allow_blocked_seats=False):
|
def __init__(self, order: Order, user=None, auth=None, notify=True, reissue_invoice=True, allow_blocked_seats=False):
|
||||||
self.order = order
|
self.order = order
|
||||||
@@ -1848,8 +1856,12 @@ class OrderChangeManager:
|
|||||||
|
|
||||||
def add_position(self, item: Item, variation: ItemVariation, price: Decimal, addon_to: OrderPosition = None,
|
def add_position(self, item: Item, variation: ItemVariation, price: Decimal, addon_to: OrderPosition = None,
|
||||||
subevent: SubEvent = None, seat: Seat = None, membership: Membership = None,
|
subevent: SubEvent = None, seat: Seat = None, membership: Membership = None,
|
||||||
valid_from: datetime = None, valid_until: datetime = None) -> 'OrderChangeManager.AddPositionResult':
|
valid_from: datetime = None, valid_until: datetime = None, count: int = 1) -> 'OrderChangeManager.AddPositionResult':
|
||||||
|
if count < 1:
|
||||||
|
raise ValueError("Count must be positive")
|
||||||
if isinstance(seat, str):
|
if isinstance(seat, str):
|
||||||
|
if count > 1:
|
||||||
|
raise ValueError("Cannot combine count > 1 with seat")
|
||||||
if not seat:
|
if not seat:
|
||||||
seat = None
|
seat = None
|
||||||
else:
|
else:
|
||||||
@@ -1903,14 +1915,14 @@ class OrderChangeManager:
|
|||||||
if self.order.event.settings.invoice_include_free or price.gross != Decimal('0.00'):
|
if self.order.event.settings.invoice_include_free or price.gross != Decimal('0.00'):
|
||||||
self._invoice_dirty = True
|
self._invoice_dirty = True
|
||||||
|
|
||||||
self._totaldiff_guesstimate += price.gross
|
self._totaldiff_guesstimate += price.gross * count
|
||||||
self._quotadiff.update(new_quotas)
|
self._quotadiff.update({q: count for q in new_quotas})
|
||||||
if seat:
|
if seat:
|
||||||
self._seatdiff.update([seat])
|
self._seatdiff.update([seat])
|
||||||
|
|
||||||
result = self.AddPositionResult()
|
result = self.AddPositionResult()
|
||||||
self._operations.append(self.AddOperation(item, variation, price, addon_to, subevent, seat, membership,
|
self._operations.append(self.AddOperation(item, variation, price, addon_to, subevent, seat, membership,
|
||||||
valid_from, valid_until, is_bundled, result))
|
valid_from, valid_until, is_bundled, result, count))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def split(self, position: OrderPosition):
|
def split(self, position: OrderPosition):
|
||||||
@@ -2530,29 +2542,35 @@ class OrderChangeManager:
|
|||||||
secret_dirty.remove(position)
|
secret_dirty.remove(position)
|
||||||
position.save(update_fields=['canceled', 'secret'])
|
position.save(update_fields=['canceled', 'secret'])
|
||||||
elif isinstance(op, self.AddOperation):
|
elif isinstance(op, self.AddOperation):
|
||||||
pos = OrderPosition.objects.create(
|
new_pos = []
|
||||||
item=op.item, variation=op.variation, addon_to=op.addon_to,
|
new_logs = []
|
||||||
price=op.price.gross, order=self.order, tax_rate=op.price.rate, tax_code=op.price.code,
|
for i in range(op.count):
|
||||||
tax_value=op.price.tax, tax_rule=op.item.tax_rule,
|
pos = OrderPosition.objects.create(
|
||||||
positionid=nextposid, subevent=op.subevent, seat=op.seat,
|
item=op.item, variation=op.variation, addon_to=op.addon_to,
|
||||||
used_membership=op.membership, valid_from=op.valid_from, valid_until=op.valid_until,
|
price=op.price.gross, order=self.order, tax_rate=op.price.rate, tax_code=op.price.code,
|
||||||
is_bundled=op.is_bundled,
|
tax_value=op.price.tax, tax_rule=op.item.tax_rule,
|
||||||
)
|
positionid=nextposid, subevent=op.subevent, seat=op.seat,
|
||||||
nextposid += 1
|
used_membership=op.membership, valid_from=op.valid_from, valid_until=op.valid_until,
|
||||||
self.order.log_action('pretix.event.order.changed.add', user=self.user, auth=self.auth, data={
|
is_bundled=op.is_bundled,
|
||||||
'position': pos.pk,
|
)
|
||||||
'item': op.item.pk,
|
nextposid += 1
|
||||||
'variation': op.variation.pk if op.variation else None,
|
new_pos.append(pos)
|
||||||
'addon_to': op.addon_to.pk if op.addon_to else None,
|
new_logs.append(self.order.log_action('pretix.event.order.changed.add', user=self.user, auth=self.auth, data={
|
||||||
'price': op.price.gross,
|
'position': pos.pk,
|
||||||
'positionid': pos.positionid,
|
'item': op.item.pk,
|
||||||
'membership': pos.used_membership_id,
|
'variation': op.variation.pk if op.variation else None,
|
||||||
'subevent': op.subevent.pk if op.subevent else None,
|
'addon_to': op.addon_to.pk if op.addon_to else None,
|
||||||
'seat': op.seat.pk if op.seat else None,
|
'price': op.price.gross,
|
||||||
'valid_from': op.valid_from.isoformat() if op.valid_from else None,
|
'positionid': pos.positionid,
|
||||||
'valid_until': op.valid_until.isoformat() if op.valid_until else None,
|
'membership': pos.used_membership_id,
|
||||||
})
|
'subevent': op.subevent.pk if op.subevent else None,
|
||||||
op.result._position = pos
|
'seat': op.seat.pk if op.seat else None,
|
||||||
|
'valid_from': op.valid_from.isoformat() if op.valid_from else None,
|
||||||
|
'valid_until': op.valid_until.isoformat() if op.valid_until else None,
|
||||||
|
}, save=False))
|
||||||
|
|
||||||
|
op.result._positions = new_pos
|
||||||
|
LogEntry.bulk_create_and_postprocess(new_logs)
|
||||||
elif isinstance(op, self.SplitOperation):
|
elif isinstance(op, self.SplitOperation):
|
||||||
position = position_cache.setdefault(op.position.pk, op.position)
|
position = position_cache.setdefault(op.position.pk, op.position)
|
||||||
split_positions.append(position)
|
split_positions.append(position)
|
||||||
@@ -2877,7 +2895,7 @@ class OrderChangeManager:
|
|||||||
return total
|
return total
|
||||||
|
|
||||||
def _check_order_size(self):
|
def _check_order_size(self):
|
||||||
if (len(self.order.positions.all()) + len([op for op in self._operations if isinstance(op, self.AddOperation)])) > settings.PRETIX_MAX_ORDER_SIZE:
|
if (len(self.order.positions.all()) + sum([op.count for op in self._operations if isinstance(op, self.AddOperation)])) > settings.PRETIX_MAX_ORDER_SIZE:
|
||||||
raise OrderError(
|
raise OrderError(
|
||||||
self.error_messages['max_order_size'] % {
|
self.error_messages['max_order_size'] % {
|
||||||
'max': settings.PRETIX_MAX_ORDER_SIZE,
|
'max': settings.PRETIX_MAX_ORDER_SIZE,
|
||||||
@@ -2938,7 +2956,7 @@ class OrderChangeManager:
|
|||||||
]) + len([
|
]) + len([
|
||||||
o for o in self._operations if isinstance(o, self.SplitOperation)
|
o for o in self._operations if isinstance(o, self.SplitOperation)
|
||||||
])
|
])
|
||||||
adds = len([o for o in self._operations if isinstance(o, self.AddOperation)])
|
adds = sum([o.count for o in self._operations if isinstance(o, self.AddOperation)])
|
||||||
if current > 0 and current - cancels + adds < 1:
|
if current > 0 and current - cancels + adds < 1:
|
||||||
raise OrderError(self.error_messages['complete_cancel'])
|
raise OrderError(self.error_messages['complete_cancel'])
|
||||||
|
|
||||||
@@ -2985,17 +3003,18 @@ class OrderChangeManager:
|
|||||||
elif isinstance(op, self.CancelOperation) and op.position in positions_to_fake_cart:
|
elif isinstance(op, self.CancelOperation) and op.position in positions_to_fake_cart:
|
||||||
fake_cart.remove(positions_to_fake_cart[op.position])
|
fake_cart.remove(positions_to_fake_cart[op.position])
|
||||||
elif isinstance(op, self.AddOperation):
|
elif isinstance(op, self.AddOperation):
|
||||||
cp = CartPosition(
|
for i in range(op.count):
|
||||||
event=self.event,
|
cp = CartPosition(
|
||||||
item=op.item,
|
event=self.event,
|
||||||
variation=op.variation,
|
item=op.item,
|
||||||
used_membership=op.membership,
|
variation=op.variation,
|
||||||
subevent=op.subevent,
|
used_membership=op.membership,
|
||||||
seat=op.seat,
|
subevent=op.subevent,
|
||||||
)
|
seat=op.seat,
|
||||||
cp.override_valid_from = op.valid_from
|
)
|
||||||
cp.override_valid_until = op.valid_until
|
cp.override_valid_from = op.valid_from
|
||||||
fake_cart.append(cp)
|
cp.override_valid_until = op.valid_until
|
||||||
|
fake_cart.append(cp)
|
||||||
try:
|
try:
|
||||||
validate_memberships_in_order(self.order.customer, fake_cart, self.event, lock=True, ignored_order=self.order, testmode=self.order.testmode)
|
validate_memberships_in_order(self.order.customer, fake_cart, self.event, lock=True, ignored_order=self.order, testmode=self.order.testmode)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
|
|||||||
@@ -331,6 +331,10 @@ class OtherOperationsForm(forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class OrderPositionAddForm(forms.Form):
|
class OrderPositionAddForm(forms.Form):
|
||||||
|
count = forms.IntegerField(
|
||||||
|
label=_('Number of products to add'),
|
||||||
|
initial=1,
|
||||||
|
)
|
||||||
itemvar = forms.ChoiceField(
|
itemvar = forms.ChoiceField(
|
||||||
label=_('Product')
|
label=_('Product')
|
||||||
)
|
)
|
||||||
@@ -432,6 +436,10 @@ 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"):
|
||||||
|
raise ValidationError({
|
||||||
|
"seat": _("You can not choose a seat when adding multiple products at once.")
|
||||||
|
})
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -329,6 +329,7 @@
|
|||||||
{{ add_form.custom_error }}
|
{{ add_form.custom_error }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% bootstrap_field add_form.count layout="control" %}
|
||||||
{% bootstrap_field add_form.itemvar layout="control" %}
|
{% bootstrap_field add_form.itemvar layout="control" %}
|
||||||
{% bootstrap_field add_form.price addon_after=request.event.currency layout="control" %}
|
{% bootstrap_field add_form.price addon_after=request.event.currency layout="control" %}
|
||||||
{% if add_form.addon_to %}
|
{% if add_form.addon_to %}
|
||||||
@@ -364,6 +365,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="form-horizontal">
|
<div class="form-horizontal">
|
||||||
|
{% bootstrap_field add_position_formset.empty_form.count layout="control" %}
|
||||||
{% bootstrap_field add_position_formset.empty_form.itemvar layout="control" %}
|
{% bootstrap_field add_position_formset.empty_form.itemvar layout="control" %}
|
||||||
{% bootstrap_field add_position_formset.empty_form.price addon_after=request.event.currency layout="control" %}
|
{% bootstrap_field add_position_formset.empty_form.price addon_after=request.event.currency layout="control" %}
|
||||||
{% if add_position_formset.empty_form.addon_to %}
|
{% if add_position_formset.empty_form.addon_to %}
|
||||||
|
|||||||
@@ -2059,12 +2059,13 @@ class OrderChange(OrderView):
|
|||||||
else:
|
else:
|
||||||
variation = None
|
variation = None
|
||||||
try:
|
try:
|
||||||
ocm.add_position(item, variation,
|
for i in range(f.cleaned_data.get("count", 1)):
|
||||||
f.cleaned_data['price'],
|
ocm.add_position(item, variation,
|
||||||
f.cleaned_data.get('addon_to'),
|
f.cleaned_data['price'],
|
||||||
f.cleaned_data.get('subevent'),
|
f.cleaned_data.get('addon_to'),
|
||||||
f.cleaned_data.get('seat'),
|
f.cleaned_data.get('subevent'),
|
||||||
f.cleaned_data.get('used_membership'))
|
f.cleaned_data.get('seat'),
|
||||||
|
f.cleaned_data.get('used_membership'))
|
||||||
except OrderError as e:
|
except OrderError as e:
|
||||||
f.custom_error = str(e)
|
f.custom_error = str(e)
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -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: getattr(self.object, k) if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
|
k: form.cleaned_data[k] if k != 'limit_events' else [e.id for e in form.cleaned_data[k]]
|
||||||
for k in form.changed_data
|
for k in form.changed_data
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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-18 12:23+0000\n"
|
"PO-Revision-Date: 2026-03-31 17:00+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 ""
|
msgstr "El NIF no es compatible con «{}»."
|
||||||
|
|
||||||
#: 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,28 +27567,30 @@ 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 "Celular con aplicación de autenticación"
|
msgstr "Smartphone con la aplicación 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 ""
|
||||||
|
"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 "Hardware compatible con token WebAuthn (p. ej. Yubikey)"
|
msgstr "Token físico compatible con WebAuthn"
|
||||||
|
|
||||||
#: 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:"
|
||||||
|
|||||||
@@ -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-18 12:23+0000\n"
|
"PO-Revision-Date: 2026-03-31 17:00+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/fr/"
|
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||||
">\n"
|
"fr/>\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 ""
|
msgstr "Le numéro de TVA n'est pas pris en charge pour « {} »."
|
||||||
|
|
||||||
#: 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,28 +27774,30 @@ 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 avec l'application Authenticator"
|
msgstr "Smartphone équipé de 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 (par ex. Yubikey)"
|
msgstr "Token matériel compatible WebAuthn"
|
||||||
|
|
||||||
#: 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,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-18 12:23+0000\n"
|
"PO-Revision-Date: 2026-03-31 17:00+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 ""
|
msgstr "Btw-nummer wordt niet ondersteund voor \"{}\"."
|
||||||
|
|
||||||
#: 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 de Authenticator-applicatie"
|
msgstr "Smartphone met Authenticator-app"
|
||||||
|
|
||||||
#: 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 hardware-token (bijvoorbeeld Yubikey)"
|
msgstr "WebAuthn-compatibel hardwaretoken"
|
||||||
|
|
||||||
#: 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-18 14:50+0000\n"
|
"PO-Revision-Date: 2026-03-31 17:00+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 ""
|
msgstr "Btw-nummer wordt niet ondersteund voor \"{}\"."
|
||||||
|
|
||||||
#: 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 de Authenticator-applicatie"
|
msgstr "Smartphone met Authenticator-app"
|
||||||
|
|
||||||
#: 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 hardware-token (bijvoorbeeld Yubikey)"
|
msgstr "WebAuthn-compatibel hardwaretoken"
|
||||||
|
|
||||||
#: 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-25 08:00+0000\n"
|
"PO-Revision-Date: 2026-03-30 21: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 "Filtro"
|
msgstr "Filtrar"
|
||||||
|
|
||||||
#: 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,6 +28003,9 @@ 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"
|
||||||
@@ -28049,6 +28052,9 @@ 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"
|
||||||
|
|||||||
@@ -296,7 +296,8 @@ class SetPasswordForm(forms.Form):
|
|||||||
}
|
}
|
||||||
email = forms.EmailField(
|
email = forms.EmailField(
|
||||||
label=_('Email'),
|
label=_('Email'),
|
||||||
disabled=True
|
widget=forms.EmailInput(attrs={'autocomplete': 'username', 'readonly': 'readonly'}),
|
||||||
|
required=False,
|
||||||
)
|
)
|
||||||
password = forms.CharField(
|
password = forms.CharField(
|
||||||
label=_('Password'),
|
label=_('Password'),
|
||||||
|
|||||||
@@ -70,18 +70,21 @@ 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)
|
cs = cart_session(request, create=False)
|
||||||
iapk = cs.get('invoice_address')
|
if cs is None:
|
||||||
if not iapk:
|
|
||||||
request._checkout_flow_invoice_address = InvoiceAddress()
|
request._checkout_flow_invoice_address = InvoiceAddress()
|
||||||
else:
|
else:
|
||||||
try:
|
iapk = cs.get('invoice_address')
|
||||||
with scopes_disabled():
|
if not iapk:
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
@@ -111,6 +114,12 @@ 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:
|
||||||
@@ -166,7 +175,8 @@ class CartMixin:
|
|||||||
else:
|
else:
|
||||||
fees = []
|
fees = []
|
||||||
|
|
||||||
if not order:
|
if not order and lcp:
|
||||||
|
# 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])
|
||||||
@@ -277,6 +287,12 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -417,7 +417,7 @@ def get_or_create_cart_id(request, create=True):
|
|||||||
return new_id
|
return new_id
|
||||||
|
|
||||||
|
|
||||||
def cart_session(request):
|
def cart_session(request, create=True):
|
||||||
"""
|
"""
|
||||||
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,7 +428,9 @@ def cart_session(request):
|
|||||||
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)
|
cart_id = get_or_create_cart_id(request, create=create)
|
||||||
|
if not cart_id and not create:
|
||||||
|
return None
|
||||||
return request.session['carts'][cart_id]
|
return request.session['carts'][cart_id]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+16
-1
@@ -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', # Will only be used from Django 4.1 onwards
|
'CONN_HEALTH_CHECKS': db_backend != 'sqlite3',
|
||||||
'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,6 +179,21 @@ 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/')
|
||||||
|
|||||||
@@ -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(33):
|
with django_assert_max_num_queries(34):
|
||||||
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(35):
|
with django_assert_max_num_queries(36):
|
||||||
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
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -991,3 +991,30 @@ 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
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ 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
|
||||||
|
|
||||||
@@ -75,47 +74,42 @@ 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, monkeypatch_on_commit):
|
def test_notification_trigger_event_specific(event, order, user, django_capture_on_commit_callbacks):
|
||||||
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 transaction.atomic():
|
with django_capture_on_commit_callbacks(execute=True):
|
||||||
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, monkeypatch_on_commit):
|
def test_notification_trigger_global(event, order, user, django_capture_on_commit_callbacks):
|
||||||
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 transaction.atomic():
|
with django_capture_on_commit_callbacks(execute=True):
|
||||||
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, monkeypatch_on_commit):
|
def test_notification_trigger_global_wildcard(event, order, user, django_capture_on_commit_callbacks):
|
||||||
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 transaction.atomic():
|
with django_capture_on_commit_callbacks(execute=True):
|
||||||
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, monkeypatch_on_commit):
|
def test_notification_enabled_global_ignored_specific(event, order, user, django_capture_on_commit_callbacks):
|
||||||
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
|
||||||
@@ -123,24 +117,24 @@ def test_notification_enabled_global_ignored_specific(event, order, user, monkey
|
|||||||
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 transaction.atomic():
|
with django_capture_on_commit_callbacks(execute=True):
|
||||||
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, monkeypatch_on_commit):
|
def test_notification_ignore_same_user(event, order, user, django_capture_on_commit_callbacks):
|
||||||
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 transaction.atomic():
|
with django_capture_on_commit_callbacks(execute=True):
|
||||||
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, monkeypatch_on_commit):
|
def test_notification_ignore_insufficient_permissions(event, order, user, team, django_capture_on_commit_callbacks):
|
||||||
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}
|
||||||
@@ -148,7 +142,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 transaction.atomic():
|
with django_capture_on_commit_callbacks(execute=True):
|
||||||
order.log_action('pretix.event.order.paid', {})
|
order.log_action('pretix.event.order.paid', {})
|
||||||
assert len(djmail.outbox) == 0
|
assert len(djmail.outbox) == 0
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ 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, override_settings
|
from django.test import TestCase, TransactionTestCase, 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
|
||||||
@@ -1225,12 +1226,6 @@ 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()
|
||||||
@@ -1258,7 +1253,6 @@ 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):
|
||||||
@@ -1351,14 +1345,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 = []
|
||||||
cancel_order(self.order.pk, send_mail=True)
|
with self.captureOnCommitCallbacks(execute=True):
|
||||||
print([s.subject for s in djmail.outbox])
|
cancel_order(self.order.pk, send_mail=True)
|
||||||
print([s.to for s in djmail.outbox])
|
|
||||||
assert len(djmail.outbox) == 2
|
assert len(djmail.outbox) == 2
|
||||||
assert ["invoice@example.org"] == djmail.outbox[0].to
|
assert ["dummy@dummy.test"] == djmail.outbox[0].to
|
||||||
assert any(["Invoice_" in a[0] for a in djmail.outbox[0].attachments])
|
assert not any(["Invoice_" in a[0] for a in djmail.outbox[0].attachments])
|
||||||
assert ["dummy@dummy.test"] == djmail.outbox[1].to
|
assert ["invoice@example.org"] == djmail.outbox[1].to
|
||||||
assert not any(["Invoice_" in a[0] for a in djmail.outbox[1].attachments])
|
assert 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):
|
||||||
@@ -1488,8 +1482,7 @@ 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()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("class_monkeypatch")
|
class BaseOrderChangeManagerTestCase:
|
||||||
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')
|
||||||
@@ -1552,7 +1545,6 @@ class OrderChangeManagerTests(TestCase):
|
|||||||
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
|
||||||
@@ -1566,6 +1558,8 @@ class OrderChangeManagerTests(TestCase):
|
|||||||
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'))
|
||||||
@@ -2406,6 +2400,15 @@ class OrderChangeManagerTests(TestCase):
|
|||||||
self.ocm.commit()
|
self.ocm.commit()
|
||||||
assert self.order.positions.count() == 2
|
assert self.order.positions.count() == 2
|
||||||
|
|
||||||
|
@classscope(attr='o')
|
||||||
|
def test_add_item_quota_partial(self):
|
||||||
|
q1 = self.event.quotas.create(name='Test', size=1)
|
||||||
|
q1.items.add(self.shirt)
|
||||||
|
self.ocm.add_position(self.shirt, None, None, None, count=2)
|
||||||
|
with self.assertRaises(OrderError):
|
||||||
|
self.ocm.commit()
|
||||||
|
assert self.order.positions.count() == 2
|
||||||
|
|
||||||
@classscope(attr='o')
|
@classscope(attr='o')
|
||||||
def test_add_item_addon(self):
|
def test_add_item_addon(self):
|
||||||
self.shirt.category = self.event.categories.create(name='Add-ons', is_addon=True)
|
self.shirt.category = self.event.categories.create(name='Add-ons', is_addon=True)
|
||||||
@@ -3895,15 +3898,16 @@ class OrderChangeManagerTests(TestCase):
|
|||||||
|
|
||||||
@classscope(attr='o')
|
@classscope(attr='o')
|
||||||
def test_set_valid_until(self):
|
def test_set_valid_until(self):
|
||||||
self.event.settings.ticket_secret_generator = "pretix_sig1"
|
with transaction.atomic():
|
||||||
assign_ticket_secret(self.event, self.op1, force_invalidate=True, save=True)
|
self.event.settings.ticket_secret_generator = "pretix_sig1"
|
||||||
old_secret = self.op1.secret
|
assign_ticket_secret(self.event, self.op1, force_invalidate=True, save=True)
|
||||||
|
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):
|
||||||
@@ -3928,6 +3932,8 @@ class OrderChangeManagerTests(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)
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ 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
|
||||||
|
|
||||||
@@ -82,14 +81,9 @@ 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, monkeypatch_on_commit):
|
def test_webhook_trigger_event_specific(event, order, webhook, django_capture_on_commit_callbacks):
|
||||||
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'),
|
||||||
@@ -97,7 +91,7 @@ def test_webhook_trigger_event_specific(event, order, webhook, monkeypatch_on_co
|
|||||||
match_querystring=None, # https://github.com/getsentry/responses/issues/464
|
match_querystring=None, # https://github.com/getsentry/responses/issues/464
|
||||||
)
|
)
|
||||||
|
|
||||||
with transaction.atomic():
|
with django_capture_on_commit_callbacks(execute=True):
|
||||||
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)) == {
|
||||||
@@ -119,12 +113,12 @@ def test_webhook_trigger_event_specific(event, order, webhook, monkeypatch_on_co
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_webhook_trigger_global(event, order, webhook, monkeypatch_on_commit):
|
def test_webhook_trigger_global(event, order, webhook, django_capture_on_commit_callbacks):
|
||||||
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 transaction.atomic():
|
with django_capture_on_commit_callbacks(execute=True):
|
||||||
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)) == {
|
||||||
@@ -138,13 +132,13 @@ def test_webhook_trigger_global(event, order, webhook, monkeypatch_on_commit):
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_webhook_trigger_global_wildcard(event, order, webhook, monkeypatch_on_commit):
|
def test_webhook_trigger_global_wildcard(event, order, webhook, django_capture_on_commit_callbacks):
|
||||||
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 transaction.atomic():
|
with django_capture_on_commit_callbacks(execute=True):
|
||||||
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)) == {
|
||||||
@@ -158,30 +152,30 @@ def test_webhook_trigger_global_wildcard(event, order, webhook, monkeypatch_on_c
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_webhook_ignore_wrong_action_type(event, order, webhook, monkeypatch_on_commit):
|
def test_webhook_ignore_wrong_action_type(event, order, webhook, django_capture_on_commit_callbacks):
|
||||||
responses.add(responses.POST, 'https://google.com', status=200)
|
responses.add(responses.POST, 'https://google.com', status=200)
|
||||||
with transaction.atomic():
|
with django_capture_on_commit_callbacks(execute=True):
|
||||||
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, monkeypatch_on_commit):
|
def test_webhook_ignore_disabled(event, order, webhook, django_capture_on_commit_callbacks):
|
||||||
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 transaction.atomic():
|
with django_capture_on_commit_callbacks(execute=True):
|
||||||
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, monkeypatch_on_commit):
|
def test_webhook_ignore_wrong_event(event, order, webhook, django_capture_on_commit_callbacks):
|
||||||
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 transaction.atomic():
|
with django_capture_on_commit_callbacks(execute=True):
|
||||||
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
|
||||||
|
|
||||||
@@ -189,10 +183,10 @@ def test_webhook_ignore_wrong_event(event, order, webhook, monkeypatch_on_commit
|
|||||||
@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, monkeypatch_on_commit):
|
def test_webhook_retry(event, order, webhook, django_capture_on_commit_callbacks):
|
||||||
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 transaction.atomic():
|
with django_capture_on_commit_callbacks(execute=True):
|
||||||
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():
|
||||||
@@ -216,9 +210,9 @@ def test_webhook_retry(event, order, webhook, monkeypatch_on_commit):
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_webhook_disable_gone(event, order, webhook, monkeypatch_on_commit):
|
def test_webhook_disable_gone(event, order, webhook, django_capture_on_commit_callbacks):
|
||||||
responses.add(responses.POST, 'https://google.com', status=410)
|
responses.add(responses.POST, 'https://google.com', status=410)
|
||||||
with transaction.atomic():
|
with django_capture_on_commit_callbacks(execute=True):
|
||||||
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()
|
||||||
|
|||||||
@@ -131,3 +131,8 @@ def set_lock_namespaces(request):
|
|||||||
yield
|
yield
|
||||||
else:
|
else:
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def class_monkeypatch(request, monkeypatch):
|
||||||
|
request.cls.monkeypatch = monkeypatch
|
||||||
|
|||||||
@@ -385,11 +385,6 @@ 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):
|
||||||
|
|
||||||
|
|||||||
@@ -49,11 +49,6 @@ 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()
|
||||||
|
|||||||
@@ -1584,10 +1584,11 @@ class OrderChangeTests(SoupTest):
|
|||||||
'add_position-MAX_NUM_FORMS': '100',
|
'add_position-MAX_NUM_FORMS': '100',
|
||||||
'add_position-0-itemvar': str(self.shirt.pk),
|
'add_position-0-itemvar': str(self.shirt.pk),
|
||||||
'add_position-0-do': 'on',
|
'add_position-0-do': 'on',
|
||||||
|
'add_position-0-count': '2',
|
||||||
'add_position-0-price': '14.00',
|
'add_position-0-price': '14.00',
|
||||||
})
|
})
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
assert self.order.positions.count() == 3
|
assert self.order.positions.count() == 4
|
||||||
assert self.order.positions.last().item == self.shirt
|
assert self.order.positions.last().item == self.shirt
|
||||||
assert self.order.positions.last().price == 14
|
assert self.order.positions.last().price == 14
|
||||||
|
|
||||||
|
|||||||
@@ -33,11 +33,6 @@ 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()
|
||||||
|
|||||||
@@ -286,11 +286,6 @@ 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):
|
||||||
|
|||||||
@@ -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
|
from django.test import TestCase, TransactionTestCase
|
||||||
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,12 +60,6 @@ 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):
|
||||||
@@ -104,7 +98,6 @@ 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
|
||||||
@@ -4420,6 +4413,8 @@ 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
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
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
|
||||||
|
|
||||||
@@ -80,6 +81,34 @@ 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user