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
|
// 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