Noah
5b6712ea97
* 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
266 lines
6.6 KiB
Go
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)
|
|
}
|