From b9dc9d37036336115a4f506de19f334a3cf926c2 Mon Sep 17 00:00:00 2001 From: Magnus Hagander Date: Tue, 14 May 2024 16:17:36 +0200 Subject: [PATCH] Add ability to schedule confsponsor emails for the future Start proper work on #90, but is not complete yet --- postgresqleu/confsponsor/forms.py | 2 +- .../commands/sponsor_send_emails.py | 88 +++++++++++++++++++ .../confsponsor/migrations/0001_initial.py | 3 +- .../migrations/0031_sponsormail_sent.py | 27 ++++++ postgresqleu/confsponsor/models.py | 11 ++- postgresqleu/confsponsor/util.py | 9 +- postgresqleu/confsponsor/views.py | 60 ++----------- template/confsponsor/admin_dashboard.html | 2 +- template/confsponsor/sent_mail.html | 2 +- 9 files changed, 145 insertions(+), 59 deletions(-) create mode 100644 postgresqleu/confsponsor/management/commands/sponsor_send_emails.py create mode 100644 postgresqleu/confsponsor/migrations/0031_sponsormail_sent.py diff --git a/postgresqleu/confsponsor/forms.py b/postgresqleu/confsponsor/forms.py index 504797d2..2738c868 100644 --- a/postgresqleu/confsponsor/forms.py +++ b/postgresqleu/confsponsor/forms.py @@ -117,7 +117,7 @@ class SponsorSendEmailForm(forms.ModelForm): class Meta: model = SponsorMail - exclude = ('conference', ) + exclude = ('conference', 'sent', ) widgets = { 'message': EmailTextWidget(), } diff --git a/postgresqleu/confsponsor/management/commands/sponsor_send_emails.py b/postgresqleu/confsponsor/management/commands/sponsor_send_emails.py new file mode 100644 index 00000000..c028fbd8 --- /dev/null +++ b/postgresqleu/confsponsor/management/commands/sponsor_send_emails.py @@ -0,0 +1,88 @@ +# Send queued sponsor emails. +# + +from django.core.management.base import BaseCommand +from django.utils import timezone +from django.db import transaction +from django.db.models import Q +from django.conf import settings + +from datetime import timedelta + +from postgresqleu.confsponsor.models import Sponsor, SponsorMail +from postgresqleu.confsponsor.util import send_conference_sponsor_notification, send_sponsor_manager_email +from postgresqleu.confreg.util import send_conference_mail + + +class Command(BaseCommand): + help = 'Send sponsor emails' + + class ScheduledJob: + scheduled_interval = timedelta(minutes=5) + internal = True + + @classmethod + def should_run(self): + return SponsorMail.objects.filter(sentat__lte=timezone.now(), sent=False).exists() + + @transaction.atomic + def handle(self, *args, **options): + for msg in SponsorMail.objects.filter(sentat__lte=timezone.now(), sent=False): + if msg.levels.exists(): + sponsors = list(Sponsor.objects.select_related('conference').filter(level__sponsormail=msg, confirmed=True)) + deststr = "sponsorship levels {}".format(", ".join(level.levelname for level in msg.levels.all())) + else: + sponsors = list(Sponsor.objects.select_related('conference').filter(sponsormail=msg)) # We include unconfirmed sponsors here intentionally! + deststr = "sponsors {}".format(", ".join(s.name for s in sponsors)) + + conference = None + for sponsor in sponsors: + conference = sponsor.conference + send_sponsor_manager_email( + sponsor, + msg.subject, + 'confsponsor/mail/sponsor_mail.txt', + { + 'body': msg.message, + 'sponsor': sponsor, + }, + ) + + # And possibly send it out to the extra address for the sponsor + if sponsor.extra_cc: + send_conference_mail(conference, + sponsor.extra_cc, + msg.subject, + 'confsponsor/mail/sponsor_mail.txt', + { + 'body': msg.message, + 'sponsor': sponsor, + }, + sender=conference.sponsoraddr, + ) + msg.sent = True + msg.save(update_fields=['sent']) + + if conference: + send_conference_sponsor_notification( + conference, + "Email sent to sponsors", + """An email was sent to sponsors of {0} + with subject '{1}'. + + It was sent to {2}. + + ------ + {3} + ------ + + To view it on the site, go to {4}/events/sponsor/admin/{5}/viewmail/{6}/""".format( + conference, + msg.subject, + deststr, + msg.message, + settings.SITEBASE, + conference.urlname, + msg.id, + ), + ) diff --git a/postgresqleu/confsponsor/migrations/0001_initial.py b/postgresqleu/confsponsor/migrations/0001_initial.py index 747beffe..8a4d40cb 100644 --- a/postgresqleu/confsponsor/migrations/0001_initial.py +++ b/postgresqleu/confsponsor/migrations/0001_initial.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals from django.db import migrations, models import postgresqleu.util.validators from django.conf import settings +from django.utils import timezone import postgresqleu.util.storage @@ -54,7 +55,7 @@ class Migration(migrations.Migration): name='SponsorMail', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('sentat', models.DateTimeField(auto_now_add=True)), + ('sentat', models.DateTimeField(default=timezone.now, verbose_name="Send at")), ('subject', models.CharField(max_length=100)), ('message', models.TextField(max_length=8000)), ('conference', models.ForeignKey(to='confreg.Conference', on_delete=models.CASCADE)), diff --git a/postgresqleu/confsponsor/migrations/0031_sponsormail_sent.py b/postgresqleu/confsponsor/migrations/0031_sponsormail_sent.py new file mode 100644 index 00000000..7c158e49 --- /dev/null +++ b/postgresqleu/confsponsor/migrations/0031_sponsormail_sent.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.11 on 2024-05-14 13:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('confsponsor', '0030_benefit_overview'), + ] + + operations = [ + migrations.AddField( + model_name='sponsormail', + name='sent', + field=models.BooleanField(default=True), + ), + migrations.AlterField( + model_name='sponsormail', + name='sent', + field=models.BooleanField(default=False), + ), + migrations.AddIndex( + model_name='sponsormail', + index=models.Index(condition=models.Q(('sent', False)), fields=['sentat'], name='confsponsor_sponsormail_unsent'), + ), + ] diff --git a/postgresqleu/confsponsor/models.py b/postgresqleu/confsponsor/models.py index d406b151..cacc5556 100644 --- a/postgresqleu/confsponsor/models.py +++ b/postgresqleu/confsponsor/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.db.models import Q from django.utils.functional import cached_property from django.core.serializers.json import DjangoJSONEncoder from django.core.validators import MinValueValidator @@ -178,7 +179,8 @@ class SponsorMail(models.Model): conference = models.ForeignKey(Conference, null=False, blank=False, on_delete=models.CASCADE) levels = models.ManyToManyField(SponsorshipLevel, blank=True) sponsors = models.ManyToManyField(Sponsor, blank=True) - sentat = models.DateTimeField(null=False, blank=False, auto_now_add=True) + sentat = models.DateTimeField(null=False, blank=False, default=timezone.now, verbose_name="Send at") + sent = models.BooleanField(null=False, blank=False, default=False) subject = models.CharField(max_length=100, null=False, blank=False) message = models.TextField(max_length=8000, null=False, blank=False) @@ -189,6 +191,13 @@ class SponsorMail(models.Model): class Meta: ordering = ('-sentat',) + indexes = [ + models.Index(name="confsponsor_sponsormail_unsent", fields=['sentat'], condition=Q(sent=False)), + ] + + @property + def future(self): + return self.sentat > timezone.now() class SponsorScanner(models.Model): diff --git a/postgresqleu/confsponsor/util.py b/postgresqleu/confsponsor/util.py index 373f9486..8f01b675 100644 --- a/postgresqleu/confsponsor/util.py +++ b/postgresqleu/confsponsor/util.py @@ -1,6 +1,7 @@ from django.db.models import Q from django.shortcuts import get_object_or_404 from django.conf import settings +from django.utils import timezone from postgresqleu.util.db import exec_to_list from postgresqleu.util.currency import format_currency @@ -151,14 +152,16 @@ def send_sponsor_manager_simple_email(sponsor, subject, message, attachments=Non attachments=attachments, sender=sponsor.conference.sponsoraddr, sendername=sponsor.conference.conferencename, - receivername='{0} {1}'.format(manager.first_name, manager.last_name) + receivername='{0} {1}'.format(manager.first_name, manager.last_name), + sendat=None, ) -def get_mails_for_sponsor(sponsor): +def get_mails_for_sponsor(sponsor, future=False): return SponsorMail.objects.filter( Q(conference=sponsor.conference), - Q(levels=sponsor.level) | Q(sponsors=sponsor) + Q(levels=sponsor.level) | Q(sponsors=sponsor), + sent=not future, ) diff --git a/postgresqleu/confsponsor/views.py b/postgresqleu/confsponsor/views.py index ab85b272..95122cc2 100644 --- a/postgresqleu/confsponsor/views.py +++ b/postgresqleu/confsponsor/views.py @@ -32,6 +32,7 @@ from postgresqleu.util.time import today_global from postgresqleu.invoices.util import InvoiceWrapper, InvoiceManager from postgresqleu.digisign.pdfutil import fill_pdf_fields, pdf_watermark_preview from postgresqleu.digisign.models import DigisignDocument, DigisignLog +from postgresqleu.scheduler.util import trigger_immediate_job_run from .models import Sponsor, SponsorshipLevel, SponsorshipBenefit from .models import SponsorClaimedBenefit, SponsorMail, SponsorshipContract, SponsorAdditionalContract @@ -1324,69 +1325,26 @@ def sponsor_admin_send_mail(request, confurlname): # Create a message record msg = SponsorMail(conference=conference, subject=form.data['subject'], - message=form.data['message']) + message=form.data['message'], + sentat=max(form.cleaned_data['sentat'], timezone.now()), # If time is set in the past, adjust to now + ) msg.save() if sendto == 'level': for level in form.data.getlist('levels'): msg.levels.add(level) - sponsors = Sponsor.objects.filter(conference=conference, level__in=form.data.getlist('levels'), confirmed=True) deststr = "sponsorship levels {0}".format(", ".join([level.levelname for level in msg.levels.all()])) else: for s in form.data.getlist('sponsors'): msg.sponsors.add(s) - sponsors = Sponsor.objects.filter(conference=conference, pk__in=form.data.getlist('sponsors')) deststr = "sponsors {0}".format(", ".join([s.name for s in msg.sponsors.all()])) msg.save() - # Now also send the email out to the *current* subscribers - for sponsor in sponsors: - send_sponsor_manager_email( - sponsor, - msg.subject, - 'confsponsor/mail/sponsor_mail.txt', - { - 'body': msg.message, - 'sponsor': sponsor, - }, - ) - - # And possibly send it out to the extra address for the sponsor - if sponsor.extra_cc: - send_conference_mail(conference, - sponsor.extra_cc, - msg.subject, - 'confsponsor/mail/sponsor_mail.txt', - { - 'body': msg.message, - 'sponsor': sponsor, - }, - sender=conference.sponsoraddr, - ) - - send_conference_sponsor_notification( - conference, - "Email sent to sponsors", - """An email was sent to sponsors of {0} -with subject '{1}'. - -It was sent to {2}. - ------- -{3} ------- - -To view it on the site, go to {4}/events/sponsor/admin/{5}/viewmail/{6}/""".format( - conference, - msg.subject, - deststr, - msg.message, - settings.SITEBASE, - conference.urlname, - msg.id, - ), - ) + if msg.sentat > timezone.now(): + messages.info(request, "Email scheduled for later sending to sponsors") + else: + trigger_immediate_job_run('sponsor_send_emails') + messages.info(request, "Email sent to sponsors, and added to their sponsor pages") - messages.info(request, "Email sent to %s sponsors, and added to their sponsor pages" % len(sponsors)) return HttpResponseRedirect("../") else: if sendto == 'sponsor' and request.GET.get('preselectsponsors', ''): diff --git a/template/confsponsor/admin_dashboard.html b/template/confsponsor/admin_dashboard.html index 834ea7a7..92268139 100644 --- a/template/confsponsor/admin_dashboard.html +++ b/template/confsponsor/admin_dashboard.html @@ -124,7 +124,7 @@ The following emails have been sent to sponsors so far: {%for m in mails%} - {{m.sentat|date:"Y-m-d H:i"}} + {{m.sentat|date:"Y-m-d H:i"}}{%if m.future%} (Not sent yet!){%endif%} {{m.subject}} {{m.levels.all|join:", "}} {{m.sponsors.all|join:", "}} diff --git a/template/confsponsor/sent_mail.html b/template/confsponsor/sent_mail.html index 7312a793..9f2f7072 100644 --- a/template/confsponsor/sent_mail.html +++ b/template/confsponsor/sent_mail.html @@ -16,7 +16,7 @@ tr.error { Date: - {{mail.sentat|date:"Y-m-d H:i:s"}} + {{mail.sentat|date:"Y-m-d H:i"}}{%if mail.future%} (Not sent yet!){%endif%} Subject: -- 2.39.5