117 lines
4.0 KiB
Python
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}"))
|