osiris/tracker/views.py

135 lines
4.9 KiB
Python

from datetime import timedelta
from typing import Optional
from django.conf import settings
from django.contrib import messages
from django.contrib.admin.views.decorators import staff_member_required
from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils import timezone
from django.utils.dateparse import parse_date
from tracker.api import build_day, pulse_trend
from tracker.models import DailyNote, DoseLog, GlobalNote, PulseReading, ScheduledDose
CHART_WIDTH = 320
CHART_HEIGHT = 140
CHART_PADDING = 24
def build_chart(readings: list[PulseReading]) -> Optional[dict]:
"""Lay the readings out as SVG coordinates, so the page needs no JavaScript."""
if len(readings) < 2:
return None
bpms = [r.bpm for r in readings]
low, high = min(bpms), max(bpms)
# A flat line would divide by zero, and a narrow range exaggerates noise.
span = max(high - low, 10)
mid = (high + low) / 2
low, high = mid - span / 2, mid + span / 2
first_day = readings[0].date
total_days = max((readings[-1].date - first_day).days, 1)
inner_width = CHART_WIDTH - 2 * CHART_PADDING
inner_height = CHART_HEIGHT - 2 * CHART_PADDING
points = []
for reading in readings:
x = CHART_PADDING + (reading.date - first_day).days / total_days * inner_width
y = CHART_PADDING + (high - reading.bpm) / (high - low) * inner_height
points.append({"x": round(x, 1), "y": round(y, 1), "reading": reading})
return {
"points": points,
"polyline": " ".join(f"{p['x']},{p['y']}" for p in points),
"low": round(low),
"high": round(high),
"average": round(sum(bpms) / len(bpms), 1),
"trend": pulse_trend(readings),
"width": CHART_WIDTH,
"height": CHART_HEIGHT,
"first_date": first_day,
"last_date": readings[-1].date,
}
@staff_member_required
def today(request):
"""The main screen: tick off the day's doses and record the pulse."""
day = parse_date(request.GET.get("date", "")) or timezone.localdate()
if request.method == "POST":
day = parse_date(request.POST.get("date", "")) or timezone.localdate()
# The shared note has its own form and save button, so that editing it
# while looking at a past day cannot touch that day's records.
if "global_note" in request.POST:
note = GlobalNote.load()
note.body = request.POST["global_note"].strip()
note.save(update_fields=["body", "updated_at"])
messages.success(request, "Shared note saved.")
return redirect(f"{reverse('tracker:today')}?date={day.isoformat()}")
doses = ScheduledDose.objects.filter(is_active=True, medication__is_active=True)
for dose in doses:
taken = f"dose-{dose.id}" in request.POST
log, _ = DoseLog.objects.get_or_create(
scheduled_dose=dose, date=day, defaults={"taken": taken}
)
log.taken = taken
log.save()
bpm = (request.POST.get("bpm") or "").strip()
if bpm:
try:
PulseReading.objects.update_or_create(
date=day, defaults={"bpm": int(bpm)}
)
except ValueError:
messages.error(request, f"'{bpm}' is not a valid pulse.")
else:
# Clearing the field removes the reading for that day.
PulseReading.objects.filter(date=day).delete()
note_body = (request.POST.get("note") or "").strip()
if note_body:
DailyNote.objects.update_or_create(date=day, defaults={"body": note_body})
else:
DailyNote.objects.filter(date=day).delete()
messages.success(request, f"Saved for {day:%A %d %B}.")
return redirect(f"{reverse('tracker:today')}?date={day.isoformat()}")
readings = list(
PulseReading.objects.filter(
date__gte=timezone.localdate() - timedelta(days=90)
).order_by("date")
)
return render(
request,
"tracker/today.html",
{
"title": "Today",
"day": build_day(day),
"is_today": day == timezone.localdate(),
"previous_date": day - timedelta(days=1),
"next_date": day + timedelta(days=1),
"chart": build_chart(readings),
"reading_count": len(readings),
"global_note": GlobalNote.load(),
# Empty when push is not configured; the card then says so rather than
# vanishing, because a missing card looks like a bug in the page.
"vapid_public_key": settings.VAPID_PUBLIC_KEY,
"doses_with_reminders": ScheduledDose.objects.filter(
is_active=True,
medication__is_active=True,
reminder_time__isnull=False,
).count(),
"subscribed_devices": request.user.push_subscriptions.count(),
},
)