Compare commits

..

9 Commits

Author SHA1 Message Date
Kian Cross
fbd8bbbeaa Disable partitioned cookies for Safari due to WebKit bugs (#5843)
Safari currently exhibits a bug where Partitioned cookies (CHIPS) are not
sent back to the originating site after multi-hop cross-site redirects,
breaking SSO login flows in pretix.

Partitioned cookies were initially introduced in Safari 18.4, removed
again in 18.5 due to a bug, and reintroduced in Safari 26.2, where the
current issue is present.

As a mitigation, disable sending the `Partitioned` attribute for Safari
user agents. This is intentionally conservative; once the Safari issue
is fixed, this check should be refined to be conditional on the affected
versions only.

WebKit issues:

  - https://bugs.webkit.org/show_bug.cgi?id=292975
  - https://bugs.webkit.org/show_bug.cgi?id=306194
2026-02-18 09:19:14 +01:00
Kara Engelhardt
1c305e4b30 Store failed offline checkin if successful online checkin with same nonce exists 2026-02-17 10:41:05 +01:00
KarlKeu00
ea114b4f64 Fix HTML closing tags in pending.html (#5893) 2026-02-17 10:20:28 +01:00
dependabot[bot]
0342613635 Update fakeredis requirement from ==2.33.* to ==2.34.* (#5899)
Updates the requirements on [fakeredis](https://github.com/cunla/fakeredis-py) to permit the latest version.
- [Release notes](https://github.com/cunla/fakeredis-py/releases)
- [Commits](https://github.com/cunla/fakeredis-py/compare/v2.33.0...v2.34.0)

---
updated-dependencies:
- dependency-name: fakeredis
  dependency-version: 2.34.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-17 10:16:35 +01:00
dependabot[bot]
743c4b796b Update sentry-sdk requirement from ==2.52.* to ==2.53.* (#5898)
Updates the requirements on [sentry-sdk](https://github.com/getsentry/sentry-python) to permit the latest version.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.52.0a1...2.53.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.53.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-17 10:16:27 +01:00
Raphael Michel
8a7f54795e Vouchers: Fix field label inconsistency (Z#23222887) (#5902)
The field Voucher.price_mode is sometimes called "Price mode" and
sometimes "Price effect" in the UI, which is inconsistent. I think
"price effect" is a little clearer, but I don't really care as long as
it is consistent.
2026-02-17 10:16:12 +01:00
Raphael Michel
cb464ad597 Remove back link from 404 error page (#23222967) (#5901)
I've kept it for 400/403/500/csrffail for now, because they also have a
"try again" link. Yes, both things have browser buttons, but they make
it a *little* clearer to technical users what one could to next, and
especially on csrffail, "step back" is always possible and possibly actually
helpful.
2026-02-17 10:16:05 +01:00
Raphael Michel
119cc50897 Fix inconsistent singular/plural use in text (Z#23223585) 2026-02-17 09:31:08 +01:00
Raphael Michel
61f9cf13b4 Order change: Fix list of unchangeable add-ons not filtered to category (Z#23223330) (#5876) 2026-02-16 15:13:24 +01:00
23 changed files with 208 additions and 124 deletions

View File

@@ -92,7 +92,7 @@ dependencies = [
"redis==7.1.*",
"reportlab==4.4.*",
"requests==2.32.*",
"sentry-sdk==2.52.*",
"sentry-sdk==2.53.*",
"sepaxml==2.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",
@@ -110,7 +110,7 @@ dev = [
"aiohttp==3.13.*",
"coverage",
"coveralls",
"fakeredis==2.33.*",
"fakeredis==2.34.*",
"flake8==7.3.*",
"freezegun",
"isort==7.0.*",

View File

@@ -188,11 +188,15 @@ class CheckinListViewSet(viewsets.ModelViewSet):
clist = self.get_object()
if serializer.validated_data.get('nonce'):
if kwargs.get('position'):
prev = kwargs['position'].all_checkins.filter(nonce=serializer.validated_data['nonce']).first()
prev = kwargs['position'].all_checkins.filter(
nonce=serializer.validated_data['nonce'],
successful=False
).first()
else:
prev = clist.checkins.filter(
nonce=serializer.validated_data['nonce'],
raw_barcode=serializer.validated_data['raw_barcode'],
successful=False
).first()
if prev:
# Ignore because nonce is already handled

View File

@@ -33,7 +33,8 @@ from pretix.base.invoicing.transmission import (
transmission_types,
)
from pretix.base.models import Invoice, InvoiceAddress
from pretix.base.services.mail import mail
from pretix.base.services.mail import mail, render_mail
from pretix.helpers.format import format_map
@transmission_types.new()
@@ -133,7 +134,9 @@ class EmailTransmissionProvider(TransmissionProvider):
subject = invoice.order.event.settings.get('mail_subject_order_invoice', as_type=LazyI18nString)
# Do not set to completed because that is done by the email sending task
outgoing_mail = mail(
subject = format_map(subject, context)
email_content = render_mail(template, context)
mail(
[recipient],
subject,
template,
@@ -148,10 +151,19 @@ class EmailTransmissionProvider(TransmissionProvider):
plain_text_only=True,
no_order_links=True,
)
if outgoing_mail:
invoice.order.log_action(
'pretix.event.order.email.invoice',
user=None,
auth=None,
data=outgoing_mail.log_data()
)
invoice.order.log_action(
'pretix.event.order.email.invoice',
user=None,
auth=None,
data={
'subject': subject,
'message': email_content,
'position': None,
'recipient': recipient,
'invoices': [invoice.pk],
'attach_tickets': False,
'attach_ical': False,
'attach_other_files': [],
'attach_cached_files': [],
}
)

View File

@@ -132,7 +132,7 @@ class AllowIgnoreQuotaColumn(BooleanColumnMixin, ImportColumn):
class PriceModeColumn(ImportColumn):
identifier = 'price_mode'
verbose_name = gettext_lazy('Price mode')
verbose_name = gettext_lazy('Price effect')
default_value = None
initial = 'static:none'
@@ -147,7 +147,7 @@ class PriceModeColumn(ImportColumn):
elif value in reverse:
return reverse[value]
else:
raise ValidationError(_("Could not parse {value} as a price mode, use one of {options}.").format(
raise ValidationError(_("Could not parse {value} as a price effect, use one of {options}.").format(
value=value, options=', '.join(d.keys())
))
@@ -162,7 +162,7 @@ class ValueColumn(DecimalColumnMixin, ImportColumn):
def clean(self, value, previous_values):
value = super().clean(value, previous_values)
if value and previous_values.get("price_mode") == "none":
raise ValidationError(_("It is pointless to set a value without a price mode."))
raise ValidationError(_("It is pointless to set a value without a price effect."))
return value
def assign(self, value, obj: Voucher, **kwargs):

View File

@@ -220,20 +220,3 @@ class OutgoingMail(models.Model):
error_log_action_type = 'pretix.email.error'
log_target = None
return log_target, error_log_action_type
def log_data(self):
return {
"subject": self.subject,
"message": self.body_plain,
"to": self.to,
"cc": self.cc,
"bcc": self.bcc,
"invoices": [i.pk for i in self.should_attach_invoices.all()],
"attach_tickets": self.should_attach_tickets,
"attach_ical": self.should_attach_ical,
"attach_other_files": self.should_attach_other_files,
"attach_cached_files": [cf.filename for cf in self.should_attach_cached_files.all()],
"position": self.orderposition.positionid if self.orderposition else None,
}

View File

@@ -87,6 +87,7 @@ from pretix.base.timemachine import time_machine_now
from ...helpers import OF_SELF
from ...helpers.countries import CachedCountries, FastCountryField
from ...helpers.format import FormattedString, format_map
from ...helpers.names import build_name
from ...testutils.middleware import debugflags_var
from ._transactions import (
@@ -1166,7 +1167,7 @@ class Order(LockModel, LoggedModel):
only be attached for this position and child positions, the link will only point to the
position and the attendee email will be used if available.
"""
from pretix.base.services.mail import mail
from pretix.base.services.mail import mail, render_mail
if not self.email and not (position and position.attendee_email):
return
@@ -1176,20 +1177,32 @@ class Order(LockModel, LoggedModel):
if position and position.attendee_email:
recipient = position.attendee_email
outgoing_mail = mail(
email_content = render_mail(template, context)
if not isinstance(subject, FormattedString):
subject = format_map(subject, context)
mail(
recipient, subject, template, context,
self.event, self.locale, self, headers=headers, sender=sender,
invoices=invoices, attach_tickets=attach_tickets,
position=position, auto_email=auto_email, attach_ical=attach_ical,
attach_other_files=attach_other_files, attach_cached_files=attach_cached_files,
)
if outgoing_mail:
self.log_action(
log_entry_type,
user=user,
auth=auth,
data=outgoing_mail.log_data(),
)
self.log_action(
log_entry_type,
user=user,
auth=auth,
data={
'subject': subject,
'message': email_content,
'position': position.positionid if position else None,
'recipient': recipient,
'invoices': [i.pk for i in invoices] if invoices else [],
'attach_tickets': attach_tickets,
'attach_ical': attach_ical,
'attach_other_files': attach_other_files,
'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [],
}
)
def resend_link(self, user=None, auth=None):
with language(self.locale, self.event.settings.region):
@@ -2887,14 +2900,17 @@ class OrderPosition(AbstractPosition):
:param attach_tickets: Attach tickets of this order, if they are existing and ready to download
:param attach_ical: Attach relevant ICS files
"""
from pretix.base.services.mail import mail
from pretix.base.services.mail import mail, render_mail
if not self.attendee_email:
return
with language(self.order.locale, self.order.event.settings.region):
recipient = self.attendee_email
outgoing_mail = mail(
email_content = render_mail(template, context)
if not isinstance(subject, FormattedString):
subject = format_map(subject, context)
mail(
recipient, subject, template, context,
self.event, self.order.locale, order=self.order, headers=headers, sender=sender,
position=self,
@@ -2903,13 +2919,21 @@ class OrderPosition(AbstractPosition):
attach_ical=attach_ical,
attach_other_files=attach_other_files,
)
if outgoing_mail:
self.order.log_action(
log_entry_type,
user=user,
auth=auth,
data=outgoing_mail.log_data(),
)
self.order.log_action(
log_entry_type,
user=user,
auth=auth,
data={
'subject': subject,
'message': email_content,
'recipient': recipient,
'invoices': [i.pk for i in invoices] if invoices else [],
'attach_tickets': attach_tickets,
'attach_ical': attach_ical,
'attach_other_files': attach_other_files,
'attach_cached_files': [],
}
)
def resend_link(self, user=None, auth=None):

View File

@@ -239,7 +239,7 @@ class Voucher(LoggedModel):
)
)
price_mode = models.CharField(
verbose_name=_("Price mode"),
verbose_name=_("Price effect"),
max_length=100,
choices=PRICE_MODES,
default='none'

View File

@@ -34,9 +34,10 @@ from phonenumber_field.modelfields import PhoneNumberField
from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import User, Voucher
from pretix.base.services.mail import mail
from pretix.base.services.mail import mail, render_mail
from pretix.helpers import OF_SELF
from ...helpers.format import format_map
from ...helpers.names import build_name
from .base import LoggedModel
from .event import Event, SubEvent
@@ -271,7 +272,9 @@ class WaitingListEntry(LoggedModel):
with language(self.locale, self.event.settings.region):
recipient = self.email
outgoing_mail = mail(
email_content = render_mail(template, context)
subject = format_map(subject, context)
mail(
recipient, subject, template, context,
self.event,
self.locale,
@@ -281,13 +284,18 @@ class WaitingListEntry(LoggedModel):
attach_other_files=attach_other_files,
attach_cached_files=attach_cached_files,
)
if outgoing_mail:
self.log_action(
log_entry_type,
user=user,
auth=auth,
data=outgoing_mail.log_data(),
)
self.log_action(
log_entry_type,
user=user,
auth=auth,
data={
'subject': subject,
'message': email_content,
'recipient': recipient,
'attach_other_files': attach_other_files,
'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [],
}
)
@staticmethod
def clean_itemvar(event, item, variation):

View File

@@ -1295,7 +1295,6 @@ class ManualPayment(BasePaymentProvider):
def format_map(self, order, payment):
return {
# Possible placeholder injection, we should make sure to never include user-controlled variables here
'order': order.code,
'amount': payment.amount,
'currency': self.event.currency,

View File

@@ -45,6 +45,7 @@ from pretix.base.services.tax import split_fee_for_taxes
from pretix.base.templatetags.money import money_filter
from pretix.celery_app import app
from pretix.helpers import OF_SELF
from pretix.helpers.format import format_map
logger = logging.getLogger(__name__)
@@ -54,7 +55,7 @@ def _send_wle_mail(wle: WaitingListEntry, subject: LazyI18nString, message: Lazy
email_context = get_email_context(event_or_subevent=subevent or wle.event, event=wle.event)
mail(
wle.email,
str(subject),
format_map(subject, email_context),
message,
email_context,
wle.event,
@@ -72,8 +73,9 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
email_context = get_email_context(event_or_subevent=subevent or order.event, refund_amount=refund_amount,
order=order, position_or_address=ia, event=order.event)
real_subject = format_map(subject, email_context)
order.send_mail(
subject, message, email_context,
real_subject, message, email_context,
'pretix.event.order.email.event_canceled',
user,
)
@@ -83,13 +85,14 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
continue
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
real_subject = format_map(subject, email_context)
email_context = get_email_context(event_or_subevent=p.subevent or order.event,
event=order.event,
refund_amount=refund_amount,
position_or_address=p,
order=order, position=p)
order.send_mail(
subject, message, email_context,
real_subject, message, email_context,
'pretix.event.order.email.event_canceled',
position=p,
user=user

View File

@@ -149,13 +149,13 @@ def prefix_subject(settings_holder, subject, highlight=False):
return subject
def mail(email: Union[str, Sequence[str]], subject: Union[str, FormattedString], template: Union[str, LazyI18nString],
def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any] = None, event: Event = None, locale: str = None, order: Order = None,
position: OrderPosition = None, *, headers: dict = None, sender: str = None, organizer: Organizer = None,
customer: Customer = None, invoices: Sequence = None, attach_tickets=False, auto_email=True, user=None,
attach_ical=False, attach_cached_files: Sequence = None, attach_other_files: list=None,
plain_text_only=False, no_order_links=False, cc: Sequence[str]=None, bcc: Sequence[str]=None,
sensitive: bool=False) -> Optional[OutgoingMail]:
sensitive: bool=False):
"""
Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation.
@@ -335,26 +335,14 @@ def mail(email: Union[str, Sequence[str]], subject: Union[str, FormattedString],
should_attach_other_files=attach_other_files or [],
sensitive=sensitive,
)
m._prefetched_objects_cache = {}
if invoices and not position:
m.should_attach_invoices.add(*invoices)
# Hack: For logging, we'll later make a `should_attach_invoices.all()` call. We can prevent a useless
# DB query by filling the cache
m._prefetched_objects_cache[m.should_attach_invoices.prefetch_cache_name] = invoices
else:
m._prefetched_objects_cache[m.should_attach_invoices.prefetch_cache_name] = Invoice.objects.none()
if attach_cached_files:
cf_list = []
for cf in attach_cached_files:
if not isinstance(cf, CachedFile):
cf = CachedFile.objects.get(pk=cf)
m.should_attach_cached_files.add(cf)
cf_list.append(cf)
# Hack: For logging, we'll later make a `should_attach_cached_files.all()` call. We can prevent a useless
# DB query by filling the cache
m._prefetched_objects_cache[m.should_attach_cached_files.prefetch_cache_name] = cf_list
else:
m._prefetched_objects_cache[m.should_attach_cached_files.prefetch_cache_name] = CachedFile.objects.none()
m.should_attach_cached_files.add(CachedFile.objects.get(pk=cf))
else:
m.should_attach_cached_files.add(cf)
send_task = mail_send_task.si(
outgoing_mail=m.id
@@ -376,8 +364,6 @@ def mail(email: Union[str, Sequence[str]], subject: Union[str, FormattedString],
lambda: chain(*task_chain).apply_async()
)
return m
class CustomEmail(EmailMultiAlternatives):
def _create_mime_attachment(self, content, mimetype):

View File

@@ -39,7 +39,7 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
with language(event.settings.locale):
email_context = get_email_context(event=event, name=r.get('name') or '',
voucher_list=[v.code for v in voucher_list])
outgoing_mail = mail(
mail(
r['email'],
subject,
LazyI18nString(message),
@@ -60,8 +60,8 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
data={
'recipient': r['email'],
'name': r.get('name'),
'subject': outgoing_mail.subject,
'message': outgoing_mail.body_plain,
'subject': subject,
'message': message,
},
save=False
))

View File

@@ -363,7 +363,7 @@ class EmailAddressShredder(BaseDataShredder):
le.save(update_fields=['data', 'shredded'])
else:
shred_log_fields(le, banlist=[
'recipient', 'message', 'subject', 'full_mail', 'old_email', 'new_email', 'bcc', 'cc',
'recipient', 'message', 'subject', 'full_mail', 'old_email', 'new_email'
])

View File

@@ -8,9 +8,6 @@
<h1>{% trans "Not found" %}</h1>
<p>{% trans "I'm afraid we could not find the the resource you requested." %}</p>
<p>{{ exception }}</p>
<p class="links">
<a id='goback' href='#'>{% trans "Take a step back" %}</a>
</p>
{% if request.user.is_staff and not staff_session %}
<form action="{% url 'control:user.sudo' %}?next={{ request.path|add:"?"|add:request.GET.urlencode|urlencode }}" method="post">
<p>

View File

@@ -24,9 +24,7 @@
{% if log.display %}
<br/><span class="fa fa-fw fa-comment-o"></span> {{ log.display }}
{% endif %}
{% if log.parsed_data.to %}
<br/><span class="fa fa-fw fa-envelope-o"></span> {{ log.parsed_data.to|join:", " }}
{% elif log.parsed_data.recipient %} {# legacy #}
{% if log.parsed_data.recipient %}
<br/><span class="fa fa-fw fa-envelope-o"></span> {{ log.parsed_data.recipient }}
{% endif %}
</p>

View File

@@ -2413,9 +2413,9 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
with language(order.locale, self.request.event.settings.region):
email_context = get_email_context(event=order.event, order=order)
email_template = LazyI18nString(form.cleaned_data['message'])
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
email_content = render_mail(email_template, email_context)
if self.request.POST.get('action') == 'preview':
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
email_content = render_mail(email_template, email_context)
self.preview_output = {
'subject': mark_safe(_('Subject: {subject}').format(
subject=prefix_subject(order.event, escape(email_subject), highlight=True)
@@ -2477,9 +2477,9 @@ class OrderPositionSendMail(OrderSendMail):
with language(position.order.locale, self.request.event.settings.region):
email_context = get_email_context(event=position.order.event, order=position.order, position=position)
email_template = LazyI18nString(form.cleaned_data['message'])
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
email_content = render_mail(email_template, email_context)
if self.request.POST.get('action') == 'preview':
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
email_content = render_mail(email_template, email_context)
self.preview_output = {
'subject': mark_safe(_('Subject: {subject}').format(
subject=prefix_subject(position.order.event, escape(email_subject), highlight=True))

View File

@@ -34,7 +34,10 @@ def set_cookie_without_samesite(request, response, key, *args, **kwargs):
if not is_secure:
# https://www.chromestatus.com/feature/5633521622188032
return
if should_send_same_site_none(request.headers.get('User-Agent', '')):
useragent = request.headers.get('User-Agent', '')
if should_send_same_site_none(useragent):
# Chromium is rolling out SameSite=Lax as a default
# https://www.chromestatus.com/feature/5088147346030592
# This however breaks all pretix-in-an-iframe things, such as the pretix Widget.
@@ -44,8 +47,29 @@ def set_cookie_without_samesite(request, response, key, *args, **kwargs):
# This will only work on secure cookies as well
# https://www.chromestatus.com/feature/5633521622188032
response.cookies[key]['secure'] = is_secure
# CHIPS
response.cookies[key]['Partitioned'] = True
if can_send_partitioned_cookie(useragent):
# CHIPS
response.cookies[key]['Partitioned'] = True
def can_send_partitioned_cookie(useragent):
# Safari currently exhibits a bug where Partitioned cookies (CHIPS) are not
# sent back to the originating site after multi-hop cross-site redirects,
# breaking SSO login flows in pretix.
#
# Partitioned cookies were initially introduced in Safari 18.4, removed
# again in 18.5 due to a bug, and reintroduced in Safari 26.2, where the
# current issue is present.
#
# Once the Safari issue is fixed, this check should be refined to be
# conditional on the affected versions only.
#
# WebKit issues:
#
# - https://bugs.webkit.org/show_bug.cgi?id=292975
# - https://bugs.webkit.org/show_bug.cgi?id=306194
return not is_safari(useragent)
# Based on https://www.chromium.org/updates/same-site/incompatible-clients

View File

@@ -21,10 +21,10 @@
<dt>{% trans "Reference code (important):" %}</dt><dd><b>{{ code }}</b></dd>
<dt>{% trans "Amount:" %}</dt><dd>{{ amount|money:event.currency }}</dd>
{% if settings.bank_details_type == "sepa" %}
<dt>{% trans "Account holder" %}:</dt><dd>{{ settings.bank_details_sepa_name }}</dt>
<dt>{% trans "IBAN" %}:</dt><dd>{{ settings.bank_details_sepa_iban|ibanformat }}</dt>
<dt>{% trans "BIC" %}:</dt><dd>{{ settings.bank_details_sepa_bic }}</dt>
<dt>{% trans "Bank" %}:</dt><dd>{{ settings.bank_details_sepa_bank }}</dt>
<dt>{% trans "Account holder" %}:</dt><dd>{{ settings.bank_details_sepa_name }}</dd>
<dt>{% trans "IBAN" %}:</dt><dd>{{ settings.bank_details_sepa_iban|ibanformat }}</dd>
<dt>{% trans "BIC" %}:</dt><dd>{{ settings.bank_details_sepa_bic }}</dd>
<dt>{% trans "Bank" %}:</dt><dd>{{ settings.bank_details_sepa_bank }}</dd>
{% endif %}
</dl>
{% if details %}
@@ -38,4 +38,4 @@
{% if payment_qr_codes %}
{% include "pretixpresale/event/payment_qr_codes.html" %}
{% endif %}
</div>
</div>

View File

@@ -38,10 +38,13 @@ from i18nfield.strings import LazyI18nString
from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import Checkin, Event, InvoiceAddress, Order, User
from pretix.base.models import (
CachedFile, Checkin, Event, InvoiceAddress, Order, User,
)
from pretix.base.services.mail import mail
from pretix.base.services.tasks import ProfiledEventTask
from pretix.celery_app import app
from pretix.helpers.format import format_map
def _chunks(lst, n):
@@ -61,6 +64,7 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
user = User.objects.get(pk=user) if user else None
subject = LazyI18nString(subject)
message = LazyI18nString(message)
attachments_for_log = [cf.filename for cf in CachedFile.objects.filter(pk__in=attachments)] if attachments else []
def _send_to_order(o):
send_to_order = recipients in ('both', 'orders')
@@ -118,7 +122,7 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
with language(o.locale, event.settings.region):
email_context = get_email_context(event=event, order=o, invoice_address=ia, position=p)
outgoing_mail = mail(
mail(
p.attendee_email,
subject,
message,
@@ -131,17 +135,25 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
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(),
)
o.log_action(
'pretix.plugins.sendmail.order.email.sent.attendee',
user=user,
data={
'position': p.positionid,
'subject': format_map(subject.localize(o.locale), email_context),
'message': format_map(message.localize(o.locale), email_context),
'recipient': p.attendee_email,
'attach_tickets': attach_tickets,
'attach_ical': attach_ical,
'attach_other_files': [],
'attach_cached_files': attachments_for_log,
}
)
if send_to_order and o.email:
with language(o.locale, event.settings.region):
email_context = get_email_context(event=event, order=o, invoice_address=ia)
outgoing_mail = mail(
mail(
o.email,
subject,
message,
@@ -153,12 +165,19 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
attach_ical=attach_ical,
attach_cached_files=attachments,
)
if outgoing_mail:
o.log_action(
'pretix.plugins.sendmail.order.email.sent',
user=user,
data=outgoing_mail.log_data(),
)
o.log_action(
'pretix.plugins.sendmail.order.email.sent',
user=user,
data={
'subject': format_map(subject.localize(o.locale), email_context),
'message': format_map(message.localize(o.locale), email_context),
'recipient': o.email,
'attach_tickets': attach_tickets,
'attach_ical': attach_ical,
'attach_other_files': [],
'attach_cached_files': attachments_for_log,
}
)
for chunk in _chunks(objects, 1000):
orders = Order.objects.filter(pk__in=chunk, event=event)

View File

@@ -1,14 +1,14 @@
{% extends "pretixpresale/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Resend order links" %}{% endblock %}
{% block title %}{% trans "Resend order link" %}{% endblock %}
{% block custom_header %}
{{ block.super }}
<meta name="robots" content="noindex, nofollow">
{% endblock %}
{% block content %}
<h2>
{% trans "Resend order links" %}
{% trans "Resend order link" %}
</h2>
<p>
{% blocktrans trimmed %}

View File

@@ -1510,7 +1510,10 @@ class OrderChangeMixin:
'max_count': iao.max_count,
'iao': iao,
'items': [i for i in items if not i.require_voucher],
'items_missing': {k: v for k, v in current_addon_products_missing.items() if v},
'items_missing': {
k: v for k, v in current_addon_products_missing.items()
if v and k[0].category_id == iao.addon_category_id
},
})
return positions

View File

@@ -1177,6 +1177,30 @@ def test_store_failed(token_client, organizer, clist, event, order):
assert resp.status_code == 400
@pytest.mark.django_db
def test_store_failed_after_success(token_client, organizer, clist, event, order):
with scopes_disabled():
p = order.positions.first()
p.all_checkins.create(
type=Checkin.TYPE_ENTRY,
nonce='foobar',
successful=True,
list=clist,
raw_barcode=p.secret
)
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/failed_checkins/'.format(
organizer.slug, event.slug, clist.pk,
), {
'raw_barcode': p.secret,
'nonce': 'foobar',
'position': p.pk,
'error_reason': 'unpaid'
}, format='json')
assert resp.status_code == 201
with scopes_disabled():
assert Checkin.all.filter(position=p).count() == 2
@pytest.mark.django_db
def test_redeem_unknown(token_client, organizer, clist, event, order):
resp = _redeem(token_client, organizer, clist, 'unknown_secret', {'force': True})

View File

@@ -170,7 +170,7 @@ def test_price_mode_validation(event, item, user):
import_vouchers.apply(
args=(event.pk, inputfile_factory().id, settings, 'en', user.pk)
).get()
assert 'It is pointless to set a value without a price mode.' in str(excinfo.value)
assert 'It is pointless to set a value without a price effect.' in str(excinfo.value)
settings['price_mode'] = 'static:percent'
import_vouchers.apply(