mirror of
https://github.com/pretix/pretix.git
synced 2026-02-18 08:42:26 +00:00
Compare commits
20 Commits
move-reque
...
error-back
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd3821983f | ||
|
|
119cc50897 | ||
|
|
61f9cf13b4 | ||
|
|
f24429a7c5 | ||
|
|
29ed07ccce | ||
|
|
dd0cd7ab0b | ||
|
|
d7df906995 | ||
|
|
839f4b4657 | ||
|
|
74f7e1f61c | ||
|
|
47919afab0 | ||
|
|
819daa99f7 | ||
|
|
8512e79d68 | ||
|
|
52672ae25b | ||
|
|
ad752dc617 | ||
|
|
43c6c33bd8 | ||
|
|
88c9f8c047 | ||
|
|
2d2663f15f | ||
|
|
ae6014708b | ||
|
|
d1686df07c | ||
|
|
4d60d7bfbc |
@@ -39,7 +39,7 @@ from pretix.base.templatetags.rich_text import (
|
||||
DEFAULT_CALLBACKS, EMAIL_RE, URL_RE, abslink_callback,
|
||||
markdown_compile_email, truelink_callback,
|
||||
)
|
||||
from pretix.helpers.format import SafeFormatter, format_map
|
||||
from pretix.helpers.format import FormattedString, SafeFormatter, format_map
|
||||
|
||||
from pretix.base.services.placeholders import ( # noqa
|
||||
get_available_placeholders, PlaceholderContext
|
||||
@@ -141,6 +141,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
return markdown_compile_email(plaintext, context=context)
|
||||
|
||||
def render(self, plain_body: str, plain_signature: str, subject: str, order, position, context) -> str:
|
||||
apply_format_map = not isinstance(plain_body, FormattedString)
|
||||
body_md = self.compile_markdown(plain_body, context)
|
||||
if context:
|
||||
linker = bleach.Linker(
|
||||
@@ -149,12 +150,13 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
|
||||
parse_email=True
|
||||
)
|
||||
body_md = format_map(
|
||||
body_md,
|
||||
context=context,
|
||||
mode=SafeFormatter.MODE_RICH_TO_HTML,
|
||||
linkifier=linker
|
||||
)
|
||||
if apply_format_map:
|
||||
body_md = format_map(
|
||||
body_md,
|
||||
context=context,
|
||||
mode=SafeFormatter.MODE_RICH_TO_HTML,
|
||||
linkifier=linker
|
||||
)
|
||||
htmlctx = {
|
||||
'site': settings.PRETIX_INSTANCE_NAME,
|
||||
'site_url': settings.SITE_URL,
|
||||
|
||||
@@ -87,7 +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 format_map
|
||||
from ...helpers.format import FormattedString, format_map
|
||||
from ...helpers.names import build_name
|
||||
from ...testutils.middleware import debugflags_var
|
||||
from ._transactions import (
|
||||
@@ -1178,7 +1178,8 @@ class Order(LockModel, LoggedModel):
|
||||
recipient = position.attendee_email
|
||||
|
||||
email_content = render_mail(template, context)
|
||||
subject = format_map(subject, 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,
|
||||
@@ -2907,7 +2908,8 @@ class OrderPosition(AbstractPosition):
|
||||
with language(self.order.locale, self.order.event.settings.region):
|
||||
recipient = self.attendee_email
|
||||
email_content = render_mail(template, context)
|
||||
subject = format_map(subject, 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,
|
||||
|
||||
@@ -81,7 +81,9 @@ from pretix.base.signals import (
|
||||
)
|
||||
from pretix.celery_app import app
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.format import SafeFormatter, format_map
|
||||
from pretix.helpers.format import (
|
||||
FormattedString, PlainHtmlAlternativeString, SafeFormatter, format_map,
|
||||
)
|
||||
from pretix.helpers.hierarkey import clean_filename
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
from pretix.presale.ical import get_private_icals
|
||||
@@ -218,6 +220,9 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
if email == INVALID_ADDRESS:
|
||||
return
|
||||
|
||||
if isinstance(template, FormattedString):
|
||||
raise TypeError("Cannot pass an already formatted body template")
|
||||
|
||||
if no_order_links and not plain_text_only:
|
||||
raise ValueError('If you set no_order_links, you also need to set plain_text_only.')
|
||||
|
||||
@@ -251,23 +256,28 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
if event and attach_tickets and not event.settings.mail_attach_tickets:
|
||||
attach_tickets = False
|
||||
|
||||
with language(locale):
|
||||
with language(locale), override(timezone):
|
||||
if isinstance(context, dict) and order:
|
||||
_autoextend_context(context, order)
|
||||
|
||||
# Build raw content
|
||||
body_plain = render_mail(template, context, placeholder_mode=SafeFormatter.MODE_RICH_TO_PLAIN)
|
||||
content_plain = render_mail(template, context, placeholder_mode=None)
|
||||
if settings_holder:
|
||||
signature = str(settings_holder.settings.get('mail_text_signature'))
|
||||
else:
|
||||
signature = ""
|
||||
|
||||
# Build full plain-text body
|
||||
if not isinstance(content_plain, FormattedString):
|
||||
body_plain = format_map(content_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
|
||||
else:
|
||||
body_plain = content_plain
|
||||
body_plain = _wrap_plain_body(body_plain, signature, event, order, position, no_order_links)
|
||||
body_plain = format_map(body_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
|
||||
|
||||
# Build subject
|
||||
subject = str(subject).format_map(TolerantDict(context))
|
||||
if not isinstance(subject, FormattedString):
|
||||
subject = format_map(subject, context)
|
||||
|
||||
subject = raw_subject = subject.replace('\n', ' ').replace('\r', '')[:900]
|
||||
if settings_holder:
|
||||
subject = prefix_subject(settings_holder, subject)
|
||||
@@ -286,26 +296,24 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
else:
|
||||
renderer = ClassicMailRenderer(None, organizer)
|
||||
|
||||
with override(timezone):
|
||||
content_plain = render_mail(template, context, placeholder_mode=None)
|
||||
try:
|
||||
if 'context' in inspect.signature(renderer.render).parameters:
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order, position, context)
|
||||
elif 'position' in inspect.signature(renderer.render).parameters:
|
||||
# Backwards compatibility
|
||||
warnings.warn('Email renderer called without context argument because context argument is not '
|
||||
'supported.',
|
||||
DeprecationWarning)
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order, position)
|
||||
else:
|
||||
# Backwards compatibility
|
||||
warnings.warn('Email renderer called without position argument because position argument is not '
|
||||
'supported.',
|
||||
DeprecationWarning)
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order)
|
||||
except:
|
||||
logger.exception('Could not render HTML body')
|
||||
body_html = None
|
||||
try:
|
||||
if 'context' in inspect.signature(renderer.render).parameters:
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order, position, context)
|
||||
elif 'position' in inspect.signature(renderer.render).parameters:
|
||||
# Backwards compatibility
|
||||
warnings.warn('Email renderer called without context argument because context argument is not '
|
||||
'supported.',
|
||||
DeprecationWarning)
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order, position)
|
||||
else:
|
||||
# Backwards compatibility
|
||||
warnings.warn('Email renderer called without position argument because position argument is not '
|
||||
'supported.',
|
||||
DeprecationWarning)
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order)
|
||||
except:
|
||||
logger.exception('Could not render HTML body')
|
||||
body_html = None
|
||||
|
||||
m = OutgoingMail.objects.create(
|
||||
organizer=organizer,
|
||||
@@ -790,7 +798,12 @@ def render_mail(template, context, placeholder_mode: Optional[int]=SafeFormatter
|
||||
body = format_map(body, context, mode=placeholder_mode)
|
||||
else:
|
||||
tpl = get_template(template)
|
||||
body = tpl.render(context)
|
||||
context = {
|
||||
# Known bug, should behave differently for plain and HTML but we'll fix after security release
|
||||
k: v.html if isinstance(v, PlainHtmlAlternativeString) else v
|
||||
for k, v in context.items()
|
||||
}
|
||||
body = FormattedString(tpl.render(context))
|
||||
return body
|
||||
|
||||
|
||||
@@ -936,7 +949,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
|
||||
body_plain += "\r\n\r\n-- \r\n"
|
||||
|
||||
if signature:
|
||||
signature = signature.format(event=event.name if event else '')
|
||||
signature = format_map(signature, {"event": event.name if event else ''})
|
||||
body_plain += signature
|
||||
body_plain += "\r\n\r\n-- \r\n"
|
||||
|
||||
@@ -948,7 +961,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
|
||||
body_plain += _(
|
||||
"You can view your order details at the following URL:\n{orderurl}."
|
||||
).replace("\n", "\r\n").format(
|
||||
event=event.name, orderurl=build_absolute_uri(
|
||||
orderurl=build_absolute_uri(
|
||||
order.event, 'presale:event.order.position', kwargs={
|
||||
'order': order.code,
|
||||
'secret': position.web_secret,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
import logging
|
||||
from string import Formatter
|
||||
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.utils.html import conditional_escape
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -37,6 +38,17 @@ class PlainHtmlAlternativeString:
|
||||
return f"PlainHtmlAlternativeString('{self.plain}', '{self.html}')"
|
||||
|
||||
|
||||
class FormattedString(str):
|
||||
"""
|
||||
A str subclass that has been specifically marked as "already formatted" for email rendering
|
||||
purposes to avoid duplicate formatting.
|
||||
"""
|
||||
__slots__ = ()
|
||||
|
||||
def __str__(self):
|
||||
return self
|
||||
|
||||
|
||||
class SafeFormatter(Formatter):
|
||||
"""
|
||||
Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and
|
||||
@@ -77,8 +89,19 @@ class SafeFormatter(Formatter):
|
||||
# Ignore format_spec
|
||||
return super().format_field(self._prepare_value(value), '')
|
||||
|
||||
def convert_field(self, value, conversion):
|
||||
# Ignore any conversions
|
||||
if conversion is None:
|
||||
return value
|
||||
else:
|
||||
return str(value)
|
||||
|
||||
def format_map(template, context, raise_on_missing=False, mode=SafeFormatter.MODE_RICH_TO_PLAIN, linkifier=None):
|
||||
|
||||
def format_map(template, context, raise_on_missing=False, mode=SafeFormatter.MODE_RICH_TO_PLAIN, linkifier=None) -> FormattedString:
|
||||
if isinstance(template, FormattedString):
|
||||
raise SuspiciousOperation("Calling format_map() on an already formatted string is likely unsafe.")
|
||||
if not isinstance(template, str):
|
||||
template = str(template)
|
||||
return SafeFormatter(context, raise_on_missing, mode=mode, linkifier=linkifier).format(template)
|
||||
return FormattedString(
|
||||
SafeFormatter(context, raise_on_missing, mode=mode, linkifier=linkifier).format(template)
|
||||
)
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-26 13:19+0000\n"
|
||||
"PO-Revision-Date: 2026-02-06 07:41+0000\n"
|
||||
"Last-Translator: Ryo Tagami <rtagami@airstrip.jp>\n"
|
||||
"PO-Revision-Date: 2026-02-12 20:00+0000\n"
|
||||
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n"
|
||||
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"ja/>\n"
|
||||
"Language: ja\n"
|
||||
@@ -2544,7 +2544,7 @@ msgstr "終了日"
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:12
|
||||
#: pretix/presale/templates/pretixpresale/organizers/customer_membership.html:91
|
||||
msgid "Product"
|
||||
msgstr "商品"
|
||||
msgstr "製品"
|
||||
|
||||
#: pretix/base/exporters/orderlist.py:652 pretix/base/models/vouchers.py:315
|
||||
#: pretix/control/templates/pretixcontrol/vouchers/bulk.html:5
|
||||
@@ -4048,7 +4048,7 @@ msgstr "複数の一致する製品が見つかりました。"
|
||||
#: pretix/base/modelimport_vouchers.py:205 pretix/base/models/items.py:1257
|
||||
#: pretix/base/models/vouchers.py:266 pretix/base/models/waitinglist.py:100
|
||||
msgid "Product variation"
|
||||
msgstr "商品バリエーション"
|
||||
msgstr "製品バリエーション"
|
||||
|
||||
#: pretix/base/modelimport_orders.py:161
|
||||
msgid "The variation can be specified by its internal ID or full name."
|
||||
@@ -9144,8 +9144,8 @@ msgid ""
|
||||
"The voucher code used for one of the items in your cart has already been too "
|
||||
"often. We adjusted the price of the item in your cart."
|
||||
msgstr ""
|
||||
"カートの商品の一つに使用されているバウチャーコードは既に使用回数を超えていま"
|
||||
"す。カート内の商品価格を調整しました。"
|
||||
"カート内のアイテムに使用されたバウチャーコードは、すでに使用回数の上限に達し"
|
||||
"ています。カート内のアイテムの価格を調整しました。"
|
||||
|
||||
#: pretix/base/services/orders.py:182
|
||||
msgid ""
|
||||
@@ -24181,7 +24181,7 @@ msgstr "高度な検索"
|
||||
#: pretix/control/templates/pretixcontrol/orders/index.html:102
|
||||
#, python-format
|
||||
msgid "List filtered by answers to question \"%(question)s\"."
|
||||
msgstr "質問 \"%(question)s\" への回答で絞り込まれたリスト"
|
||||
msgstr "質問「%(question)s」への回答で絞り込まれたリストです。"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/orders/index.html:107
|
||||
msgid "Remove filter"
|
||||
@@ -34229,8 +34229,8 @@ msgid ""
|
||||
"The items in your cart are no longer reserved for you. You can still "
|
||||
"complete your order as long as they’re available."
|
||||
msgstr ""
|
||||
"カート内の商品の確保期限が切れました。在庫があれば、このまま注文を完了するこ"
|
||||
"とができます。"
|
||||
"カート内のアイテムの予約が解除されました。在庫がある限り、引き続き注文を完了"
|
||||
"できます。"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:514
|
||||
#: pretix/presale/templates/pretixpresale/fragment_modals.html:48
|
||||
@@ -34243,7 +34243,7 @@ msgstr "確保が更新されました"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:532
|
||||
msgid "Overview of your ordered products."
|
||||
msgstr "注文した製品の概要"
|
||||
msgstr "注文した製品の概要。"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart_box.html:50
|
||||
msgid "Continue with order process"
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-26 09:10+0000\n"
|
||||
"PO-Revision-Date: 2026-02-06 07:41+0000\n"
|
||||
"Last-Translator: Ryo Tagami <rtagami@airstrip.jp>\n"
|
||||
"PO-Revision-Date: 2026-02-12 20:00+0000\n"
|
||||
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n"
|
||||
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
"js/ja/>\n"
|
||||
"Language: ja\n"
|
||||
@@ -456,11 +456,11 @@ msgstr "="
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:99
|
||||
msgid "Product"
|
||||
msgstr "商品"
|
||||
msgstr "製品"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:103
|
||||
msgid "Product variation"
|
||||
msgstr "商品バリエーション"
|
||||
msgstr "製品バリエーション"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:107
|
||||
msgid "Gate"
|
||||
@@ -726,8 +726,8 @@ msgid ""
|
||||
"The items in your cart are no longer reserved for you. You can still "
|
||||
"complete your order as long as they’re available."
|
||||
msgstr ""
|
||||
"カート内の商品の確保期限が切れました。在庫があれば、このまま注文を完了するこ"
|
||||
"とができます。"
|
||||
"カート内のアイテムの予約が解除されました。在庫がある限り、引き続き注文を完了"
|
||||
"できます。"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js:49
|
||||
msgid "Cart expired"
|
||||
|
||||
@@ -7,7 +7,7 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-26 13:19+0000\n"
|
||||
"PO-Revision-Date: 2026-02-05 23:00+0000\n"
|
||||
"PO-Revision-Date: 2026-02-12 00:00+0000\n"
|
||||
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
|
||||
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/>"
|
||||
"\n"
|
||||
@@ -7993,11 +7993,11 @@ msgid ""
|
||||
"12345 Any City\n"
|
||||
"Atlantis"
|
||||
msgstr ""
|
||||
"Piet Peeters\n"
|
||||
"Piet Janssen\n"
|
||||
"Voorbeeldbedrijf\n"
|
||||
"Sesamstraat 42\n"
|
||||
"12345 Ergens\n"
|
||||
"Niemandsland"
|
||||
"1234 AB Amsterdam\n"
|
||||
"Nederland"
|
||||
|
||||
#: pretix/base/pdf.py:198
|
||||
msgid "Attendee street"
|
||||
|
||||
32986
src/pretix/locale/nl_BE/LC_MESSAGES/django.po
Normal file
32986
src/pretix/locale/nl_BE/LC_MESSAGES/django.po
Normal file
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-01-26 13:19+0000\n"
|
||||
"PO-Revision-Date: 2026-02-05 23:00+0000\n"
|
||||
"PO-Revision-Date: 2026-02-12 00:00+0000\n"
|
||||
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
|
||||
"Language-Team: Dutch (informal) <https://translate.pretix.eu/projects/pretix/"
|
||||
"pretix/nl_Informal/>\n"
|
||||
@@ -8002,9 +8002,9 @@ msgid ""
|
||||
"Atlantis"
|
||||
msgstr ""
|
||||
"Piet Janssen\n"
|
||||
"Voorbeeld bedrijf\n"
|
||||
"Voorbeeldbedrijf\n"
|
||||
"Sesamstraat 42\n"
|
||||
"1234AB Amsterdam\n"
|
||||
"1234 AB Amsterdam\n"
|
||||
"Nederland"
|
||||
|
||||
#: pretix/base/pdf.py:198
|
||||
@@ -21786,9 +21786,9 @@ msgid ""
|
||||
"copy all products, categories, quotas, and questions as well as general "
|
||||
"event settings."
|
||||
msgstr ""
|
||||
"Wil je de instellingen van een ander evenement kopiëren? We zullen alle "
|
||||
"producten, categorieën, quota en vragen overnemen, samen met de algemene "
|
||||
"evenementsinstellingen."
|
||||
"Wil je de configuratie van een ander evenement kopiëren? We kopiëren alle "
|
||||
"producten, categorieën, quota en vragen, evenals de algemene "
|
||||
"evenementinstellingen."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/events/create_copy.html:13
|
||||
msgid ""
|
||||
@@ -21796,25 +21796,25 @@ msgid ""
|
||||
"need to change some settings manually, e.g. date and time settings and texts "
|
||||
"that contain the event name."
|
||||
msgstr ""
|
||||
"Controleer alle instellingen! Je moet waarschijnlijk nog steeds wat "
|
||||
"instellingen handmatig aanpassen, bijvoorbeeld datum- en tijdsinstellingen, "
|
||||
"en teksten die de evenementsnaam bevatten."
|
||||
"Controleer alle instellingen zorgvuldig. Waarschijnlijk moet je nog enkele "
|
||||
"instellingen handmatig wijzigen, bijvoorbeeld de datum- en tijdinstellingen "
|
||||
"en teksten die de naam van het evenement bevatten."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/events/create_foundation.html:7
|
||||
msgid "Event type"
|
||||
msgstr "Evenementssoort"
|
||||
msgstr "Type evenement"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/events/create_foundation.html:13
|
||||
msgid "Singular event or non-event shop"
|
||||
msgstr "Enkel evenement of winkel die niet voor een evenement is"
|
||||
msgstr "Eenmalig evenement of geen evenement"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/events/create_foundation.html:15
|
||||
msgid ""
|
||||
"An event with individual configuration. If you create more events later, you "
|
||||
"can copy the event to save yourself some work."
|
||||
msgstr ""
|
||||
"Een evenement met eigen instellingen. Als je later meer evenementen aanmaakt "
|
||||
"kan je de instellingen van dit evenement kopiëren."
|
||||
"Een evenement met eigen instellingen. Als je later meer evenementen "
|
||||
"aanmaakt, kan je de instellingen van dit evenement kopiëren."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/events/create_foundation.html:21
|
||||
msgid ""
|
||||
@@ -21834,8 +21834,8 @@ msgid ""
|
||||
"A series of events that share the same configuration. They can still be "
|
||||
"different in their dates, locations, prices, and capacities."
|
||||
msgstr ""
|
||||
"Een reeks evenementen die dezelfde instellingen delen. De evenementen in de "
|
||||
"reeks kunnen verschillen in hun datums, prijzen en capaciteiten."
|
||||
"Een reeks evenementen met dezelfde configuratie. Ze kunnen nog steeds "
|
||||
"verschillen qua data, locaties, prijzen en capaciteiten."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/events/create_foundation.html:40
|
||||
msgid ""
|
||||
@@ -21861,9 +21861,8 @@ msgid ""
|
||||
"The list below shows all events you have administrative access to. Click on "
|
||||
"the event name to access event details."
|
||||
msgstr ""
|
||||
"De lijst hieronder toont alle evenementen waar je administratieve toegang "
|
||||
"toe hebt. Klik op de evenementsnaam om de details van het evenement te "
|
||||
"openen."
|
||||
"De onderstaande lijst toont alle evenementen waar je beheerderstoegang toe "
|
||||
"hebt. Klik op de naam van het evenement om de details ervan te bekijken."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/events/index.html:12
|
||||
#: pretix/control/templates/pretixcontrol/organizers/detail.html:18
|
||||
@@ -21923,7 +21922,7 @@ msgstr "Aantallen bijgewerkt op %(date)s"
|
||||
#: pretix/control/templates/pretixcontrol/fragment_quota_box_paid.html:3
|
||||
#, python-format
|
||||
msgid "Currently available: %(num)s"
|
||||
msgstr "Op dit moment beschikbaar: %(num)s"
|
||||
msgstr "Nu beschikbaar: %(num)s"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/global_license.html:8
|
||||
msgid ""
|
||||
@@ -21973,7 +21972,7 @@ msgstr "Installatiegegevens"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/global_license.html:34
|
||||
msgid "Installed plugins"
|
||||
msgstr "Geïnstalleerde plugins"
|
||||
msgstr "Geïnstalleerde plug-ins"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/global_license.html:40
|
||||
msgid "Public information"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-26 13:19+0000\n"
|
||||
"PO-Revision-Date: 2026-02-05 14:34+0000\n"
|
||||
"PO-Revision-Date: 2026-02-14 06:10+0000\n"
|
||||
"Last-Translator: Nate Horst <nate@agcthailand.org>\n"
|
||||
"Language-Team: Thai <https://translate.pretix.eu/projects/pretix/pretix/th/>"
|
||||
"\n"
|
||||
@@ -1000,7 +1000,7 @@ msgstr "โดเมนอีเมลที่ใช้สั่งซื้อ
|
||||
#: pretix/plugins/reports/exporters.py:900
|
||||
#: pretix/plugins/ticketoutputpdf/exporters.py:96
|
||||
msgid "Order code"
|
||||
msgstr "รหัสการสั่งซื้อ"
|
||||
msgstr "รหัสการลงทะเบียน"
|
||||
|
||||
#: pretix/base/datasync/sourcefields.py:380
|
||||
msgid "Event and order code"
|
||||
@@ -7190,7 +7190,7 @@ msgstr "สินค้าที่ซื้อแล้ว"
|
||||
#: pretix/base/services/placeholders.py:424
|
||||
#: pretix/base/templates/pretixbase/email/order_details.html:151
|
||||
msgid "View order details"
|
||||
msgstr "ดูรายละเอียดคำสั่งซื้อ"
|
||||
msgstr "ดูรายละเอียดการลงทะเบียน"
|
||||
|
||||
#: pretix/base/notifications.py:234
|
||||
#, python-brace-format
|
||||
@@ -11567,7 +11567,7 @@ msgstr ""
|
||||
#: pretix/base/settings.py:2856 pretix/base/settings.py:2872
|
||||
#, python-brace-format
|
||||
msgid "Your ticket is ready for download: {code}"
|
||||
msgstr ""
|
||||
msgstr "ตั๋วของคุณพร้อมสำหรับการดาวน์โหลดแล้ว: {code}"
|
||||
|
||||
#: pretix/base/settings.py:2860
|
||||
#, python-brace-format
|
||||
@@ -12477,7 +12477,7 @@ msgstr ""
|
||||
#: pretix/presale/templates/pretixpresale/event/base.html:222
|
||||
#: pretix/presale/templates/pretixpresale/organizers/base.html:100
|
||||
msgid "Contact"
|
||||
msgstr ""
|
||||
msgstr "ติดต่อ"
|
||||
|
||||
#: pretix/base/templates/pretixbase/email/shred_completed.txt:2
|
||||
#, python-format
|
||||
@@ -12548,21 +12548,23 @@ msgstr ""
|
||||
|
||||
#: pretix/base/templates/source.html:5 pretix/base/templates/source.html:9
|
||||
msgid "Source code"
|
||||
msgstr ""
|
||||
msgstr "ซอร์สโค้ด (Source code)"
|
||||
|
||||
#: pretix/base/templates/source.html:10
|
||||
msgid ""
|
||||
"This site is powered by free software. If you want to read the license terms "
|
||||
"or obtain the source code, follow these links or instructions:"
|
||||
msgstr ""
|
||||
"เว็บไซต์นี้ขับเคลื่อนโดยซอฟต์แวร์เสรี หากคุณต้องการอ่านเงื่อนไขการอนุญาตใช้งานหรือรับซอร์สโค้ด โปร"
|
||||
"ดไปที่ลิงก์หรือคำแนะนำต่อไปนี้:"
|
||||
|
||||
#: pretix/base/ticketoutput.py:182
|
||||
msgid "Enable ticket format"
|
||||
msgstr ""
|
||||
msgstr "เปิดใช้งานรูปแบบตั๋ว"
|
||||
|
||||
#: pretix/base/ticketoutput.py:200
|
||||
msgid "Download ticket"
|
||||
msgstr ""
|
||||
msgstr "ดาวน์โหลดตั๋ว"
|
||||
|
||||
#: pretix/base/timeframes.py:49
|
||||
msgctxt "reporting_timeframe"
|
||||
@@ -19827,7 +19829,7 @@ msgstr ""
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_downloads.html:7
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_downloads.html:40
|
||||
msgid "Ticket download"
|
||||
msgstr ""
|
||||
msgstr "ดาวน์โหลดตั๋ว"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/tickets.html:11
|
||||
msgid "Download settings"
|
||||
@@ -21760,27 +21762,27 @@ msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:410
|
||||
msgid "Change products"
|
||||
msgstr ""
|
||||
msgstr "เปลี่ยนรายการสินค้า"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:415
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html:197
|
||||
msgid "Ordered items"
|
||||
msgstr ""
|
||||
msgstr "รายการสินค้าที่สั่งซื้อ"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:434
|
||||
#, python-format
|
||||
msgid "Denied scan: %(date)s"
|
||||
msgstr ""
|
||||
msgstr "การสแกนถูกปฏิเสธ: %(date)s"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:439
|
||||
#, python-format
|
||||
msgid "Exit scan: %(date)s"
|
||||
msgstr ""
|
||||
msgstr "สแกนขาออก: %(date)s"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:446
|
||||
#, python-format
|
||||
msgid "Entry scan: %(date)s"
|
||||
msgstr ""
|
||||
msgstr "สแกนขาเข้า: %(date)s"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:465
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:55
|
||||
@@ -21922,7 +21924,7 @@ msgstr ""
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html:90
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html:318
|
||||
msgid "ZIP code and city"
|
||||
msgstr ""
|
||||
msgstr "รหัสไปรษณีย์ และชื่อเมือง"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:1047
|
||||
msgid "Valid EU VAT ID"
|
||||
@@ -30726,46 +30728,46 @@ msgstr ""
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/ticketoutput.py:65
|
||||
msgid "Download tickets (PDF)"
|
||||
msgstr ""
|
||||
msgstr "ดาวน์โหลดตั๋ว (PDF)"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/ticketoutput.py:66
|
||||
msgid "Download ticket (PDF)"
|
||||
msgstr ""
|
||||
msgstr "ดาวน์โหลดตั๋ว (PDF)"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/views.py:62
|
||||
msgid "Default ticket layout"
|
||||
msgstr ""
|
||||
msgstr "รูปแบบตั๋วมาตรฐาน"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/views.py:119
|
||||
msgid "The new ticket layout has been created."
|
||||
msgstr ""
|
||||
msgstr "สร้างรูปแบบตั๋วใหม่เรียบร้อยแล้ว"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/views.py:168
|
||||
#: pretix/plugins/ticketoutputpdf/views.py:198
|
||||
#: pretix/plugins/ticketoutputpdf/views.py:246
|
||||
msgid "The requested layout does not exist."
|
||||
msgstr ""
|
||||
msgstr "ไม่พบรูปแบบตั๋วที่ร้องขอ"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/views.py:210
|
||||
msgid "The selected ticket layout been deleted."
|
||||
msgstr ""
|
||||
msgstr "รูปแบบตั๋วที่เลือกถูกลบออกแล้ว"
|
||||
|
||||
#: pretix/plugins/ticketoutputpdf/views.py:250
|
||||
#, python-brace-format
|
||||
msgid "Ticket PDF layout: {}"
|
||||
msgstr ""
|
||||
msgstr "รูปแบบไฟล์ PDF ของตั๋ว: {}"
|
||||
|
||||
#: pretix/plugins/webcheckin/apps.py:30 pretix/plugins/webcheckin/apps.py:33
|
||||
msgid "Web-based check-in"
|
||||
msgstr ""
|
||||
msgstr "การเช็คอินผ่านเว็บ"
|
||||
|
||||
#: pretix/plugins/webcheckin/apps.py:38
|
||||
msgid "Turn your browser into a check-in device to perform access control."
|
||||
msgstr ""
|
||||
msgstr "เปลี่ยนเบราว์เซอร์ของคุณให้เป็นอุปกรณ์เช็คอินเพื่อควบคุมการเข้างาน"
|
||||
|
||||
#: pretix/plugins/webcheckin/apps.py:40 pretix/plugins/webcheckin/signals.py:36
|
||||
msgid "Web Check-in"
|
||||
msgstr ""
|
||||
msgstr "เว็บเช็คอิน (Web Check-in)"
|
||||
|
||||
#: pretix/presale/checkoutflow.py:117
|
||||
msgctxt "checkoutflow"
|
||||
@@ -31188,7 +31190,7 @@ msgstr ""
|
||||
#: pretix/presale/templates/pretixpresale/event/base.html:242
|
||||
#: pretix/presale/templates/pretixpresale/organizers/base.html:120
|
||||
msgid "Imprint"
|
||||
msgstr ""
|
||||
msgstr "ข้อมูลทางกฎหมาย (Imprint)"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_addons.html:12
|
||||
msgid ""
|
||||
@@ -32075,24 +32077,25 @@ msgstr ""
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_downloads.html:76
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_downloads.html:82
|
||||
msgid "Please have your ticket ready when entering the event."
|
||||
msgstr ""
|
||||
msgstr "โปรดเตรียมตั๋วของคุณให้พร้อมเมื่อเข้าสู่งาน"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_downloads.html:85
|
||||
msgid "Download your tickets using the buttons below."
|
||||
msgstr ""
|
||||
msgstr "ดาวน์โหลดตั๋วของคุณโดยใช้ปุ่มด้านล่างนี้"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_downloads.html:94
|
||||
#, python-format
|
||||
msgid "You will be able to download your tickets here starting on %(date)s."
|
||||
msgstr ""
|
||||
"คุณจะสามารถดาวน์โหลดตั๋วของคุณได้ที่นี่ ตั้งแต่วันที่ %(date)s เป็นต้นไป"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_event_info.html:7
|
||||
msgid "Where does the event happen?"
|
||||
msgstr ""
|
||||
msgstr "กิจกรรมจัดขึ้นที่ไหน?"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_event_info.html:17
|
||||
msgid "When does the event happen?"
|
||||
msgstr ""
|
||||
msgstr "กิจกรรมจัดขึ้นเมื่อไหร่?"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_event_info.html:26
|
||||
#, python-format
|
||||
@@ -32491,7 +32494,7 @@ msgstr ""
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html:287
|
||||
msgid "Your information"
|
||||
msgstr ""
|
||||
msgstr "ข้อมูลของคุณ"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html:290
|
||||
msgid "Change your information"
|
||||
|
||||
@@ -179,6 +179,7 @@ def _default_context(request):
|
||||
ctx['html_page_header'] = "".join(h for h in _html_page_header if h)
|
||||
ctx['footer'] = _footer
|
||||
ctx['site_url'] = settings.SITE_URL
|
||||
ctx['request_get_items'] = request.GET.items()
|
||||
|
||||
ctx['js_datetime_format'] = get_javascript_format_without_seconds('DATETIME_INPUT_FORMATS')
|
||||
ctx['js_date_format'] = get_javascript_format_without_seconds('DATE_INPUT_FORMATS')
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</li>
|
||||
<li class="text-center">
|
||||
<form class="form-inline" method="get" id="monthselform" action="{% eventurl event "presale:event.index" cart_namespace=cart_namespace %}">
|
||||
{% for f, v in request.GET.items %}
|
||||
{% for f, v in request_get_items %}
|
||||
{% if f != "date" %}
|
||||
<input type="hidden" name="{{ f }}" value="{{ v }}">
|
||||
{% endif %}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</li>
|
||||
<li class="text-center">
|
||||
<form class="form-inline" method="get" id="monthselform" action="{% eventurl event "presale:event.index" cart_namespace=cart_namespace %}">
|
||||
{% for f, v in request.GET.items %}
|
||||
{% for f, v in request_get_items %}
|
||||
{% if f != "date" %}
|
||||
<input type="hidden" name="{{ f }}" value="{{ v }}">
|
||||
{% endif %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
{% if filter_form.fields %}
|
||||
<form class="event-list-filter-form" method="get" data-save-scrollpos>
|
||||
<input type="hidden" name="filtered" value="1">
|
||||
{% for f, v in request.GET.items %}
|
||||
{% for f, v in request_get_items %}
|
||||
{% if f not in filter_form.fields and f != "page" %}
|
||||
<input type="hidden" name="{{ f }}" value="{{ v }}">
|
||||
{% endif %}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
</li>
|
||||
<li class="text-center">
|
||||
<form class="form-inline" method="get" id="monthselform" action="{% eventurl request.organizer "presale:organizer.index" %}">
|
||||
{% for f, v in request.GET.items %}
|
||||
{% for f, v in request_get_items %}
|
||||
{% if f != "date" %}
|
||||
<input type="hidden" name="{{ f }}" value="{{ v }}">
|
||||
{% endif %}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
</li>
|
||||
<li class="text-center">
|
||||
<form class="form-inline" method="get" id="monthselform" action="{% eventurl request.organizer "presale:organizer.index" %}">
|
||||
{% for f, v in request.GET.items %}
|
||||
{% for f, v in request_get_items %}
|
||||
{% if f != "date" %}
|
||||
<input type="hidden" name="{{ f }}" value="{{ v }}">
|
||||
{% endif %}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
</li>
|
||||
<li class="text-center">
|
||||
<form class="form-inline" method="get" id="monthselform" action="{% eventurl request.organizer "presale:organizer.index" %}">
|
||||
{% for f, v in request.GET.items %}
|
||||
{% for f, v in request_get_items %}
|
||||
{% if f != "date" %}
|
||||
<input type="hidden" name="{{ f }}" value="{{ v }}">
|
||||
{% endif %}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -252,7 +252,7 @@ Vue.component('availbox', {
|
||||
variation: Object
|
||||
},
|
||||
mounted: function() {
|
||||
if (this.$root.itemnum === 1 && (!this.$root.categories[0].items[0].has_variations || this.$root.categories[0].items[0].variations.length < 2) && !this.$root.has_seating_plan ? 1 : 0) {
|
||||
if (!this.$root.cart_exists && this.$root.itemnum === 1 && (!this.$root.categories[0].items[0].has_variations || this.$root.categories[0].items[0].variations.length < 2) && !this.$root.has_seating_plan ? 1 : 0) {
|
||||
this.$refs.quantity.value = 1;
|
||||
if (this.order_max === 1) {
|
||||
this.$refs.quantity.checked = true;
|
||||
|
||||
@@ -32,21 +32,26 @@
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
from django.core import mail as djmail
|
||||
from django.test import override_settings
|
||||
from django.utils.html import escape
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import scope
|
||||
from django_scopes import scope, scopes_disabled
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.email import get_email_context
|
||||
from pretix.base.models import Event, Organizer, OutgoingMail, User
|
||||
from pretix.base.models import (
|
||||
Event, InvoiceAddress, Order, Organizer, OutgoingMail, User,
|
||||
)
|
||||
from pretix.base.services.mail import mail, mail_send_task
|
||||
|
||||
|
||||
@@ -68,6 +73,45 @@ def env():
|
||||
yield event, user, o
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@scopes_disabled()
|
||||
def item(env):
|
||||
return env[0].items.create(name="Budget Ticket", default_price=23)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@scopes_disabled()
|
||||
def order(env, item):
|
||||
event, _, _ = env
|
||||
o = Order.objects.create(
|
||||
code="FOO",
|
||||
event=event,
|
||||
email="dummy@dummy.test",
|
||||
status=Order.STATUS_PENDING,
|
||||
secret="k24fiuwvu8kxz3y1",
|
||||
sales_channel=event.organizer.sales_channels.get(identifier="web"),
|
||||
datetime=datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=datetime.timezone.utc),
|
||||
expires=datetime.datetime(2017, 12, 10, 10, 0, 0, tzinfo=datetime.timezone.utc),
|
||||
total=23,
|
||||
locale="en",
|
||||
)
|
||||
o.positions.create(
|
||||
order=o,
|
||||
item=item,
|
||||
variation=None,
|
||||
price=Decimal("23"),
|
||||
attendee_email="peter@example.org",
|
||||
attendee_name_parts={"given_name": "Peter", "family_name": "Miller"},
|
||||
secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
pseudonymization_id="ABCDEFGHKL",
|
||||
)
|
||||
InvoiceAddress.objects.create(
|
||||
order=o,
|
||||
name_parts={"given_name": "Peter", "family_name": "Miller"},
|
||||
)
|
||||
return o
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_send_mail_with_prefix(env):
|
||||
djmail.outbox = []
|
||||
@@ -279,7 +323,7 @@ def _extract_html(mail):
|
||||
def test_placeholder_html_rendering_from_template(env):
|
||||
djmail.outbox = []
|
||||
event, user, organizer = env
|
||||
event.name = "<strong>event & co. kg</strong>"
|
||||
event.name = "<strong>event & co. kg</strong> {currency}"
|
||||
event.save()
|
||||
mail('dummy@dummy.dummy', '{event} Test subject', 'mailtest.txt', get_email_context(
|
||||
event=event,
|
||||
@@ -288,25 +332,26 @@ def test_placeholder_html_rendering_from_template(env):
|
||||
|
||||
assert len(djmail.outbox) == 1
|
||||
assert djmail.outbox[0].to == [user.email]
|
||||
assert 'Event name: <strong>event & co. kg</strong>' in djmail.outbox[0].body
|
||||
assert '**IBAN**: 123 \n**BIC**: 456' in djmail.outbox[0].body
|
||||
assert '**Meta**: *Beep*' in djmail.outbox[0].body
|
||||
assert 'Event website: [<strong>event & co. kg</strong>](https://example.org/dummy)' in djmail.outbox[0].body
|
||||
assert 'Other website: [<strong>event & co. kg</strong>](https://example.com)' in djmail.outbox[0].body
|
||||
assert '<' not in djmail.outbox[0].body
|
||||
assert '&' not in djmail.outbox[0].body
|
||||
# Known bug for now: These should not have HTML for the plain body, but we'll fix this safter the security release
|
||||
assert escape('Event name: <strong>event & co. kg</strong> {currency}') in djmail.outbox[0].body
|
||||
assert '<strong>IBAN</strong>: 123<br>\n<strong>BIC</strong>: 456' in djmail.outbox[0].body
|
||||
assert '**Meta**: <em>Beep</em>' in djmail.outbox[0].body
|
||||
assert escape('Event website: [<strong>event & co. kg</strong> {currency}](https://example.org/dummy)') in djmail.outbox[0].body
|
||||
# todo: assert '<' not in djmail.outbox[0].body
|
||||
# todo: assert '&' not in djmail.outbox[0].body
|
||||
assert 'Unevaluated placeholder: {currency}' in djmail.outbox[0].body
|
||||
assert 'EUR' not in djmail.outbox[0].body
|
||||
html = _extract_html(djmail.outbox[0])
|
||||
|
||||
assert '<strong>event' not in html
|
||||
assert 'Event name: <strong>event & co. kg</strong>' in html
|
||||
assert 'Event name: <strong>event & co. kg</strong> {currency}' in html
|
||||
assert '<strong>IBAN</strong>: 123<br/>\n<strong>BIC</strong>: 456' in html
|
||||
assert '<strong>Meta</strong>: <em>Beep</em>' in html
|
||||
assert 'Unevaluated placeholder: {currency}' in html
|
||||
assert 'EUR' not in html
|
||||
assert re.search(
|
||||
r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank"><strong>event & co. kg</strong></a>',
|
||||
html
|
||||
)
|
||||
assert re.search(
|
||||
r'Other website: <a href="https://example.com" rel="noopener" style="[^"]+" target="_blank"><strong>event & co. kg</strong></a>',
|
||||
r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank">'
|
||||
r'<strong>event & co. kg</strong> {currency}</a>',
|
||||
html
|
||||
)
|
||||
|
||||
@@ -324,7 +369,7 @@ def test_placeholder_html_rendering_from_string(env):
|
||||
})
|
||||
djmail.outbox = []
|
||||
event, user, organizer = env
|
||||
event.name = "<strong>event & co. kg</strong>"
|
||||
event.name = "<strong>event & co. kg</strong> {currency}"
|
||||
event.save()
|
||||
ctx = get_email_context(
|
||||
event=event,
|
||||
@@ -335,9 +380,9 @@ def test_placeholder_html_rendering_from_string(env):
|
||||
|
||||
assert len(djmail.outbox) == 1
|
||||
assert djmail.outbox[0].to == [user.email]
|
||||
assert 'Event name: <strong>event & co. kg</strong>' in djmail.outbox[0].body
|
||||
assert 'Event website: [<strong>event & co. kg</strong>](https://example.org/dummy)' in djmail.outbox[0].body
|
||||
assert 'Other website: [<strong>event & co. kg</strong>](https://example.com)' in djmail.outbox[0].body
|
||||
assert 'Event name: <strong>event & co. kg</strong> {currency}' in djmail.outbox[0].body
|
||||
assert 'Event website: [<strong>event & co. kg</strong> {currency}](https://example.org/dummy)' in djmail.outbox[0].body
|
||||
assert 'Other website: [<strong>event & co. kg</strong> {currency}](https://example.com)' in djmail.outbox[0].body
|
||||
assert '**IBAN**: 123 \n**BIC**: 456' in djmail.outbox[0].body
|
||||
assert '**Meta**: *Beep*' in djmail.outbox[0].body
|
||||
assert 'URL: https://google.com' in djmail.outbox[0].body
|
||||
@@ -352,11 +397,13 @@ def test_placeholder_html_rendering_from_string(env):
|
||||
assert '<strong>IBAN</strong>: 123<br/>\n<strong>BIC</strong>: 456' in html
|
||||
assert '<strong>Meta</strong>: <em>Beep</em>' in html
|
||||
assert re.search(
|
||||
r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank"><strong>event & co. kg</strong></a>',
|
||||
r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank">'
|
||||
r'<strong>event & co. kg</strong> {currency}</a>',
|
||||
html
|
||||
)
|
||||
assert re.search(
|
||||
r'Other website: <a href="https://example.com" rel="noopener" style="[^"]+" target="_blank"><strong>event & co. kg</strong></a>',
|
||||
r'Other website: <a href="https://example.com" rel="noopener" style="[^"]+" target="_blank">'
|
||||
r'<strong>event & co. kg</strong> {currency}</a>',
|
||||
html
|
||||
)
|
||||
assert re.search(
|
||||
@@ -377,3 +424,141 @@ def test_placeholder_html_rendering_from_string(env):
|
||||
r'style="[^"]+" target="_blank">Link & Text</a>',
|
||||
html
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_nested_placeholder_inclusion_full_process(env, order):
|
||||
# Test that it is not possible to sneak in a placeholder like {url_cancel} inside a user-controlled
|
||||
# placeholder value like {invoice_company}
|
||||
event, user, organizer = env
|
||||
position = order.positions.get()
|
||||
order.invoice_address.company = "{url_cancel} Corp"
|
||||
order.invoice_address.save()
|
||||
event.settings.mail_text_resend_link = LazyI18nString({"en": "Ticket for {invoice_company}"})
|
||||
event.settings.mail_subject_resend_link_attendee = LazyI18nString({"en": "Ticket for {invoice_company}"})
|
||||
|
||||
djmail.outbox = []
|
||||
position.resend_link()
|
||||
assert len(djmail.outbox) == 1
|
||||
assert djmail.outbox[0].to == [position.attendee_email]
|
||||
assert "Ticket for {url_cancel} Corp" == djmail.outbox[0].subject
|
||||
assert "/cancel" not in djmail.outbox[0].body
|
||||
assert "/order" not in djmail.outbox[0].body
|
||||
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
|
||||
for part in (html, plain):
|
||||
assert "Ticket for {url_cancel} Corp" in part
|
||||
assert "/order/" not in part
|
||||
assert "/cancel" not in part
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_nested_placeholder_inclusion_mail_service(env):
|
||||
# test that it is not possible to have placeholders within the values of placeholders when
|
||||
# the mail() function is called directly
|
||||
template = LazyI18nString("Event name: {event}")
|
||||
djmail.outbox = []
|
||||
event, user, organizer = env
|
||||
event.name = "event & {currency} co. kg"
|
||||
event.slug = "event-co-ag-slug"
|
||||
event.save()
|
||||
|
||||
mail(
|
||||
"dummy@dummy.dummy",
|
||||
"{event} Test subject",
|
||||
template,
|
||||
get_email_context(
|
||||
event=event,
|
||||
payment_info="**IBAN**: 123 \n**BIC**: 456 {event}",
|
||||
),
|
||||
event,
|
||||
)
|
||||
|
||||
assert len(djmail.outbox) == 1
|
||||
assert djmail.outbox[0].to == [user.email]
|
||||
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
|
||||
for part in (html, plain, djmail.outbox[0].subject):
|
||||
assert "event & {currency} co. kg" in part or "event & {currency} co. kg" in part
|
||||
assert "EUR" not in part
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("tpl", [
|
||||
"Event: {event.__class__}",
|
||||
"Event: {{event.__class__}}",
|
||||
"Event: {{{event.__class__}}}",
|
||||
])
|
||||
def test_variable_inclusion_from_string_full_process(env, tpl, order):
|
||||
# Test that it is not possible to use placeholders that leak system information in templates
|
||||
# when run through system processes
|
||||
event, user, organizer = env
|
||||
event.name = "event & co. kg"
|
||||
event.save()
|
||||
position = order.positions.get()
|
||||
event.settings.mail_text_resend_link = LazyI18nString({"en": tpl})
|
||||
event.settings.mail_subject_resend_link_attendee = LazyI18nString({"en": tpl})
|
||||
|
||||
position.resend_link()
|
||||
assert len(djmail.outbox) == 1
|
||||
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
|
||||
for part in (html, plain, djmail.outbox[0].subject):
|
||||
assert "{event.__class__}" in part
|
||||
assert "LazyI18nString" not in part
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("tpl", [
|
||||
"Event: {event.__class__}",
|
||||
"Event: {{event.__class__}}",
|
||||
"Event: {{{event.__class__}}}",
|
||||
])
|
||||
def test_variable_inclusion_from_string_mail_service(env, tpl):
|
||||
# Test that it is not possible to use placeholders that leak system information in templates
|
||||
# when run through mail() directly
|
||||
event, user, organizer = env
|
||||
event.name = "event & co. kg"
|
||||
event.save()
|
||||
|
||||
djmail.outbox = []
|
||||
mail(
|
||||
"dummy@dummy.dummy",
|
||||
tpl,
|
||||
LazyI18nString(tpl),
|
||||
get_email_context(
|
||||
event=event,
|
||||
payment_info="**IBAN**: 123 \n**BIC**: 456\n" + tpl,
|
||||
),
|
||||
event,
|
||||
)
|
||||
assert len(djmail.outbox) == 1
|
||||
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
|
||||
for part in (html, plain, djmail.outbox[0].subject):
|
||||
assert "{event.__class__}" in part
|
||||
assert "LazyI18nString" not in part
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_escaped_braces_mail_services(env):
|
||||
# Test that braces can be escaped by doubling
|
||||
template = LazyI18nString("Event name: -{{currency}}-")
|
||||
djmail.outbox = []
|
||||
event, user, organizer = env
|
||||
event.name = "event & co. kg"
|
||||
event.save()
|
||||
|
||||
mail(
|
||||
"dummy@dummy.dummy",
|
||||
"-{{currency}}- Test subject",
|
||||
template,
|
||||
get_email_context(
|
||||
event=event,
|
||||
payment_info="**IBAN**: 123 \n**BIC**: 456 {event}",
|
||||
),
|
||||
event,
|
||||
)
|
||||
|
||||
assert len(djmail.outbox) == 1
|
||||
assert djmail.outbox[0].to == [user.email]
|
||||
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
|
||||
for part in (html, plain, djmail.outbox[0].subject):
|
||||
assert "EUR" not in part
|
||||
assert "-{currency}-" in part
|
||||
|
||||
@@ -29,6 +29,8 @@ def test_format_map():
|
||||
assert format_map("Foo {baz}", {"bar": 3}) == "Foo {baz}"
|
||||
assert format_map("Foo {bar.__module__}", {"bar": 3}) == "Foo {bar.__module__}"
|
||||
assert format_map("Foo {bar!s}", {"bar": 3}) == "Foo 3"
|
||||
assert format_map("Foo {bar!r}", {"bar": '3'}) == "Foo 3"
|
||||
assert format_map("Foo {bar!a}", {"bar": '3'}) == "Foo 3"
|
||||
assert format_map("Foo {bar:<20}", {"bar": 3}) == "Foo 3"
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{% load i18n %}
|
||||
This is a test file for sending mails.
|
||||
Event name: {event}
|
||||
Event name: {{ event }}
|
||||
Unevaluated placeholder: {currency}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
The language code used for rendering this email is {{ LANGUAGE_CODE }}.
|
||||
|
||||
Payment info:
|
||||
{payment_info}
|
||||
{{ payment_info }}
|
||||
|
||||
**Meta**: {meta_Test}
|
||||
**Meta**: {{ meta_Test }}
|
||||
|
||||
Event website: [{event}](https://example.org/{event_slug})
|
||||
Other website: [{event}]({meta_Website})
|
||||
Event website: [{{event}}](https://example.org/{{event_slug}})
|
||||
Reference in New Issue
Block a user