package core
import (
"bytes"
"errors"
"html/template"
"net/http"
"sort"
"strconv"
"strings"
"github.com/gorilla/mux"
"github.com/kirsle/blog/core/models/posts"
"github.com/kirsle/blog/core/models/users"
"github.com/urfave/negroni"
)
// PostMeta associates a Post with injected metadata.
type PostMeta struct {
Post *posts.Post
Rendered template.HTML
Author *users.User
IndexView bool
Snipped bool
}
// BlogRoutes attaches the blog routes to the app.
func (b *Blog) BlogRoutes(r *mux.Router) {
// Public routes
r.HandleFunc("/blog", b.BlogIndex)
// Login-required routers.
loginRouter := mux.NewRouter()
loginRouter.HandleFunc("/blog/edit", b.EditBlog)
loginRouter.HandleFunc("/blog/delete", b.DeletePost)
r.PathPrefix("/blog").Handler(
negroni.New(
negroni.HandlerFunc(b.LoginRequired),
negroni.Wrap(loginRouter),
),
)
adminRouter := mux.NewRouter().PathPrefix("/admin").Subrouter().StrictSlash(false)
r.HandleFunc("/admin", b.AdminHandler) // so as to not be "/admin/"
adminRouter.HandleFunc("/settings", b.SettingsHandler)
adminRouter.PathPrefix("/").HandlerFunc(b.PageHandler)
r.PathPrefix("/admin").Handler(negroni.New(
negroni.HandlerFunc(b.LoginRequired),
negroni.Wrap(adminRouter),
))
}
// BlogIndex renders the main index page of the blog.
func (b *Blog) BlogIndex(w http.ResponseWriter, r *http.Request) {
v := NewVars(map[interface{}]interface{}{})
// Get the blog index.
idx, _ := posts.GetIndex()
// The set of blog posts to show.
var pool []posts.Post
for _, post := range idx.Posts {
pool = append(pool, post)
}
sort.Sort(sort.Reverse(posts.ByUpdated(pool)))
// Query parameters.
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
if page <= 0 {
page = 1
}
perPage := 5 // TODO: configurable
offset := (page - 1) * perPage
stop := offset + perPage
// Handle pagination.
v.Data["Page"] = page
if page > 1 {
v.Data["PreviousPage"] = page - 1
} else {
v.Data["PreviousPage"] = 0
}
if offset+perPage < len(pool) {
v.Data["NextPage"] = page + 1
} else {
v.Data["NextPage"] = 0
}
var view []PostMeta
for i := offset; i < stop; i++ {
if i >= len(pool) {
continue
}
post, err := posts.Load(pool[i].ID)
if err != nil {
log.Error("couldn't load full post data for ID %d (found in index.json)", pool[i].ID)
continue
}
var rendered template.HTML
// Body has a snipped section?
if strings.Contains(post.Body, "") {
parts := strings.SplitN(post.Body, "", 1)
post.Body = parts[0]
}
// Render the post.
if post.ContentType == "markdown" {
rendered = template.HTML(b.RenderTrustedMarkdown(post.Body))
} else {
rendered = template.HTML(post.Body)
}
// Look up the author's information.
author, err := users.LoadReadonly(post.AuthorID)
if err != nil {
log.Error("Failed to look up post author ID %d (post %d): %v", post.AuthorID, post.ID, err)
author = users.DeletedUser()
}
view = append(view, PostMeta{
Post: post,
Rendered: rendered,
Author: author,
})
}
v.Data["View"] = view
b.RenderTemplate(w, r, "blog/index", v)
}
// viewPost is the underlying implementation of the handler to view a blog
// post, so that it can be called from non-http.HandlerFunc contexts.
func (b *Blog) viewPost(w http.ResponseWriter, r *http.Request, fragment string) error {
post, err := posts.LoadFragment(fragment)
if err != nil {
return err
}
v := NewVars(map[interface{}]interface{}{
"Post": post,
})
b.RenderTemplate(w, r, "blog/entry", v)
return nil
}
// RenderPost renders a blog post as a partial template and returns the HTML.
// If indexView is true, the blog headers will be hyperlinked to the dedicated
// entry view page.
func (b *Blog) RenderPost(p *posts.Post, indexView bool) template.HTML {
// Look up the author's information.
author, err := users.LoadReadonly(p.AuthorID)
if err != nil {
log.Error("Failed to look up post author ID %d (post %d): %v", p.AuthorID, p.ID, err)
author = users.DeletedUser()
}
// "Read More" snippet for index views.
var snipped bool
if indexView {
if strings.Contains(p.Body, "") {
log.Warn("HAS SNIP TAG!")
parts := strings.SplitN(p.Body, "", 2)
p.Body = parts[0]
snipped = true
}
}
// Render the post to HTML.
var rendered template.HTML
if p.ContentType == "markdown" {
rendered = template.HTML(b.RenderTrustedMarkdown(p.Body))
} else {
rendered = template.HTML(p.Body)
}
// Get the template snippet.
filepath, err := b.ResolvePath("blog/entry.partial")
if err != nil {
log.Error(err.Error())
return "[error: missing blog/entry.partial]"
}
t := template.New("entry.partial.gohtml")
t, err = t.ParseFiles(filepath.Absolute)
if err != nil {
log.Error("Failed to parse entry.partial: %s", err.Error())
return "[error parsing template in blog/entry.partial]"
}
meta := PostMeta{
Post: p,
Rendered: rendered,
Author: author,
IndexView: indexView,
Snipped: snipped,
}
output := bytes.Buffer{}
err = t.Execute(&output, meta)
if err != nil {
log.Error(err.Error())
return "[error executing template in blog/entry.partial]"
}
return template.HTML(output.String())
}
// EditBlog is the blog writing and editing page.
func (b *Blog) EditBlog(w http.ResponseWriter, r *http.Request) {
v := NewVars(map[interface{}]interface{}{
"preview": "",
})
var post *posts.Post
// Are we editing an existing post?
if idStr := r.URL.Query().Get("id"); idStr != "" {
id, err := strconv.Atoi(idStr)
if err == nil {
post, err = posts.Load(id)
if err != nil {
v.Error = errors.New("that post ID was not found")
post = posts.New()
}
}
} else {
post = posts.New()
}
if r.Method == http.MethodPost {
// Parse from form values.
post.ParseForm(r)
// Previewing, or submitting?
switch r.FormValue("submit") {
case "preview":
if post.ContentType == "markdown" || post.ContentType == "markdown+html" {
v.Data["preview"] = template.HTML(b.RenderMarkdown(post.Body))
} else {
v.Data["preview"] = template.HTML(post.Body)
}
case "post":
if err := post.Validate(); err != nil {
v.Error = err
} else {
author, _ := b.CurrentUser(r)
post.AuthorID = author.ID
err = post.Save()
if err != nil {
v.Error = err
} else {
b.Flash(w, r, "Post created!")
b.Redirect(w, "/"+post.Fragment)
}
}
}
}
v.Data["post"] = post
b.RenderTemplate(w, r, "blog/edit", v)
}
// DeletePost to delete a blog entry.
func (b *Blog) DeletePost(w http.ResponseWriter, r *http.Request) {
var post *posts.Post
v := NewVars(map[interface{}]interface{}{
"Post": nil,
})
var idStr string
if r.Method == http.MethodPost {
idStr = r.FormValue("id")
} else {
idStr = r.URL.Query().Get("id")
}
if idStr == "" {
b.FlashAndRedirect(w, r, "/admin", "No post ID given for deletion!")
return
}
// Convert the post ID to an int.
id, err := strconv.Atoi(idStr)
if err == nil {
post, err = posts.Load(id)
if err != nil {
b.FlashAndRedirect(w, r, "/admin", "That post ID was not found.")
return
}
}
if r.Method == http.MethodPost {
post.Delete()
b.FlashAndRedirect(w, r, "/admin", "Blog entry deleted!")
return
}
v.Data["Post"] = post
b.RenderTemplate(w, r, "blog/delete", v)
}