summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--srv/src/cmd/mediocre-blog/main.go2
-rw-r--r--srv/src/http/api.go19
-rw-r--r--srv/src/http/drafts.go110
-rw-r--r--srv/src/http/posts.go48
-rw-r--r--srv/src/http/tpl.go16
-rw-r--r--srv/src/http/tpl/admin.html1
-rw-r--r--srv/src/http/tpl/draft-posts.html48
-rw-r--r--srv/src/http/tpl/edit-post.html10
-rw-r--r--srv/src/post/draft_post.go1
9 files changed, 239 insertions, 16 deletions
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.
<ul>
<li><a href="{{ BlogURL "posts" }}">Posts</a></li>
<li><a href="{{ BlogURL "assets" }}">Assets</a></li>
+ <li><a href="{{ BlogURL "drafts" }}">Drafts (private)</a></li>
</ul>
{{ 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" }}
+
+ <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" . }}
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 }}
+ <input name="id" type="hidden" value="{{ .Payload.Post.ID }}" />
{{ else }}
<a href="{{ PostURL .Payload.Post.ID }}">{{ .Payload.Post.ID }}</a>
<input name="id" type="hidden" value="{{ .Payload.Post.ID }}" />
@@ -107,10 +110,17 @@
</form>
<p>
+ {{ if .Payload.IsDraft }}
+ <a href="{{ BlogURL "drafts/" }}">
+ Back to Drafts
+ </a>
+ {{ else }}
<a href="{{ BlogURL "posts/" }}">
Back to Posts
</a>
+ {{ end }}
</p>
+
{{ 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 {