osiris/tracker/management/commands/send_reminders.py

117 lines
4.0 KiB
Python

from collections import defaultdict
from datetime import datetime, timedelta
from django.core.management.base import BaseCommand
from django.db import transaction
from django.urls import reverse
from django.utils import timezone
from accounts.models import User
from tracker.models import DoseLog, ReminderSent, ScheduledDose
from tracker.push import notify_user, push_is_configured
class Command(BaseCommand):
help = (
"Push a reminder for any dose that is due and not yet ticked off. "
"Meant to run from cron every few minutes."
)
def add_arguments(self, parser):
parser.add_argument(
"--grace-minutes",
type=int,
default=120,
help=(
"Ignore reminders older than this. Stops a server that was asleep "
"or down from firing a 08:00 reminder at 23:00 (default: 120)."
),
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Report what would be sent without sending or recording anything.",
)
def handle(self, *args, **options):
if not push_is_configured():
self.stderr.write(
self.style.ERROR(
"VAPID keys are not set — run generate_vapid_keys first."
)
)
return
now = timezone.localtime()
today = now.date()
grace = timedelta(minutes=options["grace_minutes"])
already_taken = set(
DoseLog.objects.filter(date=today, taken=True).values_list(
"scheduled_dose_id", flat=True
)
)
already_reminded = set(
ReminderSent.objects.filter(date=today).values_list(
"scheduled_dose_id", flat=True
)
)
due = defaultdict(list)
for dose in ScheduledDose.objects.filter(
is_active=True,
medication__is_active=True,
reminder_time__isnull=False,
).select_related("medication"):
if dose.id in already_taken or dose.id in already_reminded:
continue
due_at = timezone.make_aware(datetime.combine(today, dose.reminder_time))
if not (due_at <= now <= due_at + grace):
continue
due[dose.reminder_time].append(dose)
if not due:
self.stdout.write("Nothing due.")
return
recipients = User.objects.filter(
is_active=True, push_subscriptions__isnull=False
).distinct()
if not recipients:
self.stdout.write("Doses are due, but no device has enabled reminders.")
return
for reminder_time, doses in sorted(due.items()):
label = doses[0].get_time_of_day_display()
body = ", ".join(f"{d.medication.name} {d.amount}" for d in doses)
payload = {
"title": f"Osiris — {label.lower()} medication",
"body": body,
"url": reverse("tracker:today"),
# Same tag replaces rather than stacks a repeat notification.
"tag": f"dose-{today.isoformat()}-{reminder_time}",
}
if options["dry_run"]:
self.stdout.write(f"[dry-run] {reminder_time}{payload['body']}")
continue
delivered = sum(notify_user(user, payload) for user in recipients)
if delivered:
with transaction.atomic():
ReminderSent.objects.bulk_create(
[
ReminderSent(scheduled_dose=dose, date=today)
for dose in doses
],
ignore_conflicts=True,
)
self.stdout.write(
self.style.SUCCESS(f"Reminded ({body}) on {delivered} device(s).")
)
else:
# Nothing recorded, so the next run tries again.
self.stderr.write(self.style.WARNING(f"Delivery failed for: {body}"))