mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
committed by
Raphael Michel
parent
e4949b6491
commit
64d07a2811
@@ -41,14 +41,35 @@ from django_scopes.forms import SafeModelMultipleChoiceField
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
||||
|
||||
from pretix.base.email import get_available_placeholders
|
||||
from pretix.base.forms import PlaceholderValidator
|
||||
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
||||
from pretix.base.forms import I18nModelForm, PlaceholderValidator
|
||||
from pretix.base.forms.widgets import (
|
||||
SplitDateTimePickerWidget, TimePickerWidget,
|
||||
)
|
||||
from pretix.base.models import CheckinList, Item, Order, SubEvent
|
||||
from pretix.control.forms import CachedFileField
|
||||
from pretix.control.forms import CachedFileField, SplitDateTimeField
|
||||
from pretix.control.forms.widgets import Select2, Select2Multiple
|
||||
from pretix.plugins.sendmail.models import Rule
|
||||
|
||||
|
||||
class MailForm(forms.Form):
|
||||
class FormPlaceholderMixin:
|
||||
def _set_field_placeholders(self, fn, base_parameters):
|
||||
phs = [
|
||||
'{%s}' % p
|
||||
for p in sorted(get_available_placeholders(self.event, base_parameters).keys())
|
||||
]
|
||||
ht = _('Available placeholders: {list}').format(
|
||||
list=', '.join(phs)
|
||||
)
|
||||
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(phs)
|
||||
)
|
||||
|
||||
|
||||
class MailForm(FormPlaceholderMixin, forms.Form):
|
||||
recipients = forms.ChoiceField(
|
||||
label=_('Send email to'),
|
||||
widget=forms.RadioSelect,
|
||||
@@ -119,22 +140,6 @@ class MailForm(forms.Form):
|
||||
raise ValidationError(pgettext_lazy('subevent', 'If you set a date range, please set both a start and an end.'))
|
||||
return d
|
||||
|
||||
def _set_field_placeholders(self, fn, base_parameters):
|
||||
phs = [
|
||||
'{%s}' % p
|
||||
for p in sorted(get_available_placeholders(self.event, base_parameters).keys())
|
||||
]
|
||||
ht = _('Available placeholders: {list}').format(
|
||||
list=', '.join(phs)
|
||||
)
|
||||
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(phs)
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
event = self.event = kwargs.pop('event')
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -217,3 +222,104 @@ class MailForm(forms.Form):
|
||||
del self.fields['subevent']
|
||||
del self.fields['subevents_from']
|
||||
del self.fields['subevents_to']
|
||||
|
||||
|
||||
class RuleForm(FormPlaceholderMixin, I18nModelForm):
|
||||
class Meta:
|
||||
model = Rule
|
||||
|
||||
fields = ['subject', 'template',
|
||||
'send_date', 'send_offset_days', 'send_offset_time',
|
||||
'include_pending', 'all_products', 'limit_products',
|
||||
'send_to']
|
||||
|
||||
field_classes = {
|
||||
'subevent': SafeModelMultipleChoiceField,
|
||||
'limit_products': SafeModelMultipleChoiceField,
|
||||
'send_date': SplitDateTimeField,
|
||||
}
|
||||
|
||||
widgets = {
|
||||
'send_date': SplitDateTimePickerWidget(attrs={
|
||||
'data-display-dependency': '#id_schedule_type_0',
|
||||
}),
|
||||
'send_offset_days': forms.NumberInput(attrs={
|
||||
'data-display-dependency': '#id_schedule_type_1,#id_schedule_type_2,#id_schedule_type_3,'
|
||||
'#id_schedule_type_4',
|
||||
}),
|
||||
'send_offset_time': TimePickerWidget(attrs={
|
||||
'data-display-dependency': '#id_schedule_type_1,#id_schedule_type_2,#id_schedule_type_3,'
|
||||
'#id_schedule_type_4',
|
||||
}),
|
||||
'limit_products': forms.CheckboxSelectMultiple(
|
||||
attrs={'class': 'scrolling-multiple-choice',
|
||||
'data-inverse-dependency': '#id_all_products'},
|
||||
),
|
||||
'send_to': forms.RadioSelect,
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
instance = kwargs.get('instance')
|
||||
|
||||
if instance:
|
||||
if instance.date_is_absolute:
|
||||
dia = "abs"
|
||||
else:
|
||||
dia = "rel"
|
||||
dia += "_a" if instance.offset_is_after else "_b"
|
||||
dia += "_e" if instance.offset_to_event_end else "_s"
|
||||
|
||||
else:
|
||||
dia = "abs"
|
||||
|
||||
kwargs.setdefault('initial', {})
|
||||
kwargs['initial']['schedule_type'] = dia
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['limit_products'].queryset = Item.objects.filter(event=self.event)
|
||||
|
||||
self.fields['schedule_type'] = forms.ChoiceField(
|
||||
label=_('Type of schedule time'),
|
||||
widget=forms.RadioSelect,
|
||||
choices=[
|
||||
('abs', _('Absolute')),
|
||||
('rel_b_s', _('Relative, before event start')),
|
||||
('rel_b_e', _('Relative, before event end')),
|
||||
('rel_a_s', _('Relative, after event start')),
|
||||
('rel_a_e', _('Relative, after event end'))
|
||||
]
|
||||
)
|
||||
|
||||
self._set_field_placeholders('subject', ['event', 'order'])
|
||||
self._set_field_placeholders('template', ['event', 'order'])
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
|
||||
dia = d.get('schedule_type')
|
||||
if dia == 'abs':
|
||||
if not d.get('send_date'):
|
||||
raise ValidationError({'send_date': _('Please specify the send date')})
|
||||
d['date_is_absolute'] = True
|
||||
d['send_offset_days'] = d['send_offset_time'] = None
|
||||
else:
|
||||
if not (d.get('send_offset_days') and d.get('send_offset_time')):
|
||||
raise ValidationError(_('Please specify the offset days and time'))
|
||||
d['offset_is_after'] = '_a' in dia
|
||||
d['offset_to_event_end'] = '_e' in dia
|
||||
d['date_is_absolute'] = False
|
||||
d['send_date'] = None
|
||||
|
||||
if d.get('all_products'):
|
||||
# having products checked while the option is ignored is probably counterintuitive
|
||||
d['limit_products'] = Item.objects.none()
|
||||
else:
|
||||
if not d.get('limit_products'):
|
||||
raise ValidationError({'limit_products': _('Please specify a product')})
|
||||
|
||||
self.instance.offset_is_after = d.get('offset_is_after', False)
|
||||
self.instance.offset_to_event_end = d.get('offset_to_event_end', False)
|
||||
self.instance.date_is_absolute = d.get('date_is_absolute', False)
|
||||
|
||||
return d
|
||||
|
||||
52
src/pretix/plugins/sendmail/migrations/0001_initial.py
Normal file
52
src/pretix/plugins/sendmail/migrations/0001_initial.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# Generated by Django 3.2.2 on 2021-06-03 09:17
|
||||
|
||||
import django.db.models.deletion
|
||||
import i18nfield.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0191_event_last_modified'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Rule',
|
||||
fields=[
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('subject', i18nfield.fields.I18nCharField(max_length=255)),
|
||||
('template', i18nfield.fields.I18nTextField()),
|
||||
('all_products', models.BooleanField(default=True)),
|
||||
('include_pending', models.BooleanField(default=False)),
|
||||
('send_date', models.DateTimeField(blank=True, null=True)),
|
||||
('send_offset_days', models.IntegerField(null=True)),
|
||||
('send_offset_time', models.TimeField(blank=True, null=True)),
|
||||
('date_is_absolute', models.BooleanField(default=True)),
|
||||
('offset_to_event_end', models.BooleanField(default=False)),
|
||||
('offset_is_after', models.BooleanField(default=False)),
|
||||
('send_to', models.CharField(default='orders', max_length=10)),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sendmail_rules', to='pretixbase.event')),
|
||||
('limit_products', models.ManyToManyField(to='pretixbase.Item')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ScheduledMail',
|
||||
fields=[
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('last_computed', models.DateTimeField(auto_now_add=True)),
|
||||
('computed_datetime', models.DateTimeField(db_index=True)),
|
||||
('state', models.CharField(default='scheduled', max_length=100)),
|
||||
('last_successful_order_id', models.BigIntegerField(null=True)),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.event')),
|
||||
('rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sendmail.rule')),
|
||||
('subevent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.subevent')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('rule', 'subevent')},
|
||||
},
|
||||
),
|
||||
]
|
||||
0
src/pretix/plugins/sendmail/migrations/__init__.py
Normal file
0
src/pretix/plugins/sendmail/migrations/__init__.py
Normal file
274
src/pretix/plugins/sendmail/models.py
Normal file
274
src/pretix/plugins/sendmail/models.py
Normal file
@@ -0,0 +1,274 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
from datetime import datetime, time, timedelta
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Exists, OuterRef
|
||||
from django.utils import timezone
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.timezone import make_aware
|
||||
from django.utils.translation import gettext_lazy as _, ngettext
|
||||
from django_scopes import ScopedManager
|
||||
from i18nfield.fields import I18nCharField, I18nTextField
|
||||
|
||||
from pretix.base.email import get_email_context
|
||||
from pretix.base.models import (
|
||||
Event, InvoiceAddress, Item, Order, OrderPosition, SubEvent,
|
||||
)
|
||||
from pretix.base.services.mail import SendMailException
|
||||
|
||||
|
||||
class ScheduledMail(models.Model):
|
||||
STATE_SCHEDULED = 'scheduled'
|
||||
STATE_FAILED = 'failed'
|
||||
STATE_COMPLETED = 'completed'
|
||||
|
||||
STATE_CHOICES = [
|
||||
(STATE_SCHEDULED, STATE_SCHEDULED),
|
||||
(STATE_FAILED, STATE_FAILED),
|
||||
(STATE_COMPLETED, STATE_COMPLETED),
|
||||
]
|
||||
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
rule = models.ForeignKey("Rule", on_delete=models.CASCADE)
|
||||
subevent = models.ForeignKey(SubEvent, null=True, on_delete=models.CASCADE)
|
||||
event = models.ForeignKey(Event, on_delete=models.CASCADE)
|
||||
|
||||
last_computed = models.DateTimeField(auto_now_add=True)
|
||||
computed_datetime = models.DateTimeField(db_index=True)
|
||||
|
||||
state = models.CharField(max_length=100, choices=STATE_CHOICES, default=STATE_SCHEDULED)
|
||||
last_successful_order_id = models.BigIntegerField(null=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('rule', 'subevent'),
|
||||
|
||||
def save(self, **kwargs):
|
||||
if not self.computed_datetime:
|
||||
self.recompute()
|
||||
super().save(**kwargs)
|
||||
|
||||
def recompute(self):
|
||||
if self.rule.date_is_absolute:
|
||||
self.computed_datetime = self.rule.send_date
|
||||
else:
|
||||
e = self.subevent or self.event
|
||||
o_days = self.rule.send_offset_days
|
||||
if not self.rule.offset_is_after:
|
||||
o_days *= -1
|
||||
|
||||
offset = timedelta(days=o_days)
|
||||
st = self.rule.send_offset_time
|
||||
base_time = (e.date_to or e.date_from) if self.rule.offset_to_event_end else e.date_from
|
||||
d = base_time.astimezone(self.event.timezone).date() + offset
|
||||
self.computed_datetime = make_aware(
|
||||
datetime.combine(d, time(hour=st.hour, minute=st.minute, second=st.second, microsecond=0)),
|
||||
self.event.timezone,
|
||||
is_dst=False, # prevent AmbiguousTimeError
|
||||
)
|
||||
|
||||
self.last_computed = timezone.now()
|
||||
|
||||
def send(self):
|
||||
if self.state not in (ScheduledMail.STATE_SCHEDULED, ScheduledMail.STATE_FAILED):
|
||||
raise ValueError("Should not be called in this state")
|
||||
|
||||
e = self.event
|
||||
|
||||
orders = e.orders.all()
|
||||
limit_products = self.rule.limit_products.values_list('pk', flat=True) if not self.rule.all_products else None
|
||||
|
||||
if self.subevent:
|
||||
orders = orders.filter(
|
||||
Exists(OrderPosition.objects.filter(order=OuterRef('pk'), subevent=self.subevent))
|
||||
)
|
||||
|
||||
if not self.rule.all_products:
|
||||
orders = orders.filter(
|
||||
Exists(OrderPosition.objects.filter(order=OuterRef('pk'), item_id__in=limit_products))
|
||||
)
|
||||
|
||||
status = [Order.STATUS_PENDING, Order.STATUS_PAID] if self.rule.include_pending else [Order.STATUS_PAID]
|
||||
|
||||
if self.last_successful_order_id:
|
||||
orders = orders.filter(
|
||||
pk__gt=self.last_successful_order_id
|
||||
)
|
||||
|
||||
orders = orders.filter(
|
||||
status__in=status,
|
||||
require_approval=False,
|
||||
).order_by('pk').select_related('invoice_address').prefetch_related('positions')
|
||||
|
||||
send_to_orders = self.rule.send_to in (Rule.CUSTOMERS, Rule.BOTH)
|
||||
send_to_attendees = self.rule.send_to in (Rule.ATTENDEES, Rule.BOTH)
|
||||
|
||||
for o in orders:
|
||||
positions = list(o.positions.all())
|
||||
o_sent = False
|
||||
|
||||
try:
|
||||
ia = o.invoice_address
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
ia = InvoiceAddress(order=o)
|
||||
|
||||
if send_to_orders and o.email:
|
||||
email_ctx = get_email_context(event=e, order=o, position_or_address=ia)
|
||||
try:
|
||||
o.send_mail(self.rule.subject, self.rule.template, email_ctx,
|
||||
log_entry_type='pretix.plugins.sendmail.rule.order.email.sent')
|
||||
o_sent = True
|
||||
except SendMailException:
|
||||
... # ¯\_(ツ)_/¯
|
||||
|
||||
if send_to_attendees:
|
||||
if not self.rule.all_products:
|
||||
positions = [p for p in positions if p.item_id in limit_products]
|
||||
if self.subevent_id:
|
||||
positions = [p for p in positions if p.subevent_id == self.subevent_id]
|
||||
|
||||
for p in positions:
|
||||
email_ctx = get_email_context(event=e, order=o, position_or_address=ia, position=p)
|
||||
try:
|
||||
if p.attendee_email and (p.attendee_email != o.email or not o_sent):
|
||||
p.send_mail(self.rule.subject, self.rule.template, email_ctx,
|
||||
log_entry_type='pretix.plugins.sendmail.rule.order.position.email.sent')
|
||||
elif not o_sent and o.email:
|
||||
o.send_mail(self.rule.subject, self.rule.template, email_ctx,
|
||||
log_entry_type='pretix.plugins.sendmail.rule.order.email.set')
|
||||
o_sent = True
|
||||
except SendMailException:
|
||||
... # ¯\_(ツ)_/¯
|
||||
|
||||
self.last_successful_order_id = o.pk
|
||||
|
||||
|
||||
class Rule(models.Model):
|
||||
CUSTOMERS = "orders"
|
||||
ATTENDEES = "attendees"
|
||||
BOTH = "both"
|
||||
|
||||
SEND_TO_CHOICES = [
|
||||
(CUSTOMERS, _("Customers")),
|
||||
(ATTENDEES, _("Attendees")),
|
||||
(BOTH, _("Both customers and attendees"))
|
||||
]
|
||||
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='sendmail_rules')
|
||||
|
||||
subject = I18nCharField(max_length=255, verbose_name=_('Subject'))
|
||||
template = I18nTextField(verbose_name=_('Message'))
|
||||
|
||||
all_products = models.BooleanField(default=True, verbose_name=_('All products'))
|
||||
limit_products = models.ManyToManyField(Item, blank=True, verbose_name=_('Limit products'))
|
||||
|
||||
include_pending = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('Include pending orders'),
|
||||
help_text=_('By default, only paid orders will receive the email')
|
||||
)
|
||||
|
||||
# either send_date or send_offset_* have to be set
|
||||
send_date = models.DateTimeField(null=True, blank=True, verbose_name=_('Send date'))
|
||||
send_offset_days = models.IntegerField(null=True, blank=True, verbose_name=_('Number of days'))
|
||||
send_offset_time = models.TimeField(null=True, blank=True, verbose_name=_('Time of day'))
|
||||
|
||||
date_is_absolute = models.BooleanField(default=True, blank=True)
|
||||
offset_to_event_end = models.BooleanField(default=False, blank=True) # no verbose name because not actually
|
||||
offset_is_after = models.BooleanField(default=False, blank=True) # displayed in any forms
|
||||
|
||||
send_to = models.CharField(max_length=10, choices=SEND_TO_CHOICES, default=CUSTOMERS)
|
||||
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
|
||||
def save(self, **kwargs):
|
||||
super().save(**kwargs)
|
||||
|
||||
create_sms = []
|
||||
if self.event.has_subevents:
|
||||
for se in self.event.subevents.annotate(has_sm=Exists(ScheduledMail.objects.filter(
|
||||
subevent=OuterRef('pk'), rule=self))).filter(has_sm=False):
|
||||
sm = ScheduledMail(rule=self, subevent=se, event=self.event)
|
||||
sm.recompute()
|
||||
create_sms.append(sm)
|
||||
ScheduledMail.objects.bulk_create(create_sms)
|
||||
else:
|
||||
ScheduledMail.objects.get_or_create(rule=self, event=self.event)
|
||||
|
||||
update_sms = []
|
||||
for sm in self.scheduledmail_set.all():
|
||||
if sm in create_sms:
|
||||
continue
|
||||
previous = sm.computed_datetime
|
||||
sm.recompute()
|
||||
if sm.computed_datetime != previous:
|
||||
update_sms.append(sm)
|
||||
|
||||
ScheduledMail.objects.bulk_update(update_sms, ['computed_datetime', 'last_computed'], 100)
|
||||
|
||||
@property
|
||||
def human_readable_time(self):
|
||||
if self.date_is_absolute:
|
||||
d = self.send_date.astimezone(self.event.timezone)
|
||||
return _('on {date} at {time}').format(date=date_format(d, 'SHORT_DATE_FORMAT'),
|
||||
time=date_format(d, 'TIME_FORMAT'))
|
||||
else:
|
||||
if self.offset_to_event_end:
|
||||
if self.offset_is_after:
|
||||
s = ngettext(
|
||||
'%(count)d day after event end at %(time)s',
|
||||
'%(count)d days after event end at %(time)s',
|
||||
self.send_offset_days
|
||||
) % {
|
||||
'count': self.send_offset_days,
|
||||
'time': date_format(self.send_offset_time, 'TIME_FORMAT')
|
||||
}
|
||||
else:
|
||||
s = ngettext(
|
||||
'%(count)d day before event end at %(time)s',
|
||||
'%(count)d days before event end at %(time)s',
|
||||
self.send_offset_days
|
||||
) % {
|
||||
'count': self.send_offset_days,
|
||||
'time': date_format(self.send_offset_time, 'TIME_FORMAT')
|
||||
}
|
||||
else:
|
||||
if self.offset_is_after:
|
||||
s = ngettext(
|
||||
'%(count)d day after event start at %(time)s',
|
||||
'%(count)d days after event start at %(time)s',
|
||||
self.send_offset_days
|
||||
) % {
|
||||
'count': self.send_offset_days,
|
||||
'time': date_format(self.send_offset_time, 'TIME_FORMAT')
|
||||
}
|
||||
else:
|
||||
s = ngettext(
|
||||
'%(count)d day before event start at %(time)s',
|
||||
'%(count)d days before event start at %(time)s',
|
||||
self.send_offset_days
|
||||
) % {
|
||||
'count': self.send_offset_days,
|
||||
'time': date_format(self.send_offset_time, 'TIME_FORMAT')
|
||||
}
|
||||
return s
|
||||
@@ -31,13 +31,41 @@
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
import copy
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import connection, transaction
|
||||
from django.db.models import F, Q
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.urls import resolve, reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import scope, scopes_disabled
|
||||
|
||||
from pretix.base.signals import logentry_display
|
||||
from pretix.base.models import SubEvent
|
||||
from pretix.base.signals import (
|
||||
event_copy_data, logentry_display, periodic_task,
|
||||
)
|
||||
from pretix.control.signals import nav_event
|
||||
from pretix.plugins.sendmail.models import ScheduledMail
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@receiver(post_save, sender=SubEvent)
|
||||
def scheduled_mail_create(sender, **kwargs):
|
||||
subevent = kwargs.get('instance')
|
||||
event = subevent.event
|
||||
with scope(organizer=event.organizer):
|
||||
existing_rules = ScheduledMail.objects.filter(subevent=subevent).values_list('rule_id', flat=True)
|
||||
to_create = []
|
||||
for rule in event.sendmail_rules.all():
|
||||
if rule.pk not in existing_rules:
|
||||
to_create.append(ScheduledMail(rule=rule, event=event, subevent=subevent))
|
||||
ScheduledMail.objects.bulk_create(to_create)
|
||||
|
||||
|
||||
@receiver(nav_event, dispatch_uid="sendmail_nav")
|
||||
@@ -52,7 +80,6 @@ def control_nav_import(sender, request=None, **kwargs):
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': (url.namespace == 'plugins:sendmail' and url.url_name == 'send'),
|
||||
'icon': 'envelope',
|
||||
'children': [
|
||||
{
|
||||
@@ -63,6 +90,14 @@ def control_nav_import(sender, request=None, **kwargs):
|
||||
}),
|
||||
'active': (url.namespace == 'plugins:sendmail' and url.url_name == 'send'),
|
||||
},
|
||||
{
|
||||
'label': _('Automated emails'),
|
||||
'url': reverse('plugins:sendmail:rule.list', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': (url.namespace == 'plugins:sendmail' and url.url_name.startswith('rule.')),
|
||||
},
|
||||
{
|
||||
'label': _('Email history'),
|
||||
'url': reverse('plugins:sendmail:history', kwargs={
|
||||
@@ -82,6 +117,81 @@ def pretixcontrol_logentry_display(sender, logentry, **kwargs):
|
||||
'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.attendee': _('A ticket holder of this order received a mass email.'),
|
||||
'pretix.plugins.sendmail.rule.order.email.sent': _('A scheduled email was sent to the order'),
|
||||
'pretix.plugins.sendmail.rule.order.position.email.sent': _('A scheduled email was sent to a ticket holder'),
|
||||
'pretix.plugins.sendmail.rule.deleted': _('An email rule was deleted'),
|
||||
}
|
||||
if logentry.action_type in plains:
|
||||
return plains[logentry.action_type]
|
||||
|
||||
|
||||
@receiver(periodic_task)
|
||||
def sendmail_run_rules(sender, **kwargs):
|
||||
with scopes_disabled():
|
||||
mails = ScheduledMail.objects.all()
|
||||
|
||||
for m in mails.filter(Q(last_computed__isnull=True)
|
||||
| Q(subevent__last_modified__gt=F('last_computed'))
|
||||
| Q(event__last_modified__gt=F('last_computed'))):
|
||||
previous = m.computed_datetime
|
||||
m.recompute()
|
||||
if m.computed_datetime != previous:
|
||||
m.save(update_fields=['last_computed', 'computed_datetime'])
|
||||
|
||||
for m_id in mails.filter(
|
||||
state__in=(ScheduledMail.STATE_SCHEDULED, ScheduledMail.STATE_FAILED),
|
||||
computed_datetime__gte=timezone.now() - datetime.timedelta(days=2),
|
||||
computed_datetime__lte=timezone.now(),
|
||||
).values_list('pk', flat=True):
|
||||
# We try to send the emails in a "reasonably safe" way.
|
||||
# - We use PostgreSQL-level locking to prevent to cronjob processes trying to
|
||||
# work on the same email at the same time if .send() takes a long time.
|
||||
# - If we fail in between emails due to some kind of pretix-level bug, such as
|
||||
# an exception during placeholder rendering, we store a ``last_successful_order_id``
|
||||
# pointer and continue from there in our retry attempt, avoiding to send all the
|
||||
# previous emails a second time.
|
||||
# - If we fail due to a system-level failure such as a signal interrupt or a lost
|
||||
# connection to the database, this won't help us recover and on the next run, all
|
||||
# emails might be sent a second time. This isn't nice, but any solution would either
|
||||
# require settings some arbitrary timeout for a process or risk not sending some
|
||||
# emails at all. Under the assumption that system-level failures are rare and (more
|
||||
# importantly) usually don't happen multiple times in a row, this seems liek a
|
||||
# good tradeoff.
|
||||
# - We never retry for more than two days.
|
||||
|
||||
with transaction.atomic(durable=True):
|
||||
m = ScheduledMail.objects.select_for_update(
|
||||
skip_locked=connection.features.has_select_for_update_skip_locked
|
||||
).filter(pk=m_id).first()
|
||||
if not m or m.state not in (ScheduledMail.STATE_SCHEDULED, ScheduledMail.STATE_FAILED):
|
||||
# object is currently locked by other thread (currently being sent)
|
||||
# or has been sent in the meantime
|
||||
continue
|
||||
|
||||
try:
|
||||
m.send()
|
||||
m.state = ScheduledMail.STATE_COMPLETED
|
||||
m.save(update_fields=['state', 'last_successful_order_id'])
|
||||
except Exception as e:
|
||||
logger.exception('Could not send emails, will retry')
|
||||
m.state = ScheduledMail.STATE_FAILED
|
||||
m.save(update_fields=['state', 'last_successful_order_id'])
|
||||
|
||||
if settings.SENTRY_ENABLED:
|
||||
from sentry_sdk import capture_exception
|
||||
capture_exception(e)
|
||||
|
||||
|
||||
@receiver(signal=event_copy_data, dispatch_uid="sendmail_copy_event")
|
||||
def sendmail_copy_data_receiver(sender, other, item_map, **kwargs):
|
||||
if sender.sendmail_rules.exists(): # idempotency
|
||||
return
|
||||
|
||||
for r in other.sendmail_rules.prefetch_related('limit_products'):
|
||||
limit_products = list(r.limit_products.all())
|
||||
r = copy.copy(r)
|
||||
r.pk = None
|
||||
r.event = sender
|
||||
r.save()
|
||||
if limit_products:
|
||||
r.limit_products.add(*[item_map[p.id] for p in limit_products if p.id in item_map])
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Send out emails" %}{% endblock %}
|
||||
{% block title %}{% trans "Email history" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Email history" %}</h1>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
This page shows you all mass emails you sent out manually. It does not include emails sent out
|
||||
automatically.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<div>
|
||||
<ul class="list-group">
|
||||
{% for log in logs %}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Create Email Rule" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Create Email Rule" %}</h1>
|
||||
{% block inner %}
|
||||
<form class="form-horizontal" method="post" action="" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
|
||||
<fieldset>
|
||||
<legend>{% trans "Content" %}</legend>
|
||||
{% bootstrap_field form.subject layout='control' %}
|
||||
{% bootstrap_field form.template layout='control' %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Recipients" %}</legend>
|
||||
{% bootstrap_field form.send_to layout='control' %}
|
||||
{% bootstrap_field form.include_pending layout='control' %}
|
||||
{% bootstrap_field form.all_products layout='control' %}
|
||||
{% bootstrap_field form.limit_products layout='horizontal' %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Time" %}</legend>
|
||||
{% bootstrap_field form.schedule_type layout='horizontal' %}
|
||||
{% bootstrap_field form.send_date layout='horizontal' %}
|
||||
{% bootstrap_field form.send_offset_days layout='horizontal' %}
|
||||
{% bootstrap_field form.send_offset_time layout='horizontal' %}
|
||||
</fieldset>
|
||||
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% block title %}{% trans "Delete Email Rule" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Delete Email Rule" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<p>{% blocktrans with subject=rule.subject %}Are you sure you want to delete the rule <strong>{{ subject }}</strong>?{% endblocktrans %}</p>
|
||||
<div class="form-group submit-group">
|
||||
<a class="btn btn-default btn-cancel" href="{% url "plugins:sendmail:rule.list" organizer=request.organizer.slug event=request.event.slug %}">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger btn-save">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,96 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% block title %}{% trans "Automated email rules" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Automated email rules" %}</h1>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Email rules allow you to automatically sent emails to your customers at a specific time before or after
|
||||
your event.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
{% if rules %}
|
||||
<p>
|
||||
<a href="{% url "plugins:sendmail:rule.create" organizer=request.organizer.slug event=request.event.slug %}">
|
||||
<button class="btn btn-default"><span class="fa fa-plus"></span> {% trans "Create a new rule" %}</button>
|
||||
</a>
|
||||
</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Email subject" %}</th>
|
||||
<th>{% trans "Recipient" %}</th>
|
||||
<th>{% trans "Scheduled time" %}</th>
|
||||
<th>{% trans "Products" %}</th>
|
||||
<th>{% trans "Sent / Total dates" context "subevent" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in rules %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong><a href="{% url "plugins:sendmail:rule.update" organizer=request.organizer.slug event=request.event.slug rule=r.pk %}">{{ r.subject }}</a></strong>
|
||||
</td>
|
||||
<td>
|
||||
{{ r.get_send_to_display }}
|
||||
</td>
|
||||
<td>
|
||||
{{ r.human_readable_time }}
|
||||
{% if not r.date_is_absolute %}
|
||||
<br>
|
||||
{% trans "Next execution:" %}
|
||||
{% if r.next_execution %}
|
||||
{{ r.next_execution|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% else %}
|
||||
{% trans "unknown" %}
|
||||
{% endif %}
|
||||
{% if r.last_execution %}
|
||||
<br>
|
||||
{% trans "Last execution:" %}
|
||||
{{ r.last_execution|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if r.all_products %}
|
||||
<em>{% trans "All" %}</em>
|
||||
{% else %}
|
||||
<ul>
|
||||
{% for item in r.limit_products.all %}
|
||||
<li>
|
||||
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ r.sent_mails }} / {{ r.total_mails }}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
<a class="btn btn-sm btn-default" href="{% url "plugins:sendmail:rule.update" organizer=request.organizer.slug event=request.event.slug rule=r.pk %}"><i class="fa fa-edit"></i></a>
|
||||
<a class="btn btn-sm btn-danger" href="{% url "plugins:sendmail:rule.delete" organizer=request.organizer.slug event=request.event.slug rule=r.pk %}"><i class="fa fa-trash"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% else %}
|
||||
<div class="empty-collection">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You haven't created any rules yet.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<a href="{% url "plugins:sendmail:rule.create" organizer=request.organizer.slug event=request.event.slug %}">
|
||||
<button class="btn btn-primary btn-lg">{% trans "Create a new rule" %}</button>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,64 @@
|
||||
{% extends 'pretixcontrol/event/base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Update Email Rule" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Update Email Rule" %}</h1>
|
||||
{% block inner %}
|
||||
<form class="form-horizontal" method="post" action="" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
|
||||
<fieldset>
|
||||
<legend>{% trans "Content" %}</legend>
|
||||
{% bootstrap_field form.subject layout='control' %}
|
||||
{% bootstrap_field form.template layout='control' %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Recipients" %}</legend>
|
||||
{% bootstrap_field form.send_to layout='control' %}
|
||||
{% bootstrap_field form.include_pending layout='control' %}
|
||||
{% bootstrap_field form.all_products layout='control' %}
|
||||
{% bootstrap_field form.limit_products layout='horizontal' %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Time" %}</legend>
|
||||
{% bootstrap_field form.schedule_type layout='horizontal' %}
|
||||
{% bootstrap_field form.send_date layout='horizontal' %}
|
||||
{% bootstrap_field form.send_offset_days layout='horizontal' %}
|
||||
{% bootstrap_field form.send_offset_time layout='horizontal' %}
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed %}
|
||||
For technical reasons, the email might actually be sent a bit later than your
|
||||
configured date. Typically, this will not be more than 10 minutes. Your email
|
||||
will never be sent earlier than the time you configured.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>{% trans "E-mail preview" %}</legend>
|
||||
<div class="tab-pane mail-preview-group">
|
||||
{% for locale, out in output.items %}
|
||||
<div lang="{{ locale }}" class="mail-preview">
|
||||
<strong>{{ out.subject|safe }}</strong><br><br>
|
||||
{{ out.html|safe }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -53,7 +53,7 @@
|
||||
{% endif %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-default btn-save pull-left" name="action" value="preview">
|
||||
{% trans "Preview email" %}
|
||||
{% trans "Preview email" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Send" %}
|
||||
|
||||
@@ -39,5 +39,16 @@ from . import views
|
||||
urlpatterns = [
|
||||
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/sendmail/$', views.SenderView.as_view(),
|
||||
name='send'),
|
||||
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/sendmail/history/', views.EmailHistoryView.as_view(), name='history')
|
||||
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/sendmail/history/', views.EmailHistoryView.as_view(),
|
||||
name='history'),
|
||||
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/sendmail/rules/create', views.CreateRule.as_view(),
|
||||
name='rule.create'),
|
||||
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/sendmail/rules/(?P<rule>[^/]+)/delete',
|
||||
views.DeleteRule.as_view(),
|
||||
name='rule.delete'),
|
||||
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/sendmail/rules/(?P<rule>[^/]+)',
|
||||
views.UpdateRule.as_view(),
|
||||
name='rule.update'),
|
||||
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/sendmail/rules', views.ListRules.as_view(),
|
||||
name='rule.list'),
|
||||
]
|
||||
|
||||
@@ -38,12 +38,14 @@ import logging
|
||||
import bleach
|
||||
import dateutil
|
||||
from django.contrib import messages
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.http import Http404
|
||||
from django.shortcuts import redirect
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, Exists, Max, Min, OuterRef, Q
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import FormView, ListView
|
||||
from django.views.generic import DeleteView, FormView, ListView
|
||||
|
||||
from pretix.base.email import get_available_placeholders
|
||||
from pretix.base.i18n import LazyI18nString, language
|
||||
@@ -52,9 +54,11 @@ from pretix.base.models.event import SubEvent
|
||||
from pretix.base.services.mail import TolerantDict
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.views import CreateView, PaginationMixin, UpdateView
|
||||
from pretix.plugins.sendmail.tasks import send_mails
|
||||
|
||||
from . import forms
|
||||
from .models import Rule
|
||||
|
||||
logger = logging.getLogger('pretix.plugins.sendmail')
|
||||
|
||||
@@ -280,3 +284,165 @@ class EmailHistoryView(EventPermissionRequiredMixin, ListView):
|
||||
pass
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
class CreateRule(EventPermissionRequiredMixin, CreateView):
|
||||
template_name = 'pretixplugins/sendmail/rule_create.html'
|
||||
permission = 'can_change_event_settings'
|
||||
form_class = forms.RuleForm
|
||||
|
||||
model = Rule
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['event'] = self.request.event
|
||||
return kwargs
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
def form_valid(self, form):
|
||||
self.output = {}
|
||||
|
||||
if self.request.POST.get("action") == "preview":
|
||||
for l in self.request.event.settings.locales:
|
||||
with language(l, self.request.event.settings.region):
|
||||
context_dict = TolerantDict()
|
||||
for k, v in get_available_placeholders(self.request.event, ['event', 'order',
|
||||
'position_or_address']).items():
|
||||
context_dict[k] = '<span class="placeholder" title="{}">{}</span>'.format(
|
||||
_('This value will be replaced based on dynamic parameters.'),
|
||||
v.render_sample(self.request.event)
|
||||
)
|
||||
|
||||
subject = bleach.clean(form.cleaned_data['subject'].localize(l), tags=[])
|
||||
preview_subject = subject.format_map(context_dict)
|
||||
template = form.cleaned_data['template'].localize(l)
|
||||
preview_text = markdown_compile_email(template.format_map(context_dict))
|
||||
|
||||
self.output[l] = {
|
||||
'subject': _('Subject: {subject}').format(subject=preview_subject),
|
||||
'html': preview_text,
|
||||
}
|
||||
|
||||
return self.get(self.request, *self.args, **self.kwargs)
|
||||
|
||||
messages.success(self.request, _('Your rule has been created.'))
|
||||
|
||||
form.instance.event = self.request.event
|
||||
|
||||
self.object = form.save()
|
||||
|
||||
return redirect(
|
||||
'plugins:sendmail:rule.update',
|
||||
event=self.request.event.slug,
|
||||
organizer=self.request.event.organizer.slug,
|
||||
rule=self.object.pk,
|
||||
)
|
||||
|
||||
|
||||
class UpdateRule(EventPermissionRequiredMixin, UpdateView):
|
||||
model = Rule
|
||||
form_class = forms.RuleForm
|
||||
template_name = 'pretixplugins/sendmail/rule_update.html'
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
def get_object(self, queryset=None) -> Rule:
|
||||
return get_object_or_404(Rule, event=self.request.event, id=self.kwargs['rule'])
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('plugins:sendmail:rule.update', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
'rule': self.object.pk,
|
||||
})
|
||||
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return super().form_valid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
o = {}
|
||||
|
||||
for lang in self.request.event.settings.locales:
|
||||
with language(lang, self.request.event.settings.region):
|
||||
placeholders = TolerantDict()
|
||||
for k, v in get_available_placeholders(self.request.event, ['event', 'order', 'position_or_address']).items():
|
||||
placeholders[k] = '<span class="placeholder" title="{}">{}</span>'.format(
|
||||
_('This value will be replaced based on dynamic parameters.'),
|
||||
v.render_sample(self.request.event)
|
||||
)
|
||||
|
||||
subject = bleach.clean(self.object.subject.localize(lang), tags=[])
|
||||
preview_subject = subject.format_map(placeholders)
|
||||
template = self.object.template.localize(lang)
|
||||
preview_text = markdown_compile_email(template.format_map(placeholders))
|
||||
|
||||
o[lang] = {
|
||||
'subject': _('Subject: {subject}'.format(subject=preview_subject)),
|
||||
'html': preview_text,
|
||||
}
|
||||
|
||||
ctx['output'] = o
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
class ListRules(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
template_name = 'pretixplugins/sendmail/rule_list.html'
|
||||
model = Rule
|
||||
context_object_name = 'rules'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.sendmail_rules.annotate(
|
||||
total_mails=Count('scheduledmail'),
|
||||
sent_mails=Count('scheduledmail', filter=Q(scheduledmail__sent=True)),
|
||||
last_execution=Max(
|
||||
'scheduledmail__computed_datetime',
|
||||
filter=Q(scheduledmail__sent=True)
|
||||
),
|
||||
next_execution=Min(
|
||||
'scheduledmail__computed_datetime',
|
||||
filter=Q(scheduledmail__sent=False)
|
||||
),
|
||||
).prefetch_related(
|
||||
'limit_products'
|
||||
)
|
||||
|
||||
|
||||
class DeleteRule(EventPermissionRequiredMixin, DeleteView):
|
||||
model = Rule
|
||||
permission = 'can_change_event_settings'
|
||||
template_name = 'pretixplugins/sendmail/rule_delete.html'
|
||||
context_object_name = 'rule'
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("plugins:sendmail:rule.list", kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
})
|
||||
|
||||
def get_object(self, queryset=None) -> Rule:
|
||||
return get_object_or_404(Rule, event=self.request.event, id=self.kwargs['rule'])
|
||||
|
||||
@transaction.atomic
|
||||
def delete(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
success_url = self.get_success_url()
|
||||
|
||||
self.request.event.log_action('pretix.plugins.sendmail.rule.deleted',
|
||||
user=self.request.user,
|
||||
data={
|
||||
'subject': self.object.subject,
|
||||
'text': self.object.template,
|
||||
})
|
||||
|
||||
self.object.delete()
|
||||
messages.success(self.request, _('The selected rule has been deleted.'))
|
||||
return HttpResponseRedirect(success_url)
|
||||
|
||||
Reference in New Issue
Block a user