feat: add daily + global notes
This commit is contained in:
parent
d2270a737b
commit
0cfe4c7108
@ -40,6 +40,11 @@
|
|||||||
color: var(--body-quiet-color); margin: 16px 0 4px;
|
color: var(--body-quiet-color); margin: 16px 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.osiris-note textarea {
|
||||||
|
width: 100%; min-height: 90px; padding: 12px; font: inherit;
|
||||||
|
border-radius: 8px; border: 1px solid var(--border-color);
|
||||||
|
background: var(--body-bg); color: var(--body-fg); resize: vertical;
|
||||||
|
}
|
||||||
.osiris-pulse input[type="number"] {
|
.osiris-pulse input[type="number"] {
|
||||||
font-size: 1.6rem; width: 100%; padding: 12px;
|
font-size: 1.6rem; width: 100%; padding: 12px;
|
||||||
border-radius: 8px; border: 1px solid var(--border-color);
|
border-radius: 8px; border: 1px solid var(--border-color);
|
||||||
@ -106,9 +111,29 @@
|
|||||||
placeholder="e.g. 180" value="{{ day.pulse.bpm|default_if_none:'' }}">
|
placeholder="e.g. 180" value="{{ day.pulse.bpm|default_if_none:'' }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="osiris-card osiris-note">
|
||||||
|
<h2 style="margin-top:0">Note for the day</h2>
|
||||||
|
<label for="note" class="dose-detail">Appetite, behaviour, anything worth remembering (leave empty to clear)</label>
|
||||||
|
<textarea id="note" name="note" placeholder="e.g. Ate well, more playful than yesterday">{{ day.note.body|default:'' }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="osiris-save">Save {{ day.date|date:"D j M" }}</button>
|
<button type="submit" class="osiris-save">Save {{ day.date|date:"D j M" }}</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<form method="post" style="margin-top:16px">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="date" value="{{ day.date|date:'Y-m-d' }}">
|
||||||
|
<div class="osiris-card osiris-note">
|
||||||
|
<h2 style="margin-top:0">Shared note</h2>
|
||||||
|
<label for="global_note" class="dose-detail">Instructions and things to do — one note for everyone, on every day</label>
|
||||||
|
<textarea id="global_note" name="global_note" placeholder="e.g. Vet appointment Friday — bring the pulse log">{{ global_note.body }}</textarea>
|
||||||
|
{% if global_note.body %}
|
||||||
|
<p class="dose-detail">Last updated {{ global_note.updated_at|date:"D j M, H:i" }}</p>
|
||||||
|
{% endif %}
|
||||||
|
<button type="submit" class="osiris-save">Save shared note</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<div class="osiris-card" style="margin-top:16px">
|
<div class="osiris-card" style="margin-top:16px">
|
||||||
<h2 style="margin-top:0">Pulse — last 90 days</h2>
|
<h2 style="margin-top:0">Pulse — last 90 days</h2>
|
||||||
{% if chart %}
|
{% if chart %}
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from tracker.models import (
|
from tracker.models import (
|
||||||
|
DailyNote,
|
||||||
DoseLog,
|
DoseLog,
|
||||||
|
GlobalNote,
|
||||||
Medication,
|
Medication,
|
||||||
PulseReading,
|
PulseReading,
|
||||||
ReminderSent,
|
ReminderSent,
|
||||||
@ -66,3 +68,29 @@ class DoseLogAdmin(admin.ModelAdmin):
|
|||||||
class PulseReadingAdmin(admin.ModelAdmin):
|
class PulseReadingAdmin(admin.ModelAdmin):
|
||||||
list_display = ["date", "bpm", "notes"]
|
list_display = ["date", "bpm", "notes"]
|
||||||
date_hierarchy = "date"
|
date_hierarchy = "date"
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(DailyNote)
|
||||||
|
class DailyNoteAdmin(admin.ModelAdmin):
|
||||||
|
"""The journal. Day-to-day writing happens on the Today page."""
|
||||||
|
|
||||||
|
list_display = ["date", "preview", "updated_at"]
|
||||||
|
date_hierarchy = "date"
|
||||||
|
search_fields = ["body"]
|
||||||
|
|
||||||
|
@admin.display(description="Note")
|
||||||
|
def preview(self, obj: DailyNote) -> str:
|
||||||
|
return obj.body[:80]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(GlobalNote)
|
||||||
|
class GlobalNoteAdmin(admin.ModelAdmin):
|
||||||
|
"""The one shared note. It can be edited here, but never added or deleted."""
|
||||||
|
|
||||||
|
list_display = ["__str__", "updated_at"]
|
||||||
|
|
||||||
|
def has_add_permission(self, request):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_delete_permission(self, request, obj=None):
|
||||||
|
return False
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
from datetime import date as date_type
|
from datetime import date as date_type
|
||||||
from datetime import 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, Max, Min
|
||||||
@ -10,7 +10,14 @@ from ninja import Router, Schema
|
|||||||
from ninja.errors import HttpError
|
from ninja.errors import HttpError
|
||||||
|
|
||||||
from accounts.auth import AUTH
|
from accounts.auth import AUTH
|
||||||
from tracker.models import DoseLog, Medication, PulseReading, ScheduledDose
|
from tracker.models import (
|
||||||
|
DailyNote,
|
||||||
|
DoseLog,
|
||||||
|
GlobalNote,
|
||||||
|
Medication,
|
||||||
|
PulseReading,
|
||||||
|
ScheduledDose,
|
||||||
|
)
|
||||||
|
|
||||||
router = Router(auth=AUTH)
|
router = Router(auth=AUTH)
|
||||||
|
|
||||||
@ -48,10 +55,30 @@ class PulseOut(Schema):
|
|||||||
notes: str
|
notes: str
|
||||||
|
|
||||||
|
|
||||||
|
class DailyNoteOut(Schema):
|
||||||
|
date: date_type
|
||||||
|
body: str
|
||||||
|
|
||||||
|
|
||||||
|
class DailyNoteIn(Schema):
|
||||||
|
body: str
|
||||||
|
date: Optional[date_type] = None
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalNoteOut(Schema):
|
||||||
|
body: str
|
||||||
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalNoteIn(Schema):
|
||||||
|
body: str
|
||||||
|
|
||||||
|
|
||||||
class DayOut(Schema):
|
class DayOut(Schema):
|
||||||
date: date_type
|
date: date_type
|
||||||
doses: list[DoseStatusOut]
|
doses: list[DoseStatusOut]
|
||||||
pulse: Optional[PulseOut]
|
pulse: Optional[PulseOut]
|
||||||
|
note: Optional[DailyNoteOut]
|
||||||
|
|
||||||
|
|
||||||
class DoseLogIn(Schema):
|
class DoseLogIn(Schema):
|
||||||
@ -116,6 +143,7 @@ def build_day(day: date_type) -> dict:
|
|||||||
for dose in doses
|
for dose in doses
|
||||||
],
|
],
|
||||||
"pulse": PulseReading.objects.filter(date=day).first(),
|
"pulse": PulseReading.objects.filter(date=day).first(),
|
||||||
|
"note": DailyNote.objects.filter(date=day).first(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -190,6 +218,37 @@ def log_dose(request: HttpRequest, payload: DoseLogIn):
|
|||||||
return build_day(day)
|
return build_day(day)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Notes -----------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/notes/daily", response=DayOut)
|
||||||
|
def save_daily_note(request: HttpRequest, payload: DailyNoteIn):
|
||||||
|
"""The day's journal entry. One per day; an empty body removes it."""
|
||||||
|
day = payload.date or timezone.localdate()
|
||||||
|
|
||||||
|
if payload.body.strip():
|
||||||
|
DailyNote.objects.update_or_create(date=day, defaults={"body": payload.body})
|
||||||
|
else:
|
||||||
|
DailyNote.objects.filter(date=day).delete()
|
||||||
|
|
||||||
|
return build_day(day)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/notes/global", response=GlobalNoteOut)
|
||||||
|
def get_global_note(request: HttpRequest):
|
||||||
|
"""The single shared note: standing instructions and things to do."""
|
||||||
|
return GlobalNote.load()
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/notes/global", response=GlobalNoteOut)
|
||||||
|
def save_global_note(request: HttpRequest, payload: GlobalNoteIn):
|
||||||
|
"""Replace the shared note's content. Last write wins."""
|
||||||
|
note = GlobalNote.load()
|
||||||
|
note.body = payload.body
|
||||||
|
note.save(update_fields=["body", "updated_at"])
|
||||||
|
return note
|
||||||
|
|
||||||
|
|
||||||
# --- Pulse -----------------------------------------------------------------
|
# --- Pulse -----------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
61
tracker/migrations/0003_dailynote_globalnote.py
Normal file
61
tracker/migrations/0003_dailynote_globalnote.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# Generated by Django 6.0.7 on 2026-07-17 11:35
|
||||||
|
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("tracker", "0002_scheduleddose_reminder_time_remindersent"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="DailyNote",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"date",
|
||||||
|
models.DateField(
|
||||||
|
default=django.utils.timezone.localdate, unique=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("body", models.TextField(blank=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["-date"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="GlobalNote",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("body", models.TextField(blank=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"constraints": [
|
||||||
|
models.CheckConstraint(
|
||||||
|
condition=models.Q(("id", 1)), name="single_global_note"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -122,6 +122,44 @@ class ReminderSent(models.Model):
|
|||||||
return f"{self.date} — {self.scheduled_dose}"
|
return f"{self.date} — {self.scheduled_dose}"
|
||||||
|
|
||||||
|
|
||||||
|
class DailyNote(models.Model):
|
||||||
|
"""Free-form journal entry for one day: appetite, behaviour, how Osiris is doing."""
|
||||||
|
|
||||||
|
date = models.DateField(default=timezone.localdate, unique=True)
|
||||||
|
body = models.TextField(blank=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["-date"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.date} — {self.body[:40]}"
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalNote(models.Model):
|
||||||
|
"""The single shared note: standing instructions and things to do.
|
||||||
|
|
||||||
|
There is exactly one row, shared by everyone; the constraint pins its pk
|
||||||
|
so a second one cannot be created even by accident.
|
||||||
|
"""
|
||||||
|
|
||||||
|
body = models.TextField(blank=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
models.CheckConstraint(condition=models.Q(id=1), name="single_global_note")
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Global note"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls) -> "GlobalNote":
|
||||||
|
note, _ = cls.objects.get_or_create(pk=1)
|
||||||
|
return note
|
||||||
|
|
||||||
|
|
||||||
class PulseReading(models.Model):
|
class PulseReading(models.Model):
|
||||||
"""One heart-rate measurement per day, in beats per minute."""
|
"""One heart-rate measurement per day, in beats per minute."""
|
||||||
|
|
||||||
|
|||||||
118
tracker/tests.py
118
tracker/tests.py
@ -12,7 +12,9 @@ from pywebpush import WebPushException
|
|||||||
|
|
||||||
from accounts.models import PushSubscription, User
|
from accounts.models import PushSubscription, User
|
||||||
from tracker.models import (
|
from tracker.models import (
|
||||||
|
DailyNote,
|
||||||
DoseLog,
|
DoseLog,
|
||||||
|
GlobalNote,
|
||||||
Medication,
|
Medication,
|
||||||
PulseReading,
|
PulseReading,
|
||||||
ReminderSent,
|
ReminderSent,
|
||||||
@ -178,6 +180,74 @@ class PulseApiTests(TrackerTestCase):
|
|||||||
self.assertIsNone(stats["trend_bpm_per_day"])
|
self.assertIsNone(stats["trend_bpm_per_day"])
|
||||||
|
|
||||||
|
|
||||||
|
class NoteApiTests(TrackerTestCase):
|
||||||
|
def test_the_day_includes_its_note(self):
|
||||||
|
DailyNote.objects.create(body="Ate well.")
|
||||||
|
|
||||||
|
response = self.client.get("/api/tracker/day", **self.auth_header())
|
||||||
|
|
||||||
|
self.assertEqual(response.json()["note"]["body"], "Ate well.")
|
||||||
|
|
||||||
|
def test_saving_a_daily_note_twice_overwrites(self):
|
||||||
|
for body in ("Sleepy.", "Playful after all."):
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/tracker/notes/daily",
|
||||||
|
{"body": body},
|
||||||
|
content_type="application/json",
|
||||||
|
**self.auth_header(),
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
self.assertEqual(DailyNote.objects.count(), 1)
|
||||||
|
self.assertEqual(DailyNote.objects.get().body, "Playful after all.")
|
||||||
|
|
||||||
|
def test_a_daily_note_can_target_a_past_day(self):
|
||||||
|
yesterday = timezone.localdate() - timedelta(days=1)
|
||||||
|
self.client.post(
|
||||||
|
"/api/tracker/notes/daily",
|
||||||
|
{"body": "Rough night.", "date": yesterday.isoformat()},
|
||||||
|
content_type="application/json",
|
||||||
|
**self.auth_header(),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(DailyNote.objects.filter(date=yesterday).exists())
|
||||||
|
self.assertFalse(DailyNote.objects.filter(date=timezone.localdate()).exists())
|
||||||
|
|
||||||
|
def test_an_empty_body_removes_the_daily_note(self):
|
||||||
|
DailyNote.objects.create(body="Old news.")
|
||||||
|
|
||||||
|
self.client.post(
|
||||||
|
"/api/tracker/notes/daily",
|
||||||
|
{"body": " "},
|
||||||
|
content_type="application/json",
|
||||||
|
**self.auth_header(),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(DailyNote.objects.count(), 0)
|
||||||
|
|
||||||
|
def test_the_global_note_exists_without_ever_being_created(self):
|
||||||
|
response = self.client.get("/api/tracker/notes/global", **self.auth_header())
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json()["body"], "")
|
||||||
|
|
||||||
|
def test_the_global_note_is_shared_and_unique(self):
|
||||||
|
response = self.client.put(
|
||||||
|
"/api/tracker/notes/global",
|
||||||
|
{"body": "Vet on Friday."},
|
||||||
|
content_type="application/json",
|
||||||
|
**self.auth_header(),
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
other = User.objects.create_superuser("marie", password="hunter2hunter2")
|
||||||
|
self.client.force_login(other)
|
||||||
|
response = self.client.get("/api/tracker/notes/global")
|
||||||
|
|
||||||
|
self.assertEqual(response.json()["body"], "Vet on Friday.")
|
||||||
|
self.assertEqual(GlobalNote.objects.count(), 1)
|
||||||
|
|
||||||
|
|
||||||
class TodayPageTests(TrackerTestCase):
|
class TodayPageTests(TrackerTestCase):
|
||||||
def test_the_page_requires_login(self):
|
def test_the_page_requires_login(self):
|
||||||
response = self.client.get(reverse("tracker:today"))
|
response = self.client.get(reverse("tracker:today"))
|
||||||
@ -229,6 +299,54 @@ class TodayPageTests(TrackerTestCase):
|
|||||||
|
|
||||||
self.assertFalse(PulseReading.objects.filter(date=today).exists())
|
self.assertFalse(PulseReading.objects.filter(date=today).exists())
|
||||||
|
|
||||||
|
def test_saving_records_the_daily_note(self):
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
today = timezone.localdate()
|
||||||
|
|
||||||
|
self.client.post(
|
||||||
|
reverse("tracker:today"),
|
||||||
|
{"date": today.isoformat(), "note": "Ate well."},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(DailyNote.objects.get(date=today).body, "Ate well.")
|
||||||
|
|
||||||
|
def test_clearing_the_note_field_removes_the_note(self):
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
today = timezone.localdate()
|
||||||
|
DailyNote.objects.create(date=today, body="Old news.")
|
||||||
|
|
||||||
|
self.client.post(reverse("tracker:today"), {"date": today.isoformat()})
|
||||||
|
|
||||||
|
self.assertFalse(DailyNote.objects.filter(date=today).exists())
|
||||||
|
|
||||||
|
def test_the_shared_note_form_saves_without_touching_the_day(self):
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
today = timezone.localdate()
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("tracker:today"),
|
||||||
|
{"date": today.isoformat(), "global_note": "Vet on Friday."},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
self.assertEqual(GlobalNote.load().body, "Vet on Friday.")
|
||||||
|
# Its own form carries no dose or pulse fields; the day must stay untouched.
|
||||||
|
self.assertFalse(DoseLog.objects.exists())
|
||||||
|
self.assertFalse(PulseReading.objects.exists())
|
||||||
|
self.assertFalse(DailyNote.objects.exists())
|
||||||
|
|
||||||
|
def test_the_page_shows_both_notes(self):
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
DailyNote.objects.create(body="More playful than yesterday.")
|
||||||
|
note = GlobalNote.load()
|
||||||
|
note.body = "Vet on Friday."
|
||||||
|
note.save()
|
||||||
|
|
||||||
|
response = self.client.get(reverse("tracker:today"))
|
||||||
|
|
||||||
|
self.assertContains(response, "More playful than yesterday.")
|
||||||
|
self.assertContains(response, "Vet on Friday.")
|
||||||
|
|
||||||
def test_the_graph_appears_once_there_are_two_readings(self):
|
def test_the_graph_appears_once_there_are_two_readings(self):
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
today = timezone.localdate()
|
today = timezone.localdate()
|
||||||
|
|||||||
@ -10,7 +10,7 @@ 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, pulse_trend
|
||||||
from tracker.models import DoseLog, PulseReading, ScheduledDose
|
from tracker.models import DailyNote, DoseLog, GlobalNote, PulseReading, ScheduledDose
|
||||||
|
|
||||||
CHART_WIDTH = 320
|
CHART_WIDTH = 320
|
||||||
CHART_HEIGHT = 140
|
CHART_HEIGHT = 140
|
||||||
@ -62,6 +62,16 @@ def today(request):
|
|||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
day = parse_date(request.POST.get("date", "")) or timezone.localdate()
|
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)
|
doses = ScheduledDose.objects.filter(is_active=True, medication__is_active=True)
|
||||||
|
|
||||||
for dose in doses:
|
for dose in doses:
|
||||||
@ -84,6 +94,12 @@ def today(request):
|
|||||||
# Clearing the field removes the reading for that day.
|
# Clearing the field removes the reading for that day.
|
||||||
PulseReading.objects.filter(date=day).delete()
|
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}.")
|
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()}")
|
||||||
|
|
||||||
@ -104,6 +120,7 @@ def today(request):
|
|||||||
"next_date": day + timedelta(days=1),
|
"next_date": day + timedelta(days=1),
|
||||||
"chart": build_chart(readings),
|
"chart": build_chart(readings),
|
||||||
"reading_count": len(readings),
|
"reading_count": len(readings),
|
||||||
|
"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.
|
||||||
"vapid_public_key": settings.VAPID_PUBLIC_KEY,
|
"vapid_public_key": settings.VAPID_PUBLIC_KEY,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user