chrono/chrono/agendas/management/commands/send_booking_reminders.py

136 lines
5.2 KiB
Python

# chrono - agendas system
# Copyright (C) 2020 Entr'ouvert
#
# 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, either version 3 of the License, or
# (at your option) any later version.
#
# 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 <http://www.gnu.org/licenses/>.
from datetime import datetime, timedelta
from smtplib import SMTPException
import pytz
from django.conf import settings
from django.core.mail import send_mail
from django.core.management.base import BaseCommand
from django.db.models import F
from django.db.transaction import atomic
from django.template.loader import render_to_string
from django.utils import timezone, translation
from django.utils.translation import ugettext_lazy as _
from requests import RequestException
from chrono.agendas.models import Booking
from chrono.utils.requests_wrapper import requests
SENDING_IN_PROGRESS = datetime(year=2, month=1, day=1, tzinfo=pytz.UTC)
class Command(BaseCommand):
help = 'Send booking reminders'
def handle(self, **options):
translation.activate(settings.LANGUAGE_CODE)
# We want to send reminders x days before event starts, say x=2. For
# that we look at events that begin no earlier than 2 days minus 6
# hours from now, AND no later than 2 days. Hence an event that is in 2
# days will only be in this range from exactly 2 days before to 2 days
# before plus 6 hours. In case command is ran once every hour and a
# sending fails, this allows 6 retries before giving up.
reminder_delta = F('event__agenda__reminder_settings__days') * timedelta(1)
starts_before = timezone.now() + reminder_delta
starts_after = timezone.now() + reminder_delta - timedelta(hours=6)
# prevent user who just booked from getting a reminder (also documented in a help_text)
created_before = timezone.now() - timedelta(hours=12)
bookings = Booking.objects.filter(
cancellation_datetime__isnull=True,
creation_datetime__lte=created_before,
reminder_datetime__isnull=True,
event__start_datetime__lte=starts_before,
event__start_datetime__gte=starts_after,
).select_related('event', 'event__agenda', 'event__agenda__reminder_settings')
bookings_list = list(bookings)
bookings_pk = list(bookings.values_list('pk', flat=True))
bookings.update(reminder_datetime=SENDING_IN_PROGRESS)
try:
for booking in bookings_list:
self.send_reminder(booking)
finally:
Booking.objects.filter(pk__in=bookings_pk, reminder_datetime=SENDING_IN_PROGRESS).update(
reminder_datetime=None
)
def send_reminder(self, booking):
agenda = booking.event.agenda
kind = agenda.kind
days = agenda.reminder_settings.days
ctx = {
'booking': booking,
'in_x_days': _('tomorrow') if days == 1 else _('in %s days') % days,
'date': booking.event.start_datetime,
'email_extra_info': agenda.reminder_settings.email_extra_info,
'sms_extra_info': agenda.reminder_settings.sms_extra_info,
}
ctx.update(settings.TEMPLATE_VARS)
if agenda.reminder_settings.send_email:
self.send_email(booking, kind, ctx)
if agenda.reminder_settings.send_sms:
self.send_sms(booking, kind, ctx)
@staticmethod
def send_email(booking, kind, ctx):
if not booking.user_email:
return
subject = render_to_string('agendas/%s_reminder_subject.txt' % kind, ctx).strip()
body = render_to_string('agendas/%s_reminder_body.txt' % kind, ctx)
html_body = render_to_string('agendas/%s_reminder_body.html' % kind, ctx)
try:
with atomic():
send_mail(
subject, body, settings.DEFAULT_FROM_EMAIL, [booking.user_email], html_message=html_body
)
booking.reminder_datetime = timezone.now()
booking.save()
except SMTPException:
pass
@staticmethod
def send_sms(booking, kind, ctx):
if not booking.user_phone_number:
return
if not settings.SMS_URL:
return
message = render_to_string('agendas/%s_reminder_message.txt' % kind, ctx).strip()
payload = {
'message': message,
'from': settings.SMS_SENDER,
'to': [booking.user_phone_number],
}
try:
with atomic():
request = requests.post(settings.SMS_URL, json=payload, remote_service='auto', timeout=10)
request.raise_for_status()
booking.reminder_datetime = timezone.now()
booking.save()
except RequestException:
pass