feat: add recap page
This commit is contained in:
parent
0cfe4c7108
commit
63a5a69c67
129
templates/tracker/recap.html
Normal file
129
templates/tracker/recap.html
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
{% extends "admin/base_site.html" %}
|
||||||
|
|
||||||
|
{% block title %}Osiris — Recap{% endblock %}
|
||||||
|
|
||||||
|
{% block extrastyle %}
|
||||||
|
{{ block.super }}
|
||||||
|
<style>
|
||||||
|
/* Mobile-first, same shell as the Today page. */
|
||||||
|
#content { padding: 12px; max-width: 640px; margin: 0 auto; }
|
||||||
|
.osiris-card {
|
||||||
|
background: var(--body-bg);
|
||||||
|
border: 1px solid var(--hairline-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.osiris-recap { width: 100%; border-collapse: collapse; }
|
||||||
|
.osiris-recap td, .osiris-recap th {
|
||||||
|
padding: 10px 4px;
|
||||||
|
border-bottom: 1px solid var(--hairline-color);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.osiris-recap tr:last-child td { border-bottom: none; }
|
||||||
|
.osiris-recap .status { text-align: center; width: 4em; }
|
||||||
|
.osiris-recap .bpm { text-align: right; width: 5em; }
|
||||||
|
.osiris-recap .incomplete { color: var(--error-fg, #ba2121); font-weight: 600; }
|
||||||
|
.osiris-recap a { font-weight: 600; }
|
||||||
|
.osiris-note-entry { padding: 10px 0; border-bottom: 1px solid var(--hairline-color); }
|
||||||
|
.osiris-note-entry:last-child { border-bottom: none; }
|
||||||
|
.osiris-note-entry p { margin: 4px 0 0; white-space: pre-line; }
|
||||||
|
.dose-detail { color: var(--body-quiet-color); font-size: 0.85rem; }
|
||||||
|
.osiris-stats { display: flex; gap: 18px; flex-wrap: wrap; margin-top: 12px; }
|
||||||
|
.osiris-stats div { font-size: 0.85rem; color: var(--body-quiet-color); }
|
||||||
|
.osiris-stats strong { display: block; font-size: 1.15rem; color: var(--body-fg); }
|
||||||
|
.osiris-chart { width: 100%; height: auto; overflow: visible; }
|
||||||
|
.osiris-empty { color: var(--body-quiet-color); font-style: italic; }
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="osiris-card">
|
||||||
|
<h2 style="margin-top:0">Doses — last 14 days</h2>
|
||||||
|
<table class="osiris-recap">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Day</th>
|
||||||
|
<th class="status">Given</th>
|
||||||
|
<th class="bpm">Pulse</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for row in recap_days %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'tracker:today' %}?date={{ row.date|date:'Y-m-d' }}">{{ row.date|date:"D j M" }}</a>
|
||||||
|
{% if forloop.first %}<span class="dose-detail">today</span>{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="status">
|
||||||
|
{% if row.complete %}
|
||||||
|
✅
|
||||||
|
{% else %}
|
||||||
|
<span class="incomplete">{{ row.taken }}/{{ row.expected }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="bpm">{% if row.bpm %}{{ row.bpm }}{% else %}<span class="dose-detail">—</span>{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="dose-detail" style="margin-bottom:0">
|
||||||
|
✅ means every active dose was ticked off that day; otherwise the count given
|
||||||
|
out of expected. Tap a day to fill it in.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="osiris-card">
|
||||||
|
<h2 style="margin-top:0">Pulse — last 90 days</h2>
|
||||||
|
{% if chart %}
|
||||||
|
<svg class="osiris-chart" viewBox="0 0 {{ chart.width }} {{ chart.height }}" role="img"
|
||||||
|
aria-label="Pulse from {{ chart.first_date }} to {{ chart.last_date }}">
|
||||||
|
<line x1="24" y1="24" x2="{{ chart.width|add:'-24' }}" y2="24"
|
||||||
|
stroke="currentColor" stroke-width="0.5" opacity="0.2"/>
|
||||||
|
<line x1="24" y1="{{ chart.height|add:'-24' }}" x2="{{ chart.width|add:'-24' }}"
|
||||||
|
y2="{{ chart.height|add:'-24' }}" stroke="currentColor" stroke-width="0.5" opacity="0.2"/>
|
||||||
|
<polyline points="{{ chart.polyline }}" fill="none" stroke="#79aec8" stroke-width="2"
|
||||||
|
stroke-linejoin="round" stroke-linecap="round" vector-effect="non-scaling-stroke"/>
|
||||||
|
{% for point in chart.points %}
|
||||||
|
<circle cx="{{ point.x }}" cy="{{ point.y }}" r="2" fill="#417690">
|
||||||
|
<title>{{ point.reading.date }} — {{ point.reading.bpm }} bpm</title>
|
||||||
|
</circle>
|
||||||
|
{% endfor %}
|
||||||
|
<text x="2" y="27" font-size="8" fill="currentColor" opacity="0.6">{{ chart.high }}</text>
|
||||||
|
<text x="2" y="{{ chart.height|add:'-21' }}" font-size="8" fill="currentColor" opacity="0.6">{{ chart.low }}</text>
|
||||||
|
</svg>
|
||||||
|
<div class="osiris-stats">
|
||||||
|
<div>Average<strong>{{ chart.average }} bpm</strong></div>
|
||||||
|
<div>Range<strong>{{ chart.low }}–{{ chart.high }}</strong></div>
|
||||||
|
<div>Readings<strong>{{ reading_count }}</strong></div>
|
||||||
|
<div>
|
||||||
|
Trend
|
||||||
|
<strong>
|
||||||
|
{% if chart.trend > 0 %}↑ +{{ chart.trend }}{% elif chart.trend < 0 %}↓ {{ chart.trend }}{% else %}→ steady{% endif %}
|
||||||
|
{% if chart.trend %}<span style="font-size:0.75rem">bpm/day</span>{% endif %}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="osiris-empty">Record the pulse on at least two days to see the graph.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="osiris-card">
|
||||||
|
<h2 style="margin-top:0">Daily notes</h2>
|
||||||
|
{% for note in daily_notes %}
|
||||||
|
<div class="osiris-note-entry">
|
||||||
|
<a href="{% url 'tracker:today' %}?date={{ note.date|date:'Y-m-d' }}" class="dose-detail">
|
||||||
|
{{ note.date|date:"l j F" }}
|
||||||
|
</a>
|
||||||
|
<p>{{ note.body }}</p>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<p class="osiris-empty">No notes yet — write one on the Today page.</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="text-align:center"><a href="{% url 'tracker:today' %}">← Back to today</a></p>
|
||||||
|
{% endblock %}
|
||||||
@ -54,10 +54,6 @@
|
|||||||
border: none; border-radius: 10px; cursor: pointer;
|
border: none; border-radius: 10px; cursor: pointer;
|
||||||
background: var(--button-bg); color: var(--button-fg);
|
background: var(--button-bg); color: var(--button-fg);
|
||||||
}
|
}
|
||||||
.osiris-stats { display: flex; gap: 18px; flex-wrap: wrap; margin-top: 12px; }
|
|
||||||
.osiris-stats div { font-size: 0.85rem; color: var(--body-quiet-color); }
|
|
||||||
.osiris-stats strong { display: block; font-size: 1.15rem; color: var(--body-fg); }
|
|
||||||
.osiris-chart { width: 100%; height: auto; overflow: visible; }
|
|
||||||
.osiris-empty { color: var(--body-quiet-color); font-style: italic; }
|
.osiris-empty { color: var(--body-quiet-color); font-style: italic; }
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -134,43 +130,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="osiris-card" style="margin-top:16px">
|
<div class="osiris-card" id="reminders" style="margin-top:16px">
|
||||||
<h2 style="margin-top:0">Pulse — last 90 days</h2>
|
|
||||||
{% if chart %}
|
|
||||||
<svg class="osiris-chart" viewBox="0 0 {{ chart.width }} {{ chart.height }}" role="img"
|
|
||||||
aria-label="Pulse from {{ chart.first_date }} to {{ chart.last_date }}">
|
|
||||||
<line x1="24" y1="24" x2="{{ chart.width|add:'-24' }}" y2="24"
|
|
||||||
stroke="currentColor" stroke-width="0.5" opacity="0.2"/>
|
|
||||||
<line x1="24" y1="{{ chart.height|add:'-24' }}" x2="{{ chart.width|add:'-24' }}"
|
|
||||||
y2="{{ chart.height|add:'-24' }}" stroke="currentColor" stroke-width="0.5" opacity="0.2"/>
|
|
||||||
<polyline points="{{ chart.polyline }}" fill="none" stroke="#79aec8" stroke-width="2"
|
|
||||||
stroke-linejoin="round" stroke-linecap="round" vector-effect="non-scaling-stroke"/>
|
|
||||||
{% for point in chart.points %}
|
|
||||||
<circle cx="{{ point.x }}" cy="{{ point.y }}" r="2" fill="#417690">
|
|
||||||
<title>{{ point.reading.date }} — {{ point.reading.bpm }} bpm</title>
|
|
||||||
</circle>
|
|
||||||
{% endfor %}
|
|
||||||
<text x="2" y="27" font-size="8" fill="currentColor" opacity="0.6">{{ chart.high }}</text>
|
|
||||||
<text x="2" y="{{ chart.height|add:'-21' }}" font-size="8" fill="currentColor" opacity="0.6">{{ chart.low }}</text>
|
|
||||||
</svg>
|
|
||||||
<div class="osiris-stats">
|
|
||||||
<div>Average<strong>{{ chart.average }} bpm</strong></div>
|
|
||||||
<div>Range<strong>{{ chart.low }}–{{ chart.high }}</strong></div>
|
|
||||||
<div>Readings<strong>{{ reading_count }}</strong></div>
|
|
||||||
<div>
|
|
||||||
Trend
|
|
||||||
<strong>
|
|
||||||
{% if chart.trend > 0 %}↑ +{{ chart.trend }}{% elif chart.trend < 0 %}↓ {{ chart.trend }}{% else %}→ steady{% endif %}
|
|
||||||
{% if chart.trend %}<span style="font-size:0.75rem">bpm/day</span>{% endif %}
|
|
||||||
</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<p class="osiris-empty">Record the pulse on at least two days to see the graph.</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="osiris-card" id="reminders">
|
|
||||||
<h2 style="margin-top:0">Reminders</h2>
|
<h2 style="margin-top:0">Reminders</h2>
|
||||||
<p class="dose-detail" id="reminder-status">Checking…</p>
|
<p class="dose-detail" id="reminder-status">Checking…</p>
|
||||||
<div style="display:flex; gap:8px; flex-wrap:wrap">
|
<div style="display:flex; gap:8px; flex-wrap:wrap">
|
||||||
@ -200,7 +160,10 @@
|
|||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p style="text-align:center"><a href="{% url 'admin:index' %}">Manage medication & history →</a></p>
|
<p style="text-align:center">
|
||||||
|
<a href="{% url 'tracker:recap' %}">Recap: doses, pulse & notes →</a><br>
|
||||||
|
<a href="{% url 'admin:index' %}">Manage medication & history →</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ from datetime import date as date_type
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django.db.models import Avg, Max, Min
|
from django.db.models import Avg, Count, Max, Min
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -81,6 +81,17 @@ class DayOut(Schema):
|
|||||||
note: Optional[DailyNoteOut]
|
note: Optional[DailyNoteOut]
|
||||||
|
|
||||||
|
|
||||||
|
class RecapDayOut(Schema):
|
||||||
|
"""One line of the recap: did every dose go out that day, and what was the pulse."""
|
||||||
|
|
||||||
|
date: date_type
|
||||||
|
taken: int
|
||||||
|
expected: int
|
||||||
|
complete: bool
|
||||||
|
bpm: Optional[int]
|
||||||
|
note: str
|
||||||
|
|
||||||
|
|
||||||
class DoseLogIn(Schema):
|
class DoseLogIn(Schema):
|
||||||
scheduled_dose_id: int
|
scheduled_dose_id: int
|
||||||
date: Optional[date_type] = None
|
date: Optional[date_type] = None
|
||||||
@ -147,6 +158,51 @@ def build_day(day: date_type) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_recap(days: int) -> list[dict]:
|
||||||
|
"""One line per day, newest first: dose completeness, pulse and note.
|
||||||
|
|
||||||
|
"Complete" is measured against the doses that are active now, not the plan
|
||||||
|
as it stood on that day — the point is spotting gaps, not auditing history.
|
||||||
|
"""
|
||||||
|
today = timezone.localdate()
|
||||||
|
since = today - timedelta(days=days - 1)
|
||||||
|
|
||||||
|
expected = ScheduledDose.objects.filter(
|
||||||
|
is_active=True, medication__is_active=True
|
||||||
|
).count()
|
||||||
|
|
||||||
|
taken_by_day = {
|
||||||
|
row["date"]: row["taken"]
|
||||||
|
for row in DoseLog.objects.filter(
|
||||||
|
date__gte=since,
|
||||||
|
taken=True,
|
||||||
|
scheduled_dose__is_active=True,
|
||||||
|
scheduled_dose__medication__is_active=True,
|
||||||
|
)
|
||||||
|
.values("date")
|
||||||
|
.annotate(taken=Count("id"))
|
||||||
|
}
|
||||||
|
pulse_by_day = {
|
||||||
|
reading.date: reading.bpm
|
||||||
|
for reading in PulseReading.objects.filter(date__gte=since)
|
||||||
|
}
|
||||||
|
note_by_day = {
|
||||||
|
note.date: note.body for note in DailyNote.objects.filter(date__gte=since)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"date": day,
|
||||||
|
"taken": taken_by_day.get(day, 0),
|
||||||
|
"expected": expected,
|
||||||
|
"complete": expected > 0 and taken_by_day.get(day, 0) == expected,
|
||||||
|
"bpm": pulse_by_day.get(day),
|
||||||
|
"note": note_by_day.get(day, ""),
|
||||||
|
}
|
||||||
|
for day in (today - timedelta(days=offset) for offset in range(days))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def pulse_trend(readings: list[PulseReading]) -> Optional[float]:
|
def pulse_trend(readings: list[PulseReading]) -> Optional[float]:
|
||||||
"""Least-squares slope in bpm/day: ~0 means stable, positive means climbing."""
|
"""Least-squares slope in bpm/day: ~0 means stable, positive means climbing."""
|
||||||
if len(readings) < 2:
|
if len(readings) < 2:
|
||||||
@ -202,6 +258,12 @@ def get_day(request: HttpRequest, date: Optional[date_type] = None):
|
|||||||
return build_day(date or timezone.localdate())
|
return build_day(date or timezone.localdate())
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/recap", response=list[RecapDayOut])
|
||||||
|
def get_recap(request: HttpRequest, days: int = 14):
|
||||||
|
"""Day-by-day recap, newest first: were all doses given, pulse and note."""
|
||||||
|
return build_recap(days)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/doses", response=DayOut)
|
@router.post("/doses", response=DayOut)
|
||||||
def log_dose(request: HttpRequest, payload: DoseLogIn):
|
def log_dose(request: HttpRequest, payload: DoseLogIn):
|
||||||
"""Tick a dose off (or untick it). Idempotent per (dose, day)."""
|
"""Tick a dose off (or untick it). Idempotent per (dose, day)."""
|
||||||
|
|||||||
@ -347,19 +347,73 @@ class TodayPageTests(TrackerTestCase):
|
|||||||
self.assertContains(response, "More playful than yesterday.")
|
self.assertContains(response, "More playful than yesterday.")
|
||||||
self.assertContains(response, "Vet on Friday.")
|
self.assertContains(response, "Vet on Friday.")
|
||||||
|
|
||||||
def test_the_graph_appears_once_there_are_two_readings(self):
|
def test_the_page_links_to_the_recap(self):
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
response = self.client.get(reverse("tracker:today"))
|
||||||
|
self.assertContains(response, reverse("tracker:recap"))
|
||||||
|
|
||||||
|
|
||||||
|
class RecapTests(TrackerTestCase):
|
||||||
|
def test_the_page_requires_login(self):
|
||||||
|
response = self.client.get(reverse("tracker:recap"))
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertIn("/admin/login/", response["Location"])
|
||||||
|
|
||||||
|
def test_a_fully_ticked_day_reads_as_complete(self):
|
||||||
|
today = timezone.localdate()
|
||||||
|
for dose in (self.morning, self.morning_only, self.evening):
|
||||||
|
DoseLog.objects.create(scheduled_dose=dose, date=today, taken=True)
|
||||||
|
|
||||||
|
recap = self.client.get("/api/tracker/recap", **self.auth_header()).json()
|
||||||
|
|
||||||
|
self.assertEqual(recap[0]["date"], today.isoformat())
|
||||||
|
self.assertTrue(recap[0]["complete"])
|
||||||
|
self.assertEqual(recap[0]["taken"], 3)
|
||||||
|
self.assertEqual(recap[0]["expected"], 3)
|
||||||
|
|
||||||
|
def test_a_missed_or_unrecorded_dose_leaves_the_day_incomplete(self):
|
||||||
|
today = timezone.localdate()
|
||||||
|
DoseLog.objects.create(scheduled_dose=self.morning, date=today, taken=True)
|
||||||
|
DoseLog.objects.create(scheduled_dose=self.evening, date=today, taken=False)
|
||||||
|
# morning_only is not recorded at all.
|
||||||
|
|
||||||
|
recap = self.client.get("/api/tracker/recap", **self.auth_header()).json()
|
||||||
|
|
||||||
|
self.assertFalse(recap[0]["complete"])
|
||||||
|
self.assertEqual(recap[0]["taken"], 1)
|
||||||
|
|
||||||
|
def test_the_recap_carries_pulse_and_note(self):
|
||||||
|
yesterday = timezone.localdate() - timedelta(days=1)
|
||||||
|
PulseReading.objects.create(date=yesterday, bpm=180)
|
||||||
|
DailyNote.objects.create(date=yesterday, body="Ate well.")
|
||||||
|
|
||||||
|
recap = self.client.get("/api/tracker/recap", **self.auth_header()).json()
|
||||||
|
|
||||||
|
self.assertEqual(recap[1]["bpm"], 180)
|
||||||
|
self.assertEqual(recap[1]["note"], "Ate well.")
|
||||||
|
self.assertIsNone(recap[0]["bpm"])
|
||||||
|
|
||||||
|
def test_the_page_shows_the_table_graph_and_notes(self):
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
today = timezone.localdate()
|
today = timezone.localdate()
|
||||||
|
|
||||||
response = self.client.get(reverse("tracker:today"))
|
|
||||||
self.assertContains(response, "at least two days")
|
|
||||||
|
|
||||||
PulseReading.objects.create(date=today - timedelta(days=1), bpm=170)
|
PulseReading.objects.create(date=today - timedelta(days=1), bpm=170)
|
||||||
PulseReading.objects.create(date=today, bpm=190)
|
PulseReading.objects.create(date=today, bpm=190)
|
||||||
|
DailyNote.objects.create(date=today, body="More playful than yesterday.")
|
||||||
|
for dose in (self.morning, self.morning_only, self.evening):
|
||||||
|
DoseLog.objects.create(scheduled_dose=dose, date=today, taken=True)
|
||||||
|
|
||||||
response = self.client.get(reverse("tracker:today"))
|
response = self.client.get(reverse("tracker:recap"))
|
||||||
|
|
||||||
|
self.assertContains(response, "✅")
|
||||||
|
self.assertContains(response, "0/3") # every earlier day is incomplete
|
||||||
self.assertContains(response, "<polyline")
|
self.assertContains(response, "<polyline")
|
||||||
self.assertContains(response, "180.0 bpm") # average
|
self.assertContains(response, "180.0 bpm") # average
|
||||||
|
self.assertContains(response, "More playful than yesterday.")
|
||||||
|
|
||||||
|
def test_the_graph_explains_itself_without_readings(self):
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
response = self.client.get(reverse("tracker:recap"))
|
||||||
|
self.assertContains(response, "at least two days")
|
||||||
|
|
||||||
|
|
||||||
class PwaTests(TrackerTestCase):
|
class PwaTests(TrackerTestCase):
|
||||||
|
|||||||
@ -6,4 +6,5 @@ app_name = "tracker"
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("today/", views.today, name="today"),
|
path("today/", views.today, name="today"),
|
||||||
|
path("recap/", views.recap, name="recap"),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -9,7 +9,7 @@ from django.urls import reverse
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.dateparse import parse_date
|
from django.utils.dateparse import parse_date
|
||||||
|
|
||||||
from tracker.api import build_day, pulse_trend
|
from tracker.api import build_day, build_recap, pulse_trend
|
||||||
from tracker.models import DailyNote, DoseLog, GlobalNote, PulseReading, ScheduledDose
|
from tracker.models import DailyNote, DoseLog, GlobalNote, PulseReading, ScheduledDose
|
||||||
|
|
||||||
CHART_WIDTH = 320
|
CHART_WIDTH = 320
|
||||||
@ -103,12 +103,6 @@ def today(request):
|
|||||||
messages.success(request, f"Saved for {day:%A %d %B}.")
|
messages.success(request, f"Saved for {day:%A %d %B}.")
|
||||||
return redirect(f"{reverse('tracker:today')}?date={day.isoformat()}")
|
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(
|
return render(
|
||||||
request,
|
request,
|
||||||
"tracker/today.html",
|
"tracker/today.html",
|
||||||
@ -118,8 +112,6 @@ def today(request):
|
|||||||
"is_today": day == timezone.localdate(),
|
"is_today": day == timezone.localdate(),
|
||||||
"previous_date": day - timedelta(days=1),
|
"previous_date": day - timedelta(days=1),
|
||||||
"next_date": day + timedelta(days=1),
|
"next_date": day + timedelta(days=1),
|
||||||
"chart": build_chart(readings),
|
|
||||||
"reading_count": len(readings),
|
|
||||||
"global_note": GlobalNote.load(),
|
"global_note": GlobalNote.load(),
|
||||||
# Empty when push is not configured; the card then says so rather than
|
# 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.
|
# vanishing, because a missing card looks like a bug in the page.
|
||||||
@ -132,3 +124,25 @@ def today(request):
|
|||||||
"subscribed_devices": request.user.push_subscriptions.count(),
|
"subscribed_devices": request.user.push_subscriptions.count(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@staff_member_required
|
||||||
|
def recap(request):
|
||||||
|
"""The overview: dose completeness per day, the pulse graph, and the journal."""
|
||||||
|
readings = list(
|
||||||
|
PulseReading.objects.filter(
|
||||||
|
date__gte=timezone.localdate() - timedelta(days=90)
|
||||||
|
).order_by("date")
|
||||||
|
)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"tracker/recap.html",
|
||||||
|
{
|
||||||
|
"title": "Recap",
|
||||||
|
"recap_days": build_recap(14),
|
||||||
|
"chart": build_chart(readings),
|
||||||
|
"reading_count": len(readings),
|
||||||
|
"daily_notes": DailyNote.objects.all(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user