drafts functionality added, needs a publish button still

This commit is contained in:
Brian Picciano 2022-08-18 23:07:09 -06:00
parent dfa9bcb9e2
commit c3135306b3
9 changed files with 239 additions and 16 deletions

View File

@ -121,11 +121,13 @@ func main() {
postStore := post.NewStore(postSQLDB) postStore := post.NewStore(postSQLDB)
postAssetStore := post.NewAssetStore(postSQLDB) postAssetStore := post.NewAssetStore(postSQLDB)
postDraftStore := post.NewDraftStore(postSQLDB)
httpParams.Logger = logger.WithNamespace("http") httpParams.Logger = logger.WithNamespace("http")
httpParams.PowManager = powMgr httpParams.PowManager = powMgr
httpParams.PostStore = postStore httpParams.PostStore = postStore
httpParams.PostAssetStore = postAssetStore httpParams.PostAssetStore = postAssetStore
httpParams.PostDraftStore = postDraftStore
httpParams.MailingList = ml httpParams.MailingList = ml
httpParams.GlobalRoom = chatGlobalRoom httpParams.GlobalRoom = chatGlobalRoom
httpParams.UserIDCalculator = chatUserIDCalc httpParams.UserIDCalculator = chatUserIDCalc

View File

@ -37,6 +37,7 @@ type Params struct {
PostStore post.Store PostStore post.Store
PostAssetStore post.AssetStore PostAssetStore post.AssetStore
PostDraftStore post.DraftStore
MailingList mailinglist.MailingList MailingList mailinglist.MailingList
@ -201,8 +202,8 @@ 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.renderPostHandler(),
"POST": a.postPostHandler(), "POST": a.postPostHandler(false),
"DELETE": a.deletePostHandler(), "DELETE": a.deletePostHandler(false),
"PREVIEW": a.previewPostHandler(), "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("/static/", http.FileServer(http.FS(staticFS)))
mux.Handle("/follow", a.renderDumbTplHandler("follow.html")) mux.Handle("/follow", a.renderDumbTplHandler("follow.html"))
mux.Handle("/admin", a.renderDumbTplHandler("admin.html")) mux.Handle("/admin", a.renderDumbTplHandler("admin.html"))

110
srv/src/http/drafts.go Normal file
View File

@ -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)
})
}

View File

