From c3135306b32e3ee18c3c412fbb2ea81b455201d5 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Thu, 18 Aug 2022 23:07:09 -0600 Subject: [PATCH] drafts functionality added, needs a publish button still --- srv/src/cmd/mediocre-blog/main.go | 2 + srv/src/http/api.go | 19 +++++- srv/src/http/drafts.go | 110 ++++++++++++++++++++++++++++++ srv/src/http/posts.go | 48 +++++++++---- srv/src/http/tpl.go | 16 ++++- srv/src/http/tpl/admin.html | 1 + srv/src/http/tpl/draft-posts.html | 48 +++++++++++++ srv/src/http/tpl/edit-post.html | 10 +++ srv/src/post/draft_post.go | 1 + 9 files changed, 239 insertions(+), 16 deletions(-) create mode 100644 srv/src/http/drafts.go create mode 100644 srv/src/http/tpl/draft-posts.html diff --git a/srv/src/cmd/mediocre-blog/main.go b/srv/src/cmd/mediocre-blog/main.go index b855110..694dd3f 100644 --- a/srv/src/cmd/mediocre-blog/main.go +++ b/srv/src/cmd/mediocre-blog/main.go @@ -121,11 +121,13 @@ func main() { postStore := post.NewStore(postSQLDB) postAssetStore := post.NewAssetStore(postSQLDB) + postDraftStore := post.NewDraftStore(postSQLDB) httpParams.Logger = logger.WithNamespace("http") httpParams.PowManager = powMgr httpParams.PostStore = postStore httpParams.PostAssetStore = postAssetStore + httpParams.PostDraftStore = postDraftStore httpParams.MailingList = ml httpParams.GlobalRoom = chatGlobalRoom httpParams.UserIDCalculator = chatUserIDCalc diff --git a/srv/src/http/api.go b/srv/src/http/api.go index 4ba6450..eb7990f 100644 --- a/srv/src/http/api.go +++ b/srv/src/http/api.go @@ -37,6 +37,7 @@ type Params struct { PostStore post.Store PostAssetStore post.AssetStore + PostDraftStore post.DraftStore MailingList mailinglist.MailingList @@ -201,8 +202,8 @@ func (a *api) blogHandler() http.Handler { mux.Handle("/posts/", http.StripPrefix("/posts", apiutil.MethodMux(map[string]http.Handler{ "GET": a.renderPostHandler(), - "POST": a.postPostHandler(), - "DELETE": a.deletePostHandler(), + "POST": a.postPostHandler(false), + "DELETE": a.deletePostHandler(false), "PREVIEW": a.previewPostHandler(), }), )) @@ -215,6 +216,20 @@ func (a *api) blogHandler() http.Handler { }), )) + mux.Handle("/drafts/", http.StripPrefix("/drafts", + + // everything to do with drafts is protected + authMiddleware(a.auther)( + + apiutil.MethodMux(map[string]http.Handler{ + "GET": a.renderDraftPostHandler(), + "POST": a.postPostHandler(true), + "DELETE": a.deletePostHandler(true), + "PREVIEW": a.previewPostHandler(), + }), + ), + )) + mux.Handle("/static/", http.FileServer(http.FS(staticFS))) mux.Handle("/follow", a.renderDumbTplHandler("follow.html")) mux.Handle("/admin", a.renderDumbTplHandler("admin.html")) diff --git a/srv/src/http/drafts.go b/srv/src/http/drafts.go new file mode 100644 index 0000000..8bb08e5 --- /dev/null +++ b/srv/src/http/drafts.go @@ -0,0 +1,110 @@ +package http + +import ( + "errors" + "fmt" + "net/http" + "path/filepath" + "strings" + + "github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil" + "github.com/mediocregopher/blog.mediocregopher.com/srv/post" +) + +func (a *api) renderDraftPostHandler() http.Handler { + + tpl := a.mustParseBasedTpl("post.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 + + 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) + if err != nil { + apiutil.BadRequest( + rw, r, fmt.Errorf("invalid page number: %w", err), + ) + return + } + + posts, hasMore, err := a.params.PostDraftStore.Get(page, pageCount) + if err != nil { + apiutil.InternalServerError( + rw, r, fmt.Errorf("fetching page %d of posts: %w", page, err), + ) + return + } + + tplPayload := struct { + Posts []post.Post + PrevPage, NextPage int + }{ + Posts: posts, + PrevPage: -1, + NextPage: -1, + } + + if page > 0 { + tplPayload.PrevPage = page - 1 + } + + if hasMore { + tplPayload.NextPage = page + 1 + } + + executeTemplate(rw, r, tpl, tplPayload) + }) +} diff --git a/srv/src/http/posts.go b/srv/src/http/posts.go index 8484817..278ae43 100644 --- a/srv/src/http/posts.go +++ b/srv/src/http/posts.go @@ -18,7 +18,7 @@ import ( "github.com/mediocregopher/blog.mediocregopher.com/srv/post" ) -func (a *api) parsePostBody(storedPost post.StoredPost) (*txttpl.Template, error) { +func (a *api) parsePostBody(post post.Post) (*txttpl.Template, error) { tpl := txttpl.New("root") tpl = tpl.Funcs(txttpl.FuncMap(a.tplFuncs())) @@ -43,7 +43,7 @@ func (a *api) parsePostBody(storedPost post.StoredPost) (*txttpl.Template, error }, }) - tpl, err := tpl.New(storedPost.ID + "-body.html").Parse(storedPost.Body) + tpl, err := tpl.New(post.ID + "-body.html").Parse(post.Body) if err != nil { return nil, err @@ -60,7 +60,7 @@ type postTplPayload struct { func (a *api) postToPostTplPayload(storedPost post.StoredPost) (postTplPayload, error) { - bodyTpl, err := a.parsePostBody(storedPost) + bodyTpl, err := a.parsePostBody(storedPost.Post) if err != nil { return postTplPayload{}, fmt.Errorf("parsing post body as template: %w", err) } @@ -232,7 +232,12 @@ func (a *api) renderEditPostHandler(isDraft bool) http.Handler { if id != "/" { var err error - storedPost, err = a.params.PostStore.GetByID(id) + + if isDraft { + storedPost.Post, err = a.params.PostDraftStore.GetByID(id) + } else { + storedPost, err = a.params.PostStore.GetByID(id) + } if errors.Is(err, post.ErrPostNotFound) { http.Error(rw, "Post not found", 404) @@ -291,7 +296,7 @@ func postFromPostReq(r *http.Request) (post.Post, error) { return p, nil } -func (a *api) postPostHandler() http.Handler { +func (a *api) postPostHandler(isDraft bool) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { @@ -301,7 +306,13 @@ func (a *api) postPostHandler() http.Handler { return } - first, err := a.params.PostStore.Set(p, time.Now()) + var first bool + + if isDraft { + err = a.params.PostDraftStore.Set(p) + } else { + first, err = a.params.PostStore.Set(p, time.Now()) + } if err != nil { apiutil.InternalServerError( @@ -310,7 +321,7 @@ func (a *api) postPostHandler() http.Handler { return } - if first { + if !isDraft && first { a.params.Logger.Info(r.Context(), "publishing blog post to mailing list") urlStr := a.postURL(p.ID, true) @@ -323,11 +334,15 @@ func (a *api) postPostHandler() http.Handler { } } - a.executeRedirectTpl(rw, r, a.postURL(p.ID, false)+"?edit") + if isDraft { + a.executeRedirectTpl(rw, r, a.draftURL(p.ID, false)+"?edit") + } else { + a.executeRedirectTpl(rw, r, a.postURL(p.ID, false)+"?edit") + } }) } -func (a *api) deletePostHandler() http.Handler { +func (a *api) deletePostHandler(isDraft bool) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { @@ -338,7 +353,13 @@ func (a *api) deletePostHandler() http.Handler { return } - err := a.params.PostStore.Delete(id) + var err error + + if isDraft { + err = a.params.PostDraftStore.Delete(id) + } else { + err = a.params.PostStore.Delete(id) + } if errors.Is(err, post.ErrPostNotFound) { http.Error(rw, "Post not found", 404) @@ -350,8 +371,11 @@ func (a *api) deletePostHandler() http.Handler { return } - a.executeRedirectTpl(rw, r, a.postsURL(false)) - + if isDraft { + a.executeRedirectTpl(rw, r, a.draftsURL(false)) + } else { + a.executeRedirectTpl(rw, r, a.postsURL(false)) + } }) } diff --git a/srv/src/http/tpl.go b/srv/src/http/tpl.go index 2edd7ac..3e1a2ba 100644 --- a/srv/src/http/tpl.go +++ b/srv/src/http/tpl.go @@ -57,6 +57,15 @@ func (a *api) assetsURL(abs bool) string { return a.blogURL("assets", abs) } +func (a *api) draftURL(id string, abs bool) string { + path := filepath.Join("drafts", id) + return a.blogURL(path, abs) +} + +func (a *api) draftsURL(abs bool) string { + return a.blogURL("drafts", abs) +} + func (a *api) tplFuncs() template.FuncMap { return template.FuncMap{ "BlogURL": func(path string) string { @@ -71,12 +80,15 @@ func (a *api) tplFuncs() template.FuncMap { b, err := staticFS.ReadFile(path) return template.CSS(b), err }, + "PostURL": func(id string) string { + return a.postURL(id, false) + }, "AssetURL": func(id string) string { path := filepath.Join("assets", id) return a.blogURL(path, false) }, - "PostURL": func(id string) string { - return a.postURL(id, false) + "DraftURL": func(id string) string { + return a.draftURL(id, false) }, "DateTimeFormat": func(t time.Time) string { return t.Format("2006-01-02") diff --git a/srv/src/http/tpl/admin.html b/srv/src/http/tpl/admin.html index 24b2770..3b10675 100644 --- a/srv/src/http/tpl/admin.html +++ b/srv/src/http/tpl/admin.html @@ -9,6 +9,7 @@ anything without providing credentials. {{ end }} diff --git a/srv/src/http/tpl/draft-posts.html b/srv/src/http/tpl/draft-posts.html new file mode 100644 index 0000000..f89fac5 --- /dev/null +++ b/srv/src/http/tpl/draft-posts.html @@ -0,0 +1,48 @@ +{{ define "body" }} + +

Drafts

+ +

+ + New Draft + +

+ + {{ if ge .Payload.PrevPage 0 }} +

+ < < Previous Page +

+ {{ end }} + + + + {{ range .Payload.Posts }} + + + + + + {{ end }} + +
{{ .Title }} + + Edit + + +
+ +
+
+ + {{ if ge .Payload.NextPage 0 }} +

+ Next Page > > +

+ {{ end }} + +{{ end }} + +{{ template "base.html" . }} diff --git a/srv/src/http/tpl/edit-post.html b/srv/src/http/tpl/edit-post.html index a585b82..ea1f2c1 100644 --- a/srv/src/http/tpl/edit-post.html +++ b/srv/src/http/tpl/edit-post.html @@ -15,6 +15,9 @@ type="text" placeholder="e.g. how-to-fly-a-kite" value="{{ .Payload.Post.ID }}" /> + {{ else if .Payload.IsDraft }} + {{ .Payload.Post.ID }} + {{ else }} {{ .Payload.Post.ID }} @@ -107,10 +110,17 @@

+ {{ if .Payload.IsDraft }} + + Back to Drafts + + {{ else }} Back to Posts + {{ end }}

+ {{ end }} {{ template "base.html" . }} diff --git a/srv/src/post/draft_post.go b/srv/src/post/draft_post.go index af52965..61283c3 100644 --- a/srv/src/post/draft_post.go +++ b/srv/src/post/draft_post.go @@ -88,6 +88,7 @@ func (s *draftStore) get( SELECT p.id, p.title, p.description, p.tags, p.series, p.body FROM post_drafts p + ` + where + ` ORDER BY p.id ASC` if limit > 0 {