Introduce EDIT and MANAGE methods

All admin "index" pages are moved under MANAGE, so that we can have (for
example) and normal "GET /posts" page later which would replace the
current index page, and potentially corresponding pages for the other
categories.

The EDIT method replaces the old `?edit` pattern, which normalizes how
we differentiate page functionality generally.
This commit is contained in:
Brian Picciano 2022-11-29 20:59:31 +01:00
parent 31f8f37c5a
commit 1f3ae665ed
11 changed files with 97 additions and 150 deletions

View File

@ -190,7 +190,9 @@ func (a *api) blogHandler() http.Handler {
mux.Handle("/posts/", http.StripPrefix("/posts", mux.Handle("/posts/", http.StripPrefix("/posts",
apiutil.MethodMux(map[string]http.Handler{ apiutil.MethodMux(map[string]http.Handler{
"GET": a.renderPostHandler(), "GET": a.getPostHandler(),
"EDIT": a.editPostHandler(false),
"MANAGE": a.managePostsHandler(),
"POST": a.postPostHandler(), "POST": a.postPostHandler(),
"DELETE": a.deletePostHandler(false), "DELETE": a.deletePostHandler(false),
"PREVIEW": a.previewPostHandler(), "PREVIEW": a.previewPostHandler(),
@ -200,6 +202,7 @@ func (a *api) blogHandler() http.Handler {
mux.Handle("/assets/", http.StripPrefix("/assets", mux.Handle("/assets/", http.StripPrefix("/assets",
apiutil.MethodMux(map[string]http.Handler{ apiutil.MethodMux(map[string]http.Handler{
"GET": a.getPostAssetHandler(), "GET": a.getPostAssetHandler(),
"MANAGE": a.managePostAssetsHandler(),
"POST": a.postPostAssetHandler(), "POST": a.postPostAssetHandler(),
"DELETE": a.deletePostAssetHandler(), "DELETE": a.deletePostAssetHandler(),
}), }),
@ -211,7 +214,8 @@ func (a *api) blogHandler() http.Handler {
authMiddleware(a.auther)( authMiddleware(a.auther)(
apiutil.MethodMux(map[string]http.Handler{ apiutil.MethodMux(map[string]http.Handler{
"GET": a.renderDraftPostHandler(), "EDIT": a.editPostHandler(true),
"MANAGE": a.manageDraftPostsHandler(),
"POST": a.postDraftPostHandler(), "POST": a.postDraftPostHandler(),
"DELETE": a.deletePostHandler(true), "DELETE": a.deletePostHandler(true),
"PREVIEW": a.previewPostHandler(), "PREVIEW": a.previewPostHandler(),
@ -227,17 +231,21 @@ func (a *api) blogHandler() http.Handler {
mux.Handle("/feed.xml", a.renderFeedHandler()) mux.Handle("/feed.xml", a.renderFeedHandler())
mux.Handle("/", a.renderIndexHandler()) mux.Handle("/", a.renderIndexHandler())
h := apiutil.MethodMux(map[string]http.Handler{ readOnlyMiddlewares := []middleware{
"GET": applyMiddlewares(
mux,
logReqMiddleware, // only log GETs on cache miss logReqMiddleware, // only log GETs on cache miss
cacheMiddleware(cache), cacheMiddleware(cache),
), }
"*": applyMiddlewares(
mux, readWriteMiddlewares := []middleware{
purgeCacheOnOKMiddleware(cache), purgeCacheOnOKMiddleware(cache),
authMiddleware(a.auther), authMiddleware(a.auther),
), }
h := apiutil.MethodMux(map[string]http.Handler{
"GET": applyMiddlewares(mux, readOnlyMiddlewares...),
"MANAGE": applyMiddlewares(mux, readOnlyMiddlewares...),
"EDIT": applyMiddlewares(mux, readOnlyMiddlewares...),
"*": applyMiddlewares(mux, readWriteMiddlewares...),
}) })
return h return h
@ -247,10 +255,15 @@ func (a *api) handler() http.Handler {
mux := http.NewServeMux() mux := http.NewServeMux()
mux.Handle("/api/", http.StripPrefix("/api", a.apiHandler())) mux.Handle("/api/", applyMiddlewares(
http.StripPrefix("/api", a.apiHandler()),
logReqMiddleware,
))
mux.Handle("/", a.blogHandler()) mux.Handle("/", a.blogHandler())
h := apiutil.MethodMux(map[string]http.Handler{ h := applyMiddlewares(
apiutil.MethodMux(map[string]http.Handler{
"GET": applyMiddlewares( "GET": applyMiddlewares(
mux, mux,
), ),
@ -262,11 +275,10 @@ func (a *api) handler() http.Handler {
"Pragma": "no-cache", "Pragma": "no-cache",
"Expires": "0", "Expires": "0",
}), }),
logReqMiddleware,
), ),
}) }),
setLoggerMiddleware(a.params.Logger),
h = setLoggerMiddleware(a.params.Logger)(h) )
return h return h
} }

View File

@ -120,6 +120,9 @@ func RandStr(numBytes int) string {
// //
// If the method "*" is defined then all methods not defined will be directed to // If the method "*" is defined then all methods not defined will be directed to
// that handler, and 405 Method Not Allowed is never returned. // that handler, and 405 Method Not Allowed is never returned.
//
// If the GET argument 'method' is present then the ToUpper of that is taken to
// be the name of the method.
func MethodMux(handlers map[string]http.Handler) http.Handler { func MethodMux(handlers map[string]http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
@ -127,7 +130,7 @@ func MethodMux(handlers map[string]http.Handler) http.Handler {
method := strings.ToUpper(r.Method) method := strings.ToUpper(r.Method)
formMethod := strings.ToUpper(r.FormValue("method")) formMethod := strings.ToUpper(r.FormValue("method"))
if method == "POST" && formMethod != "" { if formMethod != "" {
method = formMethod method = formMethod
} }

View File

@ -59,9 +59,9 @@ func resizeImage(out io.Writer, in io.Reader, maxWidth float64) error {
} }
} }
func (a *api) renderPostAssetsIndexHandler() http.Handler { func (a *api) managePostAssetsHandler() http.Handler {
tpl := a.mustParseBasedTpl("assets.html") tpl := a.mustParseBasedTpl("post-assets-manage.html")
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
@ -86,17 +86,10 @@ func (a *api) renderPostAssetsIndexHandler() http.Handler {
func (a *api) getPostAssetHandler() http.Handler { func (a *api) getPostAssetHandler() http.Handler {
renderIndexHandler := a.renderPostAssetsIndexHandler()
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
id := filepath.Base(r.URL.Path) id := filepath.Base(r.URL.Path)
if id == "/" {
renderIndexHandler.ServeHTTP(rw, r)
return
}
maxWidth, err := apiutil.StrToInt(r.FormValue("w"), 0) maxWidth, err := apiutil.StrToInt(r.FormValue("w"), 0)
if err != nil { if err != nil {
apiutil.BadRequest(rw, r, fmt.Errorf("invalid w parameter: %w", err)) apiutil.BadRequest(rw, r, fmt.Errorf("invalid w parameter: %w", err))
@ -172,7 +165,7 @@ func (a *api) postPostAssetHandler() http.Handler {
return return
} }
a.executeRedirectTpl(rw, r, a.assetsURL(false)) a.executeRedirectTpl(rw, r, a.manageAssetsURL(false))
}) })
} }
@ -199,6 +192,6 @@ func (a *api) deletePostAssetHandler() http.Handler {
return return
} }
a.executeRedirectTpl(rw, r, a.assetsURL(false)) a.executeRedirectTpl(rw, r, a.manageAssetsURL(false))
}) })
} }

View File

@ -1,77 +1,20 @@
package http package http
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"path/filepath"
"strings"
"github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil" "github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil"
"github.com/mediocregopher/blog.mediocregopher.com/srv/post" "github.com/mediocregopher/blog.mediocregopher.com/srv/post"
) )
func (a *api) renderDraftPostHandler() http.Handler { func (a *api) manageDraftPostsHandler() http.Handler {
tpl := a.mustParseBasedTpl("post.html") tpl := a.mustParseBasedTpl("draft-posts-manage.html")
renderDraftPostsIndexHandler := a.renderDraftPostsIndexHandler()
renderDraftEditPostHandler := a.renderEditPostHandler(true)
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
id := strings.TrimSuffix(filepath.Base(r.URL.Path), ".html")
if id == "/" {
renderDraftPostsIndexHandler.ServeHTTP(rw, r)
return
}
if _, ok := r.URL.Query()["edit"]; ok {
renderDraftEditPostHandler.ServeHTTP(rw, r)
return
}
p, err := a.params.PostDraftStore.GetByID(id)
if errors.Is(err, post.ErrPostNotFound) {
http.Error(rw, "Post not found", 404)
return
} else if err != nil {
apiutil.InternalServerError(
rw, r, fmt.Errorf("fetching post with id %q: %w", id, err),
)
return
}
tplPayload, err := a.postToPostTplPayload(post.StoredPost{Post: p})
if err != nil {
apiutil.InternalServerError(
rw, r, fmt.Errorf(
"generating template payload for post with id %q: %w",
id, err,
),
)
return
}
executeTemplate(rw, r, tpl, tplPayload)
})
}
func (a *api) renderDraftPostsIndexHandler() http.Handler {
renderEditPostHandler := a.renderEditPostHandler(true)
tpl := a.mustParseBasedTpl("draft-posts.html")
const pageCount = 20 const pageCount = 20
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if _, ok := r.URL.Query()["edit"]; ok {
renderEditPostHandler.ServeHTTP(rw, r)
return
}
page, err := apiutil.StrToInt(r.FormValue("p"), 0) page, err := apiutil.StrToInt(r.FormValue("p"), 0)
if err != nil { if err != nil {
apiutil.BadRequest( apiutil.BadRequest(
@ -125,6 +68,6 @@ func (a *api) postDraftPostHandler() http.Handler {
return return
} }
a.executeRedirectTpl(rw, r, a.draftURL(p.ID, false)+"?edit") a.executeRedirectTpl(rw, r, a.editDraftPostURL(p.ID, false))
}) })
} }

View File

@ -123,26 +123,14 @@ func (a *api) postToPostTplPayload(storedPost post.StoredPost) (postTplPayload,
return tplPayload, nil return tplPayload, nil
} }
func (a *api) renderPostHandler() http.Handler { func (a *api) getPostHandler() http.Handler {
tpl := a.mustParseBasedTpl("post.html") tpl := a.mustParseBasedTpl("post.html")
renderPostsIndexHandler := a.renderPostsIndexHandler()
renderEditPostHandler := a.renderEditPostHandler(false)
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
id := strings.TrimSuffix(filepath.Base(r.URL.Path), ".html") id := strings.TrimSuffix(filepath.Base(r.URL.Path), ".html")
if id == "/" {
renderPostsIndexHandler.ServeHTTP(rw, r)
return
}
if _, ok := r.URL.Query()["edit"]; ok {
renderEditPostHandler.ServeHTTP(rw, r)
return
}
storedPost, err := a.params.PostStore.GetByID(id) storedPost, err := a.params.PostStore.GetByID(id)
if errors.Is(err, post.ErrPostNotFound) { if errors.Is(err, post.ErrPostNotFound) {
@ -171,19 +159,13 @@ func (a *api) renderPostHandler() http.Handler {
}) })
} }
func (a *api) renderPostsIndexHandler() http.Handler { func (a *api) managePostsHandler() http.Handler {
renderEditPostHandler := a.renderEditPostHandler(false) tpl := a.mustParseBasedTpl("posts-manage.html")
tpl := a.mustParseBasedTpl("posts.html")
const pageCount = 20 const pageCount = 20
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if _, ok := r.URL.Query()["edit"]; ok {
renderEditPostHandler.ServeHTTP(rw, r)
return
}
page, err := apiutil.StrToInt(r.FormValue("p"), 0) page, err := apiutil.StrToInt(r.FormValue("p"), 0)
if err != nil { if err != nil {
apiutil.BadRequest( apiutil.BadRequest(
@ -221,20 +203,26 @@ func (a *api) renderPostsIndexHandler() http.Handler {
}) })
} }
func (a *api) renderEditPostHandler(isDraft bool) http.Handler { func (a *api) editPostHandler(isDraft bool) http.Handler {
tpl := a.mustParseBasedTpl("edit-post.html") tpl := a.mustParseBasedTpl("post-edit.html")
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
id := filepath.Base(r.URL.Path) id := filepath.Base(r.URL.Path)
var storedPost post.StoredPost if id == "/" && !isDraft {
http.Error(rw, "Post id required", 400)
return
}
var (
storedPost post.StoredPost
err error
)
if id != "/" { if id != "/" {
var err error
if isDraft { if isDraft {
storedPost.Post, err = a.params.PostDraftStore.GetByID(id) storedPost.Post, err = a.params.PostDraftStore.GetByID(id)
} else { } else {
@ -250,10 +238,6 @@ func (a *api) renderEditPostHandler(isDraft bool) http.Handler {
) )
return return
} }
} else if !isDraft {
http.Error(rw, "Post ID required in URL", 400)
return
} }
tags, err := a.params.PostStore.GetTags() tags, err := a.params.PostStore.GetTags()
@ -348,7 +332,7 @@ func (a *api) postPostHandler() http.Handler {
return return
} }
a.executeRedirectTpl(rw, r, a.postURL(p.ID, false)) a.executeRedirectTpl(rw, r, a.editPostURL(p.ID, false))
}) })
} }
@ -382,9 +366,9 @@ func (a *api) deletePostHandler(isDraft bool) http.Handler {
} }
if isDraft { if isDraft {
a.executeRedirectTpl(rw, r, a.draftsURL(false)) a.executeRedirectTpl(rw, r, a.manageDraftPostsURL(false))
} else { } else {
a.executeRedirectTpl(rw, r, a.postsURL(false)) a.executeRedirectTpl(rw, r, a.managePostsURL(false))
} }
}) })
} }