@ -18,7 +18,7 @@ import (
"github.com/mediocregopher/blog.mediocregopher.com/srv/post" "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 := txttpl.New("root")
tpl = tpl.Funcs(txttpl.FuncMap(a.tplFuncs())) 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 { if err != nil {
return nil, err return nil, err
@ -60,7 +60,7 @@ type postTplPayload struct {
func (a *api) postToPostTplPayload(storedPost post.StoredPost) (postTplPayload, error) { func (a *api) postToPostTplPayload(storedPost post.StoredPost) (postTplPayload, error) {
bodyTpl, err := a.parsePostBody(storedPost) bodyTpl, err := a.parsePostBody(storedPost.Post)
if err != nil { if err != nil {
return postTplPayload{}, fmt.Errorf("parsing post body as template: %w", err) 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 != "/" { if id != "/" {
var err error 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) { if errors.Is(err, post.ErrPostNotFound) {
http.Error(rw, "Post not found", 404) http.Error(rw, "Post not found", 404)
@ -291,7 +296,7 @@ func postFromPostReq(r *http.Request) (post.Post, error) {
return p, nil 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) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
@ -301,7 +306,13 @@ func (a *api) postPostHandler() http.Handler {
return 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 { if err != nil {
apiutil.InternalServerError( apiutil.InternalServerError(
@ -310,7 +321,7 @@ func (a *api) postPostHandler() http.Handler {
return return
} }
if first { if !isDraft && first {
a.params.Logger.Info(r.Context(), "publishing blog post to mailing list") a.params.Logger.Info(r.Context(), "publishing blog post to mailing list")
urlStr := a.postURL(p.ID, true) 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) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
@ -338,7 +353,13 @@ func (a *api) deletePostHandler() http.Handler {
return 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) { if errors.Is(err, post.ErrPostNotFound) {
http.Error(rw, "Post not found", 404) http.Error(rw, "Post not found", 404)
@ -350,8 +371,11 @@ func (a *api) deletePostHandler() http.Handler {
return 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))
}
}) })
} }

View File

@ -57,6 +57,15 @@ func (a *api) assetsURL(abs bool) string {
return a.blogURL("assets", abs) 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 { func (a *api) tplFuncs() template.FuncMap {
return template.FuncMap{ return template.FuncMap{
"BlogURL": func(path string) string { "BlogURL": func(path string) string {
@ -71,12 +80,15 @@ func (a *api) tplFuncs() template.FuncMap {
b, err := staticFS.ReadFile(path) b, err := staticFS.ReadFile(path)
return template.CSS(b), err return template.CSS(b), err
}, },
"PostURL": func(id string) string {
return a.postURL(id, false)
},
"AssetURL": func(id string) string { "AssetURL": func(id string) string {
path := filepath.Join("assets", id) path := filepath.Join("assets", id)
return a.blogURL(path, false) return a.blogURL(path, false)
}, },
"PostURL": func(id string) string { "DraftURL": func(id string) string {
return a.postURL(id, false) return a.draftURL(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

@ -9,6 +9,7 @@ anything without providing credentials.
<ul> <ul>
<li><a href="{{ BlogURL "posts" }}">Posts</a></li> <li><a href="{{ BlogURL "posts" }}">Posts</a></li>
<li><a href="{{ BlogURL "assets" }}">Assets</a></li> <li><a href="{{ BlogURL "assets" }}">Assets</a></li>
<li><a href="{{ BlogURL "drafts" }}">Drafts (private)</a></li>
</ul> </ul>
{{ end }} {{ end }}

View File

@ -0,0 +1,48 @@
{{ define "body" }}
<h1>Drafts</h1>
<p>
<a href="{{ BlogURL "drafts/" }}?edit">
New Draft
</a>
</p>
{{ if ge .Payload.PrevPage 0 }}
<p>
<a href="?p={{ .Payload.PrevPage}}">&lt; &lt; Previous Page</a>
</p>
{{ end }}
<table>
{{ range .Payload.Posts }}
<tr>
<td><a href="{{ DraftURL .ID }}">{{ .Title }}</a></td>
<td>
<a href="{{ DraftURL .ID }}?edit">
Edit
</a>
</td>
<td>
<form
action="{{ DraftURL .ID }}?method=delete"
method="POST"
>
<input type="submit" value="Delete" />
</form>
</td>
</tr>
{{ end }}
</table>
{{ if ge .Payload.NextPage 0 }}
<p>
<a href="?p={{ .Payload.NextPage}}">Next Page &gt; &gt;</a>
</p>
{{ end }}
{{ end }}
{{ template "base.html" . }}

View File

@ -15,6 +15,9 @@
type="text" type="text"
placeholder="e.g. how-to-fly-a-kite" placeholder="e.g. how-to-fly-a-kite"
value="{{ .Payload.Post.ID }}" /> value="{{ .Payload.Post.ID }}" />
{{ else if .Payload.IsDraft }}
{{ .Payload.Post.ID }}
<input name="id" type="hidden" value="{{ .Payload.Post.ID }}" />
{{ else }} {{ else }}
<a href="{{ PostURL .Payload.Post.ID }}">{{ .Payload.Post.ID }}</a> <a href="{{ PostURL .Payload.Post.ID }}">{{ .Payload.Post.ID }}</a>
<input name="id" type="hidden" value="{{ .Payload.Post.ID }}" /> <input name="id" type="hidden" value="{{ .Payload.Post.ID }}" />
@ -107,10 +110,17 @@
</form> </form>
<p> <p>
{{ if .Payload.IsDraft }}
<a href="{{ BlogURL "drafts/" }}">
Back to Drafts
</a>
{{ else }}
<a href="{{ BlogURL "posts/" }}"> <a href="{{ BlogURL "posts/" }}">
Back to Posts Back to Posts
</a> </a>
{{ end }}
</p> </p>
{{ end }} {{ end }}
{{ template "base.html" . }} {{ template "base.html" . }}

View File

@ -88,6 +88,7 @@ func (s *draftStore) get(
SELECT SELECT
p.id, p.title, p.description, p.tags, p.series, p.body p.id, p.title, p.description, p.tags, p.series, p.body
FROM post_drafts p FROM post_drafts p
` + where + `
ORDER BY p.id ASC` ORDER BY p.id ASC`
if limit > 0 { if limit > 0 {