2017-11-20 05:49:19 +00:00
|
|
|
package core
|
|
|
|
|
|
|
|
import (
|
2017-11-24 19:56:32 +00:00
|
|
|
"bytes"
|
|
|
|
"errors"
|
2017-11-27 02:52:14 +00:00
|
|
|
"fmt"
|
2017-11-20 05:49:19 +00:00
|
|
|
"html/template"
|
|
|
|
"net/http"
|
2017-11-24 19:56:32 +00:00
|
|
|
"sort"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
2017-11-24 21:27:08 +00:00
|
|
|
"time"
|
2017-11-20 05:49:19 +00:00
|
|
|
|
|
|
|
"github.com/gorilla/mux"
|
2017-11-27 02:52:14 +00:00
|
|
|
"github.com/kirsle/blog/core/models/comments"
|
2017-11-20 05:49:19 +00:00
|
|
|
"github.com/kirsle/blog/core/models/posts"
|
2017-11-24 19:56:32 +00:00
|
|
|
"github.com/kirsle/blog/core/models/users"
|
2017-11-20 05:49:19 +00:00
|
|
|
"github.com/urfave/negroni"
|
|
|
|
)
|
|
|
|
|
2017-11-24 19:56:32 +00:00
|
|
|
// PostMeta associates a Post with injected metadata.
|
|
|
|
type PostMeta struct {
|
2017-11-27 02:52:14 +00:00
|
|
|
Post *posts.Post
|
|
|
|
Rendered template.HTML
|
|
|
|
Author *users.User
|
|
|
|
NumComments int
|
|
|
|
IndexView bool
|
|
|
|
Snipped bool
|
2017-11-24 19:56:32 +00:00
|
|
|
}
|
|
|
|
|
2017-11-24 21:27:08 +00:00
|
|
|
// Archive holds data for a piece of the blog archive.
|
|
|
|
type Archive struct {
|
|
|
|
Label string
|
|
|
|
Date time.Time
|
|
|
|
Posts []posts.Post
|
|
|
|
}
|
|
|
|
|
2017-11-20 05:49:19 +00:00
|
|
|
// BlogRoutes attaches the blog routes to the app.
|
|
|
|
func (b *Blog) BlogRoutes(r *mux.Router) {
|
2017-11-24 19:56:32 +00:00
|
|
|
// Public routes
|
2017-12-01 16:07:21 +00:00
|
|
|
r.HandleFunc("/blog", b.IndexHandler)
|
2017-11-24 21:27:08 +00:00
|
|
|
r.HandleFunc("/archive", b.BlogArchive)
|
2017-11-24 20:53:13 +00:00
|
|
|
r.HandleFunc("/tagged/{tag}", b.Tagged)
|
2017-11-24 19:56:32 +00:00
|
|
|
|
2017-11-20 05:49:19 +00:00
|
|
|
// Login-required routers.
|
|
|
|
loginRouter := mux.NewRouter()
|
|
|
|
loginRouter.HandleFunc("/blog/edit", b.EditBlog)
|
2017-11-24 19:56:32 +00:00
|
|
|
loginRouter.HandleFunc("/blog/delete", b.DeletePost)
|
2017-11-24 20:53:13 +00:00
|
|
|
loginRouter.HandleFunc("/blog/drafts", b.Drafts)
|
|
|
|
loginRouter.HandleFunc("/blog/private", b.PrivatePosts)
|
2017-11-20 05:49:19 +00:00
|
|
|
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),
|
|
|
|
))
|
|
|
|
}
|
|
|
|
|
2017-12-01 16:07:21 +00:00
|
|
|
// IndexHandler renders the main index page of the blog.
|
|
|
|
func (b *Blog) IndexHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
b.CommonIndexHandler(w, r, "", "")
|
2017-11-24 20:53:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
|
|
|
b.BadRequest(w, r, "Missing category in URL")
|
|
|
|
}
|
|
|
|
|
2017-12-01 16:07:21 +00:00
|
|
|
b.CommonIndexHandler(w, r, tag, "")
|
2017-11-24 20:53:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Drafts renders an index view of only draft posts. Login required.
|
|
|
|
func (b *Blog) Drafts(w http.ResponseWriter, r *http.Request) {
|
2017-12-01 16:07:21 +00:00
|
|
|
b.CommonIndexHandler(w, r, "", DRAFT)
|
2017-11-24 20:53:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// PrivatePosts renders an index view of only private posts. Login required.
|
|
|
|
func (b *Blog) PrivatePosts(w http.ResponseWriter, r *http.Request) {
|
2017-12-01 16:07:21 +00:00
|
|
|
b.CommonIndexHandler(w, r, "", PRIVATE)
|
2017-11-24 20:53:13 +00:00
|
|
|
}
|
|
|
|
|
2017-12-01 16:07:21 +00:00
|
|
|
// 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"
|
|
|
|
}
|
|
|
|
|
|
|
|
b.RenderTemplate(w, r, "blog/index", NewVars(map[interface{}]interface{}{
|
|
|
|
"Title": title,
|
|
|
|
"Tag": tag,
|
|
|
|
"Privacy": privacy,
|
|
|
|
}))
|
|
|
|
}
|
2017-11-24 19:56:32 +00:00
|
|
|
|
2017-12-01 16:07:21 +00:00
|
|
|
// RenderIndex renders and returns the blog index partial.
|
|
|
|
func (b *Blog) RenderIndex(r *http.Request, tag, privacy string) template.HTML {
|
2017-11-24 19:56:32 +00:00
|
|
|
// Get the blog index.
|
|
|
|
idx, _ := posts.GetIndex()
|
|
|
|
|
|
|
|
// The set of blog posts to show.
|
|
|
|
var pool []posts.Post
|
|
|
|
for _, post := range idx.Posts {
|
2017-11-24 20:53:13 +00:00
|
|
|
// 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) && !b.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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-24 19:56:32 +00:00
|
|
|
pool = append(pool, post)
|
|
|
|
}
|
|
|
|
|
2017-11-24 20:53:13 +00:00
|
|
|
if len(pool) == 0 {
|
2017-12-01 16:07:21 +00:00
|
|
|
return template.HTML("No blog posts were found.")
|
2017-11-24 20:53:13 +00:00
|
|
|
}
|
|
|
|
|
2017-11-24 19:56:32 +00:00
|
|
|
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.
|
2017-12-01 16:07:21 +00:00
|
|
|
var previousPage, nextPage int
|
2017-11-24 19:56:32 +00:00
|
|
|
if page > 1 {
|
2017-12-01 16:07:21 +00:00
|
|
|
previousPage = page - 1
|
2017-11-24 19:56:32 +00:00
|
|
|
} else {
|
2017-12-01 16:07:21 +00:00
|
|
|
previousPage = 0
|
2017-11-24 19:56:32 +00:00
|
|
|
}
|
|
|
|
if offset+perPage < len(pool) {
|
2017-12-01 16:07:21 +00:00
|
|
|
nextPage = page + 1
|
2017-11-24 19:56:32 +00:00
|
|
|
} else {
|
2017-12-01 16:07:21 +00:00
|
|
|
nextPage = 0
|
2017-11-24 19:56:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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, "<snip>") {
|
|
|
|
parts := strings.SplitN(post.Body, "<snip>", 1)
|
|
|
|
post.Body = parts[0]
|
|
|
|
}
|
|
|
|
|
|
|
|
// Render the post.
|
2017-11-24 20:53:13 +00:00
|
|
|
if post.ContentType == string(MARKDOWN) {
|
2017-11-24 19:56:32 +00:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2017-11-27 02:52:14 +00:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2017-11-24 19:56:32 +00:00
|
|
|
view = append(view, PostMeta{
|
2017-11-27 02:52:14 +00:00
|
|
|
Post: post,
|
|
|
|
Rendered: rendered,
|
|
|
|
Author: author,
|
|
|
|
NumComments: numComments,
|
2017-11-24 19:56:32 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2017-12-01 16:07:21 +00:00
|
|
|
// Render the blog index partial.
|
|
|
|
var output bytes.Buffer
|
|
|
|
v := map[string]interface{}{
|
|
|
|
"PreviousPage": previousPage,
|
|
|
|
"NextPage": nextPage,
|
|
|
|
"View": view,
|
|
|
|
}
|
|
|
|
b.RenderPartialTemplate(&output, "blog/index.partial", v, false, nil)
|
|
|
|
|
|
|
|
return template.HTML(output.String())
|
2017-11-24 19:56:32 +00:00
|
|
|
}
|
|
|
|
|
2017-11-24 21:27:08 +00:00
|
|
|
// 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) && !b.LoggedIn(r) {
|
|
|
|
continue
|
|
|
|
} else if post.Privacy == DRAFT {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
label := post.Created.Format("2006-02")
|
|
|
|
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 := NewVars(map[interface{}]interface{}{
|
|
|
|
"Archive": result,
|
|
|
|
})
|
|
|
|
b.RenderTemplate(w, r, "blog/archive", v)
|
|
|
|
}
|
|
|
|
|
2017-11-24 19:56:32 +00:00
|
|
|
// viewPost is the underlying implementation of the handler to view a blog
|
|
|
|
// post, so that it can be called from non-http.HandlerFunc contexts.
|
2017-12-01 16:07:21 +00:00
|
|
|
// Specifically, from the catch-all page handler to allow blog URL fragments
|
|
|
|
// to map to their post.
|
2017-11-24 19:56:32 +00:00
|
|
|
func (b *Blog) viewPost(w http.ResponseWriter, r *http.Request, fragment string) error {
|
|
|
|
post, err := posts.LoadFragment(fragment)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-11-24 20:53:13 +00:00
|
|
|
// Handle post privacy.
|
|
|
|
if post.Privacy == PRIVATE || post.Privacy == DRAFT {
|
|
|
|
if !b.LoggedIn(r) {
|
|
|
|
b.NotFound(w, r)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-24 19:56:32 +00:00
|
|
|
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.
|
2017-11-27 02:52:14 +00:00
|
|
|
func (b *Blog) RenderPost(p *posts.Post, indexView bool, numComments int) template.HTML {
|
2017-11-24 19:56:32 +00:00
|
|
|
// 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>") {
|
|
|
|
log.Warn("HAS SNIP TAG!")
|
|
|
|
parts := strings.SplitN(p.Body, "<snip>", 2)
|
|
|
|
p.Body = parts[0]
|
|
|
|
snipped = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Render the post to HTML.
|
|
|
|
var rendered template.HTML
|
2017-11-24 20:53:13 +00:00
|
|
|
if p.ContentType == string(MARKDOWN) {
|
2017-11-24 19:56:32 +00:00
|
|
|
rendered = template.HTML(b.RenderTrustedMarkdown(p.Body))
|
|
|
|
} else {
|
|
|
|
rendered = template.HTML(p.Body)
|
|
|
|
}
|
|
|
|
|
|
|
|
meta := PostMeta{
|
2017-11-27 02:52:14 +00:00
|
|
|
Post: p,
|
|
|
|
Rendered: rendered,
|
|
|
|
Author: author,
|
|
|
|
IndexView: indexView,
|
|
|
|
Snipped: snipped,
|
|
|
|
NumComments: numComments,
|
2017-11-24 19:56:32 +00:00
|
|
|
}
|
|
|
|
output := bytes.Buffer{}
|
2017-12-01 16:07:21 +00:00
|
|
|
err = b.RenderPartialTemplate(&output, "blog/entry.partial", meta, false, nil)
|
2017-11-24 19:56:32 +00:00
|
|
|
if err != nil {
|
2017-12-01 16:07:21 +00:00
|
|
|
return template.HTML(fmt.Sprintf("[template error in blog/entry.partial: %s]", err.Error()))
|
2017-11-24 19:56:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return template.HTML(output.String())
|
|
|
|
}
|
|
|
|
|
2017-11-20 05:49:19 +00:00
|
|
|
// EditBlog is the blog writing and editing page.
|
|
|
|
func (b *Blog) EditBlog(w http.ResponseWriter, r *http.Request) {
|
|
|
|
v := NewVars(map[interface{}]interface{}{
|
|
|
|
"preview": "",
|
|
|
|
})
|
2017-11-24 19:56:32 +00:00
|
|
|
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()
|
|
|
|
}
|
2017-11-20 05:49:19 +00:00
|
|
|
|
|
|
|
if r.Method == http.MethodPost {
|
|
|
|
// Parse from form values.
|
2017-11-24 19:56:32 +00:00
|
|
|
post.ParseForm(r)
|
2017-11-20 05:49:19 +00:00
|
|
|
|
|
|
|
// Previewing, or submitting?
|
|
|
|
switch r.FormValue("submit") {
|
|
|
|
case "preview":
|
2017-11-24 20:53:13 +00:00
|
|
|
if post.ContentType == string(MARKDOWN) {
|
2017-11-26 23:53:10 +00:00
|
|
|
v.Data["preview"] = template.HTML(b.RenderTrustedMarkdown(post.Body))
|
2017-11-24 19:56:32 +00:00
|
|
|
} else {
|
|
|
|
v.Data["preview"] = template.HTML(post.Body)
|
|
|
|
}
|
|
|
|
case "post":
|
2017-11-20 05:49:19 +00:00
|
|
|
if err := post.Validate(); err != nil {
|
|
|
|
v.Error = err
|
2017-11-24 19:56:32 +00:00
|
|
|
} 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)
|
|
|
|
}
|
2017-11-20 05:49:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
v.Data["post"] = post
|
|
|
|
b.RenderTemplate(w, r, "blog/edit", v)
|
|
|
|
}
|
2017-11-24 19:56:32 +00:00
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|