forked from CGM_Public/pretix_original
Add the option to introduce rich-text placeholders (#4657)
* Add the option to introduce rich-text placeholders * Add tests in test_format * Add some css * Block vs inline * Some fixed css * Update src/pretix/control/forms/event.py Co-authored-by: Mira <weller@rami.io> * Add missing docstring prat --------- Co-authored-by: Mira <weller@rami.io>
This commit is contained in:
@@ -35,6 +35,7 @@ from django.utils.translation import get_language, gettext_lazy as _
|
||||
from pretix.base.models import Event
|
||||
from pretix.base.signals import register_html_mail_renderers
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
from pretix.helpers.format import SafeFormatter, format_map
|
||||
|
||||
from pretix.base.services.placeholders import ( # noqa
|
||||
get_available_placeholders, PlaceholderContext
|
||||
@@ -79,7 +80,7 @@ class BaseHTMLMailRenderer:
|
||||
return self.identifier
|
||||
|
||||
def render(self, plain_body: str, plain_signature: str, subject: str, order=None,
|
||||
position=None) -> str:
|
||||
position=None, context=None) -> str:
|
||||
"""
|
||||
This method should generate the HTML part of the email.
|
||||
|
||||
@@ -88,6 +89,7 @@ class BaseHTMLMailRenderer:
|
||||
:param subject: The email subject.
|
||||
:param order: The order if this email is connected to one, otherwise ``None``.
|
||||
:param position: The order position if this email is connected to one, otherwise ``None``.
|
||||
:param context: Context to use to render placeholders in the plain body
|
||||
:return: An HTML string
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
@@ -134,8 +136,10 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
def compile_markdown(self, plaintext):
|
||||
return markdown_compile_email(plaintext)
|
||||
|
||||
def render(self, plain_body: str, plain_signature: str, subject: str, order, position) -> str:
|
||||
def render(self, plain_body: str, plain_signature: str, subject: str, order, position, context) -> str:
|
||||
body_md = self.compile_markdown(plain_body)
|
||||
if context:
|
||||
body_md = format_map(body_md, context=context, mode=SafeFormatter.MODE_RICH_TO_HTML)
|
||||
htmlctx = {
|
||||
'site': settings.PRETIX_INSTANCE_NAME,
|
||||
'site_url': settings.SITE_URL,
|
||||
|
||||
@@ -76,7 +76,7 @@ from pretix.base.services.tasks import TransactionAwareTask
|
||||
from pretix.base.services.tickets import get_tickets_for_order
|
||||
from pretix.base.signals import email_filter, global_email_filter
|
||||
from pretix.celery_app import app
|
||||
from pretix.helpers.format import format_map
|
||||
from pretix.helpers.format import 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
|
||||
@@ -311,7 +311,13 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
try:
|
||||
if plain_text_only:
|
||||
body_html = None
|
||||
elif '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
|
||||
@@ -323,6 +329,8 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
logger.exception('Could not render HTML body')
|
||||
body_html = None
|
||||
|
||||
body_plain = format_map(body_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
|
||||
|
||||
send_task = mail_send_task.si(
|
||||
to=[email] if isinstance(email, str) else list(email),
|
||||
cc=cc,
|
||||
@@ -655,7 +663,7 @@ def render_mail(template, context):
|
||||
if isinstance(template, LazyI18nString):
|
||||
body = str(template)
|
||||
if context:
|
||||
body = format_map(body, context)
|
||||
body = format_map(body, context, mode=SafeFormatter.MODE_IGNORE_RICH)
|
||||
else:
|
||||
tpl = get_template(template)
|
||||
body = tpl.render(context)
|
||||
|
||||
@@ -26,6 +26,7 @@ from decimal import Decimal
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.html import escape
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@@ -39,7 +40,8 @@ from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
|
||||
from pretix.base.signals import (
|
||||
register_mail_placeholders, register_text_placeholders,
|
||||
)
|
||||
from pretix.helpers.format import SafeFormatter
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
from pretix.helpers.format import PlainHtmlAlternativeString, SafeFormatter
|
||||
|
||||
logger = logging.getLogger('pretix.base.services.placeholders')
|
||||
|
||||
@@ -107,6 +109,91 @@ class SimpleFunctionalTextPlaceholder(BaseTextPlaceholder):
|
||||
return self._sample
|
||||
|
||||
|
||||
class BaseRichTextPlaceholder(BaseTextPlaceholder):
|
||||
"""
|
||||
This is the base class for all placeholders which can render either to plain text
|
||||
or to a rich HTML element.
|
||||
"""
|
||||
|
||||
def __init__(self, identifier, args):
|
||||
self._identifier = identifier
|
||||
self._args = args
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
return self._identifier
|
||||
|
||||
@property
|
||||
def required_context(self):
|
||||
return self._args
|
||||
|
||||
@property
|
||||
def is_block(self):
|
||||
return False
|
||||
|
||||
def render(self, context):
|
||||
return PlainHtmlAlternativeString(
|
||||
self.render_plain(**{k: context[k] for k in self._args}),
|
||||
self.render_html(**{k: context[k] for k in self._args}),
|
||||
self.is_block,
|
||||
)
|
||||
|
||||
def render_html(self, **kwargs):
|
||||
"""
|
||||
HTML rendering of the placeholder. Should return "safe" HTML, i.e. everything needs to be
|
||||
escaped.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def render_plain(self, **kwargs):
|
||||
"""
|
||||
Plain text rendering of the placeholder.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def render_sample(self, event):
|
||||
return PlainHtmlAlternativeString(
|
||||
self.render_sample_plain(event=event),
|
||||
self.render_sample_html(event=event),
|
||||
self.is_block,
|
||||
)
|
||||
|
||||
def render_sample_html(self, event):
|
||||
raise NotImplementedError
|
||||
|
||||
def render_sample_plain(self, event):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SimpleButtonPlaceholder(BaseRichTextPlaceholder):
|
||||
def __init__(self, identifier, args, url_func, text_func, sample_url_func, sample_text_func):
|
||||
super().__init__(identifier, args)
|
||||
self._url_func = url_func
|
||||
self._text_func = text_func
|
||||
self._sample_url_func = sample_url_func
|
||||
self._sample_text_func = sample_text_func
|
||||
|
||||
def render_html(self, **context):
|
||||
text = self._text_func(**{k: context[k] for k in self._args})
|
||||
url = self._url_func(**{k: context[k] for k in self._args})
|
||||
return f'<a href="{url}" class="button">{escape(text)}</a>'
|
||||
|
||||
def render_plain(self, **context):
|
||||
text = self._text_func(**{k: context[k] for k in self._args})
|
||||
url = self._url_func(**{k: context[k] for k in self._args})
|
||||
return f'{text}: {url}'
|
||||
|
||||
def render_sample_html(self, event):
|
||||
text = self._sample_text_func(event)
|
||||
url = self._sample_url_func(event)
|
||||
return f'<a href="{url}" class="button">{escape(text)}</a>'
|
||||
|
||||
def render_sample_plain(self, event):
|
||||
text = self._sample_text_func(event)
|
||||
url = self._sample_url_func(event)
|
||||
return f'{text}: {url}'
|
||||
|
||||
|
||||
class PlaceholderContext(SafeFormatter):
|
||||
"""
|
||||
Holds the contextual arguments and corresponding list of available placeholders for formatting
|
||||
@@ -284,6 +371,27 @@ def base_placeholders(sender, **kwargs):
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleButtonPlaceholder(
|
||||
'url_button', ['order', 'event'],
|
||||
url_func=lambda order, event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.open', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret,
|
||||
'hash': order.email_confirm_secret()
|
||||
}
|
||||
),
|
||||
text_func=lambda order, event: _("View order details"),
|
||||
sample_url_func=lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.open', kwargs={
|
||||
'order': 'F8VVL',
|
||||
'secret': '6zzjnumtsx136ddy',
|
||||
'hash': '98kusd8ofsj8dnkd'
|
||||
}
|
||||
),
|
||||
sample_text_func=lambda event: _("View order details"),
|
||||
),
|
||||
SimpleFunctionalTextPlaceholder(
|
||||
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||
event,
|
||||
@@ -348,6 +456,27 @@ def base_placeholders(sender, **kwargs):
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleButtonPlaceholder(
|
||||
'url_button', ['event', 'position'],
|
||||
url_func=lambda event, position: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.position', kwargs={
|
||||
'order': position.order.code,
|
||||
'secret': position.web_secret,
|
||||
'position': position.positionid
|
||||
}
|
||||
),
|
||||
text_func=lambda event, position: _("View registration details"),
|
||||
sample_url_func=lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.order.position', kwargs={
|
||||
'order': 'F8VVL',
|
||||
'secret': '6zzjnumtsx136ddy',
|
||||
'position': '123'
|
||||
}
|
||||
),
|
||||
sample_text_func=lambda event: _("View registration details"),
|
||||
),
|
||||
SimpleFunctionalTextPlaceholder(
|
||||
'url_info_change', ['position', 'event'], lambda position, event: build_absolute_uri(
|
||||
event,
|
||||
@@ -603,8 +732,8 @@ def base_placeholders(sender, **kwargs):
|
||||
|
||||
|
||||
class FormPlaceholderMixin:
|
||||
def _set_field_placeholders(self, fn, base_parameters):
|
||||
placeholders = get_available_placeholders(self.event, base_parameters)
|
||||
def _set_field_placeholders(self, fn, base_parameters, rich=False):
|
||||
placeholders = get_available_placeholders(self.event, base_parameters, rich=rich)
|
||||
ht = format_placeholders_help_text(placeholders, self.event)
|
||||
if self.fields[fn].help_text:
|
||||
self.fields[fn].help_text += ' ' + str(ht)
|
||||
@@ -615,7 +744,7 @@ class FormPlaceholderMixin:
|
||||
)
|
||||
|
||||
|
||||
def get_available_placeholders(event, base_parameters):
|
||||
def get_available_placeholders(event, base_parameters, rich=False):
|
||||
if 'order' in base_parameters:
|
||||
base_parameters.append('invoice_address')
|
||||
base_parameters.append('position_or_address')
|
||||
@@ -624,6 +753,35 @@ def get_available_placeholders(event, base_parameters):
|
||||
if not isinstance(val, (list, tuple)):
|
||||
val = [val]
|
||||
for v in val:
|
||||
if isinstance(v, BaseRichTextPlaceholder) and not rich:
|
||||
continue
|
||||
if all(rp in base_parameters for rp in v.required_context):
|
||||
params[v.identifier] = v
|
||||
return params
|
||||
|
||||
|
||||
def get_sample_context(event, context_parameters, rich=True):
|
||||
context_dict = {}
|
||||
lbl = _('This value will be replaced based on dynamic parameters.')
|
||||
for k, v in get_available_placeholders(event, context_parameters, rich=rich).items():
|
||||
sample = v.render_sample(event)
|
||||
if isinstance(sample, PlainHtmlAlternativeString):
|
||||
context_dict[k] = PlainHtmlAlternativeString(
|
||||
sample.plain,
|
||||
'<{el} class="placeholder placeholder-html" title="{title}">{html}</{el}>'.format(
|
||||
el='div' if sample.is_block else 'span',
|
||||
title=lbl,
|
||||
html=sample.html,
|
||||
)
|
||||
)
|
||||
elif str(sample).strip().startswith('* ') or str(sample).startswith(' '):
|
||||
context_dict[k] = '<div class="placeholder" title="{}">{}</div>'.format(
|
||||
lbl,
|
||||
markdown_compile_email(str(sample))
|
||||
)
|
||||
else:
|
||||
context_dict[k] = '<span class="placeholder" title="{}">{}</span>'.format(
|
||||
lbl,
|
||||
escape(sample)
|
||||
)
|
||||
return context_dict
|
||||
|
||||
@@ -131,6 +131,9 @@
|
||||
text-align: left;
|
||||
padding: 0;
|
||||
}
|
||||
.content table td.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
a.button {
|
||||
display: inline-block;
|
||||
@@ -178,6 +181,9 @@
|
||||
pre, pre code {
|
||||
white-space: pre-line;
|
||||
}
|
||||
.text-right, .content table td.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
{% if rtl %}
|
||||
body {
|
||||
@@ -186,6 +192,9 @@
|
||||
.content {
|
||||
text-align: right;
|
||||
}
|
||||
.text-right, .content table td.text-right {
|
||||
text-align: left;
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{% block addcss %}{% endblock %}
|
||||
|
||||
@@ -305,6 +305,7 @@ def markdown_compile_email(source, allowed_tags=ALLOWED_TAGS, allowed_attributes
|
||||
source,
|
||||
extensions=[
|
||||
'markdown.extensions.sane_lists',
|
||||
'markdown.extensions.tables',
|
||||
EmailNl2BrExtension(),
|
||||
LinkifyAndCleanExtension(
|
||||
linker,
|
||||
|
||||
Reference in New Issue
Block a user