Add ability to schedule confsponsor emails for the future
authorMagnus Hagander <magnus@hagander.net>
Tue, 14 May 2024 14:17:36 +0000 (16:17 +0200)
committerMagnus Hagander <magnus@hagander.net>
Tue, 14 May 2024 14:35:17 +0000 (16:35 +0200)
Start proper work on #90, but is not complete yet

postgresqleu/confsponsor/forms.py
postgresqleu/confsponsor/management/commands/sponsor_send_emails.py [new file with mode: 0644]
postgresqleu/confsponsor/migrations/0001_initial.py
postgresqleu/confsponsor/migrations/0031_sponsormail_sent.py [new file with mode: 0644]
postgresqleu/confsponsor/models.py
postgresqleu/confsponsor/util.py
postgresqleu/confsponsor/views.py
template/confsponsor/admin_dashboard.html
template/confsponsor/sent_mail.html

index 504797d2d7235e69952799f5f96c2c55cca70125..2738c86813d7a6f341cca9bee05f41384ab937f9 100644 (file)
@@ -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 (file)
index 0000000..c028fbd
--- /dev/null
@@ -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,
+                    ),
+                )
index 747beffe6fd7b962b1967915202b8087cd13abb4..8a4d40cb56e73c25d7ca0ca89011ba033c2b02fa 100644 (file)
@@ -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 (file)
index 0000000..7c158e4
--- /dev/null
@@ -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'),
+        ),
+    ]
index d406b1512e7ec7e3bafb28f340ebc3c08ab9f328..cacc55561062017012261d9abcdcc9ba96a9afe3 100644 (file)
@@ -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):
index 373f9486ea0090ebae55cdb0bd88ec8ee7cfdf07..8f01b6755eea4999394f6c3df8890bbb4ecb6397 100644 (file)
@@ -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,
     )
 
 
index ab85b2726ebbd70cd58535e966cf347876390209..95122cc2bc8ed03513e8fda4c6221e72210789d6 100644 (file)
@@ -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', ''):
index 834ea7a77ed6d428624636cc1f3a0526ee5f4d26..92268139736531575a4b29e1aa22f098e002bc19 100644 (file)
@@ -124,7 +124,7 @@ The following emails have been sent to sponsors so far:
   </tr>
 {%for m in mails%}
   <tr>
-    <td>{{m.sentat|date:"Y-m-d H:i"}}</td>
+    <td>{{m.sentat|date:"Y-m-d H:i"}}{%if m.future%}<span title="E-mail has not been sent yet, but is in the queue to be automatically flushed at the send date"> (Not sent yet!)</span>{%endif%}</td>
     <td><a href="viewmail/{{m.id}}/">{{m.subject}}</a></td>
     <td>{{m.levels.all|join:", "}}</td>
     <td>{{m.sponsors.all|join:", "}}</td>
index 7312a7934eb3e1ddabe3b5decbf76ff3a1663fda..9f2f7072a5d34eafead4b82a27ae7fc2d6b95d6e 100644 (file)
@@ -16,7 +16,7 @@ tr.error {
  </tr>
  <tr>
    <th>Date:</th>
-   <td>{{mail.sentat|date:"Y-m-d H:i:s"}}</td>
+   <td>{{mail.sentat|date:"Y-m-d H:i"}}{%if mail.future%}<span title="E-mail has not been sent yet, but is in the queue to be automatically flushed at the send date"> (Not sent yet!)</span>{%endif%}</td>
  </tr>
  <tr>
    <th>Subject:</th>