mirror of
https://github.com/pretix/pretix.git
synced 2025-12-12 04:42:28 +00:00
Allow template syntax in event text (Z#23140046) (#3815)
* remove duplicate context generation * allow text templates in frontpage_text * refactor: move placeholder functionality to separate file * fix wrong class name, code style * update year in license header * undo license header update * use new function name * render only the placeholders that are actually used in the message * refactoring * add str(...) call * Update doc/development/api/placeholder.rst Co-authored-by: Raphael Michel <michel@rami.io> * rename register_mail_placeholders to register_template_placeholders (deprecate old name) * isort * add signals to docs --------- Co-authored-by: Raphael Michel <michel@rami.io>
This commit is contained in:
@@ -13,7 +13,8 @@ Core
|
|||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
|
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
|
||||||
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter,
|
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter,
|
||||||
register_ticket_secret_generators, gift_card_transaction_display
|
register_ticket_secret_generators, gift_card_transaction_display,
|
||||||
|
register_text_placeholders, register_mail_placeholders
|
||||||
|
|
||||||
Order events
|
Order events
|
||||||
""""""""""""
|
""""""""""""
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
.. highlight:: python
|
.. highlight:: python
|
||||||
:linenothreshold: 5
|
:linenothreshold: 5
|
||||||
|
|
||||||
Writing an e-mail placeholder plugin
|
Writing a template placeholder plugin
|
||||||
====================================
|
=====================================
|
||||||
|
|
||||||
An email placeholder is a dynamic value that pretix users can use in their email templates.
|
A template placeholder is a dynamic value that pretix users can use in their email templates and in other
|
||||||
|
configurable texts.
|
||||||
|
|
||||||
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
|
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
|
||||||
|
|
||||||
@@ -12,31 +13,31 @@ Placeholder registration
|
|||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
The placeholder API does not make a lot of usage from signals, however, it
|
The placeholder API does not make a lot of usage from signals, however, it
|
||||||
does use a signal to get a list of all available email placeholders. Your plugin
|
does use a signal to get a list of all available placeholders. Your plugin
|
||||||
should listen for this signal and return an instance of a subclass of ``pretix.base.email.BaseMailTextPlaceholder``:
|
should listen for this signal and return an instance of a subclass of ``pretix.base.services.placeholders.BaseTextPlaceholder``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from pretix.base.signals import register_mail_placeholders
|
from pretix.base.signals import register_text_placeholders
|
||||||
|
|
||||||
|
|
||||||
@receiver(register_mail_placeholders, dispatch_uid="placeholder_custom")
|
@receiver(register_text_placeholders, dispatch_uid="placeholder_custom")
|
||||||
def register_mail_renderers(sender, **kwargs):
|
def register_placeholder_renderers(sender, **kwargs):
|
||||||
from .email import MyPlaceholderClass
|
from .placeholders import MyPlaceholderClass
|
||||||
return MyPlaceholder()
|
return MyPlaceholder()
|
||||||
|
|
||||||
|
|
||||||
Context mechanism
|
Context mechanism
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
Emails are sent in different "contexts" within pretix. For example, many emails are sent in the
|
Templates are used in different "contexts" within pretix. For example, many emails are rendered from
|
||||||
the context of an order, but some are not, such as the notification of a waiting list voucher.
|
templates in the context of an order, but some are not, such as the notification of a waiting list voucher.
|
||||||
|
|
||||||
Not all placeholders make sense in every email, and placeholders usually depend some parameters
|
Not all placeholders make sense everywhere, and placeholders usually depend on some parameters
|
||||||
themselves, such as the ``Order`` object. Therefore, placeholders are expected to explicitly declare
|
themselves, such as the ``Order`` object. Therefore, placeholders are expected to explicitly declare
|
||||||
what values they depend on and they will only be available in an email if all those dependencies are
|
what values they depend on and they will only be available in a context where all those dependencies are
|
||||||
met. Currently, placeholders can depend on the following context parameters:
|
met. Currently, placeholders can depend on the following context parameters:
|
||||||
|
|
||||||
* ``event``
|
* ``event``
|
||||||
@@ -51,7 +52,7 @@ There are a few more that are only to be used internally but not by plugins.
|
|||||||
The placeholder class
|
The placeholder class
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
.. class:: pretix.base.email.BaseMailTextPlaceholder
|
.. class:: pretix.base.services.placeholders.BaseTextPlaceholder
|
||||||
|
|
||||||
.. autoattribute:: identifier
|
.. autoattribute:: identifier
|
||||||
|
|
||||||
@@ -77,7 +78,15 @@ functions:
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
placeholder = SimpleFunctionalMailTextPlaceholder(
|
placeholder = SimpleFunctionalTextPlaceholder(
|
||||||
'code', ['order'], lambda order: order.code, sample='F8VVL'
|
'code', ['order'], lambda order: order.code, sample='F8VVL'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Signals
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. automodule:: pretix.base.signals
|
||||||
|
:members: register_text_placeholders
|
||||||
|
.. automodule:: pretix.base.signals
|
||||||
|
:members: register_mail_placeholders
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,7 @@
|
|||||||
# 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/>.
|
||||||
#
|
#
|
||||||
import inspect
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
|
||||||
from decimal import Decimal
|
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from smtplib import SMTPResponseException
|
from smtplib import SMTPResponseException
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
@@ -33,21 +30,21 @@ from django.core.mail.backends.smtp import EmailBackend
|
|||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.utils.formats import date_format
|
|
||||||
from django.utils.timezone import now
|
|
||||||
from django.utils.translation import get_language, gettext_lazy as _
|
from django.utils.translation import get_language, gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.i18n import (
|
|
||||||
LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber,
|
|
||||||
)
|
|
||||||
from pretix.base.models import Event
|
from pretix.base.models import Event
|
||||||
from pretix.base.reldate import RelativeDateWrapper
|
from pretix.base.signals import register_html_mail_renderers
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
|
|
||||||
from pretix.base.signals import (
|
|
||||||
register_html_mail_renderers, register_mail_placeholders,
|
|
||||||
)
|
|
||||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||||
|
|
||||||
|
from pretix.base.services.placeholders import ( # noqa
|
||||||
|
get_available_placeholders, PlaceholderContext
|
||||||
|
)
|
||||||
|
from pretix.base.services.placeholders import ( # noqa
|
||||||
|
BaseTextPlaceholder as BaseMailTextPlaceholder,
|
||||||
|
SimpleFunctionalTextPlaceholder as SimpleFunctionalMailTextPlaceholder,
|
||||||
|
)
|
||||||
|
from pretix.base.settings import get_name_parts_localized # noqa
|
||||||
|
|
||||||
logger = logging.getLogger('pretix.base.email')
|
logger = logging.getLogger('pretix.base.email')
|
||||||
|
|
||||||
T = TypeVar("T", bound=EmailBackend)
|
T = TypeVar("T", bound=EmailBackend)
|
||||||
@@ -217,495 +214,5 @@ def base_renderers(sender, **kwargs):
|
|||||||
return [ClassicMailRenderer, UnembellishedMailRenderer]
|
return [ClassicMailRenderer, UnembellishedMailRenderer]
|
||||||
|
|
||||||
|
|
||||||
class BaseMailTextPlaceholder:
|
|
||||||
"""
|
|
||||||
This is the base class for for all email text placeholders.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def required_context(self):
|
|
||||||
"""
|
|
||||||
This property should return a list of all attribute names that need to be
|
|
||||||
contained in the base context so that this placeholder is available. By default,
|
|
||||||
it returns a list containing the string "event".
|
|
||||||
"""
|
|
||||||
return ["event"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def identifier(self):
|
|
||||||
"""
|
|
||||||
This should return the identifier of this placeholder in the email.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def render(self, context):
|
|
||||||
"""
|
|
||||||
This method is called to generate the actual text that is being
|
|
||||||
used in the email. You will be passed a context dictionary with the
|
|
||||||
base context attributes specified in ``required_context``. You are
|
|
||||||
expected to return a plain-text string.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def render_sample(self, event):
|
|
||||||
"""
|
|
||||||
This method is called to generate a text to be used in email previews.
|
|
||||||
This may only depend on the event.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleFunctionalMailTextPlaceholder(BaseMailTextPlaceholder):
|
|
||||||
def __init__(self, identifier, args, func, sample):
|
|
||||||
self._identifier = identifier
|
|
||||||
self._args = args
|
|
||||||
self._func = func
|
|
||||||
self._sample = sample
|
|
||||||
|
|
||||||
@property
|
|
||||||
def identifier(self):
|
|
||||||
return self._identifier
|
|
||||||
|
|
||||||
@property
|
|
||||||
def required_context(self):
|
|
||||||
return self._args
|
|
||||||
|
|
||||||
def render(self, context):
|
|
||||||
return self._func(**{k: context[k] for k in self._args})
|
|
||||||
|
|
||||||
def render_sample(self, event):
|
|
||||||
if callable(self._sample):
|
|
||||||
return self._sample(event)
|
|
||||||
else:
|
|
||||||
return self._sample
|
|
||||||
|
|
||||||
|
|
||||||
def get_available_placeholders(event, base_parameters):
|
|
||||||
if 'order' in base_parameters:
|
|
||||||
base_parameters.append('invoice_address')
|
|
||||||
base_parameters.append('position_or_address')
|
|
||||||
params = {}
|
|
||||||
for r, val in register_mail_placeholders.send(sender=event):
|
|
||||||
if not isinstance(val, (list, tuple)):
|
|
||||||
val = [val]
|
|
||||||
for v in val:
|
|
||||||
if all(rp in base_parameters for rp in v.required_context):
|
|
||||||
params[v.identifier] = v
|
|
||||||
return params
|
|
||||||
|
|
||||||
|
|
||||||
def get_email_context(**kwargs):
|
def get_email_context(**kwargs):
|
||||||
from pretix.base.models import InvoiceAddress
|
return PlaceholderContext(**kwargs).render_all()
|
||||||
|
|
||||||
event = kwargs['event']
|
|
||||||
if 'position' in kwargs:
|
|
||||||
kwargs.setdefault("position_or_address", kwargs['position'])
|
|
||||||
if 'order' in kwargs:
|
|
||||||
try:
|
|
||||||
if not kwargs.get('invoice_address'):
|
|
||||||
kwargs['invoice_address'] = kwargs['order'].invoice_address
|
|
||||||
except InvoiceAddress.DoesNotExist:
|
|
||||||
kwargs['invoice_address'] = InvoiceAddress(order=kwargs['order'])
|
|
||||||
finally:
|
|
||||||
kwargs.setdefault("position_or_address", kwargs['invoice_address'])
|
|
||||||
ctx = {}
|
|
||||||
for r, val in register_mail_placeholders.send(sender=event):
|
|
||||||
if not isinstance(val, (list, tuple)):
|
|
||||||
val = [val]
|
|
||||||
for v in val:
|
|
||||||
if all(rp in kwargs for rp in v.required_context):
|
|
||||||
try:
|
|
||||||
ctx[v.identifier] = v.render(kwargs)
|
|
||||||
except:
|
|
||||||
ctx[v.identifier] = '(error)'
|
|
||||||
logger.exception(f'Failed to process email placeholder {v.identifier}.')
|
|
||||||
return ctx
|
|
||||||
|
|
||||||
|
|
||||||
def _placeholder_payments(order, payments):
|
|
||||||
d = []
|
|
||||||
for payment in payments:
|
|
||||||
if 'payment' in inspect.signature(payment.payment_provider.order_pending_mail_render).parameters:
|
|
||||||
d.append(str(payment.payment_provider.order_pending_mail_render(order, payment)))
|
|
||||||
else:
|
|
||||||
d.append(str(payment.payment_provider.order_pending_mail_render(order)))
|
|
||||||
d = [line for line in d if line.strip()]
|
|
||||||
if d:
|
|
||||||
return '\n\n'.join(d)
|
|
||||||
else:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
|
|
||||||
def get_best_name(position_or_address, parts=False):
|
|
||||||
"""
|
|
||||||
Return the best name we got for either an invoice address or an order position, falling back to the respective other
|
|
||||||
"""
|
|
||||||
from pretix.base.models import InvoiceAddress, OrderPosition
|
|
||||||
if isinstance(position_or_address, InvoiceAddress):
|
|
||||||
if position_or_address.name:
|
|
||||||
return position_or_address.name_parts if parts else position_or_address.name
|
|
||||||
elif position_or_address.order:
|
|
||||||
position_or_address = position_or_address.order.positions.exclude(attendee_name_cached="").exclude(attendee_name_cached__isnull=True).first()
|
|
||||||
|
|
||||||
if isinstance(position_or_address, OrderPosition):
|
|
||||||
if position_or_address.attendee_name:
|
|
||||||
return position_or_address.attendee_name_parts if parts else position_or_address.attendee_name
|
|
||||||
elif position_or_address.order:
|
|
||||||
try:
|
|
||||||
return position_or_address.order.invoice_address.name_parts if parts else position_or_address.order.invoice_address.name
|
|
||||||
except InvoiceAddress.DoesNotExist:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return {} if parts else ""
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(register_mail_placeholders, dispatch_uid="pretixbase_register_mail_placeholders")
|
|
||||||
def base_placeholders(sender, **kwargs):
|
|
||||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
|
||||||
|
|
||||||
ph = [
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'event', ['event'], lambda event: event.name, lambda event: event.name
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'event', ['event_or_subevent'], lambda event_or_subevent: event_or_subevent.name,
|
|
||||||
lambda event_or_subevent: event_or_subevent.name
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'code', ['order'], lambda order: order.code, 'F8VVL'
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'total', ['order'], lambda order: LazyNumber(order.total), lambda event: LazyNumber(Decimal('42.23'))
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'currency', ['event'], lambda event: event.currency, lambda event: event.currency
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'order_email', ['order'], lambda order: order.email, 'john@example.org'
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'invoice_number', ['invoice'],
|
|
||||||
lambda invoice: invoice.full_invoice_no,
|
|
||||||
f'{sender.settings.invoice_numbers_prefix or (sender.slug.upper() + "-")}00000'
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'refund_amount', ['event_or_subevent', 'refund_amount'],
|
|
||||||
lambda event_or_subevent, refund_amount: LazyCurrencyNumber(refund_amount, event_or_subevent.currency),
|
|
||||||
lambda event_or_subevent: LazyCurrencyNumber(Decimal('42.23'), event_or_subevent.currency)
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'pending_sum', ['event', 'pending_sum'],
|
|
||||||
lambda event, pending_sum: LazyCurrencyNumber(pending_sum, event.currency),
|
|
||||||
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'total_with_currency', ['event', 'order'], lambda event, order: LazyCurrencyNumber(order.total,
|
|
||||||
event.currency),
|
|
||||||
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'expire_date', ['event', 'order'], lambda event, order: LazyExpiresDate(order.expires.astimezone(event.timezone)),
|
|
||||||
lambda event: LazyDate(now() + timedelta(days=15))
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'url', ['order', 'event'], lambda order, event: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.order.open', kwargs={
|
|
||||||
'order': order.code,
|
|
||||||
'secret': order.secret,
|
|
||||||
'hash': order.email_confirm_hash()
|
|
||||||
}
|
|
||||||
), lambda event: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.order.open', kwargs={
|
|
||||||
'order': 'F8VVL',
|
|
||||||
'secret': '6zzjnumtsx136ddy',
|
|
||||||
'hash': '98kusd8ofsj8dnkd'
|
|
||||||
}
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.order.modify', kwargs={
|
|
||||||
'order': order.code,
|
|
||||||
'secret': order.secret,
|
|
||||||
}
|
|
||||||
), lambda event: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.order.modify', kwargs={
|
|
||||||
'order': 'F8VVL',
|
|
||||||
'secret': '6zzjnumtsx136ddy',
|
|
||||||
}
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'url_products_change', ['order', 'event'], lambda order, event: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.order.change', kwargs={
|
|
||||||
'order': order.code,
|
|
||||||
'secret': order.secret,
|
|
||||||
}
|
|
||||||
), lambda event: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.order.change', kwargs={
|
|
||||||
'order': 'F8VVL',
|
|
||||||
'secret': '6zzjnumtsx136ddy',
|
|
||||||
}
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'url_cancel', ['order', 'event'], lambda order, event: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.order.cancel', kwargs={
|
|
||||||
'order': order.code,
|
|
||||||
'secret': order.secret,
|
|
||||||
}
|
|
||||||
), lambda event: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.order.cancel', kwargs={
|
|
||||||
'order': 'F8VVL',
|
|
||||||
'secret': '6zzjnumtsx136ddy',
|
|
||||||
}
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'url', ['event', 'position'], lambda event, position: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.order.position',
|
|
||||||
kwargs={
|
|
||||||
'order': position.order.code,
|
|
||||||
'secret': position.web_secret,
|
|
||||||
'position': position.positionid
|
|
||||||
}
|
|
||||||
),
|
|
||||||
lambda event: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.order.position', kwargs={
|
|
||||||
'order': 'F8VVL',
|
|
||||||
'secret': '6zzjnumtsx136ddy',
|
|
||||||
'position': '123'
|
|
||||||
}
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'order_modification_deadline_date_and_time', ['order', 'event'],
|
|
||||||
lambda order, event:
|
|
||||||
date_format(order.modify_deadline.astimezone(event.timezone), 'SHORT_DATETIME_FORMAT')
|
|
||||||
if order.modify_deadline
|
|
||||||
else '',
|
|
||||||
lambda event: date_format(
|
|
||||||
event.settings.get(
|
|
||||||
'last_order_modification_date', as_type=RelativeDateWrapper
|
|
||||||
).datetime(event).astimezone(event.timezone),
|
|
||||||
'SHORT_DATETIME_FORMAT'
|
|
||||||
) if event.settings.get('last_order_modification_date') else '',
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'event_location', ['event_or_subevent'], lambda event_or_subevent: str(event_or_subevent.location or ''),
|
|
||||||
lambda event: str(event.location or ''),
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'event_admission_time', ['event_or_subevent'],
|
|
||||||
lambda event_or_subevent:
|
|
||||||
date_format(event_or_subevent.date_admission.astimezone(event_or_subevent.timezone), 'TIME_FORMAT')
|
|
||||||
if event_or_subevent.date_admission
|
|
||||||
else '',
|
|
||||||
lambda event: date_format(event.date_admission.astimezone(event.timezone), 'TIME_FORMAT') if event.date_admission else '',
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'subevent', ['waiting_list_entry', 'event'],
|
|
||||||
lambda waiting_list_entry, event: str(waiting_list_entry.subevent or event),
|
|
||||||
lambda event: str(event if not event.has_subevents or not event.subevents.exists() else event.subevents.first())
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'subevent_date_from', ['waiting_list_entry', 'event'],
|
|
||||||
lambda waiting_list_entry, event: (waiting_list_entry.subevent or event).get_date_from_display(),
|
|
||||||
lambda event: (event if not event.has_subevents or not event.subevents.exists() else event.subevents.first()).get_date_from_display()
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'url_remove', ['waiting_list_voucher', 'event'],
|
|
||||||
lambda waiting_list_voucher, event: build_absolute_uri(
|
|
||||||
event, 'presale:event.waitinglist.remove'
|
|
||||||
) + '?voucher=' + waiting_list_voucher.code,
|
|
||||||
lambda event: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.waitinglist.remove',
|
|
||||||
) + '?voucher=68CYU2H6ZTP3WLK5',
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'url', ['waiting_list_voucher', 'event'],
|
|
||||||
lambda waiting_list_voucher, event: build_absolute_uri(
|
|
||||||
event, 'presale:event.redeem'
|
|
||||||
) + '?voucher=' + waiting_list_voucher.code,
|
|
||||||
lambda event: build_absolute_uri(
|
|
||||||
event,
|
|
||||||
'presale:event.redeem',
|
|
||||||
) + '?voucher=68CYU2H6ZTP3WLK5',
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'invoice_name', ['invoice_address'], lambda invoice_address: invoice_address.name or '',
|
|
||||||
_('John Doe')
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'invoice_company', ['invoice_address'], lambda invoice_address: invoice_address.company or '',
|
|
||||||
_('Sample Corporation')
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
|
|
||||||
'* {} - {}'.format(
|
|
||||||
order.full_code,
|
|
||||||
build_absolute_uri(event, 'presale:event.order.open', kwargs={
|
|
||||||
'event': event.slug,
|
|
||||||
'organizer': event.organizer.slug,
|
|
||||||
'order': order.code,
|
|
||||||
'secret': order.secret,
|
|
||||||
'hash': order.email_confirm_hash(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
for order in orders
|
|
||||||
), lambda event: '\n' + '\n\n'.join(
|
|
||||||
'* {} - {}'.format(
|
|
||||||
'{}-{}'.format(event.slug.upper(), order['code']),
|
|
||||||
build_absolute_uri(event, 'presale:event.order.open', kwargs={
|
|
||||||
'event': event.slug,
|
|
||||||
'organizer': event.organizer.slug,
|
|
||||||
'order': order['code'],
|
|
||||||
'secret': order['secret'],
|
|
||||||
'hash': order['hash'],
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
for order in [
|
|
||||||
{'code': 'F8VVL', 'secret': '6zzjnumtsx136ddy', 'hash': 'abcdefghi'},
|
|
||||||
{'code': 'HIDHK', 'secret': '98kusd8ofsj8dnkd', 'hash': 'jklmnopqr'},
|
|
||||||
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd', 'hash': 'stuvwxy2z'}
|
|
||||||
]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'hours', ['event', 'waiting_list_entry'], lambda event, waiting_list_entry:
|
|
||||||
event.settings.waiting_list_hours,
|
|
||||||
lambda event: event.settings.waiting_list_hours
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'product', ['waiting_list_entry'], lambda waiting_list_entry: waiting_list_entry.item.name,
|
|
||||||
_('Sample Admission Ticket')
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'code', ['waiting_list_voucher'], lambda waiting_list_voucher: waiting_list_voucher.code,
|
|
||||||
'68CYU2H6ZTP3WLK5'
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
# 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),
|
|
||||||
' 68CYU2H6ZTP3WLK5\n 7MB94KKPVEPSMVF2'
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
|
|
||||||
'voucher_url_list', ['event', 'voucher_list'],
|
|
||||||
lambda event, voucher_list: ' \n'.join([
|
|
||||||
build_absolute_uri(
|
|
||||||
event, 'presale:event.redeem'
|
|
||||||
) + '?voucher=' + c
|
|
||||||
for c in voucher_list
|
|
||||||
]),
|
|
||||||
lambda event: ' \n'.join([
|
|
||||||
build_absolute_uri(
|
|
||||||
event, 'presale:event.redeem'
|
|
||||||
) + '?voucher=' + c
|
|
||||||
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={
|
|
||||||
'event': event.slug,
|
|
||||||
'organizer': event.organizer.slug,
|
|
||||||
}), lambda event: build_absolute_uri(event, 'presale:event.index', kwargs={
|
|
||||||
'event': event.slug,
|
|
||||||
'organizer': event.organizer.slug,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'name', ['name'], lambda name: name,
|
|
||||||
_('John Doe')
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'comment', ['comment'], lambda comment: comment,
|
|
||||||
_('An individual text with a reason can be inserted here.'),
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'payment_info', ['order', 'payments'], _placeholder_payments,
|
|
||||||
_('The amount has been charged to your card.'),
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'payment_info', ['payment_info'], lambda payment_info: payment_info,
|
|
||||||
_('Please transfer money to this bank account: 9999-9999-9999-9999'),
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'attendee_name', ['position'], lambda position: position.attendee_name,
|
|
||||||
_('John Doe'),
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'positionid', ['position'], lambda position: str(position.positionid),
|
|
||||||
'1'
|
|
||||||
),
|
|
||||||
SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'name', ['position_or_address'],
|
|
||||||
get_best_name,
|
|
||||||
_('John Doe'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
name_scheme = PERSON_NAME_SCHEMES[sender.settings.name_scheme]
|
|
||||||
if "concatenation_for_salutation" in name_scheme:
|
|
||||||
concatenation_for_salutation = name_scheme["concatenation_for_salutation"]
|
|
||||||
else:
|
|
||||||
concatenation_for_salutation = name_scheme["concatenation"]
|
|
||||||
|
|
||||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
|
||||||
"name_for_salutation", ["waiting_list_entry"],
|
|
||||||
lambda waiting_list_entry: concatenation_for_salutation(waiting_list_entry.name_parts),
|
|
||||||
_("Mr Doe"),
|
|
||||||
))
|
|
||||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
|
||||||
"name", ["waiting_list_entry"],
|
|
||||||
lambda waiting_list_entry: waiting_list_entry.name or "",
|
|
||||||
_("Mr Doe"),
|
|
||||||
))
|
|
||||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
|
||||||
"name_for_salutation", ["position_or_address"],
|
|
||||||
lambda position_or_address: concatenation_for_salutation(get_best_name(position_or_address, parts=True)),
|
|
||||||
_("Mr Doe"),
|
|
||||||
))
|
|
||||||
|
|
||||||
for f, l, w in name_scheme['fields']:
|
|
||||||
if f == 'full_name':
|
|
||||||
continue
|
|
||||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'name_%s' % f, ['waiting_list_entry'], lambda waiting_list_entry, f=f: get_name_parts_localized(waiting_list_entry.name_parts, f),
|
|
||||||
name_scheme['sample'][f]
|
|
||||||
))
|
|
||||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'attendee_name_%s' % f, ['position'], lambda position, f=f: get_name_parts_localized(position.attendee_name_parts, f),
|
|
||||||
name_scheme['sample'][f]
|
|
||||||
))
|
|
||||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'name_%s' % f, ['position_or_address'],
|
|
||||||
lambda position_or_address, f=f: get_name_parts_localized(get_best_name(position_or_address, parts=True), f),
|
|
||||||
name_scheme['sample'][f]
|
|
||||||
))
|
|
||||||
|
|
||||||
for k, v in sender.meta_data.items():
|
|
||||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],
|
|
||||||
v
|
|
||||||
))
|
|
||||||
ph.append(SimpleFunctionalMailTextPlaceholder(
|
|
||||||
'meta_%s' % k, ['event_or_subevent'], lambda event_or_subevent, k=k: event_or_subevent.meta_data[k],
|
|
||||||
v
|
|
||||||
))
|
|
||||||
|
|
||||||
return ph
|
|
||||||
|
|||||||
@@ -1090,9 +1090,6 @@ class Order(LockModel, LoggedModel):
|
|||||||
if not self.email and not (position and position.attendee_email):
|
if not self.email and not (position and position.attendee_email):
|
||||||
return
|
return
|
||||||
|
|
||||||
for k, v in self.event.meta_data.items():
|
|
||||||
context['meta_' + k] = v
|
|
||||||
|
|
||||||
with language(self.locale, self.event.settings.region):
|
with language(self.locale, self.event.settings.region):
|
||||||
recipient = self.email
|
recipient = self.email
|
||||||
if position and position.attendee_email:
|
if position and position.attendee_email:
|
||||||
@@ -2650,9 +2647,6 @@ class OrderPosition(AbstractPosition):
|
|||||||
if not self.attendee_email:
|
if not self.attendee_email:
|
||||||
return
|
return
|
||||||
|
|
||||||
for k, v in self.event.meta_data.items():
|
|
||||||
context['meta_' + k] = v
|
|
||||||
|
|
||||||
with language(self.order.locale, self.order.event.settings.region):
|
with language(self.order.locale, self.order.event.settings.region):
|
||||||
recipient = self.attendee_email
|
recipient = self.attendee_email
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -259,9 +259,6 @@ class WaitingListEntry(LoggedModel):
|
|||||||
if not self.email:
|
if not self.email:
|
||||||
return
|
return
|
||||||
|
|
||||||
for k, v in self.event.meta_data.items():
|
|
||||||
context['meta_' + k] = v
|
|
||||||
|
|
||||||
with language(self.locale, self.event.settings.region):
|
with language(self.locale, self.event.settings.region):
|
||||||
recipient = self.email
|
recipient = self.email
|
||||||
|
|
||||||
|
|||||||
@@ -186,10 +186,6 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
|||||||
headers.setdefault('X-Mailer', 'pretix')
|
headers.setdefault('X-Mailer', 'pretix')
|
||||||
|
|
||||||
with language(locale):
|
with language(locale):
|
||||||
if isinstance(context, dict) and event:
|
|
||||||
for k, v in event.meta_data.items():
|
|
||||||
context['meta_' + k] = v
|
|
||||||
|
|
||||||
if isinstance(context, dict) and order:
|
if isinstance(context, dict) and order:
|
||||||
try:
|
try:
|
||||||
context.update({
|
context.update({
|
||||||
|
|||||||
584
src/pretix/base/services/placeholders.py
Normal file
584
src/pretix/base/services/placeholders.py
Normal file
@@ -0,0 +1,584 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import inspect
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.utils.formats import date_format
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from pretix.base.forms import PlaceholderValidator
|
||||||
|
from pretix.base.forms.widgets import format_placeholders_help_text
|
||||||
|
from pretix.base.i18n import (
|
||||||
|
LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber,
|
||||||
|
)
|
||||||
|
from pretix.base.reldate import RelativeDateWrapper
|
||||||
|
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
|
||||||
|
|
||||||
|
logger = logging.getLogger('pretix.base.services.placeholders')
|
||||||
|
|
||||||
|
|
||||||
|
class BaseTextPlaceholder:
|
||||||
|
"""
|
||||||
|
This is the base class for all email text placeholders.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def required_context(self):
|
||||||
|
"""
|
||||||
|
This property should return a list of all attribute names that need to be
|
||||||
|
contained in the base context so that this placeholder is available. By default,
|
||||||
|
it returns a list containing the string "event".
|
||||||
|
"""
|
||||||
|
return ["event"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def identifier(self):
|
||||||
|
"""
|
||||||
|
This should return the identifier of this placeholder in the email.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def render(self, context):
|
||||||
|
"""
|
||||||
|
This method is called to generate the actual text that is being
|
||||||
|
used in the email. You will be passed a context dictionary with the
|
||||||
|
base context attributes specified in ``required_context``. You are
|
||||||
|
expected to return a plain-text string.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def render_sample(self, event):
|
||||||
|
"""
|
||||||
|
This method is called to generate a text to be used in email previews.
|
||||||
|
This may only depend on the event.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleFunctionalTextPlaceholder(BaseTextPlaceholder):
|
||||||
|
def __init__(self, identifier, args, func, sample):
|
||||||
|
self._identifier = identifier
|
||||||
|
self._args = args
|
||||||
|
self._func = func
|
||||||
|
self._sample = sample
|
||||||
|
|
||||||
|
@property
|
||||||
|
def identifier(self):
|
||||||
|
return self._identifier
|
||||||
|
|
||||||
|
@property
|
||||||
|
def required_context(self):
|
||||||
|
return self._args
|
||||||
|
|
||||||
|
def render(self, context):
|
||||||
|
return self._func(**{k: context[k] for k in self._args})
|
||||||
|
|
||||||
|
def render_sample(self, event):
|
||||||
|
if callable(self._sample):
|
||||||
|
return self._sample(event)
|
||||||
|
else:
|
||||||
|
return self._sample
|
||||||
|
|
||||||
|
|
||||||
|
class PlaceholderContext(SafeFormatter):
|
||||||
|
"""
|
||||||
|
Holds the contextual arguments and corresponding list of available placeholders for formatting
|
||||||
|
an email or other templated text.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
context = PlaceholderContext(event=my_event, order=my_order)
|
||||||
|
formatted_doc = context.format(input_doc)
|
||||||
|
"""
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__({})
|
||||||
|
self.context_args = kwargs
|
||||||
|
self._extend_context_args()
|
||||||
|
self.placeholders = {}
|
||||||
|
self.cache = {}
|
||||||
|
event = kwargs['event']
|
||||||
|
for r, val in [
|
||||||
|
*register_mail_placeholders.send(sender=event),
|
||||||
|
*register_text_placeholders.send(sender=event)
|
||||||
|
]:
|
||||||
|
if not isinstance(val, (list, tuple)):
|
||||||
|
val = [val]
|
||||||
|
for v in val:
|
||||||
|
if all(rp in kwargs for rp in v.required_context):
|
||||||
|
self.placeholders[v.identifier] = v
|
||||||
|
|
||||||
|
def _extend_context_args(self):
|
||||||
|
from pretix.base.models import InvoiceAddress
|
||||||
|
|
||||||
|
if 'position' in self.context_args:
|
||||||
|
self.context_args.setdefault("position_or_address", self.context_args['position'])
|
||||||
|
if 'order' in self.context_args:
|
||||||
|
try:
|
||||||
|
if not self.context_args.get('invoice_address'):
|
||||||
|
self.context_args['invoice_address'] = self.context_args['order'].invoice_address
|
||||||
|
except InvoiceAddress.DoesNotExist:
|
||||||
|
self.context_args['invoice_address'] = InvoiceAddress(order=self.context_args['order'])
|
||||||
|
finally:
|
||||||
|
self.context_args.setdefault("position_or_address", self.context_args['invoice_address'])
|
||||||
|
|
||||||
|
def render_placeholder(self, placeholder):
|
||||||
|
try:
|
||||||
|
return self.cache[placeholder.identifier]
|
||||||
|
except KeyError:
|
||||||
|
try:
|
||||||
|
value = self.cache[placeholder.identifier] = placeholder.render(self.context_args)
|
||||||
|
return value
|
||||||
|
except:
|
||||||
|
logger.exception(f'Failed to process template placeholder {placeholder.identifier}.')
|
||||||
|
return '(error)'
|
||||||
|
|
||||||
|
def render_all(self):
|
||||||
|
return {identifier: self.render_placeholder(placeholder)
|
||||||
|
for (identifier, placeholder) in self.placeholders.items()}
|
||||||
|
|
||||||
|
def get_value(self, key, args, kwargs):
|
||||||
|
if key not in self.placeholders:
|
||||||
|
return '{' + str(key) + '}'
|
||||||
|
return self.render_placeholder(self.placeholders[key])
|
||||||
|
|
||||||
|
|
||||||
|
def _placeholder_payments(order, payments):
|
||||||
|
d = []
|
||||||
|
for payment in payments:
|
||||||
|
if 'payment' in inspect.signature(payment.payment_provider.order_pending_mail_render).parameters:
|
||||||
|
d.append(str(payment.payment_provider.order_pending_mail_render(order, payment)))
|
||||||
|
else:
|
||||||
|
d.append(str(payment.payment_provider.order_pending_mail_render(order)))
|
||||||
|
d = [line for line in d if line.strip()]
|
||||||
|
if d:
|
||||||
|
return '\n\n'.join(d)
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def get_best_name(position_or_address, parts=False):
|
||||||
|
"""
|
||||||
|
Return the best name we got for either an invoice address or an order position, falling back to the respective other
|
||||||
|
"""
|
||||||
|
from pretix.base.models import InvoiceAddress, OrderPosition
|
||||||
|
if isinstance(position_or_address, InvoiceAddress):
|
||||||
|
if position_or_address.name:
|
||||||
|
return position_or_address.name_parts if parts else position_or_address.name
|
||||||
|
elif position_or_address.order:
|
||||||
|
position_or_address = position_or_address.order.positions.exclude(attendee_name_cached="").exclude(attendee_name_cached__isnull=True).first()
|
||||||
|
|
||||||
|
if isinstance(position_or_address, OrderPosition):
|
||||||
|
if position_or_address.attendee_name:
|
||||||
|
return position_or_address.attendee_name_parts if parts else position_or_address.attendee_name
|
||||||
|
elif position_or_address.order:
|
||||||
|
try:
|
||||||
|
return position_or_address.order.invoice_address.name_parts if parts else position_or_address.order.invoice_address.name
|
||||||
|
except InvoiceAddress.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {} if parts else ""
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(register_text_placeholders, dispatch_uid="pretixbase_register_text_placeholders")
|
||||||
|
def base_placeholders(sender, **kwargs):
|
||||||
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
|
ph = [
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'event', ['event'], lambda event: event.name, lambda event: event.name
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'event', ['event_or_subevent'], lambda event_or_subevent: event_or_subevent.name,
|
||||||
|
lambda event_or_subevent: event_or_subevent.name
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'event_slug', ['event'], lambda event: event.slug, lambda event: event.slug
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'code', ['order'], lambda order: order.code, 'F8VVL'
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'total', ['order'], lambda order: LazyNumber(order.total), lambda event: LazyNumber(Decimal('42.23'))
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'currency', ['event'], lambda event: event.currency, lambda event: event.currency
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'order_email', ['order'], lambda order: order.email, 'john@example.org'
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'invoice_number', ['invoice'],
|
||||||
|
lambda invoice: invoice.full_invoice_no,
|
||||||
|
f'{sender.settings.invoice_numbers_prefix or (sender.slug.upper() + "-")}00000'
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'refund_amount', ['event_or_subevent', 'refund_amount'],
|
||||||
|
lambda event_or_subevent, refund_amount: LazyCurrencyNumber(refund_amount, event_or_subevent.currency),
|
||||||
|
lambda event_or_subevent: LazyCurrencyNumber(Decimal('42.23'), event_or_subevent.currency)
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'pending_sum', ['event', 'pending_sum'],
|
||||||
|
lambda event, pending_sum: LazyCurrencyNumber(pending_sum, event.currency),
|
||||||
|
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'total_with_currency', ['event', 'order'], lambda event, order: LazyCurrencyNumber(order.total,
|
||||||
|
event.currency),
|
||||||
|
lambda event: LazyCurrencyNumber(Decimal('42.23'), event.currency)
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'expire_date', ['event', 'order'], lambda event, order: LazyExpiresDate(order.expires.astimezone(event.timezone)),
|
||||||
|
lambda event: LazyDate(now() + timedelta(days=15))
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'url', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.open', kwargs={
|
||||||
|
'order': order.code,
|
||||||
|
'secret': order.secret,
|
||||||
|
'hash': order.email_confirm_hash()
|
||||||
|
}
|
||||||
|
), lambda event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.open', kwargs={
|
||||||
|
'order': 'F8VVL',
|
||||||
|
'secret': '6zzjnumtsx136ddy',
|
||||||
|
'hash': '98kusd8ofsj8dnkd'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'url_info_change', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.modify', kwargs={
|
||||||
|
'order': order.code,
|
||||||
|
'secret': order.secret,
|
||||||
|
}
|
||||||
|
), lambda event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.modify', kwargs={
|
||||||
|
'order': 'F8VVL',
|
||||||
|
'secret': '6zzjnumtsx136ddy',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'url_products_change', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.change', kwargs={
|
||||||
|
'order': order.code,
|
||||||
|
'secret': order.secret,
|
||||||
|
}
|
||||||
|
), lambda event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.change', kwargs={
|
||||||
|
'order': 'F8VVL',
|
||||||
|
'secret': '6zzjnumtsx136ddy',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'url_cancel', ['order', 'event'], lambda order, event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.cancel', kwargs={
|
||||||
|
'order': order.code,
|
||||||
|
'secret': order.secret,
|
||||||
|
}
|
||||||
|
), lambda event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.cancel', kwargs={
|
||||||
|
'order': 'F8VVL',
|
||||||
|
'secret': '6zzjnumtsx136ddy',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'url', ['event', 'position'], lambda event, position: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.position',
|
||||||
|
kwargs={
|
||||||
|
'order': position.order.code,
|
||||||
|
'secret': position.web_secret,
|
||||||
|
'position': position.positionid
|
||||||
|
}
|
||||||
|
),
|
||||||
|
lambda event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.position', kwargs={
|
||||||
|
'order': 'F8VVL',
|
||||||
|
'secret': '6zzjnumtsx136ddy',
|
||||||
|
'position': '123'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'order_modification_deadline_date_and_time', ['order', 'event'],
|
||||||
|
lambda order, event:
|
||||||
|
date_format(order.modify_deadline.astimezone(event.timezone), 'SHORT_DATETIME_FORMAT')
|
||||||
|
if order.modify_deadline
|
||||||
|
else '',
|
||||||
|
lambda event: date_format(
|
||||||
|
event.settings.get(
|
||||||
|
'last_order_modification_date', as_type=RelativeDateWrapper
|
||||||
|
).datetime(event).astimezone(event.timezone),
|
||||||
|
'SHORT_DATETIME_FORMAT'
|
||||||
|
) if event.settings.get('last_order_modification_date') else '',
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'event_location', ['event_or_subevent'], lambda event_or_subevent: str(event_or_subevent.location or ''),
|
||||||
|
lambda event: str(event.location or ''),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'event_admission_time', ['event_or_subevent'],
|
||||||
|
lambda event_or_subevent:
|
||||||
|
date_format(event_or_subevent.date_admission.astimezone(event_or_subevent.timezone), 'TIME_FORMAT')
|
||||||
|
if event_or_subevent.date_admission
|
||||||
|
else '',
|
||||||
|
lambda event: date_format(event.date_admission.astimezone(event.timezone), 'TIME_FORMAT') if event.date_admission else '',
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'subevent', ['waiting_list_entry', 'event'],
|
||||||
|
lambda waiting_list_entry, event: str(waiting_list_entry.subevent or event),
|
||||||
|
lambda event: str(event if not event.has_subevents or not event.subevents.exists() else event.subevents.first())
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'subevent_date_from', ['waiting_list_entry', 'event'],
|
||||||
|
lambda waiting_list_entry, event: (waiting_list_entry.subevent or event).get_date_from_display(),
|
||||||
|
lambda event: (event if not event.has_subevents or not event.subevents.exists() else event.subevents.first()).get_date_from_display()
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'url_remove', ['waiting_list_voucher', 'event'],
|
||||||
|
lambda waiting_list_voucher, event: build_absolute_uri(
|
||||||
|
event, 'presale:event.waitinglist.remove'
|
||||||
|
) + '?voucher=' + waiting_list_voucher.code,
|
||||||
|
lambda event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.waitinglist.remove',
|
||||||
|
) + '?voucher=68CYU2H6ZTP3WLK5',
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'url', ['waiting_list_voucher', 'event'],
|
||||||
|
lambda waiting_list_voucher, event: build_absolute_uri(
|
||||||
|
event, 'presale:event.redeem'
|
||||||
|
) + '?voucher=' + waiting_list_voucher.code,
|
||||||
|
lambda event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.redeem',
|
||||||
|
) + '?voucher=68CYU2H6ZTP3WLK5',
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'invoice_name', ['invoice_address'], lambda invoice_address: invoice_address.name or '',
|
||||||
|
_('John Doe')
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'invoice_company', ['invoice_address'], lambda invoice_address: invoice_address.company or '',
|
||||||
|
_('Sample Corporation')
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'orders', ['event', 'orders'], lambda event, orders: '\n' + '\n\n'.join(
|
||||||
|
'* {} - {}'.format(
|
||||||
|
order.full_code,
|
||||||
|
build_absolute_uri(event, 'presale:event.order.open', kwargs={
|
||||||
|
'event': event.slug,
|
||||||
|
'organizer': event.organizer.slug,
|
||||||
|
'order': order.code,
|
||||||
|
'secret': order.secret,
|
||||||
|
'hash': order.email_confirm_hash(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
for order in orders
|
||||||
|
), lambda event: '\n' + '\n\n'.join(
|
||||||
|
'* {} - {}'.format(
|
||||||
|
'{}-{}'.format(event.slug.upper(), order['code']),
|
||||||
|
build_absolute_uri(event, 'presale:event.order.open', kwargs={
|
||||||
|
'event': event.slug,
|
||||||
|
'organizer': event.organizer.slug,
|
||||||
|
'order': order['code'],
|
||||||
|
'secret': order['secret'],
|
||||||
|
'hash': order['hash'],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
for order in [
|
||||||
|
{'code': 'F8VVL', 'secret': '6zzjnumtsx136ddy', 'hash': 'abcdefghi'},
|
||||||
|
{'code': 'HIDHK', 'secret': '98kusd8ofsj8dnkd', 'hash': 'jklmnopqr'},
|
||||||
|
{'code': 'OPKSB', 'secret': '09pjdksflosk3njd', 'hash': 'stuvwxy2z'}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'hours', ['event', 'waiting_list_entry'], lambda event, waiting_list_entry:
|
||||||
|
event.settings.waiting_list_hours,
|
||||||
|
lambda event: event.settings.waiting_list_hours
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'product', ['waiting_list_entry'], lambda waiting_list_entry: waiting_list_entry.item.name,
|
||||||
|
_('Sample Admission Ticket')
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'code', ['waiting_list_voucher'], lambda waiting_list_voucher: waiting_list_voucher.code,
|
||||||
|
'68CYU2H6ZTP3WLK5'
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
# 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),
|
||||||
|
' 68CYU2H6ZTP3WLK5\n 7MB94KKPVEPSMVF2'
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
|
||||||
|
'voucher_url_list', ['event', 'voucher_list'],
|
||||||
|
lambda event, voucher_list: ' \n'.join([
|
||||||
|
build_absolute_uri(
|
||||||
|
event, 'presale:event.redeem'
|
||||||
|
) + '?voucher=' + c
|
||||||
|
for c in voucher_list
|
||||||
|
]),
|
||||||
|
lambda event: ' \n'.join([
|
||||||
|
build_absolute_uri(
|
||||||
|
event, 'presale:event.redeem'
|
||||||
|
) + '?voucher=' + c
|
||||||
|
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={
|
||||||
|
'event': event.slug,
|
||||||
|
'organizer': event.organizer.slug,
|
||||||
|
}), lambda event: build_absolute_uri(event, 'presale:event.index', kwargs={
|
||||||
|
'event': event.slug,
|
||||||
|
'organizer': event.organizer.slug,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'name', ['name'], lambda name: name,
|
||||||
|
_('John Doe')
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'comment', ['comment'], lambda comment: comment,
|
||||||
|
_('An individual text with a reason can be inserted here.'),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'payment_info', ['order', 'payments'], _placeholder_payments,
|
||||||
|
_('The amount has been charged to your card.'),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'payment_info', ['payment_info'], lambda payment_info: payment_info,
|
||||||
|
_('Please transfer money to this bank account: 9999-9999-9999-9999'),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'attendee_name', ['position'], lambda position: position.attendee_name,
|
||||||
|
_('John Doe'),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'positionid', ['position'], lambda position: str(position.positionid),
|
||||||
|
'1'
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'name', ['position_or_address'],
|
||||||
|
get_best_name,
|
||||||
|
_('John Doe'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
name_scheme = PERSON_NAME_SCHEMES[sender.settings.name_scheme]
|
||||||
|
if "concatenation_for_salutation" in name_scheme:
|
||||||
|
concatenation_for_salutation = name_scheme["concatenation_for_salutation"]
|
||||||
|
else:
|
||||||
|
concatenation_for_salutation = name_scheme["concatenation"]
|
||||||
|
|
||||||
|
ph.append(SimpleFunctionalTextPlaceholder(
|
||||||
|
"name_for_salutation", ["waiting_list_entry"],
|
||||||
|
lambda waiting_list_entry: concatenation_for_salutation(waiting_list_entry.name_parts),
|
||||||
|
_("Mr Doe"),
|
||||||
|
))
|
||||||
|
ph.append(SimpleFunctionalTextPlaceholder(
|
||||||
|
"name", ["waiting_list_entry"],
|
||||||
|
lambda waiting_list_entry: waiting_list_entry.name or "",
|
||||||
|
_("Mr Doe"),
|
||||||
|
))
|
||||||
|
ph.append(SimpleFunctionalTextPlaceholder(
|
||||||
|
"name_for_salutation", ["position_or_address"],
|
||||||
|
lambda position_or_address: concatenation_for_salutation(get_best_name(position_or_address, parts=True)),
|
||||||
|
_("Mr Doe"),
|
||||||
|
))
|
||||||
|
|
||||||
|
for f, l, w in name_scheme['fields']:
|
||||||
|
if f == 'full_name':
|
||||||
|
continue
|
||||||
|
ph.append(SimpleFunctionalTextPlaceholder(
|
||||||
|
'name_%s' % f, ['waiting_list_entry'], lambda waiting_list_entry, f=f: get_name_parts_localized(waiting_list_entry.name_parts, f),
|
||||||
|
name_scheme['sample'][f]
|
||||||
|
))
|
||||||
|
ph.append(SimpleFunctionalTextPlaceholder(
|
||||||
|
'attendee_name_%s' % f, ['position'], lambda position, f=f: get_name_parts_localized(position.attendee_name_parts, f),
|
||||||
|
name_scheme['sample'][f]
|
||||||
|
))
|
||||||
|
ph.append(SimpleFunctionalTextPlaceholder(
|
||||||
|
'name_%s' % f, ['position_or_address'],
|
||||||
|
lambda position_or_address, f=f: get_name_parts_localized(get_best_name(position_or_address, parts=True), f),
|
||||||
|
name_scheme['sample'][f]
|
||||||
|
))
|
||||||
|
|
||||||
|
for k, v in sender.meta_data.items():
|
||||||
|
ph.append(SimpleFunctionalTextPlaceholder(
|
||||||
|
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],
|
||||||
|
v
|
||||||
|
))
|
||||||
|
ph.append(SimpleFunctionalTextPlaceholder(
|
||||||
|
'meta_%s' % k, ['event_or_subevent'], lambda event_or_subevent, k=k: event_or_subevent.meta_data[k],
|
||||||
|
v
|
||||||
|
))
|
||||||
|
|
||||||
|
return ph
|
||||||
|
|
||||||
|
|
||||||
|
class FormPlaceholderMixin:
|
||||||
|
def _set_field_placeholders(self, fn, base_parameters):
|
||||||
|
placeholders = get_available_placeholders(self.event, base_parameters)
|
||||||
|
ht = format_placeholders_help_text(placeholders, self.event)
|
||||||
|
if self.fields[fn].help_text:
|
||||||
|
self.fields[fn].help_text += ' ' + str(ht)
|
||||||
|
else:
|
||||||
|
self.fields[fn].help_text = ht
|
||||||
|
self.fields[fn].validators.append(
|
||||||
|
PlaceholderValidator(['{%s}' % p for p in placeholders.keys()])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_placeholders(event, base_parameters):
|
||||||
|
if 'order' in base_parameters:
|
||||||
|
base_parameters.append('invoice_address')
|
||||||
|
base_parameters.append('position_or_address')
|
||||||
|
params = {}
|
||||||
|
for r, val in [*register_mail_placeholders.send(sender=event), *register_text_placeholders.send(sender=event)]:
|
||||||
|
if not isinstance(val, (list, tuple)):
|
||||||
|
val = [val]
|
||||||
|
for v in val:
|
||||||
|
if all(rp in base_parameters for rp in v.required_context):
|
||||||
|
params[v.identifier] = v
|
||||||
|
return params
|
||||||
@@ -220,14 +220,20 @@ subclass of pretix.base.payment.BasePaymentProvider or a list of these
|
|||||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
register_mail_placeholders = EventPluginSignal()
|
register_text_placeholders = EventPluginSignal()
|
||||||
"""
|
"""
|
||||||
This signal is sent out to get all known email text placeholders. Receivers should return
|
This signal is sent out to get all known text placeholders. Receivers should return
|
||||||
an instance of a subclass of pretix.base.email.BaseMailTextPlaceholder or a list of these.
|
an instance of a subclass of pretix.base.services.placeholders.BaseTextPlaceholder or a
|
||||||
|
list of these.
|
||||||
|
|
||||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
register_mail_placeholders = EventPluginSignal()
|
||||||
|
"""
|
||||||
|
**DEPRECATED**: This signal has a new name, please use ``register_text_placeholders`` instead.
|
||||||
|
"""
|
||||||
|
|
||||||
register_html_mail_renderers = EventPluginSignal()
|
register_html_mail_renderers = EventPluginSignal()
|
||||||
"""
|
"""
|
||||||
This signal is sent out to get all known HTML email renderers. Receivers should return a
|
This signal is sent out to get all known HTML email renderers. Receivers should return a
|
||||||
|
|||||||
@@ -60,12 +60,11 @@ from i18nfield.forms import (
|
|||||||
from pytz import common_timezones
|
from pytz import common_timezones
|
||||||
|
|
||||||
from pretix.base.channels import get_all_sales_channels
|
from pretix.base.channels import get_all_sales_channels
|
||||||
from pretix.base.email import get_available_placeholders
|
|
||||||
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
|
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
|
||||||
from pretix.base.forms.widgets import format_placeholders_help_text
|
|
||||||
from pretix.base.models import Event, Organizer, TaxRule, Team
|
from pretix.base.models import Event, Organizer, TaxRule, Team
|
||||||
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
|
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
|
||||||
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
||||||
|
from pretix.base.services.placeholders import FormPlaceholderMixin
|
||||||
from pretix.base.settings import (
|
from pretix.base.settings import (
|
||||||
COUNTRIES_WITH_STATE_IN_ADDRESS, DEFAULTS, PERSON_NAME_SCHEMES,
|
COUNTRIES_WITH_STATE_IN_ADDRESS, DEFAULTS, PERSON_NAME_SCHEMES,
|
||||||
PERSON_NAME_TITLE_GROUPS, validate_event_settings,
|
PERSON_NAME_TITLE_GROUPS, validate_event_settings,
|
||||||
@@ -503,7 +502,7 @@ class EventSettingsValidationMixin:
|
|||||||
del self.cleaned_data[field]
|
del self.cleaned_data[field]
|
||||||
|
|
||||||
|
|
||||||
class EventSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, SettingsForm):
|
||||||
timezone = forms.ChoiceField(
|
timezone = forms.ChoiceField(
|
||||||
choices=((a, a) for a in common_timezones),
|
choices=((a, a) for a in common_timezones),
|
||||||
label=_("Event timezone"),
|
label=_("Event timezone"),
|
||||||
@@ -593,6 +592,10 @@ class EventSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
|||||||
'og_image',
|
'og_image',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
base_context = {
|
||||||
|
'frontpage_text': ['event'],
|
||||||
|
}
|
||||||
|
|
||||||
def _resolve_virtual_keys_input(self, data, prefix=''):
|
def _resolve_virtual_keys_input(self, data, prefix=''):
|
||||||
# set all dependants of virtual_keys and
|
# set all dependants of virtual_keys and
|
||||||
# delete all virtual_fields to prevent them from being saved
|
# delete all virtual_fields to prevent them from being saved
|
||||||
@@ -682,6 +685,9 @@ class EventSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
|||||||
else:
|
else:
|
||||||
self.initial[virtual_key] = 'do_not_ask'
|
self.initial[virtual_key] = 'do_not_ask'
|
||||||
|
|
||||||
|
for k, v in self.base_context.items():
|
||||||
|
self._set_field_placeholders(k, v)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def changed_data(self):
|
def changed_data(self):
|
||||||
data = []
|
data = []
|
||||||
@@ -932,7 +938,7 @@ def contains_web_channel_validate(val):
|
|||||||
raise ValidationError(_("The online shop must be selected to receive these emails."))
|
raise ValidationError(_("The online shop must be selected to receive these emails."))
|
||||||
|
|
||||||
|
|
||||||
class MailSettingsForm(SettingsForm):
|
class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
|
||||||
auto_fields = [
|
auto_fields = [
|
||||||
'mail_prefix',
|
'mail_prefix',
|
||||||
'mail_from_name',
|
'mail_from_name',
|
||||||
@@ -1344,17 +1350,6 @@ class MailSettingsForm(SettingsForm):
|
|||||||
'mail_attach_ical_description': ['event', 'event_or_subevent'],
|
'mail_attach_ical_description': ['event', 'event_or_subevent'],
|
||||||
}
|
}
|
||||||
|
|
||||||
def _set_field_placeholders(self, fn, base_parameters):
|
|
||||||
placeholders = get_available_placeholders(self.event, base_parameters)
|
|
||||||
ht = format_placeholders_help_text(placeholders, self.event)
|
|
||||||
if self.fields[fn].help_text:
|
|
||||||
self.fields[fn].help_text += ' ' + str(ht)
|
|
||||||
else:
|
|
||||||
self.fields[fn].help_text = ht
|
|
||||||
self.fields[fn].validators.append(
|
|
||||||
PlaceholderValidator(['{%s}' % p for p in placeholders.keys()])
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.event = event = kwargs.get('obj')
|
self.event = event = kwargs.get('obj')
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ from pretix.base.models import (
|
|||||||
TaxRule,
|
TaxRule,
|
||||||
)
|
)
|
||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
|
from pretix.base.services.placeholders import FormPlaceholderMixin
|
||||||
from pretix.base.services.pricing import get_price
|
from pretix.base.services.pricing import get_price
|
||||||
from pretix.control.forms import SplitDateTimeField
|
from pretix.control.forms import SplitDateTimeField
|
||||||
from pretix.control.forms.widgets import Select2
|
from pretix.control.forms.widgets import Select2
|
||||||
@@ -767,7 +768,7 @@ class OrderRefundForm(forms.Form):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class EventCancelForm(forms.Form):
|
class EventCancelForm(FormPlaceholderMixin, forms.Form):
|
||||||
subevent = forms.ModelChoiceField(
|
subevent = forms.ModelChoiceField(
|
||||||
SubEvent.objects.none(),
|
SubEvent.objects.none(),
|
||||||
label=pgettext_lazy('subevent', 'Date'),
|
label=pgettext_lazy('subevent', 'Date'),
|
||||||
@@ -867,17 +868,6 @@ class EventCancelForm(forms.Form):
|
|||||||
send_waitinglist_subject = forms.CharField()
|
send_waitinglist_subject = forms.CharField()
|
||||||
send_waitinglist_message = forms.CharField()
|
send_waitinglist_message = forms.CharField()
|
||||||
|
|
||||||
def _set_field_placeholders(self, fn, base_parameters):
|
|
||||||
placeholders = get_available_placeholders(self.event, base_parameters)
|
|
||||||
ht = format_placeholders_help_text(placeholders, self.event)
|
|
||||||
if self.fields[fn].help_text:
|
|
||||||
self.fields[fn].help_text += ' ' + str(ht)
|
|
||||||
else:
|
|
||||||
self.fields[fn].help_text = ht
|
|
||||||
self.fields[fn].validators.append(
|
|
||||||
PlaceholderValidator(['{%s}' % p for p in placeholders.keys()])
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.event = kwargs.pop('event')
|
self.event = kwargs.pop('event')
|
||||||
kwargs.setdefault('initial', {})
|
kwargs.setdefault('initial', {})
|
||||||
|
|||||||
@@ -41,28 +41,16 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
|||||||
from django_scopes.forms import SafeModelMultipleChoiceField
|
from django_scopes.forms import SafeModelMultipleChoiceField
|
||||||
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
||||||
|
|
||||||
from pretix.base.email import get_available_placeholders
|
from pretix.base.forms import I18nModelForm
|
||||||
from pretix.base.forms import I18nModelForm, PlaceholderValidator
|
|
||||||
from pretix.base.forms.widgets import (
|
from pretix.base.forms.widgets import (
|
||||||
SplitDateTimePickerWidget, TimePickerWidget, format_placeholders_help_text,
|
SplitDateTimePickerWidget, TimePickerWidget,
|
||||||
)
|
)
|
||||||
from pretix.base.models import CheckinList, Item, Order, SubEvent
|
from pretix.base.models import CheckinList, Item, Order, SubEvent
|
||||||
from pretix.control.forms import CachedFileField, SplitDateTimeField
|
from pretix.control.forms import CachedFileField, SplitDateTimeField
|
||||||
from pretix.control.forms.widgets import Select2, Select2Multiple
|
from pretix.control.forms.widgets import Select2, Select2Multiple
|
||||||
from pretix.plugins.sendmail.models import Rule
|
from pretix.plugins.sendmail.models import Rule
|
||||||
|
|
||||||
|
from pretix.base.services.placeholders import FormPlaceholderMixin # noqa
|
||||||
class FormPlaceholderMixin:
|
|
||||||
def _set_field_placeholders(self, fn, base_parameters):
|
|
||||||
placeholders = get_available_placeholders(self.event, base_parameters)
|
|
||||||
ht = format_placeholders_help_text(placeholders, self.event)
|
|
||||||
if self.fields[fn].help_text:
|
|
||||||
self.fields[fn].help_text += ' ' + str(ht)
|
|
||||||
else:
|
|
||||||
self.fields[fn].help_text = ht
|
|
||||||
self.fields[fn].validators.append(
|
|
||||||
PlaceholderValidator(['{%s}' % p for p in placeholders.keys()])
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseMailForm(FormPlaceholderMixin, forms.Form):
|
class BaseMailForm(FormPlaceholderMixin, forms.Form):
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ from pretix.base.models.event import Event, SubEvent
|
|||||||
from pretix.base.models.items import (
|
from pretix.base.models.items import (
|
||||||
ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation,
|
ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation,
|
||||||
)
|
)
|
||||||
|
from pretix.base.services.placeholders import PlaceholderContext
|
||||||
from pretix.base.services.quotas import QuotaAvailability
|
from pretix.base.services.quotas import QuotaAvailability
|
||||||
from pretix.helpers.compat import date_fromisocalendar
|
from pretix.helpers.compat import date_fromisocalendar
|
||||||
from pretix.multidomain.urlreverse import eventreverse
|
from pretix.multidomain.urlreverse import eventreverse
|
||||||
@@ -590,10 +591,11 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
|
|||||||
context['cart'] = self.get_cart()
|
context['cart'] = self.get_cart()
|
||||||
context['has_addon_choices'] = any(cp.has_addon_choices for cp in get_cart(self.request))
|
context['has_addon_choices'] = any(cp.has_addon_choices for cp in get_cart(self.request))
|
||||||
|
|
||||||
|
templating_context = PlaceholderContext(event_or_subevent=self.subevent or self.request.event, event=self.request.event)
|
||||||
if self.subevent:
|
if self.subevent:
|
||||||
context['frontpage_text'] = str(self.subevent.frontpage_text)
|
context['frontpage_text'] = templating_context.format(str(self.subevent.frontpage_text))
|
||||||
else:
|
else:
|
||||||
context['frontpage_text'] = str(self.request.event.settings.frontpage_text)
|
context['frontpage_text'] = templating_context.format(str(self.request.event.settings.frontpage_text))
|
||||||
|
|
||||||
if self.request.event.has_subevents:
|
if self.request.event.has_subevents:
|
||||||
context['subevent_list'] = SimpleLazyObject(self._subevent_list_context)
|
context['subevent_list'] = SimpleLazyObject(self._subevent_list_context)
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ from pretix.base.models import (
|
|||||||
CartPosition, Event, ItemVariation, Quota, SubEvent, Voucher,
|
CartPosition, Event, ItemVariation, Quota, SubEvent, Voucher,
|
||||||
)
|
)
|
||||||
from pretix.base.services.cart import error_messages
|
from pretix.base.services.cart import error_messages
|
||||||
|
from pretix.base.services.placeholders import PlaceholderContext
|
||||||
from pretix.base.settings import GlobalSettingsObject
|
from pretix.base.settings import GlobalSettingsObject
|
||||||
from pretix.base.templatetags.rich_text import rich_text
|
from pretix.base.templatetags.rich_text import rich_text
|
||||||
from pretix.helpers.daterange import daterange
|
from pretix.helpers.daterange import daterange
|
||||||
@@ -699,11 +700,13 @@ class WidgetAPIProductList(EventListMixin, View):
|
|||||||
|
|
||||||
ev = self.subevent or request.event
|
ev = self.subevent or request.event
|
||||||
data['name'] = str(ev.name)
|
data['name'] = str(ev.name)
|
||||||
|
|
||||||
|
templating_context = PlaceholderContext(event_or_subevent=ev, event=request.event)
|
||||||
if self.subevent:
|
if self.subevent:
|
||||||
data['frontpage_text'] = str(rich_text(self.subevent.frontpage_text, safelinks=False))
|
data['frontpage_text'] = str(rich_text(templating_context.format(str(self.subevent.frontpage_text)), safelinks=False))
|
||||||
data['location'] = str(rich_text(self.subevent.location, safelinks=False))
|
data['location'] = str(rich_text(self.subevent.location, safelinks=False))
|
||||||
else:
|
else:
|
||||||
data['frontpage_text'] = str(rich_text(request.event.settings.frontpage_text, safelinks=False))
|
data['frontpage_text'] = str(rich_text(templating_context.format(str(request.event.settings.frontpage_text)), safelinks=False))
|
||||||
data['location'] = str(rich_text(request.event.location, safelinks=False))
|
data['location'] = str(rich_text(request.event.location, safelinks=False))
|
||||||
data['date_range'] = self._get_date_range(ev, request.event)
|
data['date_range'] = self._get_date_range(ev, request.event)
|
||||||
fail = False
|
fail = False
|
||||||
|
|||||||
Reference in New Issue
Block a user