from datetime import timedelta from typing import Optional 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 DoseLog, 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() 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() 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), }, )