Release: one log page, grouped dishes, live search #1
@@ -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
|
||||
// is treated as today rather than an error: a mangled URL should not be a
|
||||
// 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 {
|
||||
now := today(a.loc)
|
||||
if raw := r.FormValue("pvm"); raw != "" {
|
||||
if d, err := time.ParseInLocation(dateLayout, raw, a.loc); err == nil {
|
||||
if d.After(now) {
|
||||
return now
|
||||
}
|
||||
return d
|
||||
}
|
||||
}
|
||||
return today(a.loc)
|
||||
return now
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// today is the current calendar day in the configured location. Every date in
|
||||
// this app goes through here rather than time.Local, which would be UTC
|
||||
// whenever TZ is unset and quietly shift evening entries to the day before.
|
||||
// today is the current calendar day in the configured location, truncated to
|
||||
// midnight. Every date in this app goes through here rather than time.Local,
|
||||
// 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 {
|
||||
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
|
||||
|
||||
@@ -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) {
|
||||
db, err := openDB(t.TempDir() + "/test.db")
|
||||
if err != nil {
|
||||
|
||||
@@ -218,7 +218,9 @@ templ daySwitch(v logView) {
|
||||
@dayButton("Tänään", v.Today, v.Date, v.Today)
|
||||
@dayButton("Eilen", v.Today.AddDate(0, 0, -1), v.Date, v.Today)
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -135,6 +135,21 @@ check "the day is empty again" \
|
||||
check "search filters the board" \
|
||||
"$(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 --------------------------------
|
||||
|
||||
miss=$(curl -s -u ":$pass" "http://$addr/?haku=Poronkariste")
|
||||
|
||||
Reference in New Issue
Block a user