mirror of
https://github.com/pretix/pretix.git
synced 2026-06-22 03:06:16 +00:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a7388aa0a0 | |||
| 166aa33b1b | |||
| c037fd865b | |||
| 12171e0665 | |||
| 444963e952 | |||
| a57810cf41 | |||
| 2e2e57d231 | |||
| fc7e8ea67a | |||
| 23d1673403 | |||
| 92d1830f3b | |||
| d411c36414 | |||
| 84e12fea32 | |||
| b6518449d6 | |||
| 50c99e1239 | |||
| e70452ee47 | |||
| 666b496ab4 | |||
| 8bd0665f37 | |||
| ed1459b1dd | |||
| 8c251029b9 | |||
| 531f697b9a | |||
| 719ad7104d | |||
| dcb0eb765f | |||
| 86b5191e8b | |||
| b0714886bc | |||
| 438f70c730 | |||
| 608b150bf8 | |||
| c0df7c6142 | |||
| b2ea172a60 | |||
| a2cef22ea8 | |||
| 3843448812 | |||
| 49893ca9df | |||
| 4eade5070e | |||
| 32b1997208 | |||
| eaf4a310f6 | |||
| 8dc0f7c1b2 | |||
| dd3e6c4692 |
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM python:3.11-bookworm
|
||||
FROM python:3.13-trixie
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
|
||||
+3
-3
@@ -76,7 +76,7 @@ dependencies = [
|
||||
"paypal-checkout-serversdk==1.0.*",
|
||||
"PyJWT==2.12.*",
|
||||
"phonenumberslite==9.0.*",
|
||||
"Pillow==12.1.*",
|
||||
"Pillow==12.2.*",
|
||||
"pretix-plugin-build",
|
||||
"protobuf==7.34.*",
|
||||
"psycopg2-binary",
|
||||
@@ -90,10 +90,10 @@ dependencies = [
|
||||
"pytz-deprecation-shim==0.1.*",
|
||||
"pyuca",
|
||||
"qrcode==8.2",
|
||||
"redis==7.1.*",
|
||||
"redis==7.4.*",
|
||||
"reportlab==4.4.*",
|
||||
"requests==2.32.*",
|
||||
"sentry-sdk==2.56.*",
|
||||
"sentry-sdk==2.57.*",
|
||||
"sepaxml==2.7.*",
|
||||
"stripe==7.9.*",
|
||||
"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
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
__version__ = "2026.3.0.dev0"
|
||||
__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 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
|
||||
SafeWorkbook, remove_invalid_excel_chars as excel_safe,
|
||||
)
|
||||
@@ -59,11 +60,20 @@ class BaseExporter:
|
||||
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.organizer = organizer
|
||||
self.progress_callback = progress_callback
|
||||
self.is_multievent = isinstance(event, QuerySet)
|
||||
self.permission_holder = permission_holder
|
||||
if isinstance(event, QuerySet):
|
||||
self.events = event
|
||||
self.event = None
|
||||
@@ -180,7 +190,7 @@ class BaseExporter:
|
||||
return True
|
||||
|
||||
@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
|
||||
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")
|
||||
|
||||
@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
|
||||
allow everyone with any access to the organizer.
|
||||
|
||||
@@ -70,6 +70,10 @@ def parse_csv(file, length=None, mode="strict", charset=None):
|
||||
except ImportError:
|
||||
charset = file.charset
|
||||
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 '\r' in data and '\n' not in data:
|
||||
data = data.replace('\r', '\n')
|
||||
|
||||
@@ -29,7 +29,9 @@ import inspect
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
import django
|
||||
from django.conf import settings
|
||||
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:
|
||||
# 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.
|
||||
for frame in inspect.stack():
|
||||
if 'pretix/base/models/orders' in frame.filename:
|
||||
for frame in inspect.stack()[1:]:
|
||||
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
|
||||
elif 'test_' in frame.filename or 'conftest.py in frame.filename':
|
||||
elif 'test_' in frame.filename or 'conftest.py' in frame.filename:
|
||||
return
|
||||
elif 'pretix/' in frame.filename or 'pretix_' in frame.filename:
|
||||
# This went through non-test code, let's consider it non-test
|
||||
|
||||
@@ -38,6 +38,7 @@ import operator
|
||||
import secrets
|
||||
from datetime import timedelta
|
||||
from functools import reduce
|
||||
from typing import Protocol
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import (
|
||||
@@ -67,6 +68,14 @@ class EmailAddressTakenError(IntegrityError):
|
||||
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):
|
||||
"""
|
||||
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()
|
||||
|
||||
|
||||
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):
|
||||
user = models.ForeignKey('User', on_delete=models.CASCADE, related_name="known_login_sources")
|
||||
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()
|
||||
|
||||
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``
|
||||
to the event ``event``.
|
||||
@@ -238,6 +238,7 @@ class Device(LoggedModel):
|
||||
:param event: The event to check
|
||||
:param perm_name: The permission, e.g. ``event.orders:read``
|
||||
: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
|
||||
"""
|
||||
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 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 "
|
||||
"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:
|
||||
_transactions_mark_order_dirty(self.order_id, using=kwargs.get('using', 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 "
|
||||
"this.")
|
||||
|
||||
|
||||
@@ -319,6 +319,9 @@ class TeamQuerySet(models.QuerySet):
|
||||
def event_permission_q(cls, perm_name):
|
||||
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
|
||||
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)
|
||||
@@ -331,6 +334,9 @@ class TeamQuerySet(models.QuerySet):
|
||||
def organizer_permission_q(cls, perm_name):
|
||||
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
|
||||
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)
|
||||
@@ -550,7 +556,7 @@ class TeamAPIToken(models.Model):
|
||||
"""
|
||||
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``
|
||||
to the event ``event``.
|
||||
@@ -559,6 +565,7 @@ class TeamAPIToken(models.Model):
|
||||
:param event: The event to check
|
||||
:param perm_name: The permission, e.g. ``event.orders:read``
|
||||
: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
|
||||
"""
|
||||
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.contrib.staticfiles import finders
|
||||
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.dispatch import receiver
|
||||
from django.utils.deconstruct import deconstructible
|
||||
@@ -76,7 +76,7 @@ from reportlab.pdfgen.canvas import Canvas
|
||||
from reportlab.platypus import Paragraph
|
||||
|
||||
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.signals import layout_image_variables, layout_text_variables
|
||||
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)
|
||||
])
|
||||
}),
|
||||
("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", {
|
||||
"label": _("Organizer name"),
|
||||
"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)
|
||||
addons = [p for p in (
|
||||
addon_qs = (
|
||||
op.addons.all() if 'addons' in getattr(op, '_prefetched_objects_cache', {})
|
||||
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:
|
||||
itemcount[pos.item, pos.variation] += 1
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ from pretix.base.models import (
|
||||
CachedFile, Device, Event, Organizer, ScheduledEventExport, TeamAPIToken,
|
||||
User, cachedfile_name,
|
||||
)
|
||||
from pretix.base.models.auth import UserWithStaffSession
|
||||
from pretix.base.models.exports import ScheduledOrganizerExport
|
||||
from pretix.base.services.mail import mail
|
||||
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:
|
||||
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):
|
||||
continue
|
||||
@@ -243,7 +249,12 @@ def init_organizer_exporters(
|
||||
continue
|
||||
|
||||
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:
|
||||
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:
|
||||
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):
|
||||
continue
|
||||
|
||||
@@ -411,7 +411,7 @@ def mail_send_task(self, **kwargs) -> bool:
|
||||
try:
|
||||
outgoing_mail = OutgoingMail.objects.select_for_update(of=OF_SELF).get(pk=outgoing_mail)
|
||||
except OutgoingMail.DoesNotExist:
|
||||
logger.info(f"Ignoring job for non existing email {outgoing_mail.guid}")
|
||||
logger.info(f"Ignoring job for non existing email {outgoing_mail}")
|
||||
return False
|
||||
if outgoing_mail.status == OutgoingMail.STATUS_INFLIGHT:
|
||||
logger.info(f"Ignoring job for inflight email {outgoing_mail.guid}")
|
||||
|
||||
@@ -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.media import MEDIA_TYPES
|
||||
from pretix.base.models import (
|
||||
CartPosition, Device, Event, GiftCard, Item, ItemVariation, Membership,
|
||||
Order, OrderPayment, OrderPosition, Quota, Seat, SeatCategoryMapping, User,
|
||||
Voucher,
|
||||
CartPosition, Device, Event, GiftCard, Item, ItemVariation, LogEntry,
|
||||
Membership, Order, OrderPayment, OrderPosition, Quota, Seat,
|
||||
SeatCategoryMapping, User, Voucher,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.orders import (
|
||||
@@ -1618,7 +1618,7 @@ class OrderChangeManager:
|
||||
MembershipOperation = namedtuple('MembershipOperation', ('position', 'membership'))
|
||||
CancelOperation = namedtuple('CancelOperation', ('position', 'price_diff'))
|
||||
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',))
|
||||
FeeValueOperation = namedtuple('FeeValueOperation', ('fee', 'value', 'price_diff'))
|
||||
AddFeeOperation = namedtuple('AddFeeOperation', ('fee', 'price_diff'))
|
||||
@@ -1632,16 +1632,24 @@ class OrderChangeManager:
|
||||
ForceRecomputeOperation = namedtuple('ForceRecomputeOperation', tuple())
|
||||
|
||||
class AddPositionResult:
|
||||
_position: Optional[OrderPosition]
|
||||
_positions: Optional[List[OrderPosition]]
|
||||
|
||||
def __init__(self):
|
||||
self._position = None
|
||||
self._positions = None
|
||||
|
||||
@property
|
||||
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.")
|
||||
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):
|
||||
self.order = order
|
||||
@@ -1848,8 +1856,12 @@ class OrderChangeManager:
|
||||
|
||||
def add_position(self, item: Item, variation: ItemVariation, price: Decimal, addon_to: OrderPosition = 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 count > 1:
|
||||
raise ValueError("Cannot combine count > 1 with seat")
|
||||
if not seat:
|
||||
seat = None
|
||||
else:
|
||||
@@ -1903,14 +1915,14 @@ class OrderChangeManager:
|
||||
if self.order.event.settings.invoice_include_free or price.gross != Decimal('0.00'):
|
||||
self._invoice_dirty = True
|
||||
|
||||
self._totaldiff_guesstimate += price.gross
|
||||
self._quotadiff.update(new_quotas)
|
||||
self._totaldiff_guesstimate += price.gross * count
|
||||
self._quotadiff.update({q: count for q in new_quotas})
|
||||
if seat:
|
||||
self._seatdiff.update([seat])
|
||||
|
||||
result = self.AddPositionResult()
|
||||
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
|
||||
|
||||
def split(self, position: OrderPosition):
|
||||
@@ -2530,29 +2542,35 @@ class OrderChangeManager:
|
||||
secret_dirty.remove(position)
|
||||
position.save(update_fields=['canceled', 'secret'])
|
||||
elif isinstance(op, self.AddOperation):
|
||||
pos = OrderPosition.objects.create(
|
||||
item=op.item, variation=op.variation, addon_to=op.addon_to,
|
||||
price=op.price.gross, order=self.order, tax_rate=op.price.rate, tax_code=op.price.code,
|
||||
tax_value=op.price.tax, tax_rule=op.item.tax_rule,
|
||||
positionid=nextposid, subevent=op.subevent, seat=op.seat,
|
||||
used_membership=op.membership, valid_from=op.valid_from, valid_until=op.valid_until,
|
||||
is_bundled=op.is_bundled,
|
||||
)
|
||||
nextposid += 1
|
||||
self.order.log_action('pretix.event.order.changed.add', user=self.user, auth=self.auth, data={
|
||||
'position': pos.pk,
|
||||
'item': op.item.pk,
|
||||
'variation': op.variation.pk if op.variation else None,
|
||||
'addon_to': op.addon_to.pk if op.addon_to else None,
|
||||
'price': op.price.gross,
|
||||
'positionid': pos.positionid,
|
||||
'membership': pos.used_membership_id,
|
||||
'subevent': op.subevent.pk if op.subevent else None,
|
||||
'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,
|
||||
})
|
||||
op.result._position = pos
|
||||
new_pos = []
|
||||
new_logs = []
|
||||
for i in range(op.count):
|
||||
pos = OrderPosition.objects.create(
|
||||
item=op.item, variation=op.variation, addon_to=op.addon_to,
|
||||
price=op.price.gross, order=self.order, tax_rate=op.price.rate, tax_code=op.price.code,
|
||||
tax_value=op.price.tax, tax_rule=op.item.tax_rule,
|
||||
positionid=nextposid, subevent=op.subevent, seat=op.seat,
|
||||
used_membership=op.membership, valid_from=op.valid_from, valid_until=op.valid_until,
|
||||
is_bundled=op.is_bundled,
|
||||
)
|
||||
nextposid += 1
|
||||
new_pos.append(pos)
|
||||
new_logs.append(self.order.log_action('pretix.event.order.changed.add', user=self.user, auth=self.auth, data={
|
||||
'position': pos.pk,
|
||||
'item': op.item.pk,
|
||||
'variation': op.variation.pk if op.variation else None,
|
||||
'addon_to': op.addon_to.pk if op.addon_to else None,
|
||||
'price': op.price.gross,
|
||||
'positionid': pos.positionid,
|
||||
'membership': pos.used_membership_id,
|
||||
'subevent': op.subevent.pk if op.subevent else None,
|
||||
'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):
|
||||
position = position_cache.setdefault(op.position.pk, op.position)
|
||||
split_positions.append(position)
|
||||
@@ -2877,7 +2895,7 @@ class OrderChangeManager:
|
||||
return total
|
||||
|
||||
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(
|
||||
self.error_messages['max_order_size'] % {
|
||||
'max': settings.PRETIX_MAX_ORDER_SIZE,
|
||||
@@ -2938,7 +2956,7 @@ class OrderChangeManager:
|
||||
]) + len([
|
||||
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:
|
||||
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:
|
||||
fake_cart.remove(positions_to_fake_cart[op.position])
|
||||
elif isinstance(op, self.AddOperation):
|
||||
cp = CartPosition(
|
||||
event=self.event,
|
||||
item=op.item,
|
||||
variation=op.variation,
|
||||
used_membership=op.membership,
|
||||
subevent=op.subevent,
|
||||
seat=op.seat,
|
||||
)
|
||||
cp.override_valid_from = op.valid_from
|
||||
cp.override_valid_until = op.valid_until
|
||||
fake_cart.append(cp)
|
||||
for i in range(op.count):
|
||||
cp = CartPosition(
|
||||
event=self.event,
|
||||
item=op.item,
|
||||
variation=op.variation,
|
||||
used_membership=op.membership,
|
||||
subevent=op.subevent,
|
||||
seat=op.seat,
|
||||
)
|
||||
cp.override_valid_from = op.valid_from
|
||||
cp.override_valid_until = op.valid_until
|
||||
fake_cart.append(cp)
|
||||
try:
|
||||
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:
|
||||
|
||||
@@ -331,6 +331,10 @@ class OtherOperationsForm(forms.Form):
|
||||
|
||||
|
||||
class OrderPositionAddForm(forms.Form):
|
||||
count = forms.IntegerField(
|
||||
label=_('Number of products to add'),
|
||||
initial=1,
|
||||
)
|
||||
itemvar = forms.ChoiceField(
|
||||
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]
|
||||
else:
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -329,6 +329,7 @@
|
||||
{{ add_form.custom_error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% bootstrap_field add_form.count layout="control" %}
|
||||
{% bootstrap_field add_form.itemvar layout="control" %}
|
||||
{% bootstrap_field add_form.price addon_after=request.event.currency layout="control" %}
|
||||
{% if add_form.addon_to %}
|
||||
@@ -364,6 +365,7 @@
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<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.price addon_after=request.event.currency layout="control" %}
|
||||
{% if add_position_formset.empty_form.addon_to %}
|
||||
|
||||
@@ -2059,12 +2059,13 @@ class OrderChange(OrderView):
|
||||
else:
|
||||
variation = None
|
||||
try:
|
||||
ocm.add_position(item, variation,
|
||||
f.cleaned_data['price'],
|
||||
f.cleaned_data.get('addon_to'),
|
||||
f.cleaned_data.get('subevent'),
|
||||
f.cleaned_data.get('seat'),
|
||||
f.cleaned_data.get('used_membership'))
|
||||
for i in range(f.cleaned_data.get("count", 1)):
|
||||
ocm.add_position(item, variation,
|
||||
f.cleaned_data['price'],
|
||||
f.cleaned_data.get('addon_to'),
|
||||
f.cleaned_data.get('subevent'),
|
||||
f.cleaned_data.get('seat'),
|
||||
f.cleaned_data.get('used_membership'))
|
||||
except OrderError as e:
|
||||
f.custom_error = str(e)
|
||||
return False
|
||||
|
||||
@@ -1322,7 +1322,7 @@ class DeviceUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
|
||||
def form_valid(self, form):
|
||||
if form.has_changed():
|
||||
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
|
||||
})
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,7 @@ ausgecheckt
|
||||
ausgeklappt
|
||||
auswahl
|
||||
Authentication
|
||||
Authenticator
|
||||
Authenticator-App
|
||||
Autorisierungscode
|
||||
Autorisierungs-Endpunktes
|
||||
@@ -130,6 +131,7 @@ Eingangsscan
|
||||
Einlassbuchung
|
||||
Einlassdatum
|
||||
Einlasskontrolle
|
||||
Einmalpasswörter
|
||||
einzuchecken
|
||||
email
|
||||
E-Mail-Renderer
|
||||
@@ -163,6 +165,7 @@ Explorer
|
||||
FA
|
||||
Favicon
|
||||
F-Droid
|
||||
freeOTP
|
||||
Footer
|
||||
Footer-Link
|
||||
Footer-Text
|
||||
@@ -557,6 +560,7 @@ Zahlungs-ID
|
||||
Zahlungspflichtig
|
||||
Zehnerkarten
|
||||
Zeitbasiert
|
||||
zeitbasierte
|
||||
Zeitslotbuchung
|
||||
Zimpler
|
||||
ZIP-Datei
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,7 @@ ausgecheckt
|
||||
ausgeklappt
|
||||
auswahl
|
||||
Authentication
|
||||
Authenticator
|
||||
Authenticator-App
|
||||
Autorisierungscode
|
||||
Autorisierungs-Endpunktes
|
||||
@@ -130,6 +131,7 @@ Eingangsscan
|
||||
Einlassbuchung
|
||||
Einlassdatum
|
||||
Einlasskontrolle
|
||||
Einmalpasswörter
|
||||
einzuchecken
|
||||
email
|
||||
E-Mail-Renderer
|
||||
@@ -163,6 +165,7 @@ Explorer
|
||||
FA
|
||||
Favicon
|
||||
F-Droid
|
||||
freeOTP
|
||||
Footer
|
||||
Footer-Link
|
||||
Footer-Text
|
||||
@@ -557,6 +560,7 @@ Zahlungs-ID
|
||||
Zahlungspflichtig
|
||||
Zehnerkarten
|
||||
Zeitbasiert
|
||||
zeitbasierte
|
||||
Zeitslotbuchung
|
||||
Zimpler
|
||||
ZIP-Datei
|
||||
|
||||
+310
-288
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
|
||||
"POT-Creation-Date: 2026-03-30 11:25+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
|
||||
"PO-Revision-Date: 2026-03-18 12:23+0000\n"
|
||||
"PO-Revision-Date: 2026-03-30 03:00+0000\n"
|
||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
"js/es/>\n"
|
||||
@@ -329,7 +329,7 @@ msgstr "Pedido no aprobado"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
|
||||
msgid "Checked-in Tickets"
|
||||
msgstr "Registro de código QR"
|
||||
msgstr "Billetes registrados"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
|
||||
msgid "Valid Tickets"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ anonymized
|
||||
Auth
|
||||
authentification
|
||||
authenticator
|
||||
Authenticator
|
||||
automatical
|
||||
availabilities
|
||||
backend
|
||||
@@ -22,6 +23,7 @@ barcodes
|
||||
Bcc
|
||||
BCC
|
||||
BezahlCode
|
||||
biometric
|
||||
BLIK
|
||||
blocklist
|
||||
BN
|
||||
@@ -56,6 +58,7 @@ EPS
|
||||
eps
|
||||
favicon
|
||||
filetype
|
||||
freeOTP
|
||||
frontend
|
||||
frontpage
|
||||
Galician
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -296,7 +296,8 @@ class SetPasswordForm(forms.Form):
|
||||
}
|
||||
email = forms.EmailField(
|
||||
label=_('Email'),
|
||||
disabled=True
|
||||
widget=forms.EmailInput(attrs={'autocomplete': 'username', 'readonly': 'readonly'}),
|
||||
required=False,
|
||||
)
|
||||
password = forms.CharField(
|
||||
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 ;)
|
||||
request._checkout_flow_invoice_address = InvoiceAddress()
|
||||
return request._checkout_flow_invoice_address
|
||||
cs = cart_session(request)
|
||||
iapk = cs.get('invoice_address')
|
||||
if not iapk:
|
||||
cs = cart_session(request, create=False)
|
||||
if cs is None:
|
||||
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:
|
||||
iapk = cs.get('invoice_address')
|
||||
if not iapk:
|
||||
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
|
||||
|
||||
|
||||
@@ -111,6 +114,12 @@ class CartMixin:
|
||||
return cached_invoice_address(self.request)
|
||||
|
||||
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:
|
||||
prefetch = []
|
||||
if answers:
|
||||
@@ -166,7 +175,8 @@ class CartMixin:
|
||||
else:
|
||||
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])
|
||||
|
||||
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):
|
||||
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', []))
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
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.
|
||||
"""
|
||||
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]
|
||||
|
||||
|
||||
|
||||
+16
-1
@@ -157,7 +157,7 @@ DATABASES = {
|
||||
'HOST': config.get('database', 'host', fallback=''),
|
||||
'PORT': config.get('database', 'port', fallback=''),
|
||||
'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,
|
||||
'OPTIONS': db_options,
|
||||
'TEST': {}
|
||||
@@ -179,6 +179,21 @@ if config.has_section('replica'):
|
||||
}
|
||||
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/')
|
||||
|
||||
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')
|
||||
|
||||
# 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(
|
||||
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')
|
||||
|
||||
# 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(
|
||||
organizer.slug, event.slug
|
||||
))
|
||||
|
||||
@@ -991,3 +991,30 @@ def test_import_mixed_order_size_consistency(user, event, item):
|
||||
).get()
|
||||
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)
|
||||
|
||||
|
||||
@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
|
||||
from django.core import mail as djmail
|
||||
from django.db import transaction
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scope
|
||||
|
||||
@@ -75,47 +74,42 @@ def user(team):
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def monkeypatch_on_commit(monkeypatch):
|
||||
monkeypatch.setattr("django.db.transaction.on_commit", lambda t: t())
|
||||
|
||||
|
||||
@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 = []
|
||||
user.notification_settings.create(
|
||||
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', {})
|
||||
assert len(djmail.outbox) == 1
|
||||
assert djmail.outbox[0].subject.endswith("DUMMY: Order FOO has been marked as paid.")
|
||||
|
||||
|
||||
@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 = []
|
||||
user.notification_settings.create(
|
||||
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', {})
|
||||
assert len(djmail.outbox) == 1
|
||||
|
||||
|
||||
@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 = []
|
||||
user.notification_settings.create(
|
||||
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', {})
|
||||
assert len(djmail.outbox) == 1
|
||||
|
||||
|
||||
@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 = []
|
||||
user.notification_settings.create(
|
||||
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(
|
||||
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', {})
|
||||
assert len(djmail.outbox) == 0
|
||||
|
||||
|
||||
@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 = []
|
||||
user.notification_settings.create(
|
||||
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)
|
||||
assert len(djmail.outbox) == 0
|
||||
|
||||
|
||||
@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 = []
|
||||
team.all_event_permissions = False
|
||||
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(
|
||||
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', {})
|
||||
assert len(djmail.outbox) == 0
|
||||
|
||||
|
||||
@@ -28,8 +28,9 @@ from zoneinfo import ZoneInfo
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
from django.core import mail as djmail
|
||||
from django.db import transaction
|
||||
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_countries.fields import Country
|
||||
from django_scopes import scope
|
||||
@@ -1225,12 +1226,6 @@ class DownloadReminderTests(TestCase):
|
||||
assert len(djmail.outbox) == 0
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def class_monkeypatch(request, monkeypatch):
|
||||
request.cls.monkeypatch = monkeypatch
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("class_monkeypatch")
|
||||
class OrderCancelTests(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
@@ -1258,7 +1253,6 @@ class OrderCancelTests(TestCase):
|
||||
self.order.create_transactions()
|
||||
generate_invoice(self.order)
|
||||
djmail.outbox = []
|
||||
self.monkeypatch.setattr("django.db.transaction.on_commit", lambda t: t())
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_cancel_canceled(self):
|
||||
@@ -1351,14 +1345,14 @@ class OrderCancelTests(TestCase):
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
djmail.outbox = []
|
||||
cancel_order(self.order.pk, send_mail=True)
|
||||
print([s.subject for s in djmail.outbox])
|
||||
print([s.to for s in djmail.outbox])
|
||||
with self.captureOnCommitCallbacks(execute=True):
|
||||
cancel_order(self.order.pk, send_mail=True)
|
||||
|
||||
assert len(djmail.outbox) == 2
|
||||
assert ["invoice@example.org"] == djmail.outbox[0].to
|
||||
assert any(["Invoice_" in a[0] for a in djmail.outbox[0].attachments])
|
||||
assert ["dummy@dummy.test"] == djmail.outbox[1].to
|
||||
assert not any(["Invoice_" in a[0] for a in djmail.outbox[1].attachments])
|
||||
assert ["dummy@dummy.test"] == djmail.outbox[0].to
|
||||
assert not any(["Invoice_" in a[0] for a in djmail.outbox[0].attachments])
|
||||
assert ["invoice@example.org"] == djmail.outbox[1].to
|
||||
assert any(["Invoice_" in a[0] for a in djmail.outbox[1].attachments])
|
||||
|
||||
@classscope(attr='o')
|
||||
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()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("class_monkeypatch")
|
||||
class OrderChangeManagerTests(TestCase):
|
||||
class BaseOrderChangeManagerTestCase:
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
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_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.monkeypatch.setattr("django.db.transaction.on_commit", lambda t: t())
|
||||
|
||||
def _enable_reverse_charge(self):
|
||||
self.tr7.eu_reverse_charge = True
|
||||
@@ -1566,6 +1558,8 @@ class OrderChangeManagerTests(TestCase):
|
||||
country=Country('AT')
|
||||
)
|
||||
|
||||
|
||||
class OrderChangeManagerTests(BaseOrderChangeManagerTestCase, TestCase):
|
||||
@classscope(attr='o')
|
||||
def test_multiple_commits_forbidden(self):
|
||||
self.ocm.change_price(self.op1, Decimal('10.00'))
|
||||
@@ -2406,6 +2400,15 @@ class OrderChangeManagerTests(TestCase):
|
||||
self.ocm.commit()
|
||||
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')
|
||||
def test_add_item_addon(self):
|
||||
self.shirt.category = self.event.categories.create(name='Add-ons', is_addon=True)
|
||||
@@ -3895,15 +3898,16 @@ class OrderChangeManagerTests(TestCase):
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_set_valid_until(self):
|
||||
self.event.settings.ticket_secret_generator = "pretix_sig1"
|
||||
assign_ticket_secret(self.event, self.op1, force_invalidate=True, save=True)
|
||||
old_secret = self.op1.secret
|
||||
with transaction.atomic():
|
||||
self.event.settings.ticket_secret_generator = "pretix_sig1"
|
||||
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))
|
||||
self.ocm.change_valid_until(self.op1, dt)
|
||||
self.ocm.commit()
|
||||
self.op1.refresh_from_db()
|
||||
assert self.op1.secret != old_secret
|
||||
dt = make_aware(datetime(2022, 9, 20, 15, 0, 0, 0))
|
||||
self.ocm.change_valid_until(self.op1, dt)
|
||||
self.ocm.commit()
|
||||
self.op1.refresh_from_db()
|
||||
assert self.op1.secret != old_secret
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_unset_valid_from_until(self):
|
||||
@@ -3928,6 +3932,8 @@ class OrderChangeManagerTests(TestCase):
|
||||
assert len(djmail.outbox) == 1
|
||||
assert len(["Invoice_" in a[0] for a in djmail.outbox[0].attachments]) == 2
|
||||
|
||||
|
||||
class OrderChangeManagerTransactionalTests(BaseOrderChangeManagerTestCase, TransactionTestCase):
|
||||
@classscope(attr='o')
|
||||
def test_new_invoice_send_somewhere_else(self):
|
||||
generate_invoice(self.order)
|
||||
|
||||
@@ -25,7 +25,6 @@ from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from django.db import transaction
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
@@ -82,14 +81,9 @@ def force_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
|
||||
@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.POST, 'https://google.com',
|
||||
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
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
with django_capture_on_commit_callbacks(execute=True):
|
||||
le = order.log_action('pretix.event.order.paid', {})
|
||||
assert len(responses.calls) == 1
|
||||
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
|
||||
@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.all_events = True
|
||||
webhook.save()
|
||||
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', {})
|
||||
assert len(responses.calls) == 1
|
||||
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
|
||||
@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.limit_events.clear()
|
||||
webhook.all_events = True
|
||||
webhook.save()
|
||||
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', {})
|
||||
assert len(responses.calls) == 1
|
||||
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
|
||||
@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)
|
||||
with transaction.atomic():
|
||||
with django_capture_on_commit_callbacks(execute=True):
|
||||
order.log_action('pretix.event.order.changed.item', {})
|
||||
assert len(responses.calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@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.save()
|
||||
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', {})
|
||||
assert len(responses.calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@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()
|
||||
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', {})
|
||||
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.xfail(reason="retries can't be tested with celery_always_eager")
|
||||
@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=200)
|
||||
with transaction.atomic():
|
||||
with django_capture_on_commit_callbacks(execute=True):
|
||||
order.log_action('pretix.event.order.paid', {})
|
||||
assert len(responses.calls) == 2
|
||||
with scopes_disabled():
|
||||
@@ -216,9 +210,9 @@ def test_webhook_retry(event, order, webhook, monkeypatch_on_commit):
|
||||
|
||||
@pytest.mark.django_db
|
||||
@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)
|
||||
with transaction.atomic():
|
||||
with django_capture_on_commit_callbacks(execute=True):
|
||||
order.log_action('pretix.event.order.paid', {})
|
||||
assert len(responses.calls) == 1
|
||||
webhook.refresh_from_db()
|
||||
|
||||
@@ -131,3 +131,8 @@ def set_lock_namespaces(request):
|
||||
yield
|
||||
else:
|
||||
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)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def class_monkeypatch(request, monkeypatch):
|
||||
request.cls.monkeypatch = monkeypatch
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("class_monkeypatch")
|
||||
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
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def class_monkeypatch(request, monkeypatch):
|
||||
request.cls.monkeypatch = monkeypatch
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("class_monkeypatch")
|
||||
class EventsTest(SoupTest):
|
||||
@scopes_disabled()
|
||||
|
||||
@@ -1584,10 +1584,11 @@ class OrderChangeTests(SoupTest):
|
||||
'add_position-MAX_NUM_FORMS': '100',
|
||||
'add_position-0-itemvar': str(self.shirt.pk),
|
||||
'add_position-0-do': 'on',
|
||||
'add_position-0-count': '2',
|
||||
'add_position-0-price': '14.00',
|
||||
})
|
||||
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().price == 14
|
||||
|
||||
|
||||
@@ -33,11 +33,6 @@ from tests.base import SoupTest, extract_form_fields
|
||||
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")
|
||||
class OrganizerTest(SoupTest):
|
||||
@scopes_disabled()
|
||||
|
||||
@@ -286,11 +286,6 @@ class UserPasswordChangeTest(SoupTest):
|
||||
assert self.user.needs_password_change is False
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def class_monkeypatch(request, monkeypatch):
|
||||
request.cls.monkeypatch = monkeypatch
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("class_monkeypatch")
|
||||
class UserSettings2FATest(SoupTest):
|
||||
def setUp(self):
|
||||
|
||||
@@ -33,7 +33,7 @@ from django.conf import settings
|
||||
from django.core import mail as djmail
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
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.timezone import now
|
||||
from django_countries.fields import Country
|
||||
@@ -60,12 +60,6 @@ from pretix.testutils.sessions import get_cart_session_key
|
||||
from .test_timemachine import TimemachineTestMixin
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def class_monkeypatch(request, monkeypatch):
|
||||
request.cls.monkeypatch = monkeypatch
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("class_monkeypatch")
|
||||
class BaseCheckoutTestCase:
|
||||
@scopes_disabled()
|
||||
def setUp(self):
|
||||
@@ -104,7 +98,6 @@ class BaseCheckoutTestCase:
|
||||
self.workshopquota.items.add(self.workshop2)
|
||||
self.workshopquota.variations.add(self.workshop2a)
|
||||
self.workshopquota.variations.add(self.workshop2b)
|
||||
self.monkeypatch.setattr("django.db.transaction.on_commit", lambda t: t())
|
||||
|
||||
def _set_session(self, key, value):
|
||||
session = self.client.session
|
||||
@@ -4420,6 +4413,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
|
||||
assert len(djmail.outbox) == 1
|
||||
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):
|
||||
self.event.settings.invoice_address_asked = True
|
||||
self.event.settings.invoice_address_required = True
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
import datetime
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from importlib import import_module
|
||||
from json import loads
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
@@ -80,6 +81,34 @@ class EventMiddlewareTest(EventTestMixin, SoupTest):
|
||||
doc = self.get_doc('/%s/%s/' % (self.orga.slug, self.event.slug))
|
||||
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):
|
||||
resp = self.client.get('/%s/%s/' % ('foo', 'bar'))
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
Reference in New Issue
Block a user