View File

@ -49,19 +49,31 @@ func (a *api) postURL(id string, abs bool) string {
return a.blogURL(path, abs) return a.blogURL(path, abs)
} }
func (a *api) postsURL(abs bool) string { func (a *api) editPostURL(id string, abs bool) string {
return a.blogURL("posts", abs) return a.postURL(id, abs) + "?method=edit"
} }
func (a *api) assetsURL(abs bool) string { func (a *api) managePostsURL(abs bool) string {
return a.blogURL("assets", abs) return a.blogURL("posts?method=manage", abs)
} }
func (a *api) draftURL(id string, abs bool) string { func (a *api) manageAssetsURL(abs bool) string {
return a.blogURL("assets?method=manage", abs)
}
func (a *api) draftPostURL(id string, abs bool) string {
path := filepath.Join("drafts", id) path := filepath.Join("drafts", id)
return a.blogURL(path, abs) return a.blogURL(path, abs)
} }
func (a *api) editDraftPostURL(id string, abs bool) string {
return a.draftPostURL(id, abs) + "?method=edit"
}
func (a *api) manageDraftPostsURL(abs bool) string {
return a.blogURL("drafts", abs) + "?method=manage"
}
func (a *api) draftsURL(abs bool) string { func (a *api) draftsURL(abs bool) string {
return a.blogURL("drafts", abs) return a.blogURL("drafts", abs)
} }
@ -88,7 +100,7 @@ func (a *api) tplFuncs() template.FuncMap {
return a.blogURL(path, false) return a.blogURL(path, false)
}, },
"DraftURL": func(id string) string { "DraftURL": func(id string) string {
return a.draftURL(id, false) return a.draftPostURL(id, false)
}, },
"DateTimeFormat": func(t time.Time) string { "DateTimeFormat": func(t time.Time) string {
return t.Format("2006-01-02") return t.Format("2006-01-02")

View File

@ -7,9 +7,9 @@ mostly left open to inspection, but you will not able to change
anything without providing credentials. anything without providing credentials.
<ul> <ul>
<li><a href="{{ BlogURL "posts" }}">Posts</a></li> <li><a href="{{ BlogURL "posts?method=manage" }}">Posts</a></li>
<li><a href="{{ BlogURL "assets" }}">Assets</a></li> <li><a href="{{ BlogURL "assets?method=manage" }}">Assets</a></li>
<li><a href="{{ BlogURL "drafts" }}">Drafts</a> (private)</li> <li><a href="{{ BlogURL "drafts?method=manage" }}">Drafts</a> (private)</li>
</ul> </ul>
{{ end }} {{ end }}

View File

@ -7,7 +7,7 @@
<h1>Drafts</h1> <h1>Drafts</h1>
<p> <p>
<a href="{{ BlogURL "drafts/" }}?edit">New Draft</a> <a href="{{ BlogURL "drafts" }}?method=edit">New Draft</a>
</p> </p>
{{ if ge .Payload.PrevPage 0 }} {{ if ge .Payload.PrevPage 0 }}
@ -20,9 +20,9 @@
{{ range .Payload.Posts }} {{ range .Payload.Posts }}
<tr> <tr>
<td><a href="{{ DraftURL .ID }}">{{ .Title }}</a></td> <td>{{ .Title }}</td>
<td> <td>
<a href="{{ DraftURL .ID }}?edit"> <a href="{{ DraftURL .ID }}?method=edit">
Edit Edit
</a> </a>
</td> </td>

View File

@ -2,11 +2,11 @@
<p> <p>
{{ if .Payload.IsDraft }} {{ if .Payload.IsDraft }}
<a href="{{ BlogURL "drafts/" }}"> <a href="{{ BlogURL "drafts?method=manage" }}">
Back to Drafts Back to Drafts
</a> </a>
{{ else }} {{ else }}
<a href="{{ BlogURL "posts/" }}"> <a href="{{ BlogURL "posts?method=manage" }}">
Back to Posts Back to Posts
</a> </a>
{{ end }} {{ end }}

View File

@ -19,7 +19,7 @@
<td>{{ .PublishedAt.Local.Format "2006-01-02 15:04:05 MST" }}</td> <td>{{ .PublishedAt.Local.Format "2006-01-02 15:04:05 MST" }}</td>
<td><a href="{{ PostURL .ID }}">{{ .Title }}</a></td> <td><a href="{{ PostURL .ID }}">{{ .Title }}</a></td>
<td> <td>
<a href="{{ PostURL .ID }}?edit"> <a href="{{ PostURL .ID }}?method=edit">
Edit Edit
</a> </a>
</td> </td>