Skip to content

Commit bac2ce4

Browse files
committed
WIP: Needs some cleanup and release notes and further testing
1 parent edf12e7 commit bac2ce4

File tree

6 files changed

+280
-31
lines changed

6 files changed

+280
-31
lines changed

django_nyt/management/commands/notifymail.py

+65-18
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
import sys
55
import time
66
from datetime import datetime
7+
from datetime import timedelta
78

89
from django.conf import settings
910
from django.contrib.sites.models import Site
1011
from django.core import mail
1112
from django.core.management.base import BaseCommand
13+
from django.db.models import Q
1214
from django.template.loader import render_to_string
15+
from django.utils import timezone
1316
from django.utils.translation import activate
1417
from django.utils.translation import deactivate
1518

@@ -75,7 +78,7 @@ def add_arguments(self, parser):
7578
"--no-sys-exit",
7679
action="store_true",
7780
dest="no_sys_exit",
78-
help="Skip sys-exit after forking daemon (for testing purposes)",
81+
help="Skip sys-exit after forking daemon (mainly for testing purposes)",
7982
)
8083
parser.add_argument(
8184
"--daemon-sleep-interval",
@@ -84,6 +87,12 @@ def add_arguments(self, parser):
8487
help="Minimum sleep between each polling of the database.",
8588
default=SLEEP_TIME,
8689
)
90+
parser.add_argument(
91+
"--now",
92+
action="store",
93+
dest="now",
94+
help="Simulate when to start sending from (mainly for testing purposes)",
95+
)
8796

