Compare commits

..
Author SHA1 Message Date
Phin Wolkwitz 11ef4c0e0f Add sendmail-rules tests 2026-08-07 17:33:05 +02:00
Phin Wolkwitz 13ce7cecb2 Fix id check 2026-08-07 17:32:40 +02:00
Phin Wolkwitz d031dffc6f Fix ids in tests 2026-08-07 16:40:22 +02:00
Phin Wolkwitz 9003c6483b Add combined ticket-add-on-sendmail-testcases 2026-08-07 15:56:55 +02:00
Phin Wolkwitz 20539aac78 Add simple add-on-sendmail-testcases 2026-08-07 15:35:46 +02:00
Phin Wolkwitz 3202f666b6 Fix erroneous else statement 2026-08-07 14:58:21 +02:00
Phin Wolkwitz c8c2206190 Fix id check 2026-08-07 14:43:11 +02:00
Phin Wolkwitz 9635c67c8a Revert unneeded order_bys 2026-08-07 14:21:09 +02:00
Phin Wolkwitz 30403319cb Revert unneeded order_bys 2026-08-07 14:21:09 +02:00
Phin Wolkwitz d3f9e80927 Simplify code 2026-08-07 14:21:09 +02:00
5d25b25187 Apply suggestions, remove superfluous comments and add a check
Co-authored-by: Richard Schreiber <wiffbi@gmail.com>
2026-08-07 14:21:09 +02:00
Phin Wolkwitz e05e6d25f9 Add order_by 2026-08-07 14:21:09 +02:00
Phin Wolkwitz 4f342be51f Remove linebreak 2026-08-07 14:21:09 +02:00
Phin Wolkwitz d2800f99c9 Fix and improve sendmail logic 2026-08-07 14:21:09 +02:00
Phin Wolkwitz ba4c0644c6 Fix and improve changed mail-rules logic 2026-08-07 14:21:09 +02:00
Phin Wolkwitz 1f56a46918 [wip] revert changes to orders.py 2026-08-07 14:21:09 +02:00
Phin Wolkwitz c5d34b76fe [wip] Change mail-rules logic accordingly 2026-08-07 14:21:09 +02:00
Phin Wolkwitz 7b0184caf8 Fix import sorting 2026-08-07 14:21:09 +02:00
Phin Wolkwitz 2364dace78 Improve QuerySet order 2026-08-07 14:21:09 +02:00
Phin Wolkwitz 3b0afd368d Reduce amount of mails sent to the same email-addresses, Use mail from parent position if necessary 2026-08-07 14:21:09 +02:00
Phin Wolkwitz 9fb2c43362 Remove restrictions that prevent mails to be sent to addon-product-attendees 2026-08-07 14:21:09 +02:00
22 changed files with 492 additions and 288 deletions
-2
View File
@@ -71,8 +71,6 @@ Checking a ticket in
:>json object questions: List of questions to be answered for check-in, only set on status ``"incomplete"``.
:>json object media_policy: Reusable media policy (see documentation on items), only set on status ``"exchange"``.
:>json object media_type: Reusable media type (see documentation on items), only set on status ``"exchange"``.
:>json boolean simulate: Do not actually perform the check-in, only simulate the response. The ``position`` response
object will not reflect the simulated changes.
**Example request**:
-1
View File
@@ -90,7 +90,6 @@ class CheckinRPCRedeemInputSerializer(serializers.Serializer):
answers = serializers.JSONField(required=False, allow_null=True)
exchange_medium_type = serializers.ChoiceField(required=False, choices=MEDIA_TYPES)
exchange_medium_identifier = serializers.CharField(required=False)
simulate = serializers.BooleanField(default=False, required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
-6
View File
@@ -839,11 +839,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
)
if exchange_medium_identifier: # other fields are filled, see CheckinRPCRedeemInputSerializer.validate
if simulate:
raise CheckInError(
gettext('You cannot simulate a medium exchange.'),
'error'
)
with transaction.atomic():
# Do exchange and check-in atomically, i.e. both succeed or both fail
medium = perform_media_exchange(
@@ -1071,7 +1066,6 @@ class CheckinRPCRedeemView(views.APIView):
legacy_url_support=False,
exchange_medium_type=s.validated_data.get('exchange_medium_type'),
exchange_medium_identifier=s.validated_data.get('exchange_medium_identifier'),
simulate=s.validated_data.get('simulate'),
)
@@ -1,63 +0,0 @@
# Generated by Django 4.2.17 on 2025-01-01 20:25
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0307_devicelastseen"),
]
operations = [
migrations.CreateModel(
name="CheckoutSession",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
("cart_id", models.CharField(max_length=255, unique=True)),
("created", models.DateTimeField(auto_now_add=True)),
("testmode", models.BooleanField(default=False)),
("session_data", models.JSONField(default=dict)),
(
"customer",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="checkout_sessions",
to="pretixbase.customer",
),
),
(
"event",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="checkout_sessions",
to="pretixbase.event",
),
),
(
"sales_channel",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="pretixbase.saleschannel",
),
),
],
),
migrations.AddField(
model_name="invoiceaddress",
name="checkout_session",
field=models.OneToOneField(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="invoice_address",
to="pretixbase.checkoutsession",
),
),
]
-40
View File
@@ -3177,39 +3177,6 @@ class Transaction(models.Model):
return self.tax_value_includes_rounding_correction * self.count
class CheckoutSession(models.Model):
"""
A checkout session optionally bundles cart positions with additional information. This is historically
not required in pretix and currently only used in the Storefront API.
"""
event = models.ForeignKey(
Event,
verbose_name=_("Event"),
related_name="checkout_sessions",
on_delete=models.CASCADE,
)
cart_id = models.CharField(
max_length=255, unique=True,
verbose_name=_("Cart ID (e.g. session key)"),
)
created = models.DateTimeField(
verbose_name=_("Date"),
auto_now_add=True,
)
customer = models.ForeignKey(
Customer,
related_name='checkout_sessions',
null=True, blank=True,
on_delete=models.SET_NULL,
)
sales_channel = models.ForeignKey(
"SalesChannel",
on_delete=models.CASCADE,
)
testmode = models.BooleanField(default=False)
session_data = models.JSONField(default=dict)
class CartPosition(AbstractPosition):
"""
A cart position is similar to an order line, except that it is not
@@ -3414,13 +3381,6 @@ class CartPosition(AbstractPosition):
class InvoiceAddress(models.Model):
last_modified = models.DateTimeField(auto_now=True)
checkout_session = models.OneToOneField(
CheckoutSession,
null=True,
blank=True,
related_name='invoice_address',
on_delete=models.CASCADE
)
order = models.OneToOneField(Order, null=True, blank=True, related_name='invoice_address', on_delete=models.CASCADE)
customer = models.ForeignKey(
Customer,
+1 -12
View File
@@ -61,7 +61,7 @@ from pretix.base.models import (
Seat, SeatCategoryMapping, Voucher,
)
from pretix.base.models.event import SubEvent
from pretix.base.models.orders import CheckoutSession, OrderFee
from pretix.base.models.orders import OrderFee
from pretix.base.models.tax import TaxRule
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.services.checkin import _save_answers
@@ -472,16 +472,6 @@ class CartManager:
if term_last < time_machine_now(self.real_now_dt):
raise CartError(error_messages['payment_ended'])
def _ensure_checkout_session(self):
CheckoutSession.objects.get_or_create(
event=self.event,
cart_id=self.cart_id,
defaults={
"sales_channel": self._sales_channel,
"testmode": self.event.testmode,
},
)
def _extend_expiry_of_valid_existing_positions(self):
# real_now_dt is initialized at CartManager instantiation, so it's slightly in the past. Add a small
# delta to reduce risk of extending already expired CartPositions.
@@ -1569,7 +1559,6 @@ class CartManager:
def commit(self):
self._check_presale_dates()
self._ensure_checkout_session()
self._check_max_cart_size()
err = self._delete_out_of_timeframe()
+3 -4
View File
@@ -40,7 +40,7 @@ import dateutil
import dateutil.parser
from dateutil.tz import datetime_exists
from django.core.files import File
from django.db import IntegrityError
from django.db import IntegrityError, transaction
from django.db.models import (
BooleanField, Case, Count, ExpressionWrapper, F, IntegerField, Max, Min,
OuterRef, Q, Subquery, TextField, Value, When,
@@ -59,7 +59,6 @@ from pretix.base.models import (
)
from pretix.base.signals import checkin_created, periodic_task
from pretix.helpers import OF_SELF
from pretix.helpers.database import conditional_atomic
from pretix.helpers.jsonlogic import Logic
from pretix.helpers.jsonlogic_boolalg import convert_to_dnf
from pretix.helpers.jsonlogic_query import (
@@ -1044,10 +1043,10 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
if not simulate:
_save_answers(op, answers, given_answers)
with conditional_atomic(not simulate):
with transaction.atomic():
# Lock order positions, if it is an entry. We don't need it for exits, as a race condition wouldn't be problematic
opqs = OrderPosition.all.select_related("order", "item")
if type != Checkin.TYPE_EXIT and not simulate:
if type != Checkin.TYPE_EXIT:
opqs = opqs.select_for_update(of=OF_SELF)
op = opqs.get(pk=op.pk)
-5
View File
@@ -33,7 +33,6 @@ from pretix.base.models.customers import CustomerSSOGrant
from ..models import CachedFile, CartPosition, InvoiceAddress
from ..models.auth import UserKnownLoginSource
from ..models.orders import CheckoutSession
from ..signals import periodic_task
@@ -44,10 +43,6 @@ def clean_cart_positions(sender, **kwargs):
cp.delete()
for cp in CartPosition.objects.filter(expires__lt=now() - timedelta(days=14), addon_to__isnull=True):
cp.delete()
for cs in CheckoutSession.objects.filter(created__lt=now() - timedelta(days=14)).exclude(
Exists(CartPosition.objects.filter(cart_id=OuterRef("cart_id")))
):
cs.delete()
for ia in InvoiceAddress.objects.filter(order__isnull=True, customer__isnull=True, last_modified__lt=now() - timedelta(days=14)):
ia.delete()
+5 -9
View File
@@ -73,7 +73,7 @@ from pretix.base.models import (
)
from pretix.base.models.event import SubEvent
from pretix.base.models.orders import (
BlockedTicketSecret, CheckoutSession, InvoiceAddress, OrderFee, OrderRefund,
BlockedTicketSecret, InvoiceAddress, OrderFee, OrderRefund,
generate_secret,
)
from pretix.base.models.organizer import SalesChannel, TeamAPIToken
@@ -1030,8 +1030,7 @@ def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: Li
def _create_order(event: Event, *, email: str, positions: List[CartPosition], now_dt: datetime,
payment_requests: List[dict], sales_channel: SalesChannel, locale: str=None,
address: InvoiceAddress=None, meta_info: dict=None, shown_total=None,
customer=None, valid_if_pending=False, api_meta: dict=None, tax_rounding_mode=None,
cart_id: str=None):
customer=None, valid_if_pending=False, api_meta: dict=None, tax_rounding_mode=None):
payments = []
try:
@@ -1114,8 +1113,6 @@ def _create_order(event: Event, *, email: str, positions: List[CartPosition], no
if meta_info:
for msg in meta_info.get('confirm_messages', []):
order.log_action('pretix.event.order.consent', data={'msg': msg})
if cart_id:
CheckoutSession.objects.filter(event=event, cart_id=cart_id).delete()
order_placed.send(event, order=order, bulk=False)
return order, payments
@@ -1163,7 +1160,7 @@ def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosi
def _perform_order(event: Event, payment_requests: List[dict], position_ids: List[str],
email: str, locale: str, address: int, meta_info: dict=None, sales_channel: str='web',
shown_total=None, customer=None, api_meta: dict=None, tax_rounding_mode=None, cart_id: str=None):
shown_total=None, customer=None, api_meta: dict=None, tax_rounding_mode=None):
for p in payment_requests:
p['pprov'] = event.get_payment_providers(cached=True)[p['provider']]
if not p['pprov']:
@@ -1270,7 +1267,6 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
valid_if_pending=valid_if_pending,
api_meta=api_meta,
tax_rounding_mode=tax_rounding_mode,
cart_id=cart_id,
)
try:
@@ -3173,12 +3169,12 @@ class OrderChangeManager:
def perform_order(self, event: Event, payments: List[dict], positions: List[str],
email: str=None, locale: str=None, address: int=None, meta_info: dict=None,
sales_channel: str='web', shown_total=None, customer=None, override_now_dt: datetime=None,
api_meta: dict=None, cart_id: str=None):
api_meta: dict=None):
with language(locale), time_machine_now_assigned(override_now_dt):
try:
try:
return _perform_order(event, payments, positions, email, locale, address, meta_info,
sales_channel, shown_total, customer, api_meta, cart_id=cart_id)
sales_channel, shown_total, customer, api_meta)
except LockTimeoutException:
self.retry()
except (MaxRetriesExceededError, LockTimeoutException):
-9
View File
@@ -288,15 +288,6 @@ def get_deterministic_ordering(model, ordering):
return ordering
@contextlib.contextmanager
def conditional_atomic(do_atomic, **kwargs):
if do_atomic:
with transaction.atomic(**kwargs):
yield
else:
yield
class IgnoreOnSQLiteMixin:
# Mixin to allow defining PostgreSQL-specific indexes that will just not be created
# on SQLite. SQLite is supported for testing only anyways!
+2 -17
View File
@@ -23,7 +23,7 @@ import json
import logging
import urllib.parse
from collections import OrderedDict
from datetime import datetime, timedelta
from datetime import timedelta
from decimal import Decimal
from django import forms
@@ -645,7 +645,7 @@ class PaypalMethod(BasePaymentProvider):
def _execute_payment(self, request: HttpRequest, payment: OrderPayment):
payment = OrderPayment.objects.select_for_update(of=OF_SELF).get(pk=payment.pk)
if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
# payment is already confirmed; possible return-view/webhook race-condition
logger.warning('payment is already confirmed; possible return-view/webhook race-condition')
return
try:
@@ -832,7 +832,6 @@ class PaypalMethod(BasePaymentProvider):
payment.info = json.dumps(pp_captured_order.dict())
payment.save(update_fields=['info'])
payment.confirm()
self.log_payment_duration(payment)
except Quota.QuotaExceededException as e:
raise PaymentException(str(e))
# Payment has not any captures yet - so it's probably in created status
@@ -842,20 +841,6 @@ class PaypalMethod(BasePaymentProvider):
if 'payment_paypal_oid' in request.session:
del request.session['payment_paypal_oid']
@staticmethod
def log_payment_duration(payment: OrderPayment):
try:
capture = payment.info_data["purchase_units"][0]["payments"]["captures"][0]
create_time: str | None = capture["create_time"]
update_time: str | None = capture["update_time"]
except (KeyError, IndexError, TypeError):
create_time = None
update_time = None
if create_time is not None and update_time is not None:
duration = datetime.fromisoformat(update_time) - datetime.fromisoformat(create_time)
logger.info('{}: {} - paypal payment processing time'.format(str(payment.global_id), str(duration)))
def payment_pending_render(self, request, payment) -> str:
retry = True
try:
-1
View File
@@ -490,7 +490,6 @@ def webhook(request, *args, **kwargs):
payment.info = json.dumps(sale.dict())
payment.save(update_fields=['info'])
payment.confirm()
prov.log_payment_duration(payment)
except Quota.QuotaExceededException:
pass
elif sale['status'] == 'APPROVED':
+11 -1
View File
@@ -158,7 +158,12 @@ class OrderMailForm(BaseMailForm):
),
label=pgettext_lazy('sendmail_form', 'Restrict to products'),
required=True,
queryset=Item.objects.none()
queryset=Item.objects.none(),
help_text=pgettext_lazy(
'sendmail_form',
'There may be multiple mails sent out to the same mail address if one order contains multiple attendee '
'products for it, if you restrict to products while also restricting mails to attendees only. '
'This is intended, as every one of those get linked to their own separate order page restricted to only that product.')
)
filter_checkins = forms.BooleanField(
label=_('Filter check-in status'),
@@ -371,6 +376,11 @@ class RuleForm(FormPlaceholderMixin, I18nModelForm):
del self.fields['subevent']
self.fields['limit_products'].queryset = Item.objects.filter(event=self.event)
self.fields['limit_products'].help_text = pgettext_lazy(
'sendmail_form',
'There may be multiple mails sent out to the same mail address if one order contains multiple attendee '
'products for it, if you restrict to products while also restricting mails to attendees only. '
'This is intended, as every one of those get linked to their own separate order page restricted to only that product.')
self.fields['schedule_type'] = forms.ChoiceField(
label=_('Type of schedule time'),
+22 -2
View File
@@ -187,13 +187,32 @@ class ScheduledMail(models.Model):
o_sent = True
if send_to_attendees:
if not self.rule.all_products:
positions = [p for p in positions if p.item_id in limit_products]
if self.subevent_id:
positions = [p for p in positions if p.subevent_id == self.subevent_id]
parent_op = None
sent_to_positions = set()
for p in positions:
if p.addon_to_id is None:
parent_op = p
if not self.rule.all_products and p.id not in position_ids:
continue
if p.id in position_ids:
if p.addon_to_id:
if not parent_op or parent_op.id != p.addon_to_id:
# something got mixed up with this order as addons should always come after their parent-position
continue
if not p.attendee_email:
if p.addon_to_id in sent_to_positions:
continue
else:
p = parent_op
# with attendee-email but same as parent's and sent to parent
elif parent_op.attendee_email and p.attendee_email == parent_op.attendee_email and parent_op.pk in sent_to_positions:
continue
if p.attendee_email and (p.attendee_email != o.email or not o_sent):
email_ctx = get_email_context(
event=e,
@@ -205,6 +224,7 @@ class ScheduledMail(models.Model):
p.send_mail(self.rule.subject, self.rule.template, email_ctx,
attach_ical=self.rule.attach_ical,
log_entry_type='pretix.plugins.sendmail.rule.order.position.email.sent')
sent_to_positions.add(p.id)
elif not o_sent and o.email:
email_ctx = get_email_context(
event=e,
+66 -24
View File
@@ -70,6 +70,8 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
except InvoiceAddress.DoesNotExist:
ia = InvoiceAddress(order=o)
parent_op = None
sent_to_positions = set()
if recipients in ('both', 'attendees'):
for p in o.positions.annotate(
any_checkins=Exists(
@@ -85,10 +87,13 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
)
),
).prefetch_related('addons', 'subevent'):
if p.addon_to_id is not None:
continue
if p.item_id not in items and not any(a.item_id in items for a in p.addons.all()):
is_addon = p.addon_to_id is not None
if not is_addon:
parent_op = p
if p.item_id not in items:
continue
if filter_checkins:
@@ -99,12 +104,25 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
if not allowed:
continue
send_to_parent = False
if not p.attendee_email:
if recipients == 'attendees':
send_to_order = True
if is_addon:
if p.addon_to_id in sent_to_positions:
continue
elif parent_op and parent_op.id == p.addon_to_id and parent_op.attendee_email:
send_to_parent = True
else:
send_to_order = True
continue
else:
send_to_order = True
continue
# add-on's attendee-email is the same as parent's and sent to parent
elif is_addon and p.addon_to_id in sent_to_positions and p.attendee_email == parent_op.attendee_email:
continue
if p.attendee_email == o.email and send_to_order:
if p.attendee_email and p.attendee_email == o.email and send_to_order:
continue
if subevent and p.subevent_id != subevent:
@@ -117,26 +135,50 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
continue
with language(o.locale, event.settings.region):
email_context = get_email_context(event=event, order=o, invoice_address=ia, position=p)
outgoing_mail = mail(
p.attendee_email,
subject,
message,
email_context,
event,
locale=o.locale,
order=o,
position=p,
attach_tickets=attach_tickets,
attach_ical=attach_ical,
attach_cached_files=attachments
)
if outgoing_mail:
o.log_action(
'pretix.plugins.sendmail.order.email.sent.attendee',
user=user,
data=outgoing_mail.log_data(),
if send_to_parent:
email_context = get_email_context(event=event, order=o, invoice_address=ia, position=parent_op)
outgoing_mail = mail(
parent_op.attendee_email,
subject,
message,
email_context,
event,
locale=o.locale,
order=o,
position=parent_op,
attach_tickets=attach_tickets,
attach_ical=attach_ical,
attach_cached_files=attachments
)
if outgoing_mail:
o.log_action(
'pretix.plugins.sendmail.order.email.sent.attendee',
user=user,
data=outgoing_mail.log_data(),
)
sent_to_positions.add(parent_op.id)
else:
email_context = get_email_context(event=event, order=o, invoice_address=ia, position=p)
outgoing_mail = mail(
p.attendee_email,
subject,
message,
email_context,
event,
locale=o.locale,
order=o,
position=p,
attach_tickets=attach_tickets,
attach_ical=attach_ical,
attach_cached_files=attachments
)
if outgoing_mail:
o.log_action(
'pretix.plugins.sendmail.order.email.sent.attendee',
user=user,
data=outgoing_mail.log_data(),
)
sent_to_positions.add(p.id)
if send_to_order and o.email:
with language(o.locale, event.settings.region):
-1
View File
@@ -1660,7 +1660,6 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
customer=self.cart_session.get('customer'),
override_now_dt=time_machine_now(default=None),
api_meta=api_meta,
cart_id=get_or_create_cart_id(request),
)
def get_success_message(self, value):
-1
View File
@@ -174,7 +174,6 @@ class OrderPositionDetailMixin(NoSearchIndexViewMixin):
def position(self):
qs = OrderPosition.objects.filter(
order__event=self.request.event,
addon_to__isnull=True,
order__code=self.kwargs['order'],
positionid=self.kwargs['position']
).select_related('order', 'order__event')
-25
View File
@@ -1,25 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
def readonly_db(execute, sql, params, many, context):
if not sql.lower().startswith("select"):
raise Exception(f"Should not write anything to the database, but detected query: {sql}")
return execute(sql, params, many, context)
-40
View File
@@ -25,7 +25,6 @@ from unittest import mock
import pytest
from django.core.files.base import ContentFile
from django.db import connection
from django.utils.timezone import now
from django_countries.fields import Country
from django_scopes import scopes_disabled
@@ -37,7 +36,6 @@ from pretix.api.serializers.item import QuestionSerializer
from pretix.base.models import (
Checkin, InvoiceAddress, Item, Order, OrderPosition, ReusableMedium,
)
from pretix.testutils.db import readonly_db
# Lots of this code is overlapping with test_checkin.py, and some of it is arguably redundant since it's triggering
# the same backend code paths (for now). However, this is SUCH a critical part of pretix that we don't want to take
@@ -1741,41 +1739,3 @@ def test_exchange_create_gift_card(token_client, organizer, clist, event, order,
with scopes_disabled():
rm = ReusableMedium.objects.get(identifier="0412345")
assert rm.linked_giftcard.currency == "EUR"
@pytest.mark.django_db
def test_simulate(token_client, organizer, clist, event, order):
with scopes_disabled():
p = order.positions.first()
with connection.execute_wrapper(readonly_db):
resp = _redeem(token_client, organizer, clist, p.secret, {"simulate": True})
assert resp.status_code == 201
assert resp.data['status'] == 'ok'
with scopes_disabled():
assert not p.checkins.exists()
@pytest.mark.django_db
def test_simulate_no_exchange(token_client, organizer, clist, event, order, item):
organizer.settings.reusable_media_type_nfc_uid = True
item.media_type = "nfc_uid"
item.media_policy = Item.MEDIA_POLICY_NEW
item.save()
with scopes_disabled():
rm = ReusableMedium.objects.create(
type="nfc_uid",
identifier="12345678",
organizer=organizer,
)
with connection.execute_wrapper(readonly_db):
resp = _redeem(token_client, organizer, clist, "z3fsn8jyufm5kpk768q69gkbyr5f4h6w", {
"source_type": "barcode",
"exchange_medium_type": "nfc_uid",
"exchange_medium_identifier": "12345678",
"simulate": True,
})
assert resp.status_code == 400
assert resp.data['status'] == 'error'
assert resp.data['reason'] == 'error'
with scopes_disabled():
assert not rm.linked_orderpositions.exists()
-25
View File
@@ -224,28 +224,3 @@ def test_one_view(logged_in_client, url, expected, event, item, item_category, o
)
response = logged_in_client.get(url)
assert response.status_code == expected
# Do not reintroduce any CSP nonces into control responses, as discussed in PR #6387
if response['Content-Type'] != 'application/json':
assert 'script-src' in response['Content-Security-Policy']
assert 'nonce-' not in response['Content-Security-Policy']
@pytest.mark.parametrize('url', [
'/control/login',
'/',
'/{orga}/{event}/',
])
@pytest.mark.django_db
def test_csp_header_unauthenticated(client, url, event):
# Do not reintroduce any CSP nonces into most presale responses, as discussed in PR #6387
with scope(organizer=event.organizer):
url = url.format(
event=event.slug, orga=event.organizer.slug,
)
event.live = True
event.save()
response = client.get(url)
assert response.status_code == 200
assert 'script-src' in response['Content-Security-Policy']
assert 'nonce-' not in response['Content-Security-Policy']
+76
View File
@@ -197,6 +197,82 @@ def test_sendmail_rule_send_order_vs_pos(send_to, amount_mails, recipients, orde
assert set(recipients) == set(_recipients)
@pytest.mark.django_db
@pytest.mark.parametrize('send_to,amount_mails,recipients,ticket_mail,addon_mail, products', [
(Rule.ATTENDEES, 1, ['addon-attendee@dummy.test'], 'attendee@dummy.test', 'addon-attendee@dummy.test', 'addon'),
(Rule.ATTENDEES, 2, ['attendee@dummy.test', 'addon-attendee@dummy.test'], 'attendee@dummy.test',
'addon-attendee@dummy.test', 'both'),
(Rule.ATTENDEES, 1, ['attendee@dummy.test'], 'attendee@dummy.test', 'attendee@dummy.test', 'both'),
(Rule.ATTENDEES, 1, ['attendee@dummy.test'], 'attendee@dummy.test', None, 'addon'),
(Rule.ATTENDEES, 1, ['attendee@dummy.test'], 'attendee@dummy.test', None, 'both'),
(Rule.ATTENDEES, 1, ['dummy@dummy.test'], None, None, 'addon'),
(Rule.ATTENDEES, 1, ['dummy@dummy.test'], None, None, 'both'),
(Rule.ATTENDEES, 2, ['dummy@dummy.test', 'addon-attendee@dummy.test'], None, 'addon-attendee@dummy.test', 'both'),
])
@scopes_disabled()
def test_sendmail_rule_send_addons(send_to, amount_mails, recipients, ticket_mail, addon_mail, products, order,
event, pos, item, item2):
djmail.outbox = []
order.status = order.STATUS_PAID
order.save()
p = pos
p.attendee_email = ticket_mail
p.save()
order.all_positions.create(item=item2, price=0, attendee_email=addon_mail, addon_to=p)
rule = order.event.sendmail_rules.create(date_is_absolute=True, send_date=dt_now - datetime.timedelta(hours=1),
send_to=send_to, subject='meow', template='meow meow meow',
all_products=False)
if products == 'addon':
rule.limit_products.set([item2])
if products == 'both':
rule.limit_products.set([item, item2])
sendmail_run_rules(None)
assert len(djmail.outbox) == amount_mails
_recipients = [mail.to[0] for mail in djmail.outbox]
assert set(recipients) == set(_recipients)
@pytest.mark.django_db
@pytest.mark.parametrize('send_to,amount_mails,recipients,ticket_mail,addon_mail, products', [
(Rule.ATTENDEES, 2, ['attendee@dummy.test', 'addon-attendee@dummy.test'], 'attendee@dummy.test',
'addon-attendee@dummy.test', 'addon'),
(Rule.ATTENDEES, 2, ['attendee@dummy.test', 'addon-attendee@dummy.test'], 'attendee@dummy.test',
'addon-attendee@dummy.test', 'both'),
])
@scopes_disabled()
def test_sendmail_rule_send_addons_one_unp(send_to, amount_mails, recipients, ticket_mail, addon_mail, products, order,
event, pos, item, item2):
djmail.outbox = []
order.status = order.STATUS_PAID
order.save()
p = pos
p.attendee_email = ticket_mail
p.save()
order.all_positions.create(item=item2, price=0, attendee_email=addon_mail, addon_to=p)
order.all_positions.create(item=item2, price=0, addon_to=p)
rule = order.event.sendmail_rules.create(date_is_absolute=True, send_date=dt_now - datetime.timedelta(hours=1),
send_to=send_to, subject='meow', template='meow meow meow',
all_products=False)
if products == 'addon':
rule.limit_products.set([item2])
if products == 'both':
rule.limit_products.set([item, item2])
sendmail_run_rules(None)
assert len(djmail.outbox) == amount_mails
_recipients = [mail.to[0] for mail in djmail.outbox]
assert set(recipients) == set(_recipients)
@pytest.mark.django_db
@scopes_disabled()
def test_sendmail_rule_send_attendees_unset_mail(order, event, item):
+306
View File
@@ -406,6 +406,312 @@ def test_sendmail_attendee_product_filter(logged_in_client, sendmail_url, event,
assert '/order/' not in djmail.outbox[0].body
@pytest.mark.django_db
def test_sendmail_attendee_addon_filter(logged_in_client, sendmail_url, event, order, pos):
event.settings.attendee_emails_asked = True
with scopes_disabled():
addon = Item.objects.create(name='Test addon', event=event, default_price=12)
p = pos
p.attendee_email = 'attendee1@dummy.test'
p.save()
order.positions.create(
item=addon, price=0, attendee_email='add-on-attendee@dummy.test', addon_to=p
)
djmail.outbox = []
response = logged_in_client.post(sendmail_url + 'orders/',
{'sendto': 'na',
'action': 'send',
'recipients': 'attendees',
'items': addon.pk,
'subject_0': 'Test subject',
'message_0': 'This is a test file for sending mails.',
},
follow=True)
assert response.status_code == 200
assert 'alert-success' in response.rendered_content
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == ['add-on-attendee@dummy.test']
assert '/ticket/' in djmail.outbox[0].body
assert '/order/' not in djmail.outbox[0].body
@pytest.mark.django_db
def test_sendmail_attendee_ticket_and_addon_filter(logged_in_client, sendmail_url, event, order, pos):
event.settings.attendee_emails_asked = True
with scopes_disabled():
addon = Item.objects.create(name='Test addon', event=event, default_price=12)
p = pos
p.attendee_email = 'attendee1@dummy.test'
p.save()
order.positions.create(
item=addon, price=0, attendee_email='add-on-attendee@dummy.test', addon_to=p
)
djmail.outbox = []
response = logged_in_client.post(sendmail_url + 'orders/',
{'sendto': 'na',
'action': 'send',
'recipients': 'attendees',
'items': {addon.pk, p.item_id},
'subject_0': 'Test subject',
'message_0': 'This is a test file for sending mails.',
},
follow=True)
assert response.status_code == 200
assert 'alert-success' in response.rendered_content
assert len(djmail.outbox) == 2
for msg in djmail.outbox:
assert msg.to in [['attendee1@dummy.test'], ['add-on-attendee@dummy.test']]
assert '/ticket/' in msg.body
assert '/order/' not in msg.body
@pytest.mark.django_db
def test_sendmail_attendee_ticket_and_same_addon_filter(logged_in_client, sendmail_url, event, order, pos):
event.settings.attendee_emails_asked = True
with scopes_disabled():
addon = Item.objects.create(name='Test addon', event=event, default_price=12)
p = pos
p.attendee_email = 'attendee1@dummy.test'
p.save()
order.positions.create(
item=addon, price=0, attendee_email='attendee1@dummy.test', addon_to=p
)
djmail.outbox = []
response = logged_in_client.post(sendmail_url + 'orders/',
{'sendto': 'na',
'action': 'send',
'recipients': 'attendees',
'items': {addon.pk, p.item_id},
'subject_0': 'Test subject',
'message_0': 'This is a test file for sending mails.',
},
follow=True)
assert response.status_code == 200
assert 'alert-success' in response.rendered_content
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == ['attendee1@dummy.test']
assert '/ticket/' in djmail.outbox[0].body
assert '/order/' not in djmail.outbox[0].body
@pytest.mark.django_db
def test_sendmail_attendee_addon_unpersonalized_filter(logged_in_client, sendmail_url, event, order, pos):
event.settings.attendee_emails_asked = True
with scopes_disabled():
addon = Item.objects.create(name='Test addon', event=event, default_price=12)
p = pos
p.attendee_email = 'attendee1@dummy.test'
p.save()
order.positions.create(
item=addon, price=0, addon_to=p
)
djmail.outbox = []
response = logged_in_client.post(sendmail_url + 'orders/',
{'sendto': 'na',
'action': 'send',
'recipients': 'attendees',
'items': addon.pk,
'subject_0': 'Test subject',
'message_0': 'This is a test file for sending mails.',
},
follow=True)
assert response.status_code == 200
assert 'alert-success' in response.rendered_content
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == ['attendee1@dummy.test']
assert '/ticket/' in djmail.outbox[0].body
assert '/order/' not in djmail.outbox[0].body
@pytest.mark.django_db
def test_sendmail_attendee_ticket_and_addon_unp_filter(logged_in_client, sendmail_url, event, order, pos):
event.settings.attendee_emails_asked = True
with scopes_disabled():
addon = Item.objects.create(name='Test addon', event=event, default_price=12)
p = pos
p.attendee_email = 'attendee1@dummy.test'
p.save()
order.positions.create(
item=addon, price=0, addon_to=p
)
djmail.outbox = []
response = logged_in_client.post(sendmail_url + 'orders/',
{'sendto': 'na',
'action': 'send',
'recipients': 'attendees',
'items': {addon.pk, p.item_id},
'subject_0': 'Test subject',
'message_0': 'This is a test file for sending mails.',
},
follow=True)
assert response.status_code == 200
assert 'alert-success' in response.rendered_content
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == ['attendee1@dummy.test']
assert '/ticket/' in djmail.outbox[0].body
assert '/order/' not in djmail.outbox[0].body
@pytest.mark.django_db
def test_sendmail_attendee_ticket_unp_and_addon_filter(logged_in_client, sendmail_url, event, order, pos):
event.settings.attendee_emails_asked = True
with scopes_disabled():
addon = Item.objects.create(name='Test addon', event=event, default_price=12)
order.positions.create(
item=addon, price=0, attendee_email='add-on-attendee@dummy.test', addon_to=pos
)
djmail.outbox = []
response = logged_in_client.post(sendmail_url + 'orders/',
{'sendto': 'na',
'action': 'send',
'recipients': 'attendees',
'items': {addon.pk, pos.item_id},
'subject_0': 'Test subject',
'message_0': 'This is a test file for sending mails.',
},
follow=True)
assert response.status_code == 200
assert 'alert-success' in response.rendered_content
assert len(djmail.outbox) == 2
for msg in djmail.outbox:
assert msg.to in [[order.email], ['add-on-attendee@dummy.test']]
if msg.to == [order.email]:
assert '/ticket/' not in msg.body
assert '/order/' in msg.body
else:
assert msg.to == ['add-on-attendee@dummy.test']
assert '/ticket/' in msg.body
assert '/order/' not in msg.body
@pytest.mark.django_db
def test_sendmail_attendee_addon_unp_unp_filter(logged_in_client, sendmail_url, event, order, pos):
event.settings.attendee_emails_asked = True
with scopes_disabled():
addon = Item.objects.create(name='Test addon', event=event, default_price=12)
order.positions.create(
item=addon, price=0, addon_to=pos
)
djmail.outbox = []
response = logged_in_client.post(sendmail_url + 'orders/',
{'sendto': 'na',
'action': 'send',
'recipients': 'attendees',
'items': addon.pk,
'subject_0': 'Test subject',
'message_0': 'This is a test file for sending mails.',
},
follow=True)
assert response.status_code == 200
assert 'alert-success' in response.rendered_content
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == [order.email]
assert '/ticket/' not in djmail.outbox[0].body
assert '/order/' in djmail.outbox[0].body
@pytest.mark.django_db
def test_sendmail_attendee_and_addon_unp_unp_filter(logged_in_client, sendmail_url, event, order, pos):
event.settings.attendee_emails_asked = True
with scopes_disabled():
addon = Item.objects.create(name='Test addon', event=event, default_price=12)
order.positions.create(
item=addon, price=0, addon_to=pos
)
djmail.outbox = []
response = logged_in_client.post(sendmail_url + 'orders/',
{'sendto': 'na',
'action': 'send',
'recipients': 'attendees',
'items': {addon.pk, pos.item_id},
'subject_0': 'Test subject',
'message_0': 'This is a test file for sending mails.',
},
follow=True)
assert response.status_code == 200
assert 'alert-success' in response.rendered_content
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == [order.email]
assert '/ticket/' not in djmail.outbox[0].body
assert '/order/' in djmail.outbox[0].body
@pytest.mark.django_db
def test_sendmail_attendee_two_addons_one_unp_filter(logged_in_client, sendmail_url, event, order, pos):
event.settings.attendee_emails_asked = True
with scopes_disabled():
p = pos
p.attendee_email = 'attendee1@dummy.test'
p.save()
addon = Item.objects.create(name='Test addon', event=event, default_price=12)
order.positions.create(
item=addon, price=0, attendee_email='add-on-attendee@dummy.test', addon_to=p
)
order.positions.create(
item=addon, price=0, addon_to=p
)
djmail.outbox = []
response = logged_in_client.post(sendmail_url + 'orders/',
{'sendto': 'na',
'action': 'send',
'recipients': 'attendees',
'items': addon.pk,
'subject_0': 'Test subject',
'message_0': 'This is a test file for sending mails.',
},
follow=True)
assert response.status_code == 200
assert 'alert-success' in response.rendered_content
assert len(djmail.outbox) == 2
for msg in djmail.outbox:
assert msg.to in [['attendee1@dummy.test'], ['add-on-attendee@dummy.test']]
assert '/ticket/' in msg.body
assert '/order/' not in msg.body
@pytest.mark.django_db
def test_sendmail_attendee_and_two_addons_one_unp_filter(logged_in_client, sendmail_url, event, order, pos):
event.settings.attendee_emails_asked = True
with scopes_disabled():
p = pos
p.attendee_email = 'attendee1@dummy.test'
p.save()
addon = Item.objects.create(name='Test addon', event=event, default_price=12)
order.positions.create(
item=addon, price=0, attendee_email='add-on-attendee@dummy.test', addon_to=p
)
order.positions.create(
item=addon, price=0, addon_to=p
)
djmail.outbox = []
response = logged_in_client.post(sendmail_url + 'orders/',
{'sendto': 'na',
'action': 'send',
'recipients': 'attendees',
'items': {addon.pk, p.item_id},
'subject_0': 'Test subject',
'message_0': 'This is a test file for sending mails.',
},
follow=True)
assert response.status_code == 200
assert 'alert-success' in response.rendered_content
assert len(djmail.outbox) == 2
for msg in djmail.outbox:
assert msg.to in [['attendee1@dummy.test'], ['add-on-attendee@dummy.test']]
assert '/ticket/' in msg.body
assert '/order/' not in msg.body
@pytest.mark.django_db
def test_sendmail_attendee_subevent_filter(logged_in_client, sendmail_url, event, item, order, pos):
event.settings.attendee_emails_asked = True