gophertype/pkg/controllers/posts.go
Noah 5b6712ea97 Blog Archive, RSS Feeds, and Model Cleanup
* Legacy-importer tool updates the DB primary key serial after migrating
  the posts, to be max(posts.id)+1 -- especially important for
  PostgreSQL and MySQL (SQLite3 correctly picked the next ID by
  default?)
* Add blog archive page and RSS, Atom and JSON feeds for the blog.
  URLs are /blog.rss, /blog.atom and /blog.json
2020-02-17 18:10:35 -08:00

266 lines
6.6 KiB
Go

package controllers
import (
"bytes"
"fmt"
"html/template"
"net/http"
"strconv"
"strings"
"time"
"git.kirsle.net/apps/gophertype/pkg/authentication"
"git.kirsle.net/apps/gophertype/pkg/glue"
"git.kirsle.net/apps/gophertype/pkg/markdown"
"git.kirsle.net/apps/gophertype/pkg/models"
"git.kirsle.net/apps/gophertype/pkg/responses"
"git.kirsle.net/apps/gophertype/pkg/session"
"git.kirsle.net/apps/gophertype/pkg/settings"
"github.com/albrow/forms"
"github.com/gorilla/mux"
)
func init() {
glue.Register(glue.Endpoint{
Path: "/blog",
Methods: []string{"GET"},
Handler: BlogIndex(models.Public, false),
})
glue.Register(glue.Endpoint{
Path: "/tagged",
Methods: []string{"GET"},
Handler: TagIndex,
})
glue.Register(glue.Endpoint{
Path: "/tagged/{tag}",
Methods: []string{"GET"},
Handler: BlogIndex(models.Public, true),
})
glue.Register(glue.Endpoint{
Path: "/archive",
Methods: []string{"GET"},
Handler: BlogArchive,
})
glue.Register(glue.Endpoint{
Path: "/blog/drafts",
Middleware: []mux.MiddlewareFunc{
authentication.LoginRequired,
},
Methods: []string{"GET"},
Handler: BlogIndex(models.Draft, false),
})
glue.Register(glue.Endpoint{
Path: "/blog/private",
Middleware: []mux.MiddlewareFunc{
authentication.LoginRequired,
},
Methods: []string{"GET"},
Handler: BlogIndex(models.Private, false),
})
glue.Register(glue.Endpoint{
Path: "/blog/unlisted",
Middleware: []mux.MiddlewareFunc{
authentication.LoginRequired,
},
Methods: []string{"GET"},
Handler: BlogIndex(models.Unlisted, false),
})
glue.Register(glue.Endpoint{
Path: "/blog/edit",
Methods: []string{"GET", "POST"},
Middleware: []mux.MiddlewareFunc{
authentication.LoginRequired,
},
Handler: EditPost,
})
}
// BlogIndex handles all of the top-level blog index routes:
// - /blog
// - /tagged/{tag}
// - /blog/unlisted
// - /blog/drafts
// - /blog/private
func BlogIndex(privacy string, tagged bool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var (
v = responses.NewTemplateVars(w, r)
tagName string
)
// Tagged view?
if tagged {
params := mux.Vars(r)
tagName = params["tag"]
}
// Page title to use.
var title = "Blog"
if tagged {
title = "Tagged as: " + tagName
} else if privacy == models.Draft {
title = "Drafts"
} else if privacy == models.Unlisted {
title = "Unlisted"
} else if privacy == models.Private {
title = "Private"
}
v.V["title"] = title
v.V["tag"] = tagName
v.V["privacy"] = privacy
responses.RenderTemplate(w, r, "_builtin/blog/index.gohtml", v)
}
}
// PostFragment at "/<fragment>" for viewing blog entries.
func PostFragment(w http.ResponseWriter, r *http.Request) {
fragment := strings.Trim(r.URL.Path, "/")
post, err := models.Posts.LoadFragment(fragment)
if err != nil {
responses.NotFound(w, r)
return
}
// Is it a private post and are we logged in?
if post.Privacy != models.Public && post.Privacy != models.Unlisted && !authentication.LoggedIn(r) {
responses.Forbidden(w, r, "Permission denied to view that post.")
return
}
v := responses.NewTemplateVars(w, r)
v.V["post"] = post
// Render the body.
if post.ContentType == models.Markdown {
v.V["rendered"] = template.HTML(markdown.RenderTrustedMarkdown(post.Body))
} else {
v.V["rendered"] = template.HTML(post.Body)
}
responses.RenderTemplate(w, r, "_builtin/blog/view-post.gohtml", v)
}
// PartialBlogIndex is a template function to embed a blog index view on any page.
func PartialBlogIndex(r *http.Request, tag, privacy string) template.HTML {
html := bytes.NewBuffer([]byte{})
v := responses.NewTemplateVars(html, r)
page, _ := strconv.Atoi(r.FormValue("page"))
var (
posts models.PagedPosts
err error
)
if tag != "" {
posts, err = models.Posts.GetPostsByTag(tag, privacy, page, settings.Current.PostsPerPage)
} else {
posts, err = models.Posts.GetIndexPosts(privacy, page, settings.Current.PostsPerPage)
}
if err != nil && err.Error() != "sql: no rows in result set" {
return template.HTML(fmt.Sprintf("[BlogIndex: %s]", err))
}
v.V["posts"] = posts.Posts
v.V["paging"] = posts
responses.PartialTemplate(html, r, "_builtin/blog/index.partial.gohtml", v)
return template.HTML(html.String())
}
// TagIndex for "/tagged" to return all tags sorted by popularity.
func TagIndex(w http.ResponseWriter, r *http.Request) {
// If not logged in, only summarize public post tags.
var public = !authentication.LoggedIn(r)
tags := models.SummarizeTags(public)
v := responses.NewTemplateVars(w, r)
v.V["tags"] = tags
responses.RenderTemplate(w, r, "_builtin/blog/tags.gohtml", v)
}
// BlogArchive shows the archive page of ALL blog posts.
func BlogArchive(w http.ResponseWriter, r *http.Request) {
v := responses.NewTemplateVars(w, r)
// Show private and unlisted posts?
showPrivate := authentication.LoggedIn(r)
archive, err := models.Posts.GetArchive(showPrivate)
if err != nil {
responses.Error(w, r, http.StatusInternalServerError, err.Error())
return
}
v.V["archive"] = archive
responses.RenderTemplate(w, r, "_builtin/blog/archive.gohtml", v)
}
// EditPost at "/blog/edit"
func EditPost(w http.ResponseWriter, r *http.Request) {
v := responses.NewTemplateVars(w, r)
v.V["preview"] = ""
// The blog post we're working with.
var post = models.Posts.New()
var isNew = true
// Editing an existing post?
if r.FormValue("id") != "" {
id, _ := strconv.Atoi(r.FormValue("id"))
if p, err := models.Posts.Load(id); err == nil {
post = p
isNew = false
}
}
// POST handler: create the admin account.
for r.Method == http.MethodPost {
form, _ := forms.Parse(r)
// Validate form parameters.
val := form.Validator()
val.Require("title")
val.Require("body")
post.ParseForm(form)
if val.HasErrors() {
v.ValidationError = val.ErrorMap()
break
}
// Previewing or submitting the post?
switch form.Get("submit") {
case "preview":
if post.ContentType == models.Markdown {
v.V["preview"] = template.HTML(markdown.RenderTrustedMarkdown(post.Body))
} else {
v.V["preview"] = template.HTML(post.Body)
}
case "post":
author, _ := authentication.CurrentUser(r)
post.AuthorID = author.ID
// When editing, allow to not touch the Last Updated time.
if !isNew && form.GetBool("no-update") == true {
post.UpdatedAt = post.CreatedAt
} else {
post.UpdatedAt = time.Now().UTC()
}
err := post.Save()
if err != nil {
v.Error = err
} else {
session.Flash(w, r, "Post created!")
responses.Redirect(w, r, "/"+post.Fragment)
}
}
break
}
v.V["post"] = post
v.V["isNew"] = isNew
responses.RenderTemplate(w, r, "_builtin/blog/edit.gohtml", v)
}