Controller breakout for blog and comments
This commit is contained in:
parent
eb1880d348
commit
c69c14ea09
553
core/blog.go
553
core/blog.go
|
@ -1,553 +0,0 @@
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"html/template"
|
|
||||||
"net/http"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/feeds"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/kirsle/blog/core/internal/log"
|
|
||||||
"github.com/kirsle/blog/core/internal/markdown"
|
|
||||||
"github.com/kirsle/blog/core/internal/middleware/auth"
|
|
||||||
"github.com/kirsle/blog/core/internal/models/comments"
|
|
||||||
"github.com/kirsle/blog/core/internal/models/posts"
|
|
||||||
"github.com/kirsle/blog/core/internal/models/settings"
|
|
||||||
"github.com/kirsle/blog/core/internal/models/users"
|
|
||||||
"github.com/kirsle/blog/core/internal/render"
|
|
||||||
"github.com/kirsle/blog/core/internal/responses"
|
|
||||||
"github.com/urfave/negroni"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PostMeta associates a Post with injected metadata.
|
|
||||||
type PostMeta struct {
|
|
||||||
Post *posts.Post
|
|
||||||
Rendered template.HTML
|
|
||||||
Author *users.User
|
|
||||||
NumComments int
|
|
||||||
IndexView bool
|
|
||||||
Snipped bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Archive holds data for a piece of the blog archive.
|
|
||||||
type Archive struct {
|
|
||||||
Label string
|
|
||||||
Date time.Time
|
|
||||||
Posts []posts.Post
|
|
||||||
}
|
|
||||||
|
|
||||||
// BlogRoutes attaches the blog routes to the app.
|
|
||||||
func (b *Blog) BlogRoutes(r *mux.Router) {
|
|
||||||
render.Funcs["RenderIndex"] = b.RenderIndex
|
|
||||||
render.Funcs["RenderPost"] = b.RenderPost
|
|
||||||
render.Funcs["RenderTags"] = b.RenderTags
|
|
||||||
|
|
||||||
// Public routes
|
|
||||||
r.HandleFunc("/blog", b.IndexHandler)
|
|
||||||
r.HandleFunc("/blog.rss", b.RSSHandler)
|
|
||||||
r.HandleFunc("/blog.atom", b.RSSHandler)
|
|
||||||
r.HandleFunc("/archive", b.BlogArchive)
|
|
||||||
r.HandleFunc("/tagged", b.Tagged)
|
|
||||||
r.HandleFunc("/tagged/{tag}", b.Tagged)
|
|
||||||
r.HandleFunc("/blog/category/{tag}", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
params := mux.Vars(r)
|
|
||||||
tag, ok := params["tag"]
|
|
||||||
if !ok {
|
|
||||||
b.NotFound(w, r, "Not Found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
responses.Redirect(w, "/tagged/"+tag)
|
|
||||||
})
|
|
||||||
r.HandleFunc("/blog/entry/{fragment}", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
params := mux.Vars(r)
|
|
||||||
fragment, ok := params["fragment"]
|
|
||||||
if !ok {
|
|
||||||
b.NotFound(w, r, "Not Found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
responses.Redirect(w, "/"+fragment)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Login-required routers.
|
|
||||||
loginRouter := mux.NewRouter()
|
|
||||||
loginRouter.HandleFunc("/blog/edit", b.EditBlog)
|
|
||||||
loginRouter.HandleFunc("/blog/delete", b.DeletePost)
|
|
||||||
loginRouter.HandleFunc("/blog/drafts", b.Drafts)
|
|
||||||
loginRouter.HandleFunc("/blog/private", b.PrivatePosts)
|
|
||||||
r.PathPrefix("/blog").Handler(
|
|
||||||
negroni.New(
|
|
||||||
negroni.HandlerFunc(auth.LoginRequired(b.MustLogin)),
|
|
||||||
negroni.Wrap(loginRouter),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RSSHandler renders an RSS feed from the blog.
|
|
||||||
func (b *Blog) RSSHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
config, _ := settings.Load()
|
|
||||||
admin, err := users.Load(1)
|
|
||||||
if err != nil {
|
|
||||||
b.Error(w, r, "Blog isn't ready yet.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
feed := &feeds.Feed{
|
|
||||||
Title: config.Site.Title,
|
|
||||||
Link: &feeds.Link{Href: config.Site.URL},
|
|
||||||
Description: config.Site.Description,
|
|
||||||
Author: &feeds.Author{
|
|
||||||
Name: admin.Name,
|
|
||||||
Email: admin.Email,
|
|
||||||
},
|
|
||||||
Created: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
feed.Items = []*feeds.Item{}
|
|
||||||
for i, p := range b.RecentPosts(r, "", "") {
|
|
||||||
post, _ := posts.Load(p.ID)
|
|
||||||
var suffix string
|
|
||||||
if strings.Contains(post.Body, "<snip>") {
|
|
||||||
post.Body = strings.Split(post.Body, "<snip>")[0]
|
|
||||||
suffix = "..."
|
|
||||||
}
|
|
||||||
|
|
||||||
feed.Items = append(feed.Items, &feeds.Item{
|
|
||||||
Title: p.Title,
|
|
||||||
Link: &feeds.Link{Href: config.Site.URL + p.Fragment},
|
|
||||||
Description: post.Body + suffix,
|
|
||||||
Created: p.Created,
|
|
||||||
})
|
|
||||||
if i >= 5 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// What format to encode it in?
|
|
||||||
if strings.Contains(r.URL.Path, ".atom") {
|
|
||||||
atom, _ := feed.ToAtom()
|
|
||||||
w.Header().Set("Content-Type", "application/atom+xml")
|
|
||||||
w.Write([]byte(atom))
|
|
||||||
} else {
|
|
||||||
rss, _ := feed.ToRss()
|
|
||||||
w.Header().Set("Content-Type", "application/rss+xml")
|
|
||||||
w.Write([]byte(rss))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IndexHandler renders the main index page of the blog.
|
|
||||||
func (b *Blog) IndexHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
b.CommonIndexHandler(w, r, "", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tagged lets you browse blog posts by category.
|
|
||||||
func (b *Blog) Tagged(w http.ResponseWriter, r *http.Request) {
|
|
||||||
params := mux.Vars(r)
|
|
||||||
tag, ok := params["tag"]
|
|
||||||
if !ok {
|
|
||||||
// They're listing all the tags.
|
|
||||||
render.Template(w, r, "blog/tags.gohtml", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
b.CommonIndexHandler(w, r, tag, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drafts renders an index view of only draft posts. Login required.
|
|
||||||
func (b *Blog) Drafts(w http.ResponseWriter, r *http.Request) {
|
|
||||||
b.CommonIndexHandler(w, r, "", DRAFT)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrivatePosts renders an index view of only private posts. Login required.
|
|
||||||
func (b *Blog) PrivatePosts(w http.ResponseWriter, r *http.Request) {
|
|
||||||
b.CommonIndexHandler(w, r, "", PRIVATE)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommonIndexHandler handles common logic for blog index views.
|
|
||||||
func (b *Blog) CommonIndexHandler(w http.ResponseWriter, r *http.Request, tag, privacy string) {
|
|
||||||
// Page title.
|
|
||||||
var title string
|
|
||||||
if privacy == DRAFT {
|
|
||||||
title = "Draft Posts"
|
|
||||||
} else if privacy == PRIVATE {
|
|
||||||
title = "Private Posts"
|
|
||||||
} else if tag != "" {
|
|
||||||
title = "Tagged as: " + tag
|
|
||||||
} else {
|
|
||||||
title = "Blog"
|
|
||||||
}
|
|
||||||
|
|
||||||
render.Template(w, r, "blog/index", map[string]interface{}{
|
|
||||||
"Title": title,
|
|
||||||
"Tag": tag,
|
|
||||||
"Privacy": privacy,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// RecentPosts gets and filters the blog entries and orders them by most recent.
|
|
||||||
func (b *Blog) RecentPosts(r *http.Request, tag, privacy string) []posts.Post {
|
|
||||||
// Get the blog index.
|
|
||||||
idx, _ := posts.GetIndex()
|
|
||||||
|
|
||||||
// The set of blog posts to show.
|
|
||||||
var pool []posts.Post
|
|
||||||
for _, post := range idx.Posts {
|
|
||||||
// Limiting by a specific privacy setting? (drafts or private only)
|
|
||||||
if privacy != "" {
|
|
||||||
switch privacy {
|
|
||||||
case DRAFT:
|
|
||||||
if post.Privacy != DRAFT {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
case PRIVATE:
|
|
||||||
if post.Privacy != PRIVATE && post.Privacy != UNLISTED {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Exclude certain posts in generic index views.
|
|
||||||
if (post.Privacy == PRIVATE || post.Privacy == UNLISTED) && !auth.LoggedIn(r) {
|
|
||||||
continue
|
|
||||||
} else if post.Privacy == DRAFT {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Limit by tag?
|
|
||||||
if tag != "" {
|
|
||||||
var tagMatch bool
|
|
||||||
if tag != "" {
|
|
||||||
for _, check := range post.Tags {
|
|
||||||
if check == tag {
|
|
||||||
tagMatch = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tagMatch {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pool = append(pool, post)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Sort(sort.Reverse(posts.ByUpdated(pool)))
|
|
||||||
return pool
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenderIndex renders and returns the blog index partial.
|
|
||||||
func (b *Blog) RenderIndex(r *http.Request, tag, privacy string) template.HTML {
|
|
||||||
// Get the recent blog entries, filtered by the tag/privacy settings.
|
|
||||||
pool := b.RecentPosts(r, tag, privacy)
|
|
||||||
if len(pool) == 0 {
|
|
||||||
return template.HTML("No blog posts were found.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
var previousPage, nextPage int
|
|
||||||
if page > 1 {
|
|
||||||
previousPage = page - 1
|
|
||||||
} else {
|
|
||||||
previousPage = 0
|
|
||||||
}
|
|
||||||
if offset+perPage < len(pool) {
|
|
||||||
nextPage = page + 1
|
|
||||||
} else {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count the comments on this post.
|
|
||||||
var numComments int
|
|
||||||
if thread, err := comments.Load(fmt.Sprintf("post-%d", post.ID)); err == nil {
|
|
||||||
numComments = len(thread.Comments)
|
|
||||||
}
|
|
||||||
|
|
||||||
view = append(view, PostMeta{
|
|
||||||
Post: post,
|
|
||||||
Author: author,
|
|
||||||
NumComments: numComments,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render the blog index partial.
|
|
||||||
var output bytes.Buffer
|
|
||||||
v := map[string]interface{}{
|
|
||||||
"PreviousPage": previousPage,
|
|
||||||
"NextPage": nextPage,
|
|
||||||
"View": view,
|
|
||||||
}
|
|
||||||
render.Template(&output, r, "blog/index.partial", v)
|
|
||||||
|
|
||||||
return template.HTML(output.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenderTags renders the tags partial.
|
|
||||||
func (b *Blog) RenderTags(r *http.Request, indexView bool) template.HTML {
|
|
||||||
idx, err := posts.GetIndex()
|
|
||||||
if err != nil {
|
|
||||||
return template.HTML("[RenderTags: error getting blog index]")
|
|
||||||
}
|
|
||||||
|
|
||||||
tags, err := idx.Tags()
|
|
||||||
if err != nil {
|
|
||||||
return template.HTML("[RenderTags: error getting tags]")
|
|
||||||
}
|
|
||||||
|
|
||||||
var output bytes.Buffer
|
|
||||||
v := map[string]interface{}{
|
|
||||||
"IndexView": indexView,
|
|
||||||
"Tags": tags,
|
|
||||||
}
|
|
||||||
render.Template(&output, r, "blog/tags.partial", v)
|
|
||||||
|
|
||||||
return template.HTML(output.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// BlogArchive summarizes all blog entries in an archive view.
|
|
||||||
func (b *Blog) BlogArchive(w http.ResponseWriter, r *http.Request) {
|
|
||||||
idx, err := posts.GetIndex()
|
|
||||||
if err != nil {
|
|
||||||
b.BadRequest(w, r, "Error getting blog index")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group posts by calendar month.
|
|
||||||
var months []string
|
|
||||||
byMonth := map[string]*Archive{}
|
|
||||||
for _, post := range idx.Posts {
|
|
||||||
// Exclude certain posts
|
|
||||||
if (post.Privacy == PRIVATE || post.Privacy == UNLISTED) && !auth.LoggedIn(r) {
|
|
||||||
continue
|
|
||||||
} else if post.Privacy == DRAFT {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
label := post.Created.Format("2006-01")
|
|
||||||
if _, ok := byMonth[label]; !ok {
|
|
||||||
months = append(months, label)
|
|
||||||
byMonth[label] = &Archive{
|
|
||||||
Label: label,
|
|
||||||
Date: time.Date(post.Created.Year(), post.Created.Month(), post.Created.Day(), 0, 0, 0, 0, time.UTC),
|
|
||||||
Posts: []posts.Post{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
byMonth[label].Posts = append(byMonth[label].Posts, post)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort the months.
|
|
||||||
sort.Sort(sort.Reverse(sort.StringSlice(months)))
|
|
||||||
|
|
||||||
// Prepare the response.
|
|
||||||
result := []*Archive{}
|
|
||||||
for _, label := range months {
|
|
||||||
sort.Sort(sort.Reverse(posts.ByUpdated(byMonth[label].Posts)))
|
|
||||||
result = append(result, byMonth[label])
|
|
||||||
}
|
|
||||||
|
|
||||||
v := map[string]interface{}{
|
|
||||||
"Archive": result,
|
|
||||||
}
|
|
||||||
render.Template(w, r, "blog/archive", 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.
|
|
||||||
// Specifically, from the catch-all page handler to allow blog URL fragments
|
|
||||||
// to map to their post.
|
|
||||||
func (b *Blog) viewPost(w http.ResponseWriter, r *http.Request, fragment string) error {
|
|
||||||
post, err := posts.LoadFragment(fragment)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle post privacy.
|
|
||||||
if post.Privacy == PRIVATE || post.Privacy == DRAFT {
|
|
||||||
if !auth.LoggedIn(r) {
|
|
||||||
b.NotFound(w, r, "That post is not public.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
v := map[string]interface{}{
|
|
||||||
"Post": post,
|
|
||||||
}
|
|
||||||
render.Template(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(r *http.Request, p *posts.Post, indexView bool, numComments int) 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, "<snip>") {
|
|
||||||
parts := strings.SplitN(p.Body, "<snip>", 2)
|
|
||||||
p.Body = parts[0]
|
|
||||||
snipped = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
p.Body = strings.Replace(p.Body, "<snip>", "<div id=\"snip\"></div>", 1)
|
|
||||||
|
|
||||||
// Render the post to HTML.
|
|
||||||
var rendered template.HTML
|
|
||||||
if p.ContentType == string(MARKDOWN) {
|
|
||||||
rendered = template.HTML(markdown.RenderTrustedMarkdown(p.Body))
|
|
||||||
} else {
|
|
||||||
rendered = template.HTML(p.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
meta := map[string]interface{}{
|
|
||||||
"Post": p,
|
|
||||||
"Rendered": rendered,
|
|
||||||
"Author": author,
|
|
||||||
"IndexView": indexView,
|
|
||||||
"Snipped": snipped,
|
|
||||||
"NumComments": numComments,
|
|
||||||
}
|
|
||||||
output := bytes.Buffer{}
|
|
||||||
err = render.Template(&output, r, "blog/entry.partial", meta)
|
|
||||||
if err != nil {
|
|
||||||
return template.HTML(fmt.Sprintf("[template error in blog/entry.partial: %s]", err.Error()))
|
|
||||||
}
|
|
||||||
|
|
||||||
return template.HTML(output.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// EditBlog is the blog writing and editing page.
|
|
||||||
func (b *Blog) EditBlog(w http.ResponseWriter, r *http.Request) {
|
|
||||||
v := map[string]interface{}{
|
|
||||||
"preview": "",
|
|
||||||
}
|
|
||||||
var post *posts.Post
|
|
||||||
|
|
||||||
// Are we editing an existing post?
|
|
||||||
if idStr := r.FormValue("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 == string(MARKDOWN) {
|
|
||||||
v["preview"] = template.HTML(markdown.RenderTrustedMarkdown(post.Body))
|
|
||||||
} else {
|
|
||||||
v["preview"] = template.HTML(post.Body)
|
|
||||||
}
|
|
||||||
case "post":
|
|
||||||
if err := post.Validate(); err != nil {
|
|
||||||
v["Error"] = err
|
|
||||||
} else {
|
|
||||||
author, _ := auth.CurrentUser(r)
|
|
||||||
post.AuthorID = author.ID
|
|
||||||
|
|
||||||
post.Updated = time.Now().UTC()
|
|
||||||
err = post.Save()
|
|
||||||
if err != nil {
|
|
||||||
v["Error"] = err
|
|
||||||
} else {
|
|
||||||
responses.Flash(w, r, "Post created!")
|
|
||||||
responses.Redirect(w, "/"+post.Fragment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
v["post"] = post
|
|
||||||
render.Template(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 := map[string]interface{}{
|
|
||||||
"Post": nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
var idStr string
|
|
||||||
if r.Method == http.MethodPost {
|
|
||||||
idStr = r.FormValue("id")
|
|
||||||
} else {
|
|
||||||
idStr = r.URL.Query().Get("id")
|
|
||||||
}
|
|
||||||
if idStr == "" {
|
|
||||||
responses.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 {
|
|
||||||
responses.FlashAndRedirect(w, r, "/admin", "That post ID was not found.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Method == http.MethodPost {
|
|
||||||
post.Delete()
|
|
||||||
responses.FlashAndRedirect(w, r, "/admin", "Blog entry deleted!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
v["Post"] = post
|
|
||||||
render.Template(w, r, "blog/delete", v)
|
|
||||||
}
|
|
10
core/core.go
10
core/core.go
|
@ -9,7 +9,9 @@ import (
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/kirsle/blog/core/internal/controllers/admin"
|
"github.com/kirsle/blog/core/internal/controllers/admin"
|
||||||
"github.com/kirsle/blog/core/internal/controllers/authctl"
|
"github.com/kirsle/blog/core/internal/controllers/authctl"
|
||||||
|
commentctl "github.com/kirsle/blog/core/internal/controllers/comments"
|
||||||
"github.com/kirsle/blog/core/internal/controllers/contact"
|
"github.com/kirsle/blog/core/internal/controllers/contact"
|
||||||
|
postctl "github.com/kirsle/blog/core/internal/controllers/posts"
|
||||||
"github.com/kirsle/blog/core/internal/controllers/setup"
|
"github.com/kirsle/blog/core/internal/controllers/setup"
|
||||||
"github.com/kirsle/blog/core/internal/log"
|
"github.com/kirsle/blog/core/internal/log"
|
||||||
"github.com/kirsle/blog/core/internal/markdown"
|
"github.com/kirsle/blog/core/internal/markdown"
|
||||||
|
@ -113,9 +115,9 @@ func (b *Blog) SetupHTTP() {
|
||||||
setup.Register(r)
|
setup.Register(r)
|
||||||
authctl.Register(r)
|
authctl.Register(r)
|
||||||
admin.Register(r, b.MustLogin)
|
admin.Register(r, b.MustLogin)
|
||||||
contact.Register(r, b.Error)
|
contact.Register(r)
|
||||||
b.BlogRoutes(r)
|
postctl.Register(r, b.MustLogin)
|
||||||
b.CommentRoutes(r)
|
commentctl.Register(r)
|
||||||
|
|
||||||
// GitHub Flavored Markdown CSS.
|
// GitHub Flavored Markdown CSS.
|
||||||
r.Handle("/css/gfm.css", http.StripPrefix("/css", http.FileServer(gfmstyle.Assets)))
|
r.Handle("/css/gfm.css", http.StripPrefix("/css", http.FileServer(gfmstyle.Assets)))
|
||||||
|
@ -127,7 +129,7 @@ func (b *Blog) SetupHTTP() {
|
||||||
negroni.NewRecovery(),
|
negroni.NewRecovery(),
|
||||||
negroni.NewLogger(),
|
negroni.NewLogger(),
|
||||||
negroni.HandlerFunc(sessions.Middleware),
|
negroni.HandlerFunc(sessions.Middleware),
|
||||||
negroni.HandlerFunc(middleware.CSRF(b.Forbidden)),
|
negroni.HandlerFunc(middleware.CSRF(responses.Forbidden)),
|
||||||
negroni.HandlerFunc(auth.Middleware),
|
negroni.HandlerFunc(auth.Middleware),
|
||||||
)
|
)
|
||||||
n.UseHandler(r)
|
n.UseHandler(r)
|
||||||
|
|
|
@ -5,10 +5,12 @@ import (
|
||||||
|
|
||||||
"github.com/kirsle/blog/core/internal/log"
|
"github.com/kirsle/blog/core/internal/log"
|
||||||
"github.com/kirsle/blog/core/internal/render"
|
"github.com/kirsle/blog/core/internal/render"
|
||||||
|
"github.com/kirsle/blog/core/internal/responses"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NotFound sends a 404 response.
|
// registerErrors loads the error handlers into the responses subpackage.
|
||||||
func (b *Blog) NotFound(w http.ResponseWriter, r *http.Request, message string) {
|
func (b *Blog) registerErrors() {
|
||||||
|
responses.NotFound = func(w http.ResponseWriter, r *http.Request, message string) {
|
||||||
if message == "" {
|
if message == "" {
|
||||||
message = "The page you were looking for was not found."
|
message = "The page you were looking for was not found."
|
||||||
}
|
}
|
||||||
|
@ -23,8 +25,7 @@ func (b *Blog) NotFound(w http.ResponseWriter, r *http.Request, message string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forbidden sends an HTTP 403 Forbidden response.
|
responses.Forbidden = func(w http.ResponseWriter, r *http.Request, message string) {
|
||||||
func (b *Blog) Forbidden(w http.ResponseWriter, r *http.Request, message string) {
|
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
err := render.Template(w, r, ".errors/403", map[string]string{
|
err := render.Template(w, r, ".errors/403", map[string]string{
|
||||||
"Message": message,
|
"Message": message,
|
||||||
|
@ -35,8 +36,7 @@ func (b *Blog) Forbidden(w http.ResponseWriter, r *http.Request, message string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error sends an HTTP 500 Internal Server Error response.
|
responses.Error = func(w http.ResponseWriter, r *http.Request, message string) {
|
||||||
func (b *Blog) Error(w http.ResponseWriter, r *http.Request, message string) {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
err := render.Template(w, r, ".errors/500", map[string]string{
|
err := render.Template(w, r, ".errors/500", map[string]string{
|
||||||
"Message": message,
|
"Message": message,
|
||||||
|
@ -47,8 +47,7 @@ func (b *Blog) Error(w http.ResponseWriter, r *http.Request, message string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BadRequest sends an HTTP 400 Bad Request.
|
responses.BadRequest = func(w http.ResponseWriter, r *http.Request, message string) {
|
||||||
func (b *Blog) BadRequest(w http.ResponseWriter, r *http.Request, message string) {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
err := render.Template(w, r, ".errors/400", map[string]string{
|
err := render.Template(w, r, ".errors/400", map[string]string{
|
||||||
"Message": message,
|
"Message": message,
|
||||||
|
@ -58,3 +57,5 @@ func (b *Blog) BadRequest(w http.ResponseWriter, r *http.Request, message string
|
||||||
w.Write([]byte("Unrecoverable template error for BadRequest()"))
|
w.Write([]byte("Unrecoverable template error for BadRequest()"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package core
|
package comments
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
@ -19,13 +19,16 @@ import (
|
||||||
"github.com/kirsle/blog/core/internal/sessions"
|
"github.com/kirsle/blog/core/internal/sessions"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CommentRoutes attaches the comment routes to the app.
|
var badRequest func(http.ResponseWriter, *http.Request, string)
|
||||||
func (b *Blog) CommentRoutes(r *mux.Router) {
|
|
||||||
render.Funcs["RenderComments"] = b.RenderComments
|
|
||||||
|
|
||||||
r.HandleFunc("/comments", b.CommentHandler)
|
// Register the comment routes to the app.
|
||||||
r.HandleFunc("/comments/subscription", b.SubscriptionHandler)
|
func Register(r *mux.Router) {
|
||||||
r.HandleFunc("/comments/quick-delete", b.QuickDeleteHandler)
|
badRequest = responses.BadRequest
|
||||||
|
render.Funcs["RenderComments"] = RenderComments
|
||||||
|
|
||||||
|
r.HandleFunc("/comments", commentHandler)
|
||||||
|
r.HandleFunc("/comments/subscription", subscriptionHandler)
|
||||||
|
r.HandleFunc("/comments/quick-delete", quickDeleteHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommentMeta is the template variables for comment threads.
|
// CommentMeta is the template variables for comment threads.
|
||||||
|
@ -40,7 +43,7 @@ type CommentMeta struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderComments renders a comment form partial and returns the HTML.
|
// RenderComments renders a comment form partial and returns the HTML.
|
||||||
func (b *Blog) RenderComments(r *http.Request, subject string, ids ...string) template.HTML {
|
func RenderComments(r *http.Request, subject string, ids ...string) template.HTML {
|
||||||
id := strings.Join(ids, "-")
|
id := strings.Join(ids, "-")
|
||||||
session := sessions.Get(r)
|
session := sessions.Get(r)
|
||||||
url := r.URL.Path
|
url := r.URL.Path
|
||||||
|
@ -141,14 +144,13 @@ func (b *Blog) RenderComments(r *http.Request, subject string, ids ...string) te
|
||||||
return template.HTML(output.String())
|
return template.HTML(output.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommentHandler handles the /comments URI for previewing and posting.
|
func commentHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
func (b *Blog) CommentHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
b.BadRequest(w, r, "That method is not allowed.")
|
badRequest(w, r, "That method is not allowed.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
currentUser, _ := auth.CurrentUser(r)
|
currentUser, _ := auth.CurrentUser(r)
|
||||||
editToken := b.GetEditToken(w, r)
|
editToken := getEditToken(w, r)
|
||||||
submit := r.FormValue("submit")
|
submit := r.FormValue("submit")
|
||||||
|
|
||||||
// Load the comment data from the form.
|
// Load the comment data from the form.
|
||||||
|
@ -207,14 +209,14 @@ func (b *Blog) CommentHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// Previewing, deleting, or posting?
|
// Previewing, deleting, or posting?
|
||||||
switch submit {
|
switch submit {
|
||||||
case ActionPreview, ActionDelete:
|
case "preview", "delete":
|
||||||
if !c.Editing && currentUser.IsAuthenticated {
|
if !c.Editing && currentUser.IsAuthenticated {
|
||||||
c.Name = currentUser.Name
|
c.Name = currentUser.Name
|
||||||
c.Email = currentUser.Email
|
c.Email = currentUser.Email
|
||||||
c.LoadAvatar()
|
c.LoadAvatar()
|
||||||
}
|
}
|
||||||
c.HTML = template.HTML(markdown.RenderMarkdown(c.Body))
|
c.HTML = template.HTML(markdown.RenderMarkdown(c.Body))
|
||||||
case ActionPost:
|
case "post":
|
||||||
if err := c.Validate(); err != nil {
|
if err := c.Validate(); err != nil {
|
||||||
v["Error"] = err
|
v["Error"] = err
|
||||||
} else {
|
} else {
|
||||||
|
@ -258,55 +260,22 @@ func (b *Blog) CommentHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
v["Thread"] = t
|
v["Thread"] = t
|
||||||
v["Comment"] = c
|
v["Comment"] = c
|
||||||
v["Editing"] = c.Editing
|
v["Editing"] = c.Editing
|
||||||
v["Deleting"] = submit == ActionDelete
|
v["Deleting"] = submit == "delete"
|
||||||
|
|
||||||
render.Template(w, r, "comments/index.gohtml", v)
|
render.Template(w, r, "comments/index.gohtml", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubscriptionHandler to opt out of subscriptions.
|
func quickDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
func (b *Blog) SubscriptionHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// POST to unsubscribe from all threads.
|
|
||||||
if r.Method == http.MethodPost {
|
|
||||||
email := r.FormValue("email")
|
|
||||||
if email == "" {
|
|
||||||
b.BadRequest(w, r, "email address is required to unsubscribe from comment threads")
|
|
||||||
} else if _, err := mail.ParseAddress(email); err != nil {
|
|
||||||
b.BadRequest(w, r, "invalid email address")
|
|
||||||
}
|
|
||||||
|
|
||||||
m := comments.LoadMailingList()
|
|
||||||
m.UnsubscribeAll(email)
|
|
||||||
responses.FlashAndRedirect(w, r, "/comments/subscription",
|
|
||||||
"You have been unsubscribed from all mailing lists.",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET to unsubscribe from a single thread.
|
|
||||||
thread := r.URL.Query().Get("t")
|
|
||||||
email := r.URL.Query().Get("e")
|
|
||||||
if thread != "" && email != "" {
|
|
||||||
m := comments.LoadMailingList()
|
|
||||||
m.Unsubscribe(thread, email)
|
|
||||||
responses.FlashAndRedirect(w, r, "/comments/subscription", "You have been unsubscribed successfully.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
render.Template(w, r, "comments/subscription.gohtml", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// QuickDeleteHandler allows the admin to quickly delete spam without logging in.
|
|
||||||
func (b *Blog) QuickDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
thread := r.URL.Query().Get("t")
|
thread := r.URL.Query().Get("t")
|
||||||
token := r.URL.Query().Get("d")
|
token := r.URL.Query().Get("d")
|
||||||
if thread == "" || token == "" {
|
if thread == "" || token == "" {
|
||||||
b.BadRequest(w, r, "Bad Request")
|
badRequest(w, r, "Bad Request")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t, err := comments.Load(thread)
|
t, err := comments.Load(thread)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.BadRequest(w, r, "Comment thread does not exist.")
|
badRequest(w, r, "Comment thread does not exist.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,9 +286,9 @@ func (b *Blog) QuickDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
responses.FlashAndRedirect(w, r, "/", "Comment deleted!")
|
responses.FlashAndRedirect(w, r, "/", "Comment deleted!")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEditToken gets or generates an edit token from the user's session, which
|
// getEditToken gets or generates an edit token from the user's session, which
|
||||||
// allows a user to edit their comment for a short while after they post it.
|
// allows a user to edit their comment for a short while after they post it.
|
||||||
func (b *Blog) GetEditToken(w http.ResponseWriter, r *http.Request) string {
|
func getEditToken(w http.ResponseWriter, r *http.Request) string {
|
||||||
session := sessions.Get(r)
|
session := sessions.Get(r)
|
||||||
if token, ok := session.Values["c.token"].(string); ok && len(token) > 0 {
|
if token, ok := session.Values["c.token"].(string); ok && len(token) > 0 {
|
||||||
return token
|
return token
|
41
core/internal/controllers/comments/subscriptions.go
Normal file
41
core/internal/controllers/comments/subscriptions.go
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
package comments
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/mail"
|
||||||
|
|
||||||
|
"github.com/kirsle/blog/core/internal/models/comments"
|
||||||
|
"github.com/kirsle/blog/core/internal/render"
|
||||||
|
"github.com/kirsle/blog/core/internal/responses"
|
||||||
|
)
|
||||||
|
|
||||||
|
func subscriptionHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// POST to unsubscribe from all threads.
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
email := r.FormValue("email")
|
||||||
|
if email == "" {
|
||||||
|
badRequest(w, r, "email address is required to unsubscribe from comment threads")
|
||||||
|
} else if _, err := mail.ParseAddress(email); err != nil {
|
||||||
|
badRequest(w, r, "invalid email address")
|
||||||
|
}
|
||||||
|
|
||||||
|
m := comments.LoadMailingList()
|
||||||
|
m.UnsubscribeAll(email)
|
||||||
|
responses.FlashAndRedirect(w, r, "/comments/subscription",
|
||||||
|
"You have been unsubscribed from all mailing lists.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET to unsubscribe from a single thread.
|
||||||
|
thread := r.URL.Query().Get("t")
|
||||||
|
email := r.URL.Query().Get("e")
|
||||||
|
if thread != "" && email != "" {
|
||||||
|
m := comments.LoadMailingList()
|
||||||
|
m.Unsubscribe(thread, email)
|
||||||
|
responses.FlashAndRedirect(w, r, "/comments/subscription", "You have been unsubscribed successfully.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Template(w, r, "comments/subscription.gohtml", nil)
|
||||||
|
}
|
|
@ -18,7 +18,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register attaches the contact URL to the app.
|
// Register attaches the contact URL to the app.
|
||||||
func Register(r *mux.Router, onError func(http.ResponseWriter, *http.Request, string)) {
|
func Register(r *mux.Router) {
|
||||||
r.HandleFunc("/contact", func(w http.ResponseWriter, r *http.Request) {
|
r.HandleFunc("/contact", func(w http.ResponseWriter, r *http.Request) {
|
||||||
form := &forms.Contact{}
|
form := &forms.Contact{}
|
||||||
v := map[string]interface{}{
|
v := map[string]interface{}{
|
||||||
|
@ -28,13 +28,13 @@ func Register(r *mux.Router, onError func(http.ResponseWriter, *http.Request, st
|
||||||
// If there is no site admin, show an error.
|
// If there is no site admin, show an error.
|
||||||
cfg, err := settings.Load()
|
cfg, err := settings.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
onError(w, r, "Error loading site configuration!")
|
responses.Error(w, r, "Error loading site configuration!")
|
||||||
return
|
return
|
||||||
} else if cfg.Site.AdminEmail == "" {
|
} else if cfg.Site.AdminEmail == "" {
|
||||||
onError(w, r, "There is no admin email configured for this website!")
|
responses.Error(w, r, "There is no admin email configured for this website!")
|
||||||
return
|
return
|
||||||
} else if !cfg.Mail.Enabled {
|
} else if !cfg.Mail.Enabled {
|
||||||
onError(w, r, "This website doesn't have an e-mail gateway configured.")
|
responses.Error(w, r, "This website doesn't have an e-mail gateway configured.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
60
core/internal/controllers/posts/archive.go
Normal file
60
core/internal/controllers/posts/archive.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package postctl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kirsle/blog/core/internal/middleware/auth"
|
||||||
|
"github.com/kirsle/blog/core/internal/models/posts"
|
||||||
|
"github.com/kirsle/blog/core/internal/render"
|
||||||
|
"github.com/kirsle/blog/core/internal/responses"
|
||||||
|
"github.com/kirsle/blog/core/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// archiveHandler summarizes all blog entries in an archive view.
|
||||||
|
func archiveHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
idx, err := posts.GetIndex()
|
||||||
|
if err != nil {
|
||||||
|
responses.BadRequest(w, r, "Error getting blog index")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group posts by calendar month.
|
||||||
|
var months []string
|
||||||
|
byMonth := map[string]*Archive{}
|
||||||
|
for _, post := range idx.Posts {
|
||||||
|
// Exclude certain posts
|
||||||
|
if (post.Privacy == types.PRIVATE || post.Privacy == types.UNLISTED) && !auth.LoggedIn(r) {
|
||||||
|
continue
|
||||||
|
} else if post.Privacy == types.DRAFT {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
label := post.Created.Format("2006-01")
|
||||||
|
if _, ok := byMonth[label]; !ok {
|
||||||
|
months = append(months, label)
|
||||||
|
byMonth[label] = &Archive{
|
||||||
|
Label: label,
|
||||||
|
Date: time.Date(post.Created.Year(), post.Created.Month(), post.Created.Day(), 0, 0, 0, 0, time.UTC),
|
||||||
|
Posts: []posts.Post{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
byMonth[label].Posts = append(byMonth[label].Posts, post)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the months.
|
||||||
|
sort.Sort(sort.Reverse(sort.StringSlice(months)))
|
||||||
|
|
||||||
|
// Prepare the response.
|
||||||
|
result := []*Archive{}
|
||||||
|
for _, label := range months {
|
||||||
|
sort.Sort(sort.Reverse(posts.ByUpdated(byMonth[label].Posts)))
|
||||||
|
result = append(result, byMonth[label])
|
||||||
|
}
|
||||||
|
|
||||||
|
v := map[string]interface{}{
|
||||||
|
"Archive": result,
|
||||||
|
}
|
||||||
|
render.Template(w, r, "blog/archive", v)
|
||||||
|
}
|
110
core/internal/controllers/posts/edit.go
Normal file
110
core/internal/controllers/posts/edit.go
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
package postctl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kirsle/blog/core/internal/markdown"
|
||||||
|
"github.com/kirsle/blog/core/internal/middleware/auth"
|
||||||
|
"github.com/kirsle/blog/core/internal/models/posts"
|
||||||
|
"github.com/kirsle/blog/core/internal/render"
|
||||||
|
"github.com/kirsle/blog/core/internal/responses"
|
||||||
|
"github.com/kirsle/blog/core/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// editHandler is the blog writing and editing page.
|
||||||
|
func editHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
v := map[string]interface{}{
|
||||||
|
"preview": "",
|
||||||
|
}
|
||||||
|
var post *posts.Post
|
||||||
|
|
||||||
|
// Are we editing an existing post?
|
||||||
|
if idStr := r.FormValue("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 == string(types.MARKDOWN) {
|
||||||
|
v["preview"] = template.HTML(markdown.RenderTrustedMarkdown(post.Body))
|
||||||
|
} else {
|
||||||
|
v["preview"] = template.HTML(post.Body)
|
||||||
|
}
|
||||||
|
case "post":
|
||||||
|
if err := post.Validate(); err != nil {
|
||||||
|
v["Error"] = err
|
||||||
|
} else {
|
||||||
|
author, _ := auth.CurrentUser(r)
|
||||||
|
post.AuthorID = author.ID
|
||||||
|
|
||||||
|
post.Updated = time.Now().UTC()
|
||||||
|
err = post.Save()
|
||||||
|
if err != nil {
|
||||||
|
v["Error"] = err
|
||||||
|
} else {
|
||||||
|
responses.Flash(w, r, "Post created!")
|
||||||
|
responses.Redirect(w, "/"+post.Fragment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v["post"] = post
|
||||||
|
render.Template(w, r, "blog/edit", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteHandler to delete a blog entry.
|
||||||
|
func deleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var post *posts.Post
|
||||||
|
v := map[string]interface{}{
|
||||||
|
"Post": nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
var idStr string
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
idStr = r.FormValue("id")
|
||||||
|
} else {
|
||||||
|
idStr = r.URL.Query().Get("id")
|
||||||
|
}
|
||||||
|
if idStr == "" {
|
||||||
|
responses.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 {
|
||||||
|
responses.FlashAndRedirect(w, r, "/admin", "That post ID was not found.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
post.Delete()
|
||||||
|
responses.FlashAndRedirect(w, r, "/admin", "Blog entry deleted!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
v["Post"] = post
|
||||||
|
render.Template(w, r, "blog/delete", v)
|
||||||
|
}
|
64
core/internal/controllers/posts/feeds.go
Normal file
64
core/internal/controllers/posts/feeds.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package postctl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/feeds"
|
||||||
|
"github.com/kirsle/blog/core/internal/models/posts"
|
||||||
|
"github.com/kirsle/blog/core/internal/models/settings"
|
||||||
|
"github.com/kirsle/blog/core/internal/models/users"
|
||||||
|
"github.com/kirsle/blog/core/internal/responses"
|
||||||
|
)
|
||||||
|
|
||||||
|
func feedHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
config, _ := settings.Load()
|
||||||
|
admin, err := users.Load(1)
|
||||||
|
if err != nil {
|
||||||
|
responses.Error(w, r, "Blog isn't ready yet.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
feed := &feeds.Feed{
|
||||||
|
Title: config.Site.Title,
|
||||||
|
Link: &feeds.Link{Href: config.Site.URL},
|
||||||
|
Description: config.Site.Description,
|
||||||
|
Author: &feeds.Author{
|
||||||
|
Name: admin.Name,
|
||||||
|
Email: admin.Email,
|
||||||
|
},
|
||||||
|
Created: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
feed.Items = []*feeds.Item{}
|
||||||
|
for i, p := range RecentPosts(r, "", "") {
|
||||||
|
post, _ := posts.Load(p.ID)
|
||||||
|
var suffix string
|
||||||
|
if strings.Contains(post.Body, "<snip>") {
|
||||||
|
post.Body = strings.Split(post.Body, "<snip>")[0]
|
||||||
|
suffix = "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
feed.Items = append(feed.Items, &feeds.Item{
|
||||||
|
Title: p.Title,
|
||||||
|
Link: &feeds.Link{Href: config.Site.URL + p.Fragment},
|
||||||
|
Description: post.Body + suffix,
|
||||||
|
Created: p.Created,
|
||||||
|
})
|
||||||
|
if i >= 5 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// What format to encode it in?
|
||||||
|
if strings.Contains(r.URL.Path, ".atom") {
|
||||||
|
atom, _ := feed.ToAtom()
|
||||||
|
w.Header().Set("Content-Type", "application/atom+xml")
|
||||||
|
w.Write([]byte(atom))
|
||||||
|
} else {
|
||||||
|
rss, _ := feed.ToRss()
|
||||||
|
w.Header().Set("Content-Type", "application/rss+xml")
|
||||||
|
w.Write([]byte(rss))
|
||||||
|
}
|
||||||
|
}
|
125
core/internal/controllers/posts/index.go
Normal file
125
core/internal/controllers/posts/index.go
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
package postctl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/kirsle/blog/core/internal/log"
|
||||||
|
"github.com/kirsle/blog/core/internal/models/comments"
|
||||||
|
"github.com/kirsle/blog/core/internal/models/posts"
|
||||||
|
"github.com/kirsle/blog/core/internal/models/users"
|
||||||
|
"github.com/kirsle/blog/core/internal/render"
|
||||||
|
"github.com/kirsle/blog/core/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// partialIndex renders and returns the blog index partial.
|
||||||
|
func partialIndex(r *http.Request, tag, privacy string) template.HTML {
|
||||||
|
// Get the recent blog entries, filtered by the tag/privacy settings.
|
||||||
|
pool := RecentPosts(r, tag, privacy)
|
||||||
|
if len(pool) == 0 {
|
||||||
|
return template.HTML("No blog posts were found.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
var previousPage, nextPage int
|
||||||
|
if page > 1 {
|
||||||
|
previousPage = page - 1
|
||||||
|
} else {
|
||||||
|
previousPage = 0
|
||||||
|
}
|
||||||
|
if offset+perPage < len(pool) {
|
||||||
|
nextPage = page + 1
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count the comments on this post.
|
||||||
|
var numComments int
|
||||||
|
if thread, err := comments.Load(fmt.Sprintf("post-%d", post.ID)); err == nil {
|
||||||
|
numComments = len(thread.Comments)
|
||||||
|
}
|
||||||
|
|
||||||
|
view = append(view, PostMeta{
|
||||||
|
Post: post,
|
||||||
|
Author: author,
|
||||||
|
NumComments: numComments,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the blog index partial.
|
||||||
|
var output bytes.Buffer
|
||||||
|
v := map[string]interface{}{
|
||||||
|
"PreviousPage": previousPage,
|
||||||
|
"NextPage": nextPage,
|
||||||
|
"View": view,
|
||||||
|
}
|
||||||
|
render.Template(&output, r, "blog/index.partial", v)
|
||||||
|
|
||||||
|
return template.HTML(output.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// indexHandler renders the main index page of the blog.
|
||||||
|
func indexHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
commonIndexHandler(w, r, "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// drafts renders an index view of only draft posts. Login required.
|
||||||
|
func drafts(w http.ResponseWriter, r *http.Request) {
|
||||||
|
commonIndexHandler(w, r, "", types.DRAFT)
|
||||||
|
}
|
||||||
|
|
||||||
|
// privatePosts renders an index view of only private posts. Login required.
|
||||||
|
func privatePosts(w http.ResponseWriter, r *http.Request) {
|
||||||
|
commonIndexHandler(w, r, "", types.PRIVATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// commonIndexHandler handles common logic for blog index views.
|
||||||
|
func commonIndexHandler(w http.ResponseWriter, r *http.Request, tag, privacy string) {
|
||||||
|
// Page title.
|
||||||
|
var title string
|
||||||
|
if privacy == types.DRAFT {
|
||||||
|
title = "Draft Posts"
|
||||||
|
} else if privacy == types.PRIVATE {
|
||||||
|
title = "Private Posts"
|
||||||
|
} else if tag != "" {
|
||||||
|
title = "Tagged as: " + tag
|
||||||
|
} else {
|
||||||
|
title = "Blog"
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Template(w, r, "blog/index", map[string]interface{}{
|
||||||
|
"Title": title,
|
||||||
|
"Tag": tag,
|
||||||
|
"Privacy": privacy,
|
||||||
|
})
|
||||||
|
}
|
212
core/internal/controllers/posts/posts.go
Normal file
212
core/internal/controllers/posts/posts.go
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
package postctl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/kirsle/blog/core/internal/log"
|
||||||
|
"github.com/kirsle/blog/core/internal/markdown"
|
||||||
|
"github.com/kirsle/blog/core/internal/middleware/auth"
|
||||||
|
"github.com/kirsle/blog/core/internal/models/posts"
|
||||||
|
"github.com/kirsle/blog/core/internal/models/users"
|
||||||
|
"github.com/kirsle/blog/core/internal/render"
|
||||||
|
"github.com/kirsle/blog/core/internal/responses"
|
||||||
|
"github.com/kirsle/blog/core/internal/types"
|
||||||
|
"github.com/urfave/negroni"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PostMeta associates a Post with injected metadata.
|
||||||
|
type PostMeta struct {
|
||||||
|
Post *posts.Post
|
||||||
|
Rendered template.HTML
|
||||||
|
Author *users.User
|
||||||
|
NumComments int
|
||||||
|
IndexView bool
|
||||||
|
Snipped bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archive holds data for a piece of the blog archive.
|
||||||
|
type Archive struct {
|
||||||
|
Label string
|
||||||
|
Date time.Time
|
||||||
|
Posts []posts.Post
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the blog routes to the app.
|
||||||
|
func Register(r *mux.Router, loginError http.HandlerFunc) {
|
||||||
|
render.Funcs["RenderIndex"] = partialIndex
|
||||||
|
render.Funcs["RenderPost"] = partialPost
|
||||||
|
render.Funcs["RenderTags"] = partialTags
|
||||||
|
|
||||||
|
// Public routes
|
||||||
|
r.HandleFunc("/blog", indexHandler)
|
||||||
|
r.HandleFunc("/blog.rss", feedHandler)
|
||||||
|
r.HandleFunc("/blog.atom", feedHandler)
|
||||||
|
r.HandleFunc("/archive", archiveHandler)
|
||||||
|
r.HandleFunc("/tagged", taggedHandler)
|
||||||
|
r.HandleFunc("/tagged/{tag}", taggedHandler)
|
||||||
|
r.HandleFunc("/blog/category/{tag}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
params := mux.Vars(r)
|
||||||
|
tag, ok := params["tag"]
|
||||||
|
if !ok {
|
||||||
|
responses.NotFound(w, r, "Not Found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
responses.Redirect(w, "/tagged/"+tag)
|
||||||
|
})
|
||||||
|
r.HandleFunc("/blog/entry/{fragment}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
params := mux.Vars(r)
|
||||||
|
fragment, ok := params["fragment"]
|
||||||
|
if !ok {
|
||||||
|
responses.NotFound(w, r, "Not Found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
responses.Redirect(w, "/"+fragment)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Login-required routers.
|
||||||
|
loginRouter := mux.NewRouter()
|
||||||
|
loginRouter.HandleFunc("/blog/edit", editHandler)
|
||||||
|
loginRouter.HandleFunc("/blog/delete", deleteHandler)
|
||||||
|
loginRouter.HandleFunc("/blog/drafts", drafts)
|
||||||
|
loginRouter.HandleFunc("/blog/private", privatePosts)
|
||||||
|
r.PathPrefix("/blog").Handler(
|
||||||
|
negroni.New(
|
||||||
|
negroni.HandlerFunc(auth.LoginRequired(loginError)),
|
||||||
|
negroni.Wrap(loginRouter),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecentPosts gets and filters the blog entries and orders them by most recent.
|
||||||
|
func RecentPosts(r *http.Request, tag, privacy string) []posts.Post {
|
||||||
|
// Get the blog index.
|
||||||
|
idx, _ := posts.GetIndex()
|
||||||
|
|
||||||
|
// The set of blog posts to show.
|
||||||
|
var pool []posts.Post
|
||||||
|
for _, post := range idx.Posts {
|
||||||
|
// Limiting by a specific privacy setting? (drafts or private only)
|
||||||
|
if privacy != "" {
|
||||||
|
switch privacy {
|
||||||
|
case types.DRAFT:
|
||||||
|
if post.Privacy != types.DRAFT {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
case types.PRIVATE:
|
||||||
|
if post.Privacy != types.PRIVATE && post.Privacy != types.UNLISTED {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Exclude certain posts in generic index views.
|
||||||
|
if (post.Privacy == types.PRIVATE || post.Privacy == types.UNLISTED) && !auth.LoggedIn(r) {
|
||||||
|
continue
|
||||||
|
} else if post.Privacy == types.DRAFT {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit by tag?
|
||||||
|
if tag != "" {
|
||||||
|
var tagMatch bool
|
||||||
|
if tag != "" {
|
||||||
|
for _, check := range post.Tags {
|
||||||
|
if check == tag {
|
||||||
|
tagMatch = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tagMatch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pool = append(pool, post)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(sort.Reverse(posts.ByUpdated(pool)))
|
||||||
|
return pool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewPost is the underlying implementation of the handler to view a blog
|
||||||
|
// post, so that it can be called from non-http.HandlerFunc contexts.
|
||||||
|
// Specifically, from the catch-all page handler to allow blog URL fragments
|
||||||
|
// to map to their post.
|
||||||
|
func ViewPost(w http.ResponseWriter, r *http.Request, fragment string) error {
|
||||||
|
post, err := posts.LoadFragment(fragment)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle post privacy.
|
||||||
|
if post.Privacy == types.PRIVATE || post.Privacy == types.DRAFT {
|
||||||
|
if !auth.LoggedIn(r) {
|
||||||
|
responses.NotFound(w, r, "That post is not public.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v := map[string]interface{}{
|
||||||
|
"Post": post,
|
||||||
|
}
|
||||||
|
render.Template(w, r, "blog/entry", v)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// partialPost 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 partialPost(r *http.Request, p *posts.Post, indexView bool, numComments int) 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, "<snip>") {
|
||||||
|
parts := strings.SplitN(p.Body, "<snip>", 2)
|
||||||
|
p.Body = parts[0]
|
||||||
|
snipped = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Body = strings.Replace(p.Body, "<snip>", "<div id=\"snip\"></div>", 1)
|
||||||
|
|
||||||
|
// Render the post to HTML.
|
||||||
|
var rendered template.HTML
|
||||||
|
if p.ContentType == string(types.MARKDOWN) {
|
||||||
|
rendered = template.HTML(markdown.RenderTrustedMarkdown(p.Body))
|
||||||
|
} else {
|
||||||
|
rendered = template.HTML(p.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
meta := map[string]interface{}{
|
||||||
|
"Post": p,
|
||||||
|
"Rendered": rendered,
|
||||||
|
"Author": author,
|
||||||
|
"IndexView": indexView,
|
||||||
|
"Snipped": snipped,
|
||||||
|
"NumComments": numComments,
|
||||||
|
}
|
||||||
|
output := bytes.Buffer{}
|
||||||
|
err = render.Template(&output, r, "blog/entry.partial", meta)
|
||||||
|
if err != nil {
|
||||||
|
return template.HTML(fmt.Sprintf("[template error in blog/entry.partial: %s]", err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return template.HTML(output.String())
|
||||||
|
}
|
46
core/internal/controllers/posts/tagged.go
Normal file
46
core/internal/controllers/posts/tagged.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package postctl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/kirsle/blog/core/internal/models/posts"
|
||||||
|
"github.com/kirsle/blog/core/internal/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
// tagged lets you browse blog posts by category.
|
||||||
|
func taggedHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
params := mux.Vars(r)
|
||||||
|
tag, ok := params["tag"]
|
||||||
|
if !ok {
|
||||||
|
// They're listing all the tags.
|
||||||
|
render.Template(w, r, "blog/tags.gohtml", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
commonIndexHandler(w, r, tag, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// partialTags renders the tags partial.
|
||||||
|
func partialTags(r *http.Request, indexView bool) template.HTML {
|
||||||
|
idx, err := posts.GetIndex()
|
||||||
|
if err != nil {
|
||||||
|
return template.HTML("[RenderTags: error getting blog index]")
|
||||||
|
}
|
||||||
|
|
||||||
|
tags, err := idx.Tags()
|
||||||
|
if err != nil {
|
||||||
|
return template.HTML("[RenderTags: error getting tags]")
|
||||||
|
}
|
||||||
|
|
||||||
|
var output bytes.Buffer
|
||||||
|
v := map[string]interface{}{
|
||||||
|
"IndexView": indexView,
|
||||||
|
"Tags": tags,
|
||||||
|
}
|
||||||
|
render.Template(&output, r, "blog/tags.partial", v)
|
||||||
|
|
||||||
|
return template.HTML(output.String())
|
||||||
|
}
|
|
@ -7,6 +7,14 @@ import (
|
||||||
"github.com/kirsle/blog/core/internal/sessions"
|
"github.com/kirsle/blog/core/internal/sessions"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Error handlers to be filled in by the blog app.
|
||||||
|
var (
|
||||||
|
NotFound func(http.ResponseWriter, *http.Request, string)
|
||||||
|
Forbidden func(http.ResponseWriter, *http.Request, string)
|
||||||
|
BadRequest func(http.ResponseWriter, *http.Request, string)
|
||||||
|
Error func(http.ResponseWriter, *http.Request, string)
|
||||||
|
)
|
||||||
|
|
||||||
// Flash adds a flash message to the user's session.
|
// Flash adds a flash message to the user's session.
|
||||||
func Flash(w http.ResponseWriter, r *http.Request, message string, args ...interface{}) {
|
func Flash(w http.ResponseWriter, r *http.Request, message string, args ...interface{}) {
|
||||||
session := sessions.Get(r)
|
session := sessions.Get(r)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package core
|
package types
|
||||||
|
|
||||||
// PostPrivacy values.
|
// PostPrivacy values.
|
||||||
type PostPrivacy string
|
type PostPrivacy string
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/kirsle/blog/core/internal/controllers/posts"
|
||||||
"github.com/kirsle/blog/core/internal/markdown"
|
"github.com/kirsle/blog/core/internal/markdown"
|
||||||
"github.com/kirsle/blog/core/internal/render"
|
"github.com/kirsle/blog/core/internal/render"
|
||||||
"github.com/kirsle/blog/core/internal/responses"
|
"github.com/kirsle/blog/core/internal/responses"
|
||||||
|
@ -24,7 +25,7 @@ func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// Restrict special paths.
|
// Restrict special paths.
|
||||||
if strings.HasPrefix(strings.ToLower(path), "/.") {
|
if strings.HasPrefix(strings.ToLower(path), "/.") {
|
||||||
b.Forbidden(w, r, "Forbidden")
|
responses.Forbidden(w, r, "Forbidden")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,9 +33,9 @@ func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
filepath, err := render.ResolvePath(path)
|
filepath, err := render.ResolvePath(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// See if it resolves as a blog entry.
|
// See if it resolves as a blog entry.
|
||||||
err = b.viewPost(w, r, strings.TrimLeft(path, "/"))
|
err = postctl.ViewPost(w, r, strings.TrimLeft(path, "/"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.NotFound(w, r, "The page you were looking for was not found.")
|
responses.NotFound(w, r, "The page you were looking for was not found.")
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -49,7 +50,7 @@ func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if strings.HasSuffix(filepath.URI, ".md") || strings.HasSuffix(filepath.URI, ".markdown") {
|
if strings.HasSuffix(filepath.URI, ".md") || strings.HasSuffix(filepath.URI, ".markdown") {
|
||||||
source, err := ioutil.ReadFile(filepath.Absolute)
|
source, err := ioutil.ReadFile(filepath.Absolute)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(w, r, "Couldn't read Markdown source!")
|
responses.Error(w, r, "Couldn't read Markdown source!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user