forked from CGM_Public/pretix_original
Merge branch 'pretix:master' into vite-vue3
This commit is contained in:
@@ -216,7 +216,10 @@ class OutboundSyncProvider:
|
||||
|
||||
try:
|
||||
mapped_objects = self.sync_order(sq.order)
|
||||
if not all(all(not res or res.sync_info.get("action", "") == "nothing_to_do" for res in res_list) for res_list in mapped_objects.values()):
|
||||
actions_taken = [res and res.sync_info.get("action", "") for res_list in mapped_objects.values() for res in res_list]
|
||||
should_write_logentry = any(action not in (None, "nothing_to_do") for action in actions_taken)
|
||||
logger.info('Synced order %s to %s, actions: %r, log: %r', sq.order.code, sq.sync_provider, actions_taken, should_write_logentry)
|
||||
if should_write_logentry:
|
||||
sq.order.log_action("pretix.event.order.data_sync.success", {
|
||||
"provider": self.identifier,
|
||||
"objects": {
|
||||
@@ -237,7 +240,7 @@ class OutboundSyncProvider:
|
||||
sq.set_sync_error("exceeded", e.messages, e.full_message)
|
||||
else:
|
||||
logger.info(
|
||||
f"Could not sync order {sq.order.code} to {type(self).__name__} "
|
||||
f"Could not sync order {sq.order.code} to {sq.sync_provider} "
|
||||
f"(transient error, attempt #{sq.failed_attempts}, next {sq.not_before})",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
@@ -315,8 +315,9 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
for id, vn in payment_methods:
|
||||
headers.append(_('Paid by {method}').format(method=vn))
|
||||
|
||||
# get meta_data labels from first cached event
|
||||
headers += next(iter(self.event_object_cache.values())).meta_data.keys()
|
||||
if self.event_object_cache:
|
||||
# get meta_data labels from first cached event if any
|
||||
headers += next(iter(self.event_object_cache.values())).meta_data.keys()
|
||||
yield headers
|
||||
|
||||
full_fee_sum_cache = {
|
||||
@@ -503,8 +504,9 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
headers.append(_('External customer ID'))
|
||||
headers.append(_('Payment providers'))
|
||||
|
||||
# get meta_data labels from first cached event
|
||||
headers += next(iter(self.event_object_cache.values())).meta_data.keys()
|
||||
if self.event_object_cache:
|
||||
# get meta_data labels from first cached event if any
|
||||
headers += next(iter(self.event_object_cache.values())).meta_data.keys()
|
||||
yield headers
|
||||
|
||||
yield self.ProgressSetTotal(total=qs.count())
|
||||
@@ -707,9 +709,9 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
_('Position order link')
|
||||
]
|
||||
|
||||
# get meta_data labels from first cached event
|
||||
meta_data_labels = next(iter(self.event_object_cache.values())).meta_data.keys()
|
||||
if has_subevents:
|
||||
# get meta_data labels from first cached event
|
||||
meta_data_labels = next(iter(self.event_object_cache.values())).meta_data.keys()
|
||||
headers += meta_data_labels
|
||||
yield headers
|
||||
|
||||
|
||||
@@ -1415,6 +1415,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
if not data.get(r):
|
||||
raise ValidationError({r: _("This field is required for the selected type of invoice transmission.")})
|
||||
|
||||
transmission_type.validate_invoice_address_data(data)
|
||||
self.instance.transmission_type = transmission_type.identifier
|
||||
self.instance.transmission_info = transmission_type.form_data_to_transmission_info(data)
|
||||
elif transmission_type.is_exclusive(self.event, data.get("country"), data.get("is_business")):
|
||||
|
||||
@@ -42,6 +42,8 @@ from django.utils.html import escape
|
||||
from django.utils.timezone import get_current_timezone, now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.helpers.format import PlainHtmlAlternativeString
|
||||
|
||||
|
||||
def replace_arabic_numbers(inp):
|
||||
if not isinstance(inp, str):
|
||||
@@ -61,11 +63,18 @@ def replace_arabic_numbers(inp):
|
||||
return inp.translate(table)
|
||||
|
||||
|
||||
def format_placeholder_help_text(placeholder_name, sample_value):
|
||||
if isinstance(sample_value, PlainHtmlAlternativeString):
|
||||
sample_value = sample_value.plain
|
||||
title = (_("Sample: %s") % sample_value) if sample_value else ""
|
||||
return ('<button type="button" class="content-placeholder" title="%s">{%s}</button>' % (escape(title), escape(placeholder_name)))
|
||||
|
||||
|
||||
def format_placeholders_help_text(placeholders, event=None):
|
||||
placeholders = [(k, v.render_sample(event) if event else v) for k, v in placeholders.items()]
|
||||
placeholders.sort(key=lambda x: x[0])
|
||||
phs = [
|
||||
'<button type="button" class="content-placeholder" title="%s">{%s}</button>' % (escape(_("Sample: %s") % v) if v else "", escape(k))
|
||||
format_placeholder_help_text(k, v)
|
||||
for k, v in placeholders
|
||||
]
|
||||
return _('Available placeholders: {list}').format(
|
||||
|
||||
@@ -33,8 +33,7 @@ from pretix.base.invoicing.transmission import (
|
||||
transmission_types,
|
||||
)
|
||||
from pretix.base.models import Invoice, InvoiceAddress
|
||||
from pretix.base.services.mail import mail, render_mail
|
||||
from pretix.helpers.format import format_map
|
||||
from pretix.base.services.mail import mail
|
||||
|
||||
|
||||
@transmission_types.new()
|
||||
@@ -134,9 +133,7 @@ class EmailTransmissionProvider(TransmissionProvider):
|
||||
subject = invoice.order.event.settings.get('mail_subject_order_invoice', as_type=LazyI18nString)
|
||||
|
||||
# Do not set to completed because that is done by the email sending task
|
||||
subject = format_map(subject, context)
|
||||
email_content = render_mail(template, context)
|
||||
mail(
|
||||
outgoing_mail = mail(
|
||||
[recipient],
|
||||
subject,
|
||||
template,
|
||||
@@ -151,19 +148,10 @@ class EmailTransmissionProvider(TransmissionProvider):
|
||||
plain_text_only=True,
|
||||
no_order_links=True,
|
||||
)
|
||||
invoice.order.log_action(
|
||||
'pretix.event.order.email.invoice',
|
||||
user=None,
|
||||
auth=None,
|
||||
data={
|
||||
'subject': subject,
|
||||
'message': email_content,
|
||||
'position': None,
|
||||
'recipient': recipient,
|
||||
'invoices': [invoice.pk],
|
||||
'attach_tickets': False,
|
||||
'attach_ical': False,
|
||||
'attach_other_files': [],
|
||||
'attach_cached_files': [],
|
||||
}
|
||||
)
|
||||
if outgoing_mail:
|
||||
invoice.order.log_action(
|
||||
'pretix.event.order.email.invoice',
|
||||
user=None,
|
||||
auth=None,
|
||||
data=outgoing_mail.log_data()
|
||||
)
|
||||
|
||||
@@ -148,6 +148,10 @@ class NumberedCanvas(Canvas):
|
||||
self.restoreState()
|
||||
|
||||
|
||||
class InvoiceNotReadyException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BaseInvoiceRenderer:
|
||||
"""
|
||||
This is the base class for all invoice renderers.
|
||||
|
||||
@@ -204,6 +204,12 @@ class PeppolTransmissionType(TransmissionType):
|
||||
}
|
||||
return base | {"transmission_peppol_participant_id"}
|
||||
|
||||
def validate_invoice_address_data(self, address_data: dict):
|
||||
# Special case Belgium: If a Belgian business ID is used as Peppol ID, it should match the VAT ID
|
||||
if address_data.get("transmission_peppol_participant_id").startswith("0208:") and address_data.get("vat_id"):
|
||||
if address_data["vat_id"].removeprefix("BE") != address_data["transmission_peppol_participant_id"].removeprefix("0208:"):
|
||||
raise ValidationError({"transmission_peppol_participant_id": _("The Peppol participant ID does not match your VAT ID.")})
|
||||
|
||||
def pdf_watermark(self) -> str:
|
||||
return pgettext("peppol_invoice", "Visual copy")
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ from typing import Optional
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_countries.fields import Country
|
||||
|
||||
from pretix.base.models import Invoice, InvoiceAddress
|
||||
from pretix.base.models import Invoice
|
||||
from pretix.base.signals import EventPluginRegistry, Registry
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ class TransmissionType:
|
||||
def invoice_address_form_fields_visible(self, country: Country, is_business: bool) -> set:
|
||||
return set(self.invoice_address_form_fields.keys())
|
||||
|
||||
def validate_address(self, ia: InvoiceAddress):
|
||||
def validate_invoice_address_data(self, address_data: dict):
|
||||
pass
|
||||
|
||||
@property
|
||||
|
||||
@@ -346,7 +346,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
{
|
||||
'user': self,
|
||||
'messages': msg,
|
||||
'url': build_absolute_uri('control:user.settings')
|
||||
'url': build_absolute_uri('control:user.settings'),
|
||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||
},
|
||||
event=None,
|
||||
user=self,
|
||||
@@ -391,6 +392,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
'user': self,
|
||||
'reason': msg,
|
||||
'code': code,
|
||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||
},
|
||||
event=None,
|
||||
user=self,
|
||||
@@ -430,6 +432,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
mail(
|
||||
self.email, _('Password recovery'), 'pretixcontrol/email/forgot.txt',
|
||||
{
|
||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||
'user': self,
|
||||
'url': (build_absolute_uri('control:auth.forgot.recover')
|
||||
+ '?id=%d&token=%s' % (self.id, default_token_generator.make_token(self)))
|
||||
|
||||
@@ -86,7 +86,7 @@ class OrderSyncQueue(models.Model):
|
||||
|
||||
def set_sync_error(self, failure_mode, messages, full_message):
|
||||
logger.exception(
|
||||
f"Could not sync order {self.order.code} to {type(self).__name__} ({failure_mode})"
|
||||
f"Could not sync order {self.order.code} to {self.sync_provider} ({failure_mode})"
|
||||
)
|
||||
self.order.log_action(f"pretix.event.order.data_sync.failed.{failure_mode}", {
|
||||
"provider": self.sync_provider,
|
||||
|
||||
@@ -220,3 +220,20 @@ class OutgoingMail(models.Model):
|
||||
error_log_action_type = 'pretix.email.error'
|
||||
log_target = None
|
||||
return log_target, error_log_action_type
|
||||
|
||||
def log_data(self):
|
||||
return {
|
||||
"subject": self.subject,
|
||||
"message": self.body_plain,
|
||||
"to": self.to,
|
||||
"cc": self.cc,
|
||||
"bcc": self.bcc,
|
||||
|
||||
"invoices": [i.pk for i in self.should_attach_invoices.all()],
|
||||
"attach_tickets": self.should_attach_tickets,
|
||||
"attach_ical": self.should_attach_ical,
|
||||
"attach_other_files": self.should_attach_other_files,
|
||||
"attach_cached_files": [cf.filename for cf in self.should_attach_cached_files.all()],
|
||||
|
||||
"position": self.orderposition.positionid if self.orderposition else None,
|
||||
}
|
||||
|
||||
@@ -87,7 +87,6 @@ from pretix.base.timemachine import time_machine_now
|
||||
|
||||
from ...helpers import OF_SELF
|
||||
from ...helpers.countries import CachedCountries, FastCountryField
|
||||
from ...helpers.format import FormattedString, format_map
|
||||
from ...helpers.names import build_name
|
||||
from ...testutils.middleware import debugflags_var
|
||||
from ._transactions import (
|
||||
@@ -1167,7 +1166,7 @@ class Order(LockModel, LoggedModel):
|
||||
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 mail, render_mail
|
||||
from pretix.base.services.mail import mail
|
||||
|
||||
if not self.email and not (position and position.attendee_email):
|
||||
return
|
||||
@@ -1177,32 +1176,20 @@ class Order(LockModel, LoggedModel):
|
||||
if position and position.attendee_email:
|
||||
recipient = position.attendee_email
|
||||
|
||||
email_content = render_mail(template, context)
|
||||
if not isinstance(subject, FormattedString):
|
||||
subject = format_map(subject, context)
|
||||
mail(
|
||||
outgoing_mail = mail(
|
||||
recipient, subject, template, context,
|
||||
self.event, self.locale, self, headers=headers, sender=sender,
|
||||
invoices=invoices, attach_tickets=attach_tickets,
|
||||
position=position, auto_email=auto_email, attach_ical=attach_ical,
|
||||
attach_other_files=attach_other_files, attach_cached_files=attach_cached_files,
|
||||
)
|
||||
self.log_action(
|
||||
log_entry_type,
|
||||
user=user,
|
||||
auth=auth,
|
||||
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,
|
||||
'attach_ical': attach_ical,
|
||||
'attach_other_files': attach_other_files,
|
||||
'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [],
|
||||
}
|
||||
)
|
||||
if outgoing_mail:
|
||||
self.log_action(
|
||||
log_entry_type,
|
||||
user=user,
|
||||
auth=auth,
|
||||
data=outgoing_mail.log_data(),
|
||||
)
|
||||
|
||||
def resend_link(self, user=None, auth=None):
|
||||
with language(self.locale, self.event.settings.region):
|
||||
@@ -2900,17 +2887,14 @@ class OrderPosition(AbstractPosition):
|
||||
:param attach_tickets: Attach tickets of this order, if they are existing and ready to download
|
||||
:param attach_ical: Attach relevant ICS files
|
||||
"""
|
||||
from pretix.base.services.mail import mail, render_mail
|
||||
from pretix.base.services.mail import mail
|
||||
|
||||
if not self.attendee_email:
|
||||
return
|
||||
|
||||
with language(self.order.locale, self.order.event.settings.region):
|
||||
recipient = self.attendee_email
|
||||
email_content = render_mail(template, context)
|
||||
if not isinstance(subject, FormattedString):
|
||||
subject = format_map(subject, context)
|
||||
mail(
|
||||
outgoing_mail = mail(
|
||||
recipient, subject, template, context,
|
||||
self.event, self.order.locale, order=self.order, headers=headers, sender=sender,
|
||||
position=self,
|
||||
@@ -2919,21 +2903,13 @@ class OrderPosition(AbstractPosition):
|
||||
attach_ical=attach_ical,
|
||||
attach_other_files=attach_other_files,
|
||||
)
|
||||
self.order.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,
|
||||
'attach_ical': attach_ical,
|
||||
'attach_other_files': attach_other_files,
|
||||
'attach_cached_files': [],
|
||||
}
|
||||
)
|
||||
if outgoing_mail:
|
||||
self.order.log_action(
|
||||
log_entry_type,
|
||||
user=user,
|
||||
auth=auth,
|
||||
data=outgoing_mail.log_data(),
|
||||
)
|
||||
|
||||
def resend_link(self, user=None, auth=None):
|
||||
|
||||
|
||||
@@ -34,10 +34,9 @@ from phonenumber_field.modelfields import PhoneNumberField
|
||||
from pretix.base.email import get_email_context
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import User, Voucher
|
||||
from pretix.base.services.mail import mail, render_mail
|
||||
from pretix.base.services.mail import mail
|
||||
from pretix.helpers import OF_SELF
|
||||
|
||||
from ...helpers.format import format_map
|
||||
from ...helpers.names import build_name
|
||||
from .base import LoggedModel
|
||||
from .event import Event, SubEvent
|
||||
@@ -181,10 +180,11 @@ class WaitingListEntry(LoggedModel):
|
||||
block_quota=True,
|
||||
item_id=self.item_id,
|
||||
subevent_id=self.subevent_id,
|
||||
waitinglistentries__isnull=False
|
||||
waitinglistentries__isnull=False,
|
||||
seat__isnull=True
|
||||
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
|
||||
free_seats = num_free_seats_for_product - num_valid_vouchers_for_product
|
||||
if not free_seats:
|
||||
if free_seats < 1:
|
||||
raise WaitingListException(_('No seat with this product is currently available.'))
|
||||
|
||||
if '@' not in self.email:
|
||||
@@ -272,9 +272,7 @@ class WaitingListEntry(LoggedModel):
|
||||
with language(self.locale, self.event.settings.region):
|
||||
recipient = self.email
|
||||
|
||||
email_content = render_mail(template, context)
|
||||
subject = format_map(subject, context)
|
||||
mail(
|
||||
outgoing_mail = mail(
|
||||
recipient, subject, template, context,
|
||||
self.event,
|
||||
self.locale,
|
||||
@@ -284,18 +282,13 @@ class WaitingListEntry(LoggedModel):
|
||||
attach_other_files=attach_other_files,
|
||||
attach_cached_files=attach_cached_files,
|
||||
)
|
||||
self.log_action(
|
||||
log_entry_type,
|
||||
user=user,
|
||||
auth=auth,
|
||||
data={
|
||||
'subject': subject,
|
||||
'message': email_content,
|
||||
'recipient': recipient,
|
||||
'attach_other_files': attach_other_files,
|
||||
'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [],
|
||||
}
|
||||
)
|
||||
if outgoing_mail:
|
||||
self.log_action(
|
||||
log_entry_type,
|
||||
user=user,
|
||||
auth=auth,
|
||||
data=outgoing_mail.log_data(),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def clean_itemvar(event, item, variation):
|
||||
|
||||
@@ -1295,6 +1295,7 @@ class ManualPayment(BasePaymentProvider):
|
||||
|
||||
def format_map(self, order, payment):
|
||||
return {
|
||||
# Possible placeholder injection, we should make sure to never include user-controlled variables here
|
||||
'order': order.code,
|
||||
'amount': payment.amount,
|
||||
'currency': self.event.currency,
|
||||
|
||||
@@ -45,7 +45,6 @@ from pretix.base.services.tax import split_fee_for_taxes
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.celery_app import app
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.format import format_map
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -55,7 +54,7 @@ def _send_wle_mail(wle: WaitingListEntry, subject: LazyI18nString, message: Lazy
|
||||
email_context = get_email_context(event_or_subevent=subevent or wle.event, event=wle.event)
|
||||
mail(
|
||||
wle.email,
|
||||
format_map(subject, email_context),
|
||||
str(subject),
|
||||
message,
|
||||
email_context,
|
||||
wle.event,
|
||||
@@ -73,9 +72,8 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
|
||||
|
||||
email_context = get_email_context(event_or_subevent=subevent or order.event, refund_amount=refund_amount,
|
||||
order=order, position_or_address=ia, event=order.event)
|
||||
real_subject = format_map(subject, email_context)
|
||||
order.send_mail(
|
||||
real_subject, message, email_context,
|
||||
subject, message, email_context,
|
||||
'pretix.event.order.email.event_canceled',
|
||||
user,
|
||||
)
|
||||
@@ -85,14 +83,13 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
|
||||
continue
|
||||
|
||||
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
|
||||
real_subject = format_map(subject, email_context)
|
||||
email_context = get_email_context(event_or_subevent=p.subevent or order.event,
|
||||
event=order.event,
|
||||
refund_amount=refund_amount,
|
||||
position_or_address=p,
|
||||
order=order, position=p)
|
||||
order.send_mail(
|
||||
real_subject, message, email_context,
|
||||
subject, message, email_context,
|
||||
'pretix.event.order.email.event_canceled',
|
||||
position=p,
|
||||
user=user
|
||||
|
||||
@@ -334,7 +334,8 @@ def _check_position_constraints(
|
||||
raise CartPositionError(error_messages['voucher_invalid_subevent'])
|
||||
|
||||
# Voucher expired
|
||||
if voucher and voucher.valid_until and voucher.valid_until < time_machine_now_dt:
|
||||
# (checked using real_now_dt as vouchers influence quota calculations)
|
||||
if voucher and voucher.valid_until and voucher.valid_until < real_now_dt:
|
||||
raise CartPositionError(error_messages['voucher_expired'])
|
||||
|
||||
# Subevent has been disabled
|
||||
|
||||
@@ -51,6 +51,7 @@ from django_scopes import scope, scopes_disabled
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.invoicing.pdf import InvoiceNotReadyException
|
||||
from pretix.base.invoicing.transmission import (
|
||||
get_transmission_types, transmission_providers,
|
||||
)
|
||||
@@ -504,7 +505,7 @@ def generate_invoice(order: Order, trigger_pdf=True):
|
||||
return invoice
|
||||
|
||||
|
||||
@app.task(base=TransactionAwareTask)
|
||||
@app.task(base=TransactionAwareTask, throws=(InvoiceNotReadyException,))
|
||||
def invoice_pdf_task(invoice: int):
|
||||
with scopes_disabled():
|
||||
i = Invoice.objects.get(pk=invoice)
|
||||
|
||||
@@ -149,13 +149,13 @@ def prefix_subject(settings_holder, subject, highlight=False):
|
||||
return subject
|
||||
|
||||
|
||||
def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, LazyI18nString],
|
||||
def mail(email: Union[str, Sequence[str]], subject: Union[str, FormattedString], template: Union[str, LazyI18nString],
|
||||
context: Dict[str, Any] = None, event: Event = None, locale: str = None, order: Order = None,
|
||||
position: OrderPosition = None, *, headers: dict = None, sender: str = None, organizer: Organizer = None,
|
||||
customer: Customer = None, invoices: Sequence = None, attach_tickets=False, auto_email=True, user=None,
|
||||
attach_ical=False, attach_cached_files: Sequence = None, attach_other_files: list=None,
|
||||
plain_text_only=False, no_order_links=False, cc: Sequence[str]=None, bcc: Sequence[str]=None,
|
||||
sensitive: bool=False):
|
||||
sensitive: bool=False) -> Optional[OutgoingMail]:
|
||||
"""
|
||||
Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation.
|
||||
|
||||
@@ -335,14 +335,26 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
should_attach_other_files=attach_other_files or [],
|
||||
sensitive=sensitive,
|
||||
)
|
||||
m._prefetched_objects_cache = {}
|
||||
if invoices and not position:
|
||||
m.should_attach_invoices.add(*invoices)
|
||||
# Hack: For logging, we'll later make a `should_attach_invoices.all()` call. We can prevent a useless
|
||||
# DB query by filling the cache
|
||||
m._prefetched_objects_cache[m.should_attach_invoices.prefetch_cache_name] = invoices
|
||||
else:
|
||||
m._prefetched_objects_cache[m.should_attach_invoices.prefetch_cache_name] = Invoice.objects.none()
|
||||
if attach_cached_files:
|
||||
cf_list = []
|
||||
for cf in attach_cached_files:
|
||||
if not isinstance(cf, CachedFile):
|
||||
m.should_attach_cached_files.add(CachedFile.objects.get(pk=cf))
|
||||
else:
|
||||
m.should_attach_cached_files.add(cf)
|
||||
cf = CachedFile.objects.get(pk=cf)
|
||||
m.should_attach_cached_files.add(cf)
|
||||
cf_list.append(cf)
|
||||
# Hack: For logging, we'll later make a `should_attach_cached_files.all()` call. We can prevent a useless
|
||||
# DB query by filling the cache
|
||||
m._prefetched_objects_cache[m.should_attach_cached_files.prefetch_cache_name] = cf_list
|
||||
else:
|
||||
m._prefetched_objects_cache[m.should_attach_cached_files.prefetch_cache_name] = CachedFile.objects.none()
|
||||
|
||||
send_task = mail_send_task.si(
|
||||
outgoing_mail=m.id
|
||||
@@ -364,6 +376,8 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
lambda: chain(*task_chain).apply_async()
|
||||
)
|
||||
|
||||
return m
|
||||
|
||||
|
||||
class CustomEmail(EmailMultiAlternatives):
|
||||
def _create_mime_attachment(self, content, mimetype):
|
||||
@@ -389,7 +403,7 @@ def mail_send_task(self, **kwargs) -> bool:
|
||||
# mail_send_task(self, *, outgoing_mail)
|
||||
with scopes_disabled():
|
||||
mail_send(**kwargs)
|
||||
return
|
||||
return False
|
||||
else:
|
||||
raise ValueError("Unknown arguments")
|
||||
|
||||
@@ -409,6 +423,18 @@ def mail_send_task(self, **kwargs) -> bool:
|
||||
outgoing_mail.inflight_since = now()
|
||||
outgoing_mail.save(update_fields=["status", "inflight_since"])
|
||||
|
||||
# Performance optimization, saves database queries later on if we resolve the known relationships
|
||||
if outgoing_mail.event_id:
|
||||
assert outgoing_mail.event.organizer_id == outgoing_mail.organizer.pk
|
||||
outgoing_mail.event.organizer = outgoing_mail.organizer
|
||||
if outgoing_mail.order_id:
|
||||
assert outgoing_mail.order.event_id == outgoing_mail.event_id
|
||||
outgoing_mail.order.event = outgoing_mail.event
|
||||
outgoing_mail.order.organizer = outgoing_mail.organizer
|
||||
if outgoing_mail.orderposition_id:
|
||||
assert outgoing_mail.orderposition.order_id == outgoing_mail.order_id
|
||||
outgoing_mail.orderposition.order = outgoing_mail.order
|
||||
|
||||
headers = dict(outgoing_mail.headers)
|
||||
headers.setdefault('X-PX-Correlation', str(outgoing_mail.guid))
|
||||
email = CustomEmail(
|
||||
@@ -443,15 +469,24 @@ def mail_send_task(self, **kwargs) -> bool:
|
||||
content = ct.file.read()
|
||||
args.append((name, content, ct.type))
|
||||
attach_size += len(content)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
# This sometimes fails e.g. with FileNotFoundError. We haven't been able to figure out
|
||||
# why (probably some race condition with ticket cache invalidation?), so retry later.
|
||||
try:
|
||||
self.retry(max_retries=5, countdown=60)
|
||||
logger.exception(f'Could not attach tickets to email {outgoing_mail.guid}, will retry')
|
||||
retry_after = 60
|
||||
outgoing_mail.error = "Tickets not ready"
|
||||
outgoing_mail.error_detail = str(e)
|
||||
outgoing_mail.sent = now()
|
||||
outgoing_mail.status = OutgoingMail.STATUS_AWAITING_RETRY
|
||||
outgoing_mail.retry_after = now() + timedelta(seconds=retry_after)
|
||||
outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent", "retry_after",
|
||||
"actual_attachments"])
|
||||
self.retry(max_retries=5, countdown=retry_after)
|
||||
except MaxRetriesExceededError:
|
||||
# Well then, something is really wrong, let's send it without attachment before we
|
||||
# don't send at all
|
||||
logger.exception(f'Could not attach tickets to email {outgoing_mail.guid}')
|
||||
logger.exception(f'Too many retries attaching tickets to email {outgoing_mail.guid}, skip attachment')
|
||||
pass
|
||||
|
||||
if attach_size * 1.37 < settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT - 1024 * 1024:
|
||||
|
||||
@@ -1799,8 +1799,6 @@ class OrderChangeManager:
|
||||
tax_rule = tax_rules.get(pos.pk, pos.tax_rule)
|
||||
if not tax_rule:
|
||||
continue
|
||||
if not pos.price:
|
||||
continue
|
||||
|
||||
try:
|
||||
new_rate = tax_rule.tax_rate_for(ia)
|
||||
@@ -1817,7 +1815,9 @@ class OrderChangeManager:
|
||||
override_tax_rate=new_rate, override_tax_code=new_code)
|
||||
self._totaldiff_guesstimate += new_tax.gross - pos.price
|
||||
self._operations.append(self.PriceOperation(pos, new_tax, new_tax.gross - pos.price))
|
||||
self._invoice_dirty = True
|
||||
if pos.price:
|
||||
# We do not consider the invoice dirty if only 0€-valued taxes are changed
|
||||
self._invoice_dirty = True
|
||||
|
||||
def cancel_fee(self, fee: OrderFee):
|
||||
self._totaldiff_guesstimate -= fee.value
|
||||
|
||||
@@ -24,6 +24,7 @@ import logging
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db.models import Prefetch, prefetch_related_objects
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.html import escape, mark_safe
|
||||
@@ -35,6 +36,7 @@ from pretix.base.forms.widgets import format_placeholders_help_text
|
||||
from pretix.base.i18n import (
|
||||
LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber,
|
||||
)
|
||||
from pretix.base.models import EventMetaValue
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
|
||||
from pretix.base.signals import (
|
||||
@@ -752,6 +754,11 @@ def base_placeholders(sender, **kwargs):
|
||||
name_scheme['sample'][f]
|
||||
))
|
||||
|
||||
prefetch_related_objects(
|
||||
[sender],
|
||||
Prefetch('meta_values', queryset=EventMetaValue.objects.select_related("property"), to_attr="meta_values_cached")
|
||||
)
|
||||
prefetch_related_objects([sender.organizer], Prefetch('meta_properties'))
|
||||
for k, v in sender.meta_data.items():
|
||||
ph.append(MarkdownTextPlaceholder(
|
||||
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],
|
||||
|
||||
@@ -176,6 +176,7 @@ def shred(self, event: Event, fileid: str, confirm_code: str, user: int=None, lo
|
||||
_('Data shredding completed'),
|
||||
'pretixbase/email/shred_completed.txt',
|
||||
{
|
||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
||||
'user': user,
|
||||
'organizer': event.organizer.name,
|
||||
'event': str(event.name),
|
||||
|
||||
@@ -39,7 +39,7 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
|
||||
with language(event.settings.locale):
|
||||
email_context = get_email_context(event=event, name=r.get('name') or '',
|
||||
voucher_list=[v.code for v in voucher_list])
|
||||
mail(
|
||||
outgoing_mail = mail(
|
||||
r['email'],
|
||||
subject,
|
||||
LazyI18nString(message),
|
||||
@@ -60,8 +60,8 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
|
||||
data={
|
||||
'recipient': r['email'],
|
||||
'name': r.get('name'),
|
||||
'subject': subject,
|
||||
'message': message,
|
||||
'subject': outgoing_mail.subject,
|
||||
'message': outgoing_mail.body_plain,
|
||||
},
|
||||
save=False
|
||||
))
|
||||
|
||||
@@ -363,7 +363,7 @@ class EmailAddressShredder(BaseDataShredder):
|
||||
le.save(update_fields=['data', 'shredded'])
|
||||
else:
|
||||
shred_log_fields(le, banlist=[
|
||||
'recipient', 'message', 'subject', 'full_mail', 'old_email', 'new_email'
|
||||
'recipient', 'message', 'subject', 'full_mail', 'old_email', 'new_email', 'bcc', 'cc',
|
||||
])
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
<meta charset="utf-8">
|
||||
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
|
||||
{% block custom_header %}{% endblock %}
|
||||
{% if css_theme %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ css_theme }}" />
|
||||
{% endif %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
||||
@@ -13,5 +13,5 @@ Start time: {{ start_time }} (new data added after this time might not have been
|
||||
|
||||
Best regards,
|
||||
|
||||
Your pretix team
|
||||
Your {{ instance }} team
|
||||
{% endblocktrans %}
|
||||
|
||||
34
src/pretix/base/templatetags/anonymize_email.py
Normal file
34
src/pretix/base/templatetags/anonymize_email.py
Normal file
@@ -0,0 +1,34 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-today pretix 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/>.
|
||||
#
|
||||
from django import template
|
||||
from django.utils.html import mark_safe
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter("anon_email")
|
||||
def anon_email(value):
|
||||
"""Replaces @ with [at] and . with [dot] for anonymization."""
|
||||
if not isinstance(value, str):
|
||||
return value
|
||||
value = value.replace("@", "[at]").replace(".", "[dot]")
|
||||
return mark_safe(''.join(['&#{0};'.format(ord(char)) for char in value]))
|
||||
@@ -423,7 +423,7 @@ def resolve_timeframe_to_dates_inclusive(ref_dt, frame, timezone) -> Tuple[Optio
|
||||
raise ValueError(f"Invalid timeframe '{frame}'")
|
||||
|
||||
|
||||
def resolve_timeframe_to_datetime_start_inclusive_end_exclusive(ref_dt, frame, timezone) -> Tuple[Optional[date], Optional[date]]:
|
||||
def resolve_timeframe_to_datetime_start_inclusive_end_exclusive(ref_dt, frame, timezone) -> Tuple[Optional[datetime], Optional[datetime]]:
|
||||
"""
|
||||
Given a serialized timeframe, evaluate it relative to `ref_dt` and return a tuple of datetimes
|
||||
where the first element ist the first possible datetime within the timeframe and the second
|
||||
|
||||
Reference in New Issue
Block a user