Refuse to log meals in the future
The log is a record of what was eaten, so there is nothing to write down for a dinner that has not happened, and a stray entry dated next year would sit at the top of the history forever. A future date is now clamped to today. The clamp lives in the one function every read and write already goes through, so ?pvm=, the date picker, saving and deleting are all covered. The picker also gets max=today, which avoids offering the dead end at all. Fixes a latent bug found alongside it: today() returned the current instant with its time of day, while dates parsed from ?pvm= are midnight, so the two never compared equal. After saving today's dinner the redirect landed on /?pvm=... and the card then read "la 5.9. kirjattu" instead of "Tänään kirjattu", and "Tänään" stopped highlighting whenever the date was spelled out. today() now truncates to midnight in the configured location.
This commit is contained in:
@@ -38,13 +38,22 @@ func render(w http.ResponseWriter, r *http.Request, c templ.Component) {
|
|||||||
// date reads the ?pvm= parameter, falling back to today. An unparseable value
|
// date reads the ?pvm= parameter, falling back to today. An unparseable value
|
||||||
// is treated as today rather than an error: a mangled URL should not be a
|
// is treated as today rather than an error: a mangled URL should not be a
|
||||||
// dead end.
|
// dead end.
|
||||||
|
//
|
||||||
|
// A future date is clamped to today. This is a record of what was eaten, so
|
||||||
|
// there is nothing to write down for a dinner that has not happened, and a
|
||||||
|
// stray entry dated next year would sit at the top of the history forever.
|
||||||
|
// Every read and write goes through here, so the clamp covers them all.
|
||||||
func (a *app) date(r *http.Request) time.Time {
|
func (a *app) date(r *http.Request) time.Time {
|
||||||
|
now := today(a.loc)
|
||||||
if raw := r.FormValue("pvm"); raw != "" {
|
if raw := r.FormValue("pvm"); raw != "" {
|
||||||
if d, err := time.ParseInLocation(dateLayout, raw, a.loc); err == nil {
|
if d, err := time.ParseInLocation(dateLayout, raw, a.loc); err == nil {
|
||||||
|
if d.After(now) {
|
||||||
|
return now
|
||||||
|
}
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return today(a.loc)
|
return now
|
||||||
}
|
}
|
||||||
|
|
||||||
// logView is everything the log screen needs. Logging and history are one
|
// logView is everything the log screen needs. Logging and history are one
|
||||||
|
|||||||
+10
-4
@@ -212,11 +212,17 @@ func challenge(w http.ResponseWriter) {
|
|||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
|
|
||||||
// today is the current calendar day in the configured location. Every date in
|
// today is the current calendar day in the configured location, truncated to
|
||||||
// this app goes through here rather than time.Local, which would be UTC
|
// midnight. Every date in this app goes through here rather than time.Local,
|
||||||
// whenever TZ is unset and quietly shift evening entries to the day before.
|
// which would be UTC whenever TZ is unset and quietly shift evening entries to
|
||||||
|
// the day before.
|
||||||
|
//
|
||||||
|
// The truncation matters: dates parsed from ?pvm= are midnight, so a today
|
||||||
|
// carrying a time of day would never compare equal to one of them, and the UI
|
||||||
|
// would stop recognising today as today the moment the date was explicit.
|
||||||
func today(loc *time.Location) time.Time {
|
func today(loc *time.Location) time.Time {
|
||||||
return time.Now().In(loc)
|
now := time.Now().In(loc)
|
||||||
|
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// normalizeName collapses whitespace and capitalises the first letter for
|
// normalizeName collapses whitespace and capitalises the first letter for
|
||||||
|
|||||||
@@ -30,6 +30,51 @@ func TestNormalizeName(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTodayIsMidnight(t *testing.T) {
|
||||||
|
now := today(time.UTC)
|
||||||
|
if h, m, s := now.Clock(); h != 0 || m != 0 || s != 0 {
|
||||||
|
t.Errorf("today() = %s, want midnight", now)
|
||||||
|
}
|
||||||
|
// A date parsed from a URL must compare equal to it, or the UI stops
|
||||||
|
// recognising today as today whenever the date is spelled out.
|
||||||
|
parsed, err := time.ParseInLocation(dateLayout, now.Format(dateLayout), time.UTC)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse: %v", err)
|
||||||
|
}
|
||||||
|
if !parsed.Equal(now) {
|
||||||
|
t.Errorf("parsed %s != today %s", parsed, now)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateRejectsTheFuture(t *testing.T) {
|
||||||
|
a := &app{loc: time.UTC}
|
||||||
|
now := today(time.UTC)
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
pvm string
|
||||||
|
want time.Time
|
||||||
|
}{
|
||||||
|
{"no parameter", "", now},
|
||||||
|
{"today", now.Format(dateLayout), now},
|
||||||
|
{"yesterday", now.AddDate(0, 0, -1).Format(dateLayout), now.AddDate(0, 0, -1)},
|
||||||
|
// Nothing was eaten tomorrow, and a stray entry dated next year would
|
||||||
|
// sit at the top of the history forever.
|
||||||
|
{"tomorrow", now.AddDate(0, 0, 1).Format(dateLayout), now},
|
||||||
|
{"next year", now.AddDate(1, 0, 0).Format(dateLayout), now},
|
||||||
|
{"nonsense", "eilen", now},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/?pvm="+c.pvm, nil)
|
||||||
|
if got := a.date(r); !got.Equal(c.want) {
|
||||||
|
t.Errorf("date = %s, want %s", got.Format(dateLayout), c.want.Format(dateLayout))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMigrateCreatesSchema(t *testing.T) {
|
func TestMigrateCreatesSchema(t *testing.T) {
|
||||||
db, err := openDB(t.TempDir() + "/test.db")
|
db, err := openDB(t.TempDir() + "/test.db")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -218,7 +218,9 @@ templ daySwitch(v logView) {
|
|||||||
@dayButton("Tänään", v.Today, v.Date, v.Today)
|
@dayButton("Tänään", v.Today, v.Date, v.Today)
|
||||||
@dayButton("Eilen", v.Today.AddDate(0, 0, -1), v.Date, v.Today)
|
@dayButton("Eilen", v.Today.AddDate(0, 0, -1), v.Date, v.Today)
|
||||||
<form method="get" action="/" class="daypick">
|
<form method="get" action="/" class="daypick">
|
||||||
<input type="date" name="pvm" value={ isoDate(v.Date) } aria-label="Muu päivä"/>
|
// max stops the picker offering days that have not happened yet;
|
||||||
|
// the server clamps anyway, this just avoids the dead end.
|
||||||
|
<input type="date" name="pvm" value={ isoDate(v.Date) } max={ isoDate(v.Today) } aria-label="Muu päivä"/>
|
||||||
<button type="submit">Näytä</button>
|
<button type="submit">Näytä</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -135,6 +135,21 @@ check "the day is empty again" \
|
|||||||
check "search filters the board" \
|
check "search filters the board" \
|
||||||
"$(curl -s -u ":$pass" "http://$addr/?haku=keitto")" "keitto"
|
"$(curl -s -u ":$pass" "http://$addr/?haku=keitto")" "keitto"
|
||||||
|
|
||||||
|
# Nothing was eaten tomorrow. A future date is clamped rather than logged.
|
||||||
|
future=$(date -d '+30 days' +%Y-%m-%d)
|
||||||
|
check "a future date falls back to today" \
|
||||||
|
"$(curl -s -u ":$pass" "http://$addr/?pvm=$future")" "$(date +%-d.%-m.%Y)"
|
||||||
|
|
||||||
|
check "saving a future date is clamped too" \
|
||||||
|
"$(curl -s -o /dev/null -w '%{redirect_url}' -u ":$pass" \
|
||||||
|
-d "pvm=$future&ruoka=$ruoka" "http://$addr/kirjaa")" "/"
|
||||||
|
|
||||||
|
check "tomorrow was not written to the log" \
|
||||||
|
"$(curl -s -u ":$pass" "http://$addr/?pvm=$future")" "$(date +%-d.%-m.%Y)"
|
||||||
|
|
||||||
|
# Clean up the entry that clamped onto today.
|
||||||
|
curl -s -o /dev/null -u ":$pass" -d "pvm=$(date +%Y-%m-%d)" "http://$addr/poista"
|
||||||
|
|
||||||
# ---- adding a dish without leaving Kirjaa --------------------------------
|
# ---- adding a dish without leaving Kirjaa --------------------------------
|
||||||
|
|
||||||
miss=$(curl -s -u ":$pass" "http://$addr/?haku=Poronkariste")
|
miss=$(curl -s -u ":$pass" "http://$addr/?haku=Poronkariste")
|
||||||
|
|||||||
Reference in New Issue
Block a user