mirror of
https://github.com/pretix/pretix.git
synced 2025-12-14 13:32:28 +00:00
Compare commits
2 Commits
v2025.7.2
...
org-level-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9693625758 | ||
|
|
7ecc64ec73 |
@@ -42,7 +42,7 @@ dependencies = [
|
|||||||
"django-filter==25.1",
|
"django-filter==25.1",
|
||||||
"django-formset-js-improved==0.5.0.3",
|
"django-formset-js-improved==0.5.0.3",
|
||||||
"django-formtools==2.5.1",
|
"django-formtools==2.5.1",
|
||||||
"django-hierarkey==2.0.*,>=2.0.1",
|
"django-hierarkey==2.0.*",
|
||||||
"django-hijack==3.7.*",
|
"django-hijack==3.7.*",
|
||||||
"django-i18nfield==1.10.*",
|
"django-i18nfield==1.10.*",
|
||||||
"django-libsass==0.9",
|
"django-libsass==0.9",
|
||||||
|
|||||||
@@ -19,4 +19,4 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "2025.7.2"
|
__version__ = "2025.8.0.dev0"
|
||||||
|
|||||||
@@ -133,11 +133,11 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
|||||||
def template_name(self):
|
def template_name(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def compile_markdown(self, plaintext, context=None):
|
def compile_markdown(self, plaintext):
|
||||||
return markdown_compile_email(plaintext, context=context)
|
return markdown_compile_email(plaintext)
|
||||||
|
|
||||||
def render(self, plain_body: str, plain_signature: str, subject: str, order, position, context) -> str:
|
def render(self, plain_body: str, plain_signature: str, subject: str, order, position, context) -> str:
|
||||||
body_md = self.compile_markdown(plain_body, context)
|
body_md = self.compile_markdown(plain_body)
|
||||||
if context:
|
if context:
|
||||||
body_md = format_map(body_md, context=context, mode=SafeFormatter.MODE_RICH_TO_HTML)
|
body_md = format_map(body_md, context=context, mode=SafeFormatter.MODE_RICH_TO_HTML)
|
||||||
htmlctx = {
|
htmlctx = {
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
|||||||
'invoice_company': ''
|
'invoice_company': ''
|
||||||
})
|
})
|
||||||
renderer = ClassicMailRenderer(None, organizer)
|
renderer = ClassicMailRenderer(None, organizer)
|
||||||
body_plain = render_mail(template, context, placeholder_mode=SafeFormatter.MODE_RICH_TO_PLAIN)
|
content_plain = body_plain = render_mail(template, context)
|
||||||
subject = str(subject).format_map(TolerantDict(context))
|
subject = str(subject).format_map(TolerantDict(context))
|
||||||
sender = (
|
sender = (
|
||||||
sender or
|
sender or
|
||||||
@@ -316,7 +316,6 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
|||||||
|
|
||||||
with override(timezone):
|
with override(timezone):
|
||||||
try:
|
try:
|
||||||
content_plain = render_mail(template, context, placeholder_mode=None)
|
|
||||||
if plain_text_only:
|
if plain_text_only:
|
||||||
body_html = None
|
body_html = None
|
||||||
elif 'context' in inspect.signature(renderer.render).parameters:
|
elif 'context' in inspect.signature(renderer.render).parameters:
|
||||||
@@ -714,11 +713,11 @@ def mail_send(*args, **kwargs):
|
|||||||
mail_send_task.apply_async(args=args, kwargs=kwargs)
|
mail_send_task.apply_async(args=args, kwargs=kwargs)
|
||||||
|
|
||||||
|
|
||||||
def render_mail(template, context, placeholder_mode=SafeFormatter.MODE_RICH_TO_PLAIN):
|
def render_mail(template, context):
|
||||||
if isinstance(template, LazyI18nString):
|
if isinstance(template, LazyI18nString):
|
||||||
body = str(template)
|
body = str(template)
|
||||||
if context and placeholder_mode:
|
if context:
|
||||||
body = format_map(body, context, mode=placeholder_mode)
|
body = format_map(body, context, mode=SafeFormatter.MODE_IGNORE_RICH)
|
||||||
else:
|
else:
|
||||||
tpl = get_template(template)
|
tpl = get_template(template)
|
||||||
body = tpl.render(context)
|
body = tpl.render(context)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ from decimal import Decimal
|
|||||||
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.html import escape, mark_safe
|
from django.utils.html import escape
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
@@ -123,10 +123,6 @@ class BaseRichTextPlaceholder(BaseTextPlaceholder):
|
|||||||
def identifier(self):
|
def identifier(self):
|
||||||
return self._identifier
|
return self._identifier
|
||||||
|
|
||||||
@property
|
|
||||||
def allowed_in_plain_content(self):
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def required_context(self):
|
def required_context(self):
|
||||||
return self._args
|
return self._args
|
||||||
@@ -198,33 +194,6 @@ class SimpleButtonPlaceholder(BaseRichTextPlaceholder):
|
|||||||
return f'{text}: {url}'
|
return f'{text}: {url}'
|
||||||
|
|
||||||
|
|
||||||
class MarkdownTextPlaceholder(BaseRichTextPlaceholder):
|
|
||||||
def __init__(self, identifier, args, func, sample, inline):
|
|
||||||
super().__init__(identifier, args)
|
|
||||||
self._func = func
|
|
||||||
self._sample = sample
|
|
||||||
self._snippet = inline
|
|
||||||
|
|
||||||
@property
|
|
||||||
def allowed_in_plain_content(self):
|
|
||||||
return self._snippet
|
|
||||||
|
|
||||||
def render_plain(self, **context):
|
|
||||||
return self._func(**{k: context[k] for k in self._args})
|
|
||||||
|
|
||||||
def render_html(self, **context):
|
|
||||||
return mark_safe(markdown_compile_email(self.render_plain(**context), snippet=self._snippet))
|
|
||||||
|
|
||||||
def render_sample_plain(self, event):
|
|
||||||
if callable(self._sample):
|
|
||||||
return self._sample(event)
|
|
||||||
else:
|
|
||||||
return self._sample
|
|
||||||
|
|
||||||
def render_sample_html(self, event):
|
|
||||||
return mark_safe(markdown_compile_email(self.render_sample_plain(event), snippet=self._snippet))
|
|
||||||
|
|
||||||
|
|
||||||
class PlaceholderContext(SafeFormatter):
|
class PlaceholderContext(SafeFormatter):
|
||||||
"""
|
"""
|
||||||
Holds the contextual arguments and corresponding list of available placeholders for formatting
|
Holds the contextual arguments and corresponding list of available placeholders for formatting
|
||||||
@@ -605,7 +574,7 @@ def base_placeholders(sender, **kwargs):
|
|||||||
'invoice_company', ['invoice_address'], lambda invoice_address: invoice_address.company or '',
|
'invoice_company', ['invoice_address'], lambda invoice_address: invoice_address.company or '',
|
||||||
_('Sample Corporation')
|
_('Sample Corporation')
|
||||||
),
|
),
|
||||||
MarkdownTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
|
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
|
||||||
'* {} - {}'.format(
|
'* {} - {}'.format(
|
||||||
order.full_code,
|
order.full_code,
|
||||||
@@ -635,7 +604,6 @@ def base_placeholders(sender, **kwargs):
|
|||||||
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd', 'hash': 'stuvwxy2z'}
|
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd', 'hash': 'stuvwxy2z'}
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
inline=False,
|
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'hours', ['event', 'waiting_list_entry'], lambda event, waiting_list_entry:
|
'hours', ['event', 'waiting_list_entry'], lambda event, waiting_list_entry:
|
||||||
@@ -650,13 +618,12 @@ def base_placeholders(sender, **kwargs):
|
|||||||
'code', ['waiting_list_voucher'], lambda waiting_list_voucher: waiting_list_voucher.code,
|
'code', ['waiting_list_voucher'], lambda waiting_list_voucher: waiting_list_voucher.code,
|
||||||
'68CYU2H6ZTP3WLK5'
|
'68CYU2H6ZTP3WLK5'
|
||||||
),
|
),
|
||||||
MarkdownTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
|
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
|
||||||
'voucher_list', ['voucher_list'], lambda voucher_list: ' \n'.join(voucher_list),
|
'voucher_list', ['voucher_list'], lambda voucher_list: ' \n'.join(voucher_list),
|
||||||
'68CYU2H6ZTP3WLK5 \n7MB94KKPVEPSMVF2',
|
' 68CYU2H6ZTP3WLK5\n 7MB94KKPVEPSMVF2'
|
||||||
inline=False,
|
|
||||||
),
|
),
|
||||||
MarkdownTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
|
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
|
||||||
'voucher_url_list', ['event', 'voucher_list'],
|
'voucher_url_list', ['event', 'voucher_list'],
|
||||||
lambda event, voucher_list: ' \n'.join([
|
lambda event, voucher_list: ' \n'.join([
|
||||||
@@ -671,7 +638,6 @@ def base_placeholders(sender, **kwargs):
|
|||||||
) + '?voucher=' + c
|
) + '?voucher=' + c
|
||||||
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
|
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
|
||||||
]),
|
]),
|
||||||
inline=False,
|
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={
|
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={
|
||||||
@@ -690,13 +656,13 @@ def base_placeholders(sender, **kwargs):
|
|||||||
'comment', ['comment'], lambda comment: comment,
|
'comment', ['comment'], lambda comment: comment,
|
||||||
_('An individual text with a reason can be inserted here.'),
|
_('An individual text with a reason can be inserted here.'),
|
||||||
),
|
),
|
||||||
MarkdownTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'payment_info', ['order', 'payments'], _placeholder_payments,
|
'payment_info', ['order', 'payments'], _placeholder_payments,
|
||||||
_('The amount has been charged to your card.'), inline=False,
|
_('The amount has been charged to your card.'),
|
||||||
),
|
),
|
||||||
MarkdownTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'payment_info', ['payment_info'], lambda payment_info: payment_info,
|
'payment_info', ['payment_info'], lambda payment_info: payment_info,
|
||||||
_('Please transfer money to this bank account: 9999-9999-9999-9999'), inline=False,
|
_('Please transfer money to this bank account: 9999-9999-9999-9999'),
|
||||||
),
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'attendee_name', ['position'], lambda position: position.attendee_name,
|
'attendee_name', ['position'], lambda position: position.attendee_name,
|
||||||
@@ -753,13 +719,13 @@ def base_placeholders(sender, **kwargs):
|
|||||||
))
|
))
|
||||||
|
|
||||||
for k, v in sender.meta_data.items():
|
for k, v in sender.meta_data.items():
|
||||||
ph.append(MarkdownTextPlaceholder(
|
ph.append(SimpleFunctionalTextPlaceholder(
|
||||||
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],
|
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],
|
||||||
v, inline=True,
|
v
|
||||||
))
|
))
|
||||||
ph.append(MarkdownTextPlaceholder(
|
ph.append(SimpleFunctionalTextPlaceholder(
|
||||||
'meta_%s' % k, ['event_or_subevent'], lambda event_or_subevent, k=k: event_or_subevent.meta_data[k],
|
'meta_%s' % k, ['event_or_subevent'], lambda event_or_subevent, k=k: event_or_subevent.meta_data[k],
|
||||||
v, inline=True,
|
v
|
||||||
))
|
))
|
||||||
|
|
||||||
return ph
|
return ph
|
||||||
@@ -787,7 +753,7 @@ def get_available_placeholders(event, base_parameters, rich=False):
|
|||||||
if not isinstance(val, (list, tuple)):
|
if not isinstance(val, (list, tuple)):
|
||||||
val = [val]
|
val = [val]
|
||||||
for v in val:
|
for v in val:
|
||||||
if isinstance(v, BaseRichTextPlaceholder) and not rich and not v.allowed_in_plain_content:
|
if isinstance(v, BaseRichTextPlaceholder) and not rich:
|
||||||
continue
|
continue
|
||||||
if all(rp in base_parameters for rp in v.required_context):
|
if all(rp in base_parameters for rp in v.required_context):
|
||||||
params[v.identifier] = v
|
params[v.identifier] = v
|
||||||
@@ -809,13 +775,13 @@ def get_sample_context(event, context_parameters, rich=True):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif str(sample).strip().startswith('* ') or str(sample).startswith(' '):
|
elif str(sample).strip().startswith('* ') or str(sample).startswith(' '):
|
||||||
context_dict[k] = mark_safe('<div class="placeholder" title="{}">{}</div>'.format(
|
context_dict[k] = '<div class="placeholder" title="{}">{}</div>'.format(
|
||||||
lbl,
|
lbl,
|
||||||
markdown_compile_email(str(sample))
|
markdown_compile_email(str(sample))
|
||||||
))
|
)
|
||||||
else:
|
else:
|
||||||
context_dict[k] = mark_safe('<span class="placeholder" title="{}">{}</span>'.format(
|
context_dict[k] = '<span class="placeholder" title="{}">{}</span>'.format(
|
||||||
lbl,
|
lbl,
|
||||||
escape(sample)
|
escape(sample)
|
||||||
))
|
)
|
||||||
return context_dict
|
return context_dict
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ from django.conf import settings
|
|||||||
from django.core import signing
|
from django.core import signing
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.functional import SimpleLazyObject
|
from django.utils.functional import SimpleLazyObject
|
||||||
from django.utils.html import escape
|
|
||||||
from django.utils.http import url_has_allowed_host_and_scheme
|
from django.utils.http import url_has_allowed_host_and_scheme
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from markdown import Extension
|
from markdown import Extension
|
||||||
@@ -53,8 +52,6 @@ from markdown.postprocessors import Postprocessor
|
|||||||
from markdown.treeprocessors import UnescapeTreeprocessor
|
from markdown.treeprocessors import UnescapeTreeprocessor
|
||||||
from tlds import tld_set
|
from tlds import tld_set
|
||||||
|
|
||||||
from pretix.helpers.format import SafeFormatter, format_map
|
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
ALLOWED_TAGS_SNIPPET = {
|
ALLOWED_TAGS_SNIPPET = {
|
||||||
@@ -297,28 +294,16 @@ class LinkifyAndCleanExtension(Extension):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def markdown_compile_email(source, allowed_tags=None, allowed_attributes=ALLOWED_ATTRIBUTES, snippet=False, context=None):
|
def markdown_compile_email(source, allowed_tags=ALLOWED_TAGS, allowed_attributes=ALLOWED_ATTRIBUTES):
|
||||||
if allowed_tags is None:
|
|
||||||
allowed_tags = ALLOWED_TAGS_SNIPPET if snippet else ALLOWED_TAGS
|
|
||||||
|
|
||||||
context_callbacks = []
|
|
||||||
if context:
|
|
||||||
# This is a workaround to fix placeholders in URL targets
|
|
||||||
def context_callback(attrs, new=False):
|
|
||||||
if (None, "href") in attrs and "{" in attrs[None, "href"]:
|
|
||||||
# Do not use MODE_RICH_TO_HTML to avoid recursive linkification
|
|
||||||
attrs[None, "href"] = escape(format_map(attrs[None, "href"], context=context, mode=SafeFormatter.MODE_RICH_TO_PLAIN))
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
context_callbacks.append(context_callback)
|
|
||||||
|
|
||||||
linker = bleach.Linker(
|
linker = bleach.Linker(
|
||||||
url_re=URL_RE,
|
url_re=URL_RE,
|
||||||
email_re=EMAIL_RE,
|
email_re=EMAIL_RE,
|
||||||
callbacks=context_callbacks + DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
|
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
|
||||||
parse_email=True
|
parse_email=True
|
||||||
)
|
)
|
||||||
exts = [
|
return markdown.markdown(
|
||||||
|
source,
|
||||||
|
extensions=[
|
||||||
'markdown.extensions.sane_lists',
|
'markdown.extensions.sane_lists',
|
||||||
'markdown.extensions.tables',
|
'markdown.extensions.tables',
|
||||||
EmailNl2BrExtension(),
|
EmailNl2BrExtension(),
|
||||||
@@ -327,14 +312,9 @@ def markdown_compile_email(source, allowed_tags=None, allowed_attributes=ALLOWED
|
|||||||
tags=set(allowed_tags),
|
tags=set(allowed_tags),
|
||||||
attributes=allowed_attributes,
|
attributes=allowed_attributes,
|
||||||
protocols=ALLOWED_PROTOCOLS,
|
protocols=ALLOWED_PROTOCOLS,
|
||||||
strip=snippet,
|
strip=False,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
if snippet:
|
|
||||||
exts.append(SnippetExtension())
|
|
||||||
return markdown.markdown(
|
|
||||||
source,
|
|
||||||
extensions=exts
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -308,8 +308,8 @@ class VoucherBulkForm(VoucherForm):
|
|||||||
)
|
)
|
||||||
Recipient = namedtuple('Recipient', 'email number name tag')
|
Recipient = namedtuple('Recipient', 'email number name tag')
|
||||||
|
|
||||||
def _set_field_placeholders(self, fn, base_parameters, rich=False):
|
def _set_field_placeholders(self, fn, base_parameters):
|
||||||
placeholders = get_available_placeholders(self.instance.event, base_parameters, rich=rich)
|
placeholders = get_available_placeholders(self.instance.event, base_parameters)
|
||||||
ht = format_placeholders_help_text(placeholders, self.instance.event)
|
ht = format_placeholders_help_text(placeholders, self.instance.event)
|
||||||
|
|
||||||
if self.fields[fn].help_text:
|
if self.fields[fn].help_text:
|
||||||
@@ -345,7 +345,7 @@ class VoucherBulkForm(VoucherForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self._set_field_placeholders('send_subject', ['event', 'name'])
|
self._set_field_placeholders('send_subject', ['event', 'name'])
|
||||||
self._set_field_placeholders('send_message', ['event', 'voucher_list', 'name'], rich=True)
|
self._set_field_placeholders('send_message', ['event', 'voucher_list', 'name'])
|
||||||
|
|
||||||
with language(self.instance.event.settings.locale, self.instance.event.settings.region):
|
with language(self.instance.event.settings.locale, self.instance.event.settings.region):
|
||||||
for f in ("send_subject", "send_message"):
|
for f in ("send_subject", "send_message"):
|
||||||
|
|||||||
@@ -69,13 +69,12 @@
|
|||||||
{% if show_meta %}
|
{% if show_meta %}
|
||||||
<span class="text-muted text-sm">{{ plugin.version }}</span>
|
<span class="text-muted text-sm">{{ plugin.version }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if is_active or plugin.level == "event" %}
|
{% if is_active and level == "organizer" %}
|
||||||
{% if plugin.level == "organizer" %}
|
|
||||||
<span class="label label-success" data-is-active>
|
<span class="label label-success" data-is-active>
|
||||||
<span class="fa fa-check" aria-hidden="true"></span>
|
<span class="fa fa-check" aria-hidden="true"></span>
|
||||||
{% trans "Active" %}
|
{% trans "Active" %}
|
||||||
</span>
|
</span>
|
||||||
{% elif events_total and events_counter == events_total %}
|
{% elif events_counter == events_total %}
|
||||||
<span class="label label-success" data-is-active>
|
<span class="label label-success" data-is-active>
|
||||||
<span class="fa fa-check" aria-hidden="true"></span>
|
<span class="fa fa-check" aria-hidden="true"></span>
|
||||||
{% trans "Active (all events)" %}
|
{% trans "Active (all events)" %}
|
||||||
@@ -99,7 +98,6 @@
|
|||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
</h4>
|
</h4>
|
||||||
{% include "pretixcontrol/event/fragment_plugin_description.html" with plugin=plugin %}
|
{% include "pretixcontrol/event/fragment_plugin_description.html" with plugin=plugin %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,8 +22,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from string import Formatter
|
from string import Formatter
|
||||||
|
|
||||||
from django.utils.html import conditional_escape
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -42,10 +40,11 @@ class SafeFormatter(Formatter):
|
|||||||
Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and
|
Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and
|
||||||
(b) does not allow any unwanted shenanigans like attribute access or format specifiers.
|
(b) does not allow any unwanted shenanigans like attribute access or format specifiers.
|
||||||
"""
|
"""
|
||||||
|
MODE_IGNORE_RICH = 0
|
||||||
MODE_RICH_TO_PLAIN = 1
|
MODE_RICH_TO_PLAIN = 1
|
||||||
MODE_RICH_TO_HTML = 2
|
MODE_RICH_TO_HTML = 2
|
||||||
|
|
||||||
def __init__(self, context, raise_on_missing=False, mode=MODE_RICH_TO_PLAIN):
|
def __init__(self, context, raise_on_missing=False, mode=MODE_IGNORE_RICH):
|
||||||
self.context = context
|
self.context = context
|
||||||
self.raise_on_missing = raise_on_missing
|
self.raise_on_missing = raise_on_missing
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
@@ -56,26 +55,22 @@ class SafeFormatter(Formatter):
|
|||||||
def get_value(self, key, args, kwargs):
|
def get_value(self, key, args, kwargs):
|
||||||
if not self.raise_on_missing and key not in self.context:
|
if not self.raise_on_missing and key not in self.context:
|
||||||
return '{' + str(key) + '}'
|
return '{' + str(key) + '}'
|
||||||
return self.context[key]
|
r = self.context[key]
|
||||||
|
if isinstance(r, PlainHtmlAlternativeString):
|
||||||
def _prepare_value(self, value):
|
if self.mode == self.MODE_IGNORE_RICH:
|
||||||
if isinstance(value, PlainHtmlAlternativeString):
|
return '{' + str(key) + '}'
|
||||||
if self.mode == self.MODE_RICH_TO_PLAIN:
|
elif self.mode == self.MODE_RICH_TO_PLAIN:
|
||||||
return value.plain
|
return r.plain
|
||||||
elif self.mode == self.MODE_RICH_TO_HTML:
|
elif self.mode == self.MODE_RICH_TO_HTML:
|
||||||
return value.html
|
return r.html
|
||||||
else:
|
return r
|
||||||
value = str(value)
|
|
||||||
if self.mode == self.MODE_RICH_TO_HTML:
|
|
||||||
value = conditional_escape(value)
|
|
||||||
return value
|
|
||||||
|
|
||||||
def format_field(self, value, format_spec):
|
def format_field(self, value, format_spec):
|
||||||
# Ignore format_spec
|
# Ignore format_spec
|
||||||
return super().format_field(self._prepare_value(value), '')
|
return super().format_field(value, '')
|
||||||
|
|
||||||
|
|
||||||
def format_map(template, context, raise_on_missing=False, mode=SafeFormatter.MODE_RICH_TO_PLAIN):
|
def format_map(template, context, raise_on_missing=False, mode=SafeFormatter.MODE_IGNORE_RICH):
|
||||||
if not isinstance(template, str):
|
if not isinstance(template, str):
|
||||||
template = str(template)
|
template = str(template)
|
||||||
return SafeFormatter(context, raise_on_missing, mode=mode).format(template)
|
return SafeFormatter(context, raise_on_missing, mode=mode).format(template)
|
||||||
|
|||||||
@@ -33,8 +33,6 @@
|
|||||||
# License for the specific language governing permissions and limitations under the License.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
from email.mime.text import MIMEText
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -42,9 +40,7 @@ from django.core import mail as djmail
|
|||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_scopes import scope
|
from django_scopes import scope
|
||||||
from i18nfield.strings import LazyI18nString
|
|
||||||
|
|
||||||
from pretix.base.email import get_email_context
|
|
||||||
from pretix.base.models import Event, Organizer, User
|
from pretix.base.models import Event, Organizer, User
|
||||||
from pretix.base.services.mail import mail
|
from pretix.base.services.mail import mail
|
||||||
|
|
||||||
@@ -52,14 +48,10 @@ from pretix.base.services.mail import mail
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def env():
|
def env():
|
||||||
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
||||||
prop1 = o.meta_properties.get_or_create(name="Test")[0]
|
|
||||||
prop2 = o.meta_properties.get_or_create(name="Website")[0]
|
|
||||||
event = Event.objects.create(
|
event = Event.objects.create(
|
||||||
organizer=o, name='Dummy', slug='dummy',
|
organizer=o, name='Dummy', slug='dummy',
|
||||||
date_from=now()
|
date_from=now()
|
||||||
)
|
)
|
||||||
event.meta_values.update_or_create(property=prop1, defaults={'value': "*Beep*"})
|
|
||||||
event.meta_values.update_or_create(property=prop2, defaults={'value': "https://example.com"})
|
|
||||||
user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
||||||
user.email = 'dummy@dummy.dummy'
|
user.email = 'dummy@dummy.dummy'
|
||||||
user.save()
|
user.save()
|
||||||
@@ -166,95 +158,8 @@ def test_send_mail_with_user_locale(env):
|
|||||||
def test_sendmail_placeholder(env):
|
def test_sendmail_placeholder(env):
|
||||||
djmail.outbox = []
|
djmail.outbox = []
|
||||||
event, user, organizer = env
|
event, user, organizer = env
|
||||||
mail('dummy@dummy.dummy', '{event} Test subject', 'mailtest.txt', {"event": event.name}, event)
|
mail('dummy@dummy.dummy', '{event} Test subject', 'mailtest.txt', {"event": event}, event)
|
||||||
|
|
||||||
assert len(djmail.outbox) == 1
|
assert len(djmail.outbox) == 1
|
||||||
assert djmail.outbox[0].to == [user.email]
|
assert djmail.outbox[0].to == [user.email]
|
||||||
assert djmail.outbox[0].subject == 'Dummy Test subject'
|
assert djmail.outbox[0].subject == 'Dummy Test subject'
|
||||||
|
|
||||||
|
|
||||||
def _extract_html(mail):
|
|
||||||
for content, mimetype in mail.alternatives:
|
|
||||||
if "multipart/related" in mimetype:
|
|
||||||
for sp in content._payload:
|
|
||||||
if isinstance(sp, MIMEText):
|
|
||||||
return sp._payload
|
|
||||||
break
|
|
||||||
elif "text/html" in mimetype:
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_placeholder_html_rendering_from_template(env):
|
|
||||||
djmail.outbox = []
|
|
||||||
event, user, organizer = env
|
|
||||||
event.name = "<strong>event & co. kg</strong>"
|
|
||||||
event.save()
|
|
||||||
mail('dummy@dummy.dummy', '{event} Test subject', 'mailtest.txt', get_email_context(
|
|
||||||
event=event,
|
|
||||||
payment_info="**IBAN**: 123 \n**BIC**: 456",
|
|
||||||
), event)
|
|
||||||
|
|
||||||
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
|
|
||||||
html = _extract_html(djmail.outbox[0])
|
|
||||||
|
|
||||||
assert '<strong>event' not in html
|
|
||||||
assert 'Event name: <strong>event & co. kg</strong>' 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 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>',
|
|
||||||
html
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_placeholder_html_rendering_from_string(env):
|
|
||||||
template = LazyI18nString({
|
|
||||||
"en": "Event name: {event}\n\nPayment info:\n{payment_info}\n\n**Meta**: {meta_Test}\n\n"
|
|
||||||
"Event website: [{event}](https://example.org/{event_slug})\n\n"
|
|
||||||
"Other website: [{event}]({meta_Website})"
|
|
||||||
})
|
|
||||||
djmail.outbox = []
|
|
||||||
event, user, organizer = env
|
|
||||||
event.name = "<strong>event & co. kg</strong>"
|
|
||||||
event.save()
|
|
||||||
mail('dummy@dummy.dummy', '{event} Test subject', template, get_email_context(
|
|
||||||
event=event,
|
|
||||||
payment_info="**IBAN**: 123 \n**BIC**: 456",
|
|
||||||
), event)
|
|
||||||
|
|
||||||
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 '**IBAN**: 123 \n**BIC**: 456' in djmail.outbox[0].body
|
|
||||||
assert '**Meta**: *Beep*' in djmail.outbox[0].body
|
|
||||||
assert '<' not in djmail.outbox[0].body
|
|
||||||
assert '&' 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 '<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>',
|
|
||||||
html
|
|
||||||
)
|
|
||||||
assert re.search(
|
|
||||||
r'Other website: <a href="https://example.com" rel="noopener" style="[^"]+" target="_blank"><strong>event & co. kg</strong></a>',
|
|
||||||
html
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -40,5 +40,6 @@ def test_format_alternatives():
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert format_map("Foo {bar}", ctx, mode=SafeFormatter.MODE_IGNORE_RICH) == "Foo {bar}"
|
||||||
assert format_map("Foo {bar}", ctx, mode=SafeFormatter.MODE_RICH_TO_PLAIN) == "Foo plain text"
|
assert format_map("Foo {bar}", ctx, mode=SafeFormatter.MODE_RICH_TO_PLAIN) == "Foo plain text"
|
||||||
assert format_map("Foo {bar}", ctx, mode=SafeFormatter.MODE_RICH_TO_HTML) == "Foo <span>HTML version</span>"
|
assert format_map("Foo {bar}", ctx, mode=SafeFormatter.MODE_RICH_TO_HTML) == "Foo <span>HTML version</span>"
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ from pretix.plugins.banktransfer.models import BankImportJob, BankTransaction
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def env():
|
def env():
|
||||||
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
o = Organizer.objects.create(name='Dummy', slug='dummy', plugins='pretix.plugins.banktransfer')
|
||||||
event = Event.objects.create(
|
event = Event.objects.create(
|
||||||
organizer=o, name='Dummy', slug='dummy',
|
organizer=o, name='Dummy', slug='dummy',
|
||||||
date_from=now(), plugins='pretix.plugins.banktransfer'
|
date_from=now(), plugins='pretix.plugins.banktransfer'
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ from pretix.plugins.banktransfer.models import BankImportJob, BankTransaction
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def env():
|
def env():
|
||||||
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
o = Organizer.objects.create(name='Dummy', slug='dummy', plugins='pretix.plugins.banktransfer')
|
||||||
event = Event.objects.create(
|
event = Event.objects.create(
|
||||||
organizer=o, name='Dummy', slug='dummy',
|
organizer=o, name='Dummy', slug='dummy',
|
||||||
date_from=now(), plugins='pretix.plugins.banktransfer'
|
date_from=now(), plugins='pretix.plugins.banktransfer'
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ from pretix.plugins.banktransfer.tasks import process_banktransfers
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def env():
|
def env():
|
||||||
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
o = Organizer.objects.create(name='Dummy', slug='dummy', plugins='pretix.plugins.banktransfer')
|
||||||
event = Event.objects.create(
|
event = Event.objects.create(
|
||||||
organizer=o, name='Dummy', slug='dummy',
|
organizer=o, name='Dummy', slug='dummy',
|
||||||
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
|
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ from pretix.base.models import (
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def env():
|
def env():
|
||||||
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
o = Organizer.objects.create(name='Dummy', slug='dummy', plugins='pretix.plugins.banktransfer')
|
||||||
event = Event.objects.create(
|
event = Event.objects.create(
|
||||||
organizer=o, name='Dummy', slug='dummy',
|
organizer=o, name='Dummy', slug='dummy',
|
||||||
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
|
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ from pretix.plugins.banktransfer.views import (
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def env():
|
def env():
|
||||||
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
o = Organizer.objects.create(name='Dummy', slug='dummy', plugins='pretix.plugins.banktransfer')
|
||||||
event = Event.objects.create(
|
event = Event.objects.create(
|
||||||
organizer=o, name='Dummy', slug='dummy',
|
organizer=o, name='Dummy', slug='dummy',
|
||||||
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
|
date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
|
||||||
|
|||||||
@@ -1,13 +1,4 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
This is a test file for sending mails.
|
This is a test file for sending mails.
|
||||||
Event name: {event}
|
|
||||||
{% get_current_language as LANGUAGE_CODE %}
|
{% get_current_language as LANGUAGE_CODE %}
|
||||||
The language code used for rendering this email is {{ LANGUAGE_CODE }}.
|
The language code used for rendering this email is {{ LANGUAGE_CODE }}.
|
||||||
|
|
||||||
Payment info:
|
|
||||||
{payment_info}
|
|
||||||
|
|
||||||
**Meta**: {meta_Test}
|
|
||||||
|
|
||||||
Event website: [{event}](https://example.org/{event_slug})
|
|
||||||
Other website: [{event}]({meta_Website})
|
|
||||||
Reference in New Issue
Block a user