Review round 1 F1 (ADR 0517 §3, §5): a form act went back to the list or the pane, which recorded page opened or opened again, and the script's refresh did too, so last looked became the last act and opened counts grew. The redirect after an act carries acted=1 and the refresh an X-Mail-Refresh header; that page records nothing. The 1860 test now follows each redirect and counts every line, and reminds the task (F2, 1858/AC9), which the store's own integration test also reads back as a date.
131 lines
5.0 KiB
Go
131 lines
5.0 KiB
Go
package web
|
|
|
|
// What the operator does on the mail page (novox/hq ADR 0517 §1, §2, §4, §5; issue 1859): tick an item done,
|
|
// undo it, remind it tomorrow, choose a mode. Each is a post RequireFormToken has already let through (the
|
|
// session's own token, csrf.go), calling its one mail-reading verb for the signed-in operator; the page keeps
|
|
// no state of its own (ADR 0440 §4). Posted by the page's script (X-Requested-With: fetch), it answers JSON:
|
|
// done, where to go, or failed with the cause. Posted by a plain form, it goes back to the list, or shows the
|
|
// list again saying the act failed and why — never dropped silently.
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// byScript says whether the page's own script posted the form.
|
|
func byScript(r *http.Request) bool { return r.Header.Get("X-Requested-With") == "fetch" }
|
|
|
|
func answerJSON(w http.ResponseWriter, status int, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
// actFailed says an act failed, with its cause: JSON to the script, the list page with the cause otherwise.
|
|
func (b *Bundle) actFailed(w http.ResponseWriter, r *http.Request, mode Mode, cause string) {
|
|
if byScript(r) {
|
|
answerJSON(w, http.StatusBadGateway, map[string]any{"failed": cause})
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
|
|
defer cancel()
|
|
data := b.listDataQuiet(ctx, r, mode)
|
|
data.Notice = cause
|
|
renderNamed(w, http.StatusBadGateway, "list", data)
|
|
}
|
|
|
|
// actDone answers an act that succeeded: done to the script, or back to the page at back, marked as the page
|
|
// after an act (ActedParam) so it records nothing.
|
|
func actDone(w http.ResponseWriter, r *http.Request, back string) {
|
|
if byScript(r) {
|
|
answerJSON(w, http.StatusOK, map[string]any{"done": true})
|
|
return
|
|
}
|
|
http.Redirect(w, r, afterAct(back), http.StatusSeeOther)
|
|
}
|
|
|
|
// ActedParam marks the page a form act goes back to (review round 1 F1; ADR 0517 §5: no act writes two
|
|
// lines): that page records neither page opened nor opened, since the act already wrote its one line. The
|
|
// script's own refresh says the same with RefreshHeader.
|
|
const ActedParam = "acted"
|
|
|
|
// afterAct is back with ActedParam added.
|
|
func afterAct(back string) string {
|
|
sep := "?"
|
|
if strings.Contains(back, "?") {
|
|
sep = "&"
|
|
}
|
|
return back + sep + ActedParam + "=1"
|
|
}
|
|
|
|
// afterAnAct says whether r asks the page shown after an act: by ActedParam, or the script's refresh.
|
|
func afterAnAct(r *http.Request) bool {
|
|
return r.URL.Query().Get(ActedParam) != "" || r.Header.Get(RefreshHeader) != ""
|
|
}
|
|
|
|
// listAddress is the list page's own address for mode.
|
|
func listAddress(mode Mode) string { return "/?" + url.Values{"mode": {mode.Name}}.Encode() }
|
|
|
|
// itemHandler is POST /item (1859/AC5): act handle, unhandle or remind, on the item (a task, a promise, or
|
|
// the mail itself) of the mail entry id, in the mode the page is in.
|
|
func (b *Bundle) itemHandler(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "post only", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
mode := modeNamed(r.PostFormValue("mode"))
|
|
id, err := strconv.Atoi(r.PostFormValue("id"))
|
|
if err != nil || id <= 0 {
|
|
http.Error(w, "an item needs its mail entry's id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
ask := map[string]func(context.Context, map[string]any) (json.RawMessage, error){
|
|
"handle": b.mailHandleAsk, "unhandle": b.mailUnhandleAsk, "remind": b.mailRemindAsk,
|
|
}[r.PostFormValue("act")]
|
|
if ask == nil {
|
|
http.Error(w, "the act is none of handle, unhandle, remind", http.StatusBadRequest)
|
|
return
|
|
}
|
|
args := map[string]any{"id": float64(id), "mode": mode.Name}
|
|
if item := r.PostFormValue("item"); item != "" {
|
|
args["item"] = item
|
|
}
|
|
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
|
|
defer cancel()
|
|
if _, err := ask(ctx, args); err != nil {
|
|
b.actFailed(w, r, mode, "That did not happen: "+err.Error())
|
|
return
|
|
}
|
|
back := r.PostFormValue("back")
|
|
if back == "" || back[0] != '/' || (len(back) > 1 && back[1] == '/') {
|
|
back = listAddress(mode) // only an address of this page itself
|
|
}
|
|
actDone(w, r, back)
|
|
}
|
|
|
|
// modeHandler is POST /mode (1859/AC2, AC5): the mode chosen, recorded (ADR 0517 §4), then its list.
|
|
func (b *Bundle) modeHandler(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "post only", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
mode := modeNamed(r.PostFormValue("mode"))
|
|
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
|
|
defer cancel()
|
|
if _, err := b.mailRecordAsk(ctx, map[string]any{"what": "mode", "mode": mode.Name}); err != nil {
|
|
b.actFailed(w, r, mode, "The mode chosen could not be kept: "+err.Error())
|
|
return
|
|
}
|
|
// The mode chosen is its own line: the list it goes to records nothing more (F1).
|
|
if byScript(r) {
|
|
answerJSON(w, http.StatusOK, map[string]any{"done": true, "go": afterAct(listAddress(mode))})
|
|
return
|
|
}
|
|
http.Redirect(w, r, afterAct(listAddress(mode)), http.StatusSeeOther)
|
|
}
|