8897
def _render_and_send(
8998
self, template_name, template_subject_name, context, connection
@@ -168,7 +177,13 @@ def handle(self, *args, **options): # noqa: max-complexity=12
168177
connection = mail.get_connection()
169178

170179
if cron:
171-
self.send_mails(connection)
180+
if self.options.get("now"):
181+
now = self.options.get("now")
182+
self.logger.info(f"using now: {now}")
183+
else:
184+
now = timezone.now()
185+
186+
self.send_mails(connection, now)
172187
return
173188

174189
if not daemon:
@@ -183,34 +198,33 @@ def handle(self, *args, **options): # noqa: max-complexity=12
183198

184199
def send_loop(self, connection, sleep_time):
185200

186-
# This could be /improved by looking up the last notified person
187201
last_sent = None
188202

189203
while True:
190204

191205
started_sending_at = datetime.now()
192206
self.logger.info("Starting send loop at %s" % str(started_sending_at))
207+
208+
# When we are looping, we don't want to iterate over user_settings that have
209+
# an interval greater than our last_sent marker.
193210
if last_sent:
194211
user_settings = models.Settings.objects.filter(
195212
interval__lte=((started_sending_at - last_sent).seconds // 60) // 60
196213
).order_by("user")
214+
now = self.options.get("now") or timezone.now()
197215
else:
216+
# TOD: This isn't perfect. If we are simulating a "now", we should also make a
217+
# step-wise approach to incrementing it.
218+
now = timezone.now()
198219
user_settings = None
199220

200221
self.send_mails(
201-
connection, last_sent=last_sent, user_settings=user_settings
222+
connection, now, last_sent=last_sent, user_settings=user_settings
202223
)
203224

204225
connection.close()
205226
last_sent = datetime.now()
206-
elapsed_seconds = (last_sent - started_sending_at).seconds
207-
time.sleep(
208-
max(
209-
(min(app_settings.NYT_INTERVALS)[0] - elapsed_seconds) * 60,
210-
sleep_time,
211-
0,
212-
)
213-
)
227+
time.sleep(sleep_time)
214228

215229
def _send_with_retry(
216230
self, template_name, subject_template_name, context, connection, setting
@@ -239,6 +253,8 @@ def _send_with_retry(
239253
for n in notifications:
240254
n.is_emailed = True
241255
n.save()
256+
n.subscription.last_sent = timezone.now()
257+
n.subscription.save()
242258
break
243259
except smtplib.SMTPSenderRefused:
244260
self.logger.error(
@@ -262,14 +278,16 @@ def _send_with_retry(
262278
)
263279
raise
264280

265-
def send_mails(self, connection, last_sent=None, user_settings=None):
281+
def send_mails( # noqa: max-complexity=12
282+
self, connection, now, last_sent=None, user_settings=None
283+
):
266284
"""
267285
Does the lookups and sends out email digests to anyone who has them due.
268286
Since the system may have different templates depending on which notification is being sent,
269287
we will generate a call for each template.
270288
"""
271289

272-
self.logger.debug("Entering send_mails()")
290+
self.logger.debug(f"Entering send_mails(now={now}, last_sent={last_sent}, ...)")
273291

274292
connection.open()
275293

@@ -300,6 +318,8 @@ def send_mails(self, connection, last_sent=None, user_settings=None):
300318
# this job is running in parallel with another unfinished process. Or a global lock.
301319
for setting in user_settings:
302320

321+
threshold = now - timedelta(minutes=setting.interval)
322+
303323
context = {
304324
"user": None,
305325
"username": None,
@@ -319,9 +339,22 @@ def send_mails(self, connection, last_sent=None, user_settings=None):
319339

320340
emails_per_template = {}
321341

342+
filter_qs = Q()
343+
344+
if setting.interval:
345+
346+
# How much time must have passed since either
347+
# a) the subscription was created (in case nothing hast been sent)
348+
# b) the subscription was last active (in case something has been sent)
349+
filter_qs = Q(Q(created__lte=threshold) & Q(last_sent=None)) | Q(
350+
Q(last_sent__lte=threshold) & Q(latest__is_emailed=False)
351+
)
352+
353+
# The ordering by notification_type__key is because we want a predictable
354+
# order currently just for testing purposes.
322355
for subscription in setting.subscription_set.filter(
323-
send_emails=True, latest__is_emailed=False
324-
):
356+
filter_qs, send_emails=True
357+
).order_by("notification_type__key"):
325358
try:
326359
template_name = (
327360
subscription.notification_type.get_email_template_name()
@@ -338,15 +371,29 @@ def send_mails(self, connection, last_sent=None, user_settings=None):
338371
emails_per_template.setdefault(
339372
(template_name, subject_template_name), []
340373
)
341-
emails_per_template[(template_name, subject_template_name)].append(
342-
subscription.latest
374+
375+
# If there is an interval, we use the threshold.
376+
if setting.interval:
377+
filter_kwargs = {"created__lte": threshold}
378+
else:
379+
filter_kwargs = {}
380+
emails_per_template[(template_name, subject_template_name)] += list(
381+
subscription.notification_set.filter(
382+
is_emailed=False, **filter_kwargs
383+
),
343384
)
385+
print("found:")
386+
print(emails_per_template[(template_name, subject_template_name)])
344387

345388
# Send the prepared template names, subjects and context to the user
346389
for (
347390
template_name,
348391
subject_template_name,
349392
), notifications in emails_per_template.items():
393+
394+
if not notifications:
395+
continue
396+
350397
context["notifications"] = notifications
351398

352399
self._send_with_retry(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Generated by Django 4.2.1 on 2024-01-06 22:12
2+
import django.utils.timezone
3+
from django.db import migrations
4+
from django.db import models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("django_nyt", "0009_alter_notification_subscription_and_more"),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name="settings",
16+
name="created",
17+
field=models.DateTimeField(
18+
auto_now_add=True,
19+
default=django.utils.timezone.now,
20+
verbose_name="created",
21+
),
22+
preserve_default=False,
23+
),
24+
migrations.AddField(
25+
model_name="settings",
26+
name="modified",
27+
field=models.DateTimeField(auto_now=True, verbose_name="modified"),
28+
),
29+
migrations.AddField(
30+
model_name="subscription",
31+
name="created",
32+
field=models.DateTimeField(
33+
auto_now_add=True,
34+
default=django.utils.timezone.now,
35+
verbose_name="created",
36+
),
37+
preserve_default=False,
38+
),
39+
migrations.AddField(
40+
model_name="subscription",
41+
name="last_sent",
42+
field=models.DateTimeField(blank=True, null=True, verbose_name="last sent"),
43+
),
44+
migrations.AddField(
45+
model_name="subscription",
46+
name="modified",
47+
field=models.DateTimeField(auto_now=True, verbose_name="modified"),
48+
),
49+
migrations.AlterField(
50+
model_name="notification",
51+
name="created",
52+
field=models.DateTimeField(auto_now_add=True, verbose_name="created"),
53+
),
54+
migrations.AlterField(
55+
model_name="notification",
56+
name="is_emailed",
57+
field=models.BooleanField(default=False, verbose_name="mail sent"),
58+
),
59+
migrations.AlterField(
60+
model_name="notification",
61+
name="is_viewed",
62+
field=models.BooleanField(
63+
default=False, verbose_name="notification viewed"
64+
),
65+
),
66+
migrations.AlterField(
67+
model_name="notification",
68+
name="modified",
69+
field=models.DateTimeField(auto_now=True, verbose_name="modified"),
70+
),
71+
migrations.AlterField(
72+
model_name="notification",
73+
name="url",
74+
field=models.CharField(
75+
blank=True,
76+
max_length=2000,
77+
null=True,
78+
verbose_name="link for notification",
79+
),
80+
),
81+
migrations.AlterField(
82+
model_name="settings",
83+
name="interval",
84+
field=models.SmallIntegerField(
85+
choices=[(0, "instantly"), (1380, "daily"), (9660, "weekly")],
86+
default=0,
87+
help_text="interval in minutes (0=instant, 60=notify once per hour)",
88+
verbose_name="interval",
89+
),
90+
),
91+
]

0 commit comments

Comments
 (0)