Allow to send e-mails to attendees individually (#1299)

* .

* Add a position detail page to the frontend

* Mail templates

* Send mails

* Send reminder email

* Add position support to sendmail plugin

* Add and fix some tests

* Fix failing test on real databases
This commit is contained in:
Raphael Michel
2019-05-24 09:41:44 +02:00
committed by GitHub
parent d22a7844ea
commit f1bce0c08b
29 changed files with 1078 additions and 213 deletions

View File

@@ -8,7 +8,7 @@ from django.template.loader import get_template
from django.utils.translation import ugettext_lazy as _
from inlinestyler.utils import inline_css
from pretix.base.models import Event, Order
from pretix.base.models import Event, Order, OrderPosition
from pretix.base.signals import register_html_mail_renderers
from pretix.base.templatetags.rich_text import markdown_compile_email
@@ -44,7 +44,8 @@ class BaseHTMLMailRenderer:
def __str__(self):
return self.identifier
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order=None) -> str:
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order=None,
position: OrderPosition=None) -> str:
"""
This method should generate the HTML part of the email.
@@ -52,6 +53,7 @@ class BaseHTMLMailRenderer:
:param plain_signature: The signature with event organizer contact details in plain text.
: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``.
:return: An HTML string
"""
raise NotImplementedError()
@@ -95,7 +97,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
def template_name(self):
raise NotImplementedError()
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order) -> str:
def render(self, plain_body: str, plain_signature: str, subject: str, order: Order, position: OrderPosition) -> str:
body_md = markdown_compile_email(plain_body)
htmlctx = {
'site': settings.PRETIX_INSTANCE_NAME,
@@ -116,6 +118,9 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
if order:
htmlctx['order'] = order
if position:
htmlctx['position'] = position
tpl = get_template(self.template_name)
body_html = inline_css(tpl.render(htmlctx))
return body_html

View File

@@ -0,0 +1,20 @@
# Generated by Django 2.2.1 on 2019-05-15 13:23
from django.db import migrations, models
import pretix.base.models.orders
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0121_order_email_known_to_work'),
]
operations = [
migrations.AddField(
model_name='orderposition',
name='web_secret',
field=models.CharField(db_index=True, default=pretix.base.models.orders.generate_secret, max_length=32),
),
]

View File

@@ -673,7 +673,7 @@ class Order(LockModel, LoggedModel):
def send_mail(self, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent',
user: User=None, headers: dict=None, sender: str=None, invoices: list=None,
auth=None, attach_tickets=False):
auth=None, attach_tickets=False, position: 'OrderPosition'=None):
"""
Sends an email to the user that placed this order. Basically, this method does two things:
@@ -690,6 +690,9 @@ class Order(LockModel, LoggedModel):
:param headers: Dictionary with additional mail headers
:param sender: Custom email sender.
:param attach_tickets: Attach tickets of this order, if they are existing and ready to download
:param position: An order position this refers to. If given, no invoices will be attached, the tickets will
only be attached for this position and child positions, the link will only point to the
position and the attendee email will be used if available.
"""
from pretix.base.services.mail import SendMailException, mail, render_mail
@@ -701,12 +704,16 @@ class Order(LockModel, LoggedModel):
with language(self.locale):
recipient = self.email
if position and position.attendee_email:
recipient = position.attendee_email
try:
email_content = render_mail(template, context)
mail(
recipient, subject, template, context,
self.event, self.locale, self, headers, sender,
invoices=invoices, attach_tickets=attach_tickets
self.event, self.locale, self, headers=headers, sender=sender,
invoices=invoices, attach_tickets=attach_tickets,
position=position
)
except SendMailException:
raise
@@ -718,6 +725,7 @@ class Order(LockModel, LoggedModel):
data={
'subject': subject,
'message': email_content,
'position': position.positionid if position else None,
'recipient': recipient,
'invoices': [i.pk for i in invoices] if invoices else [],
'attach_tickets': attach_tickets,
@@ -1194,8 +1202,6 @@ class OrderPayment(models.Model):
:raises Quota.QuotaExceededException: if the quota is exceeded and ``force`` is ``False``
"""
from pretix.base.services.invoices import generate_invoice, invoice_qualified
from pretix.base.services.mail import SendMailException
from pretix.multidomain.urlreverse import build_absolute_uri
with transaction.atomic():
locked_instance = OrderPayment.objects.select_for_update().get(pk=self.pk)
@@ -1259,36 +1265,77 @@ class OrderPayment(models.Model):
)
if send_mail:
with language(self.order.locale):
try:
invoice_name = self.order.invoice_address.name
invoice_company = self.order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_template = self.order.event.settings.mail_text_order_paid
email_context = {
'event': self.order.event.name,
'url': build_absolute_uri(self.order.event, 'presale:event.order.open', kwargs={
'order': self.order.code,
'secret': self.order.secret,
'hash': self.order.email_confirm_hash()
}),
'downloads': self.order.event.settings.get('ticket_download', as_type=bool),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
'payment_info': mail_text
}
email_subject = _('Payment received for your order: %(code)s') % {'code': self.order.code}
try:
self.order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_paid', user,
invoices=[invoice] if invoice and self.order.event.settings.invoice_email_attachment else [],
attach_tickets=True
)
except SendMailException:
logger.exception('Order paid email could not be sent')
self._send_paid_mail(invoice, user, mail_text)
if self.order.event.settings.mail_send_order_paid_attendee:
for p in self.order.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != self.order.email:
self._send_paid_mail_attendee(p, user)
def _send_paid_mail_attendee(self, position, user):
from pretix.base.services.mail import SendMailException
from pretix.multidomain.urlreverse import build_absolute_uri
with language(self.order.locale):
name_scheme = PERSON_NAME_SCHEMES[self.order.event.settings.name_scheme]
email_template = self.order.event.settings.mail_text_order_paid_attendee
email_context = {
'event': self.order.event.name,
'downloads': self.order.event.settings.get('ticket_download', as_type=bool),
'url': build_absolute_uri(self.order.event, 'presale:event.order.position', kwargs={
'order': self.order.code,
'secret': position.web_secret,
'position': position.positionid
}),
'attendee_name': position.attendee_name,
}
for f, l, w in name_scheme['fields']:
email_context['attendee_name_%s' % f] = position.attendee_name_parts.get(f, '')
email_subject = _('Event registration confirmed: %(code)s') % {'code': self.order.code}
try:
self.order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_paid', user,
invoices=[], position=position,
attach_tickets=True
)
except SendMailException:
logger.exception('Order paid email could not be sent')
def _send_paid_mail(self, invoice, user, mail_text):
from pretix.base.services.mail import SendMailException
from pretix.multidomain.urlreverse import build_absolute_uri
with language(self.order.locale):
try:
invoice_name = self.order.invoice_address.name
invoice_company = self.order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_template = self.order.event.settings.mail_text_order_paid
email_context = {
'event': self.order.event.name,
'url': build_absolute_uri(self.order.event, 'presale:event.order.open', kwargs={
'order': self.order.code,
'secret': self.order.secret,
'hash': self.order.email_confirm_hash()
}),
'downloads': self.order.event.settings.get('ticket_download', as_type=bool),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
'payment_info': mail_text
}
email_subject = _('Payment received for your order: %(code)s') % {'code': self.order.code}
try:
self.order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_paid', user,
invoices=[invoice] if invoice and self.order.event.settings.invoice_email_attachment else [],
attach_tickets=True
)
except SendMailException:
logger.exception('Order paid email could not be sent')
@property
def refunded_amount(self):
@@ -1677,6 +1724,7 @@ class OrderPosition(AbstractPosition):
verbose_name=_('Tax value')
)
secret = models.CharField(max_length=64, default=generate_position_secret, db_index=True)
web_secret = models.CharField(max_length=32, default=generate_secret, db_index=True)
pseudonymization_id = models.CharField(
max_length=16,
unique=True,
@@ -1800,6 +1848,60 @@ class OrderPosition(AbstractPosition):
def event(self):
return self.order.event
def send_mail(self, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent',
user: User=None, headers: dict=None, sender: str=None, invoices: list=None,
auth=None, attach_tickets=False):
"""
Sends an email to the user that placed this order. Basically, this method does two things:
* Call ``pretix.base.services.mail.mail`` with useful values for the ``event``, ``locale``, ``recipient`` and
``order`` parameters.
* Create a ``LogEntry`` with the email contents.
:param subject: Subject of the email
:param template: LazyI18nString or template filename, see ``pretix.base.services.mail.mail`` for more details
:param context: Dictionary to use for rendering the template
:param log_entry_type: Key to be used for the log entry
:param user: Administrative user who triggered this mail to be sent
:param headers: Dictionary with additional mail headers
:param sender: Custom email sender.
:param attach_tickets: Attach tickets of this order, if they are existing and ready to download
"""
from pretix.base.services.mail import SendMailException, mail, render_mail
if not self.email:
return
for k, v in self.event.meta_data.items():
context['meta_' + k] = v
with language(self.locale):
recipient = self.email
try:
email_content = render_mail(template, context)
mail(
recipient, subject, template, context,
self.event, self.locale, self, headers, sender,
invoices=invoices, attach_tickets=attach_tickets
)
except SendMailException:
raise
else:
self.log_action(
log_entry_type,
user=user,
auth=auth,
data={
'subject': subject,
'message': email_content,
'recipient': recipient,
'invoices': [i.pk for i in invoices] if invoices else [],
'attach_tickets': attach_tickets,
}
)
class CartPosition(AbstractPosition):
"""

View File

@@ -1,5 +1,6 @@
import logging
import smtplib
import warnings
from email.utils import formataddr
from typing import Any, Dict, List, Union
@@ -13,7 +14,9 @@ from i18nfield.strings import LazyI18nString
from pretix.base.email import ClassicMailRenderer
from pretix.base.i18n import language
from pretix.base.models import Event, Invoice, InvoiceAddress, Order
from pretix.base.models import (
Event, Invoice, InvoiceAddress, Order, OrderPosition,
)
from pretix.base.services.invoices import invoice_pdf_task
from pretix.base.services.tasks import TransactionAwareTask
from pretix.base.services.tickets import get_tickets_for_order
@@ -38,8 +41,8 @@ class SendMailException(Exception):
def mail(email: str, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, event: Event=None, locale: str=None,
order: Order=None, headers: dict=None, sender: str=None, invoices: list=None,
attach_tickets=False):
order: Order=None, position: OrderPosition=None, headers: dict=None, sender: str=None,
invoices: list=None, attach_tickets=False):
"""
Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation.
@@ -60,6 +63,9 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
:param order: The order this email is related to (optional). If set, this will be used to include a link to the
order below the email.
:param order: The order position this email is related to (optional). If set, this will be used to include a link
to the order position instead of the order below the email.
:param headers: A dict of custom mail headers to add to the mail
:param locale: The locale to be used while evaluating the subject and the template
@@ -132,9 +138,26 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
body_plain += signature
body_plain += "\r\n\r\n-- \r\n"
if order:
if order.testmode:
subject = "[TESTMODE] " + subject
if order and order.testmode:
subject = "[TESTMODE] " + subject
if order and position:
body_plain += _(
"You are receiving this email because someone placed an order for {event} for you."
).format(event=event.name)
body_plain += "\r\n"
body_plain += _(
"You can view your order details at the following URL:\n{orderurl}."
).replace("\n", "\r\n").format(
event=event.name, orderurl=build_absolute_uri(
order.event, 'presale:event.order.position', kwargs={
'order': order.code,
'secret': position.web_secret,
'position': position.positionid,
}
)
)
elif order:
body_plain += _(
"You are receiving this email because you placed an order for {event}."
).format(event=event.name)
@@ -153,7 +176,14 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
body_plain += "\r\n"
try:
body_html = renderer.render(content_plain, signature, str(subject), order)
try:
body_html = renderer.render(content_plain, signature, str(subject), order, position)
except TypeError:
# Backwards compatibility
warnings.warn('E-mail renderer called without position argument because position argument is not '
'supported.',
DeprecationWarning)
body_html = renderer.render(content_plain, signature, str(subject), order)
except:
logger.exception('Could not render HTML body')
body_html = None
@@ -167,8 +197,9 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
sender=sender,
event=event.id if event else None,
headers=headers,
invoices=[i.pk for i in invoices] if invoices else [],
invoices=[i.pk for i in invoices] if invoices and not position else [],
order=order.pk if order else None,
position=position.pk if position else None,
attach_tickets=attach_tickets
)
@@ -183,8 +214,8 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
@app.task(base=TransactionAwareTask, bind=True)
def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: str, sender: str,
event: int=None, headers: dict=None, bcc: List[str]=None, invoices: List[int]=None,
order: int=None, attach_tickets=False) -> bool:
event: int=None, position: int=None, headers: dict=None, bcc: List[str]=None,
invoices: List[int]=None, order: int=None, attach_tickets=False) -> bool:
email = EmailMultiAlternatives(subject, body, sender, to=to, bcc=bcc, headers=headers)
if html is not None:
email.attach_alternative(html, "text/html")
@@ -215,10 +246,15 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
except Order.DoesNotExist:
order = None
else:
if position:
try:
position = order.positions.get(pk=position)
except OrderPosition.DoesNotExist:
attach_tickets = False
if attach_tickets:
args = []
attach_size = 0
for name, ct in get_tickets_for_order(order):
for name, ct in get_tickets_for_order(order, base_position=position):
content = ct.file.read()
args.append((name, content, ct.type))
attach_size += len(content)

View File

@@ -43,6 +43,7 @@ from pretix.base.services.locking import LockTimeoutException, NoLockManager
from pretix.base.services.mail import SendMailException
from pretix.base.services.pricing import get_price
from pretix.base.services.tasks import ProfiledTask
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import (
allow_ticket_download, order_approved, order_canceled, order_changed,
order_denied, order_expired, order_fee_calculation, order_placed,
@@ -645,6 +646,75 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
return order, p
def _order_placed_email(event: Event, order: Order, pprov: BasePaymentProvider, email_template, log_entry: str,
invoice):
try:
invoice_name = order.invoice_address.name
invoice_company = order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
if pprov:
payment_info = str(pprov.order_pending_mail_render(order))
else:
payment_info = None
email_context = {
'total': LazyNumber(order.total),
'currency': event.currency,
'total_with_currency': LazyCurrencyNumber(order.total, event.currency),
'date': LazyDate(order.expires),
'event': event.name,
'url': build_absolute_uri(event, 'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
}),
'payment_info': payment_info,
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
email_subject = _('Your order: %(code)s') % {'code': order.code}
try:
order.send_mail(
email_subject, email_template, email_context,
log_entry,
invoices=[invoice] if invoice and event.settings.invoice_email_attachment else [],
attach_tickets=True
)
except SendMailException:
logger.exception('Order received email could not be sent')
def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosition, email_template, log_entry: str):
name_scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
email_context = {
'event': event.name,
'url': build_absolute_uri(event, 'presale:event.order.position', kwargs={
'order': order.code,
'secret': position.web_secret,
'position': position.positionid
}),
'attendee_name': position.attendee_name,
}
for f, l, w in name_scheme['fields']:
email_context['attendee_name_%s' % f] = position.attendee_name_parts.get(f, '')
email_subject = _('Your event registration: %(code)s') % {'code': order.code}
try:
order.send_mail(
email_subject, email_template, email_context,
log_entry,
invoices=[],
attach_tickets=True,
position=position
)
except SendMailException:
logger.exception('Order received email could not be sent to attendee')
def _perform_order(event: str, payment_provider: str, position_ids: List[str],
email: str, locale: str, address: int, meta_info: dict=None, sales_channel: str='web'):
@@ -709,50 +779,26 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
if order.require_approval:
email_template = event.settings.mail_text_order_placed_require_approval
log_entry = 'pretix.event.order.email.order_placed_require_approval'
email_attendees = False
elif free_order_flow:
email_template = event.settings.mail_text_order_free
log_entry = 'pretix.event.order.email.order_free'
email_attendees = event.settings.mail_send_order_free_attendee
email_attendees_template = event.settings.mail_text_order_free_attendee
else:
email_template = event.settings.mail_text_order_placed
log_entry = 'pretix.event.order.email.order_placed'
try:
invoice_name = order.invoice_address.name
invoice_company = order.invoice_address.company
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
email_attendees = event.settings.mail_send_order_placed_attendee
email_attendees_template = event.settings.mail_text_order_placed_attendee
if pprov:
payment_info = str(pprov.order_pending_mail_render(order))
else:
payment_info = None
email_context = {
'total': LazyNumber(order.total),
'currency': event.currency,
'total_with_currency': LazyCurrencyNumber(order.total, event.currency),
'date': LazyDate(order.expires),
'event': event.name,
'url': build_absolute_uri(event, 'presale:event.order.open', kwargs={
'order': order.code,
'secret': order.secret,
'hash': order.email_confirm_hash()
}),
'payment_info': payment_info,
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
email_subject = _('Your order: %(code)s') % {'code': order.code}
try:
order.send_mail(
email_subject, email_template, email_context,
log_entry,
invoices=[invoice] if invoice and event.settings.invoice_email_attachment else [],
attach_tickets=True
)
except SendMailException:
logger.exception('Order received email could not be sent')
_order_placed_email(event, order, pprov, email_template, log_entry, invoice)
if email_attendees:
for p in order.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
_order_placed_email_attendee(event, order, p, email_attendees_template, log_entry)
return order.id
@@ -873,6 +919,31 @@ def send_download_reminders(sender, **kwargs):
except SendMailException:
logger.exception('Reminder email could not be sent')
if e.settings.mail_send_download_reminder_attendee:
name_scheme = PERSON_NAME_SCHEMES[e.settings.name_scheme]
for p in o.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != o.email:
email_template = e.settings.mail_text_download_reminder_attendee
email_context = {
'event': e.name,
'url': build_absolute_uri(e, 'presale:event.order.position', kwargs={
'order': o.code,
'secret': p.web_secret,
'position': p.positionid
}),
'attendee_name': p.attendee_name,
}
for f, l, w in name_scheme['fields']:
email_context['attendee_name_%s' % f] = p.attendee_name_parts.get(f, '')
try:
o.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.download_reminder_sent',
attach_tickets=True, position=p
)
except SendMailException:
logger.exception('Reminder email could not be sent to attendee')
class OrderChangeManager:
error_messages = {

View File

@@ -96,7 +96,7 @@ def preview(event: int, provider: str):
return prov.generate(p)
def get_tickets_for_order(order):
def get_tickets_for_order(order, base_position=None):
can_download = all([r for rr, r in allow_ticket_download.send(order.event, order=order)])
if not can_download:
return []
@@ -111,13 +111,20 @@ def get_tickets_for_order(order):
tickets = []
positions = list(order.positions_with_tickets)
if base_position:
# Only the given position and its children
positions = [
p for p in positions if p.pk == base_position.pk or p.addon_to_id == base_position.pk
]
for p in providers:
if not p.is_enabled:
continue
if p.multi_download_enabled:
if p.multi_download_enabled and not base_position:
try:
if len(list(order.positions_with_tickets)) == 0:
if len(positions) == 0:
continue
ct = CachedCombinedTicket.objects.filter(
order=order, provider=p.identifier, file__isnull=False
@@ -136,7 +143,7 @@ def get_tickets_for_order(order):
except:
logger.exception('Failed to generate ticket.')
else:
for pos in order.positions_with_tickets:
for pos in positions:
try:
ct = CachedTicket.objects.filter(
order_position=pos, provider=p.identifier, file__isnull=False

View File

@@ -331,6 +331,18 @@ The list is as follows:
{orders}
Best regards,
Your {event} team"""))
},
'mail_text_order_free_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name},
you have been registered for {event} successfully.
You can view the details and status of your ticket here:
{url}
Best regards,
Your {event} team"""))
},
@@ -347,6 +359,10 @@ You can change your order details and view the status of your order at
Best regards,
Your {event} team"""))
},
'mail_send_order_free_attendee': {
'type': bool,
'default': 'False'
},
'mail_text_order_placed_require_approval': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
@@ -373,6 +389,22 @@ of {total_with_currency}. Please complete your payment before {date}.
You can change your order details and view the status of your order at
{url}
Best regards,
Your {event} team"""))
},
'mail_send_order_placed_attendee': {
'type': bool,
'default': 'False'
},
'mail_text_order_placed_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name},
a ticket for {event} has been ordered for you.
You can view the details and status of your ticket here:
{url}
Best regards,
Your {event} team"""))
},
@@ -399,6 +431,22 @@ we successfully received your payment for {event}. Thank you!
You can change your order details and view the status of your order at
{url}
Best regards,
Your {event} team"""))
},
'mail_send_order_paid_attendee': {
'type': bool,
'default': 'False'
},
'mail_text_order_paid_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name},
a ticket for {event} that has been ordered for you is now paid.
You can view the details and status of your ticket here:
{url}
Best regards,
Your {event} team"""))
},
@@ -500,6 +548,22 @@ Your {event} team"""))
'type': int,
'default': None
},
'mail_send_download_reminder_attendee': {
'type': bool,
'default': 'False'
},
'mail_text_download_reminder_attendee': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello {attendee_name},
you are registered for {event}.
If you did not do so already, you can download your ticket here:
{url}
Best regards,
Your {event} team"""))
},
'mail_text_download_reminder': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,

View File

@@ -23,13 +23,23 @@
<table cellpadding="20"><tr><td>
<![endif]-->
<div class="content">
{% trans "You are receiving this email because you placed an order for the following event:" %}<br>
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_hash order=order.code secret=order.secret %}">
{% trans "View order details" %}
</a>
{% if position %}
{% trans "You are receiving this email because someone signed you up for the following event:" %}<br>
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
<a href="{% abseventurl event "presale:event.order.position" order=order.code secret=position.web_secret position=position.positionid %}">
{% trans "View registration details" %}
</a>
{% else %}
{% trans "You are receiving this email because you placed an order for the following event:" %}<br>
<strong>{% trans "Event:" %}</strong> {{ event.name }}<br>
<strong>{% trans "Order code:" %}</strong> {{ order.code }}<br>
<strong>{% trans "Order date:" %}</strong> {{ order.datetime|date:"SHORT_DATE_FORMAT" }}<br>
<a href="{% abseventurl event "presale:event.order.open" hash=order.email_confirm_hash order=order.code secret=order.secret %}">
{% trans "View order details" %}
</a>
{% endif %}
</div>
<!--[if gte mso 9]>
</td></tr></table>

View File

@@ -844,7 +844,7 @@ class MailSettingsForm(SettingsForm):
)
mail_text_order_placed = I18nFormField(
label=_("Text"),
label=_("Text sent to order contact address"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {total_with_currency}, {total}, {currency}, {date}, "
@@ -852,20 +852,62 @@ class MailSettingsForm(SettingsForm):
validators=[PlaceholderValidator(['{event}', '{total_with_currency}', '{total}', '{currency}', '{date}',
'{payment_info}', '{url}', '{invoice_name}', '{invoice_company}'])]
)
mail_send_order_placed_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
help_text=_('If the order contains attendees with email addresses different from the person who orders the '
'tickets, the following email will be sent out to the attendees.'),
required=False,
)
mail_text_order_placed_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {attendee_name}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{attendee_name}'])],
)
mail_text_order_paid = I18nFormField(
label=_("Text"),
label=_("Text sent to order contact address"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}, {payment_info}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{invoice_name}', '{invoice_company}', '{payment_info}'])]
)
mail_send_order_paid_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
help_text=_('If the order contains attendees with email addresses different from the person who orders the '
'tickets, the following email will be sent out to the attendees.'),
required=False,
)
mail_text_order_paid_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {attendee_name}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{attendee_name}'])],
)
mail_text_order_free = I18nFormField(
label=_("Text"),
label=_("Text sent to order contact address"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {invoice_name}, {invoice_company}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{invoice_name}', '{invoice_company}'])]
)
mail_send_order_free_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
help_text=_('If the order contains attendees with email addresses different from the person who orders the '
'tickets, the following email will be sent out to the attendees.'),
required=False,
)
mail_text_order_free_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {attendee_name}"),
validators=[PlaceholderValidator(['{event}', '{url}', '{attendee_name}'])],
)
mail_text_order_changed = I18nFormField(
label=_("Text"),
required=False,
@@ -925,12 +967,25 @@ class MailSettingsForm(SettingsForm):
'{invoice_name}', '{invoice_company}'])]
)
mail_text_download_reminder = I18nFormField(
label=_("Text"),
label=_("Text sent to order contact address"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}"),
validators=[PlaceholderValidator(['{event}', '{url}'])]
)
mail_send_download_reminder_attendee = forms.BooleanField(
label=_("Send an email to attendees"),
help_text=_('If the order contains attendees with email addresses different from the person who orders the '
'tickets, the following email will be sent out to the attendees.'),
required=False,
)
mail_text_download_reminder_attendee = I18nFormField(
label=_("Text sent to attendees"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {attendee_name}, {event}, {url}"),
validators=[PlaceholderValidator(['{attendee_name}', '{event}', '{url}'])]
)
mail_days_download_reminder = forms.IntegerField(
label=_("Number of days"),
required=False,
@@ -1011,13 +1066,26 @@ class MailSettingsForm(SettingsForm):
(r.identifier, r.verbose_name) for r in event.get_html_mail_renderers().values()
]
keys = list(event.meta_data.keys())
for k, v in self.fields.items():
name_scheme = PERSON_NAME_SCHEMES[event.settings.name_scheme]
for k, v in list(self.fields.items()):
if k.startswith('mail_text_'):
v.help_text = str(v.help_text) + ', ' + ', '.join({
'{meta_' + p + '}' for p in keys
})
v.validators[0].limit_value += ['{meta_' + p + '}' for p in keys]
if '{attendee_name}' in v.validators[0].limit_value:
for f, l, w in name_scheme['fields']:
if f == 'full_name':
continue
v.help_text = str(v.help_text) + ', ' + '{attendee_name_%s}' % f
v.validators[0].limit_value += ['{attendee_name_' + f + '}']
if k.endswith('_attendee') and not event.settings.attendee_emails_asked:
# If we don't ask for attendee emails, we can't send them anything and we don't need to clutter
# the user interface with it
del self.fields[k]
def clean(self):
data = self.cleaned_data
if not data.get('smtp_password') and data.get('smtp_username'):

View File

@@ -41,13 +41,13 @@
<legend>{% trans "E-mail content" %}</legend>
<div class="panel-group" id="questions_group">
{% blocktrans asvar title_placed_order %}Placed order{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_placed" title=title_placed_order items="mail_text_order_placed" %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_placed" title=title_placed_order items="mail_text_order_placed,mail_send_order_placed_attendee,mail_text_order_placed_attendee" exclude="mail_send_order_placed_attendee" %}
{% blocktrans asvar title_paid_order %}Paid order{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_paid" title=title_paid_order items="mail_text_order_paid" %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_paid" title=title_paid_order items="mail_text_order_paid,mail_send_order_paid_attendee,mail_text_order_paid_attendee" exclude="mail_send_order_paid_attendee" %}
{% blocktrans asvar title_free_order %}Free order{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_free" title=title_free_order items="mail_text_order_free" %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="order_free" title=title_free_order items="mail_text_order_free,mail_send_order_free_attendee,mail_text_order_free_attendee" exclude="mail_send_order_free_attendee" %}
{% blocktrans asvar title_resend_link %}Resend link{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="resend_link" title=title_resend_link items="mail_text_resend_link,mail_text_resend_all_links" %}
@@ -68,7 +68,7 @@
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="custom_mail" title=title_order_custom_mail items="mail_text_order_custom_mail" %}
{% blocktrans asvar title_download_tickets_reminder %}Reminder to download tickets{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_download_tickets_reminder items="mail_days_download_reminder,mail_text_download_reminder" exclude="mail_days_download_reminder" %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_download_tickets_reminder items="mail_days_download_reminder,mail_text_download_reminder,mail_send_download_reminder_attendee,mail_text_download_reminder_attendee" exclude="mail_days_download_reminder,mail_send_download_reminder_attendee" %}
{% blocktrans asvar title_require_approval %}Order approval process{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_require_approval items="mail_text_order_placed_require_approval,mail_text_order_approved,mail_text_order_denied" %}

View File

@@ -13,11 +13,11 @@
{% with exclude|split as exclusion %}
{% with items|split as item_list %}
{% for item in item_list %}
{% if item in exclusion %}
{% if item in exclusion and form|hasattr:item %}
{% with form|getattr:item as field %}
{% bootstrap_field field layout="horizontal" %}
{% endwith %}
{% else %}
{% elif form|hasattr:item %}
<div id="{{ item }}_panel" class="preview-panel form-group" for="{{ item }}">
{% with form|getattr:item as field %}
<label class="col-md-3 control-label">{{ field.label }}</label>

View File

@@ -11,3 +11,12 @@ def split(value, delimiter=","):
@register.filter(name="getattr")
def get_attribute(value, key):
return value[key]
@register.filter(name="hasattr")
def has_attribute(value, key):
try:
value[key]
return True
except:
return False

View File

@@ -9,6 +9,12 @@ from pretix.control.forms.widgets import Select2
class MailForm(forms.Form):
recipients = forms.ChoiceField(
label=_('Send email to'),
widget=forms.RadioSelect,
initial='orders',
choices=[]
)
sendto = forms.MultipleChoiceField() # overridden later
subject = forms.CharField(label=_("Subject"))
message = forms.CharField(label=_("Message"))
@@ -30,6 +36,18 @@ class MailForm(forms.Form):
def __init__(self, *args, **kwargs):
event = kwargs.pop('event')
super().__init__(*args, **kwargs)
recp_choices = [
('orders', _('Everyone who created a ticket order'))
]
if event.settings.attendee_emails_asked:
recp_choices += [
('attendees', _('Every attendee (falling back to the order contact when no attendee email address is '
'given)')),
('both', _('Both (all order contact addresses and all attendee email addresses)'))
]
self.fields['recipients'].choices = recp_choices
self.fields['subject'] = I18nFormField(
label=_('Subject'),
widget=I18nTextInput, required=True,

View File

@@ -46,7 +46,8 @@ def control_nav_import(sender, request=None, **kwargs):
def pretixcontrol_logentry_display(sender, logentry, **kwargs):
plains = {
'pretix.plugins.sendmail.sent': _('Email was sent'),
'pretix.plugins.sendmail.order.email.sent': _('The order received a mass email.')
'pretix.plugins.sendmail.order.email.sent': _('The order received a mass email.'),
'pretix.plugins.sendmail.order.email.sent.attendee': _('A ticket holder of this order received a mass email.'),
}
if logentry.action_type in plains:
return plains[logentry.action_type]

View File

@@ -11,11 +11,11 @@ from pretix.multidomain.urlreverse import build_absolute_uri
@app.task(base=ProfiledTask)
def send_mails(event: int, user: int, subject: dict, message: dict, orders: list) -> None:
def send_mails(event: int, user: int, subject: dict, message: dict, orders: list, items: list, recipients: str) -> None:
failures = []
event = Event.objects.get(pk=event)
user = User.objects.get(pk=user) if user else None
orders = Order.objects.filter(pk__in=orders)
orders = Order.objects.filter(pk__in=orders, event=event)
subject = LazyI18nString(subject)
message = LazyI18nString(message)
tz = pytz.timezone(event.settings.timezone)
@@ -27,37 +27,95 @@ def send_mails(event: int, user: int, subject: dict, message: dict, orders: list
except InvoiceAddress.DoesNotExist:
invoice_name = ""
invoice_company = ""
try:
with language(o.locale):
email_context = {
'event': o.event,
'code': o.code,
'date': date_format(o.datetime.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
'expire_date': date_format(o.expires, 'SHORT_DATE_FORMAT'),
'url': build_absolute_uri(event, 'presale:event.order', kwargs={
'order': o.code,
'secret': o.secret
}),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
mail(
o.email,
subject,
message,
email_context,
event,
locale=o.locale,
order=o
)
o.log_action(
'pretix.plugins.sendmail.order.email.sent',
user=user,
data={
'subject': subject.localize(o.locale).format_map(email_context),
'message': message.localize(o.locale).format_map(email_context),
'recipient': o.email
send_to_order = recipients in ('both', 'orders')
if recipients in ('both', 'attendees'):
for p in o.positions.prefetch_related('addons'):
if p.addon_to_id is not None:
continue
if p.item_id not in items and not any(a.item_id in items for a in p.addons.all()):
continue
if not p.attendee_email:
if recipients == 'attendees':
send_to_order = True
continue
if p.attendee_email == o.email and send_to_order:
continue
try:
with language(o.locale):
email_context = {
'event': event,
'code': o.code,
'date': date_format(o.datetime.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
'expire_date': date_format(o.expires, 'SHORT_DATE_FORMAT'),
'url': build_absolute_uri(event, 'presale:event.order.position', kwargs={
'order': o.code,
'secret': p.web_secret,
'position': p.positionid
}),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
mail(
p.attendee_email,
subject,
message,
email_context,
event,
locale=o.locale,
order=o,
position=p
)
o.log_action(
'pretix.plugins.sendmail.order.email.sent.attendee',
user=user,
data={
'position': p.positionid,
'subject': subject.localize(o.locale).format_map(email_context),
'message': message.localize(o.locale).format_map(email_context),
'recipient': p.attendee_email
}
)
except SendMailException:
failures.append(p.attendee_email)
if send_to_order and o.email:
try:
with language(o.locale):
email_context = {
'event': event,
'code': o.code,
'date': date_format(o.datetime.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
'expire_date': date_format(o.expires, 'SHORT_DATE_FORMAT'),
'url': build_absolute_uri(event, 'presale:event.order.open', kwargs={
'order': o.code,
'secret': o.secret,
'hash': o.email_confirm_hash()
}),
'invoice_name': invoice_name,
'invoice_company': invoice_company,
}
)
except SendMailException:
failures.append(o.email)
mail(
o.email,
subject,
message,
email_context,
event,
locale=o.locale,
order=o
)
o.log_action(
'pretix.plugins.sendmail.order.email.sent',
user=user,
data={
'subject': subject.localize(o.locale).format_map(email_context),
'message': message.localize(o.locale).format_map(email_context),
'recipient': o.email
}
)
except SendMailException:
failures.append(o.email)

View File

@@ -26,6 +26,13 @@
{% if log.pdata.subevent_obj %}
<br/><span class="fa fa-calendar fa-fw"></span> {{ log.pdata.subevent_obj }}
{% endif %}
{% if log.pdata.recipients == "attendees" %}
<br/><span class="fa fa-envelope fa-fw"></span> {% trans "Attendee contact addresses" %}
{% elif log.pdata.recipients == "both" %}
<br/><span class="fa fa-envelope fa-fw"></span> {% trans "All contact addresses" %}
{% else%}
<br/><span class="fa fa-envelope fa-fw"></span> {% trans "Order contact addresses" %}
{% endif %}
</p>
<p>
{% for locale, value in log.pdata.locales.items %}

View File

@@ -7,6 +7,7 @@
{% block inner %}
<form class="form-horizontal" method="post" action="">
{% csrf_token %}
{% bootstrap_field form.recipients layout='horizontal' %}
{% bootstrap_field form.sendto layout='horizontal' %}
{% if form.subevent %}
{% bootstrap_field form.subevent layout='horizontal' %}

View File

@@ -40,6 +40,7 @@ class SenderView(EventPermissionRequiredMixin, FormView):
action_type='pretix.plugins.sendmail.sent'
)
kwargs['initial'] = {
'recipients': logentry.parsed_data['recipients'],
'message': LazyI18nString(logentry.parsed_data['message']),
'subject': LazyI18nString(logentry.parsed_data['subject']),
'sendto': logentry.parsed_data['sendto'],
@@ -68,7 +69,7 @@ class SenderView(EventPermissionRequiredMixin, FormView):
return super().form_invalid(form)
def form_valid(self, form):
qs = Order.objects.filter(event=self.request.event, email__isnull=False)
qs = Order.objects.filter(event=self.request.event)
statusq = Q(status__in=form.cleaned_data['sendto'])
if 'overdue' in form.cleaned_data['sendto']:
statusq |= Q(status=Order.STATUS_PENDING, expires__lt=now())
@@ -118,17 +119,20 @@ class SenderView(EventPermissionRequiredMixin, FormView):
send_mails.apply_async(
kwargs={
'recipients': form.cleaned_data['recipients'],
'event': self.request.event.pk,
'user': self.request.user.pk,
'subject': form.cleaned_data['subject'].data,
'message': form.cleaned_data['message'].data,
'orders': [o.pk for o in orders],
'items': [i.pk for i in form.cleaned_data.get('items')]
}
)
self.request.event.log_action('pretix.plugins.sendmail.sent',
user=self.request.user,
data=dict(form.cleaned_data))
messages.success(self.request, _('Your message has been queued and will be sent to %d users in the next minutes.') % len(orders))
messages.success(self.request, _('Your message has been queued and will be sent to the contact addresses of %d '
'orders in the next minutes.') % len(orders))
return redirect(
'plugins:sendmail:send',

View File

@@ -169,6 +169,15 @@ This signal is sent out to display additional information on the order detail pa
As with all plugin signals, the ``sender`` keyword argument will contain the event.
"""
position_info = EventPluginSignal(
providing_args=["order", "position"]
)
"""
This signal is sent out to display additional information on the position detail page
As with all plugin signals, the ``sender`` keyword argument will contain the event.
"""
process_request = EventPluginSignal(
providing_args=["request"]
)

View File

@@ -64,7 +64,7 @@
<div class="download-desktop">
{% if line.generate_ticket %}
{% for b in download_buttons %}
<form action="{% eventurl event "presale:event.order.download" secret=order.secret order=order.code output=b.identifier position=line.id %}"
<form action="{% if position_page %}{% eventurl event "presale:event.order.position.download" secret=line.web_secret order=order.code output=b.identifier pid=line.pk position=line.positionid %}{% else %}{% eventurl event "presale:event.order.download" secret=order.secret order=order.code output=b.identifier position=line.pk %}{% endif %}"
method="post" data-asynctask data-asynctask-download class="download-btn-form">
{% csrf_token %}
<button type="submit"
@@ -157,7 +157,7 @@
<div class="download-mobile">
{% if line.generate_ticket %}
{% for b in download_buttons %}
<form action="{% eventurl event "presale:event.order.download" secret=order.secret order=order.code output=b.identifier position=line.id %}"
<form action="{% if position_page %}{% eventurl event "presale:event.order.position.download" secret=line.web_secret order=order.code pid=line.pk output=b.identifier position=line.positionid %}{% else %}{% eventurl event "presale:event.order.download" secret=order.secret order=order.code output=b.identifier position=line.id %}{% endif %}"
method="post" data-asynctask data-asynctask-download class="download-btn-form">
{% csrf_token %}
<button type="submit"

View File

@@ -0,0 +1,34 @@
{% load i18n %}
{% load eventurl %}
{% if can_download and download_buttons and order.count_positions %}
<div class="alert alert-info info-download">
{% blocktrans trimmed %}
You can download your tickets using the buttons below. Please have your ticket ready when entering the event.
{% endblocktrans %}
</div>
{% if cart.positions|length > 1 and can_download_multi %}
<p class="info-download">
{% trans "Download all tickets at once:" %}
{% for b in download_buttons %}
{% if b.multi %}
<form action="{% eventurl event "presale:event.order.download.combined" secret=order.secret order=order.code output=b.identifier %}"
method="post" data-asynctask data-asynctask-download class="download-btn-form">
{% csrf_token %}
<button type="submit"
class="btn btn-sm {% if b.identifier == "pdf" %}btn-primary{% else %}btn-default{% endif %}">
<span class="fa {{ b.icon }}"></span> {{ b.text }}
</button>
</form>
{% endif %}
{% endfor %}
</p>
{% endif %}
{% elif not download_buttons and ticket_download_date %}
{% if order.status == 'p' %}
<div class="alert alert-info info-download">
{% blocktrans trimmed with date=ticket_download_date|date:"SHORT_DATE_FORMAT" %}
You will be able to download your tickets here starting on {{ date }}.
{% endblocktrans %}
</div>
{% endif %}
{% endif %}

View File

@@ -111,38 +111,7 @@
</div>
{% endif %}
{% endif %}
{% if can_download and download_buttons and order.count_positions %}
<div class="alert alert-info info-download">
{% blocktrans trimmed %}
You can download your tickets using the buttons below. Please have your ticket ready when entering the event.
{% endblocktrans %}
</div>
{% if cart.positions|length > 1 and can_download_multi %}
<p class="info-download">
{% trans "Download all tickets at once:" %}
{% for b in download_buttons %}
{% if b.multi %}
<form action="{% eventurl event "presale:event.order.download.combined" secret=order.secret order=order.code output=b.identifier %}"
method="post" data-asynctask data-asynctask-download class="download-btn-form">
{% csrf_token %}
<button type="submit"
class="btn btn-sm {% if b.identifier == "pdf" %}btn-primary{% else %}btn-default{% endif %}">
<span class="fa {{ b.icon }}"></span> {{ b.text }}
</button>
</form>
{% endif %}
{% endfor %}
</p>
{% endif %}
{% elif not download_buttons and ticket_download_date %}
{% if order.status == 'p' %}
<div class="alert alert-info info-download">
{% blocktrans trimmed with date=ticket_download_date|date:"SHORT_DATE_FORMAT" %}
You will be able to download your tickets here starting on {{ date }}.
{% endblocktrans %}
</div>
{% endif %}
{% endif %}
{% include "pretixpresale/event/fragment_downloads.html" %}
<div class="panel panel-primary cart">
<div class="panel-heading">
{% if order.can_modify_answers %}

View File

@@ -0,0 +1,51 @@
{% extends "pretixpresale/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load eventsignal %}
{% load money %}
{% load eventurl %}
{% block title %}{% trans "Registration details" %}{% endblock %}
{% block content %}
<h2>
{% blocktrans trimmed %}
Your registration
{% endblocktrans %}
{% if order.testmode %}
<span class="label label-warning">{% trans "TEST MODE" %}</span>
{% endif %}
{% if backend_user %}
<a href="{% url "control:event.order" event=request.event.slug organizer=request.organizer.slug code=order.code %}" class="btn btn-default">
{% trans "View in backend" %}
</a>
{% endif %}
{% include "pretixpresale/event/fragment_order_status.html" with order=order class="pull-right" %}
<div class="clearfix"></div>
</h2>
{% include "pretixpresale/event/fragment_downloads.html" %}
<div class="panel panel-primary cart">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Your items" %}
</h3>
</div>
<div class="panel-body">
{% include "pretixpresale/event/fragment_cart.html" with cart=cart event=request.event download=can_download position_page=True editable=False %}
</div>
</div>
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Additional information" %}
</h3>
</div>
<div class="panel-body">
<p>
{% blocktrans trimmed with email="<strong>"|add:order.email|add:"</strong>"|safe %}
This order is managed for you by {{ email }}. Please contact them for any questions regarding
payment, cancellation or changes to this order.
{% endblocktrans %}
</p>
</div>
</div>
{% eventsignal event "pretix.presale.signals.position_info" order=order position=position %}
{% endblock %}

View File

@@ -49,6 +49,7 @@ event_patterns = [
name='event.cart.add'),
url(r'resend/$', pretix.presale.views.user.ResendLinkView.as_view(), name='event.resend_link'),
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/open/(?P<hash>[a-z0-9]+)/$', pretix.presale.views.order.OrderOpen.as_view(),
name='event.order.open'),
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/$', pretix.presale.views.order.OrderDetails.as_view(),
@@ -89,6 +90,14 @@ event_patterns = [
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/invoice/(?P<invoice>[0-9]+)$',
pretix.presale.views.order.InvoiceDownload.as_view(),
name='event.invoice.download'),
url(r'^ticket/(?P<order>[^/]+)/(?P<position>\d+)/(?P<secret>[A-Za-z0-9]+)/$',
pretix.presale.views.order.OrderPositionDetails.as_view(),
name='event.order.position'),
url(r'^ticket/(?P<order>[^/]+)/(?P<position>\d+)/(?P<secret>[A-Za-z0-9]+)/download/(?P<pid>[0-9]+)/(?P<output>[^/]+)$',
pretix.presale.views.order.OrderPositionDownload.as_view(),
name='event.order.position.download'),
url(r'^ical/?$',
pretix.presale.views.event.EventIcalDownload.as_view(),
name='event.ical.download'),

View File

@@ -65,6 +65,39 @@ class OrderDetailMixin(NoSearchIndexViewMixin):
})
class OrderPositionDetailMixin(NoSearchIndexViewMixin):
@cached_property
def position(self):
p = OrderPosition.objects.filter(
order__event=self.request.event,
addon_to__isnull=True,
order__code=self.kwargs['order'],
positionid=self.kwargs['position']
).select_related('order', 'order__event').first()
if p:
if p.web_secret.lower() == self.kwargs['secret'].lower():
return p
else:
return None
else:
# Do a comparison as well to harden timing attacks
if 'abcdefghijklmnopq'.lower() == self.kwargs['secret'].lower():
return None
else:
return None
@cached_property
def order(self):
return self.position.order if self.position else None
def get_position_url(self):
return eventreverse(self.request.event, 'presale:event.order.position', kwargs={
'order': self.order.code,
'secret': self.position.web_secret,
'position': self.position.positionid,
})
@method_decorator(xframe_options_exempt, 'dispatch')
class OrderOpen(EventViewMixin, OrderDetailMixin, View):
def get(self, request, *args, **kwargs):
@@ -76,8 +109,28 @@ class OrderOpen(EventViewMixin, OrderDetailMixin, View):
return redirect(self.get_order_url())
class TicketPageMixin:
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['order'] = self.order
can_download = all([r for rr, r in allow_ticket_download.send(self.request.event, order=self.order)])
if self.request.event.settings.ticket_download_date:
ctx['ticket_download_date'] = self.order.ticket_download_date
ctx['can_download'] = can_download and self.order.ticket_download_available and self.order.positions_with_tickets
ctx['download_buttons'] = self.download_buttons
ctx['backend_user'] = (
self.request.user.is_authenticated
and self.request.user.has_event_permission(self.request.organizer, self.request.event, 'can_view_orders', request=self.request)
)
return ctx
@method_decorator(xframe_options_exempt, 'dispatch')
class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TemplateView):
class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TicketPageMixin, TemplateView):
template_name = "pretixpresale/event/order.html"
def get(self, request, *args, **kwargs):
@@ -105,13 +158,6 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TemplateView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['order'] = self.order
can_download = all([r for rr, r in allow_ticket_download.send(self.request.event, order=self.order)])
if self.request.event.settings.ticket_download_date:
ctx['ticket_download_date'] = self.order.ticket_download_date
ctx['can_download'] = can_download and self.order.ticket_download_available and self.order.positions_with_tickets
ctx['download_buttons'] = self.download_buttons
ctx['cart'] = self.get_cart(
answers=True, downloads=ctx['can_download'],
queryset=self.order.positions.select_related('tax_rule'),
@@ -142,11 +188,6 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TemplateView):
self.order.total != Decimal('0.00') or not self.request.event.settings.invoice_address_not_asked_free
)
ctx['backend_user'] = (
self.request.user.is_authenticated
and self.request.user.has_event_permission(self.request.organizer, self.request.event, 'can_view_orders', request=self.request)
)
if self.order.status == Order.STATUS_PENDING:
ctx['pending_sum'] = self.order.pending_sum
@@ -179,6 +220,47 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TemplateView):
return ctx
@method_decorator(xframe_options_exempt, 'dispatch')
class OrderPositionDetails(EventViewMixin, OrderPositionDetailMixin, CartMixin, TicketPageMixin, TemplateView):
template_name = "pretixpresale/event/position.html"
def get(self, request, *args, **kwargs):
self.kwargs = kwargs
if not self.position:
raise Http404(_('Unknown order code or not authorized to access this order.'))
return super().get(request, *args, **kwargs)
@cached_property
def download_buttons(self):
buttons = []
responses = register_ticket_outputs.send(self.request.event)
for receiver, response in responses:
provider = response(self.request.event)
if not provider.is_enabled:
continue
buttons.append({
'text': provider.download_button_text or 'Download',
'icon': provider.download_button_icon or 'fa-download',
'identifier': provider.identifier,
'multi': provider.multi_download_enabled
})
return buttons
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['can_download_multi'] = False
ctx['position'] = self.position
ctx['cart'] = self.get_cart(
answers=True, downloads=ctx['can_download'],
queryset=self.order.positions.select_related('tax_rule').filter(
Q(pk=self.position.pk) | Q(addon_to__id=self.position.pk)
),
order=self.order
)
return ctx
@method_decorator(xframe_options_exempt, 'dispatch')
class OrderPaymentStart(EventViewMixin, OrderDetailMixin, TemplateView):
"""
@@ -669,22 +751,10 @@ class AnswerDownload(EventViewMixin, OrderDetailMixin, View):
return resp
@method_decorator(xframe_options_exempt, 'dispatch')
class OrderDownload(EventViewMixin, OrderDetailMixin, AsyncAction, View):
task = generate
class OrderDownloadMixin:
def get_success_url(self, value):
return self.get_self_url()
def get_error_url(self):
return self.get_order_url()
def get_self_url(self):
return eventreverse(self.request.event,
'presale:event.order.download' if 'position' in self.kwargs
else 'presale:event.order.download.combined',
kwargs=self.kwargs)
@cached_property
def output(self):
if not all([r for rr, r in allow_ticket_download.send(self.request.event, order=self.order)]):
@@ -695,13 +765,6 @@ class OrderDownload(EventViewMixin, OrderDetailMixin, AsyncAction, View):
if provider.identifier == self.kwargs.get('output'):
return provider
@cached_property
def order_position(self):
try:
return self.order.positions.get(pk=self.kwargs.get('position'))
except OrderPosition.DoesNotExist:
return None
def get(self, request, *args, **kwargs):
if not self.output or not self.output.is_enabled:
return self.error(_('You requested an invalid ticket output type.'))
@@ -770,6 +833,51 @@ class OrderDownload(EventViewMixin, OrderDetailMixin, AsyncAction, View):
return ct
@method_decorator(xframe_options_exempt, 'dispatch')
class OrderDownload(OrderDownloadMixin, EventViewMixin, OrderDetailMixin, AsyncAction, View):
task = generate
def get_error_url(self):
return self.get_order_url()
def get_self_url(self):
return eventreverse(self.request.event,
'presale:event.order.download' if 'position' in self.kwargs
else 'presale:event.order.download.combined',
kwargs=self.kwargs)
@cached_property
def order_position(self):
try:
return self.order.positions.get(pk=self.kwargs.get('position'))
except OrderPosition.DoesNotExist:
return None
@method_decorator(xframe_options_exempt, 'dispatch')
class OrderPositionDownload(OrderDownloadMixin, EventViewMixin, OrderPositionDetailMixin, AsyncAction, View):
task = generate
def get_error_url(self):
return self.get_position_url()
def get_self_url(self):
return eventreverse(self.request.event,
'presale:event.order.position.download',
kwargs=self.kwargs)
@cached_property
def order_position(self):
try:
return self.order.positions.get(
Q(pk=self.kwargs.get('pid')) & Q(
Q(pk=self.position.pk) | Q(addon_to__id=self.position.pk)
)
)
except OrderPosition.DoesNotExist:
return None
@method_decorator(xframe_options_exempt, 'dispatch')
class InvoiceDownload(EventViewMixin, OrderDetailMixin, View):