forked from CGM_Public/pretix_original
committed by
Raphael Michel
parent
e4949b6491
commit
64d07a2811
@@ -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])
|
||||
|
||||
Reference in New Issue
Block a user