A web blog and personal homepage engine written in Go.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

301 lines
7.4 KiB

  1. package controllers
  2. import (
  3. "bytes"
  4. "fmt"
  5. "html/template"
  6. "net/http"
  7. "strconv"
  8. "strings"
  9. "git.kirsle.net/apps/gophertype/pkg/authentication"
  10. "git.kirsle.net/apps/gophertype/pkg/glue"
  11. "git.kirsle.net/apps/gophertype/pkg/markdown"
  12. "git.kirsle.net/apps/gophertype/pkg/models"
  13. "git.kirsle.net/apps/gophertype/pkg/responses"
  14. "git.kirsle.net/apps/gophertype/pkg/session"
  15. "git.kirsle.net/apps/gophertype/pkg/settings"
  16. "github.com/albrow/forms"
  17. "github.com/gorilla/mux"
  18. )
  19. func init() {
  20. glue.Register(glue.Endpoint{
  21. Path: "/blog",
  22. Methods: []string{"GET"},
  23. Handler: BlogIndex(models.Public, false),
  24. })
  25. glue.Register(glue.Endpoint{
  26. Path: "/tagged",
  27. Methods: []string{"GET"},
  28. Handler: TagIndex,
  29. })
  30. glue.Register(glue.Endpoint{
  31. Path: "/tagged/{tag}",
  32. Methods: []string{"GET"},
  33. Handler: BlogIndex(models.Public, true),
  34. })
  35. glue.Register(glue.Endpoint{
  36. Path: "/archive",
  37. Methods: []string{"GET"},
  38. Handler: BlogArchive,
  39. })
  40. glue.Register(glue.Endpoint{
  41. Path: "/blog/random",
  42. Methods: []string{"GET"},
  43. Handler: BlogRandom,
  44. })
  45. glue.Register(glue.Endpoint{
  46. Path: "/blog/drafts",
  47. Middleware: []mux.MiddlewareFunc{
  48. authentication.LoginRequired,
  49. },
  50. Methods: []string{"GET"},
  51. Handler: BlogIndex(models.Draft, false),
  52. })
  53. glue.Register(glue.Endpoint{
  54. Path: "/blog/private",
  55. Middleware: []mux.MiddlewareFunc{
  56. authentication.LoginRequired,
  57. },
  58. Methods: []string{"GET"},
  59. Handler: BlogIndex(models.Private, false),
  60. })
  61. glue.Register(glue.Endpoint{
  62. Path: "/blog/unlisted",
  63. Middleware: []mux.MiddlewareFunc{
  64. authentication.LoginRequired,
  65. },
  66. Methods: []string{"GET"},
  67. Handler: BlogIndex(models.Unlisted, false),
  68. })
  69. glue.Register(glue.Endpoint{
  70. Path: "/blog/edit",
  71. Methods: []string{"GET", "POST"},
  72. Middleware: []mux.MiddlewareFunc{
  73. authentication.LoginRequired,
  74. },
  75. Handler: EditPost,
  76. })
  77. }
  78. // BlogIndex handles all of the top-level blog index routes:
  79. // - /blog
  80. // - /tagged/{tag}
  81. // - /blog/unlisted
  82. // - /blog/drafts
  83. // - /blog/private
  84. func BlogIndex(privacy string, tagged bool) http.HandlerFunc {
  85. return func(w http.ResponseWriter, r *http.Request) {
  86. var (
  87. v = responses.NewTemplateVars(w, r)
  88. tagName string
  89. // Multitag values.
  90. isMultitag bool
  91. include []string
  92. exclude []string
  93. )
  94. // Tagged view?
  95. if tagged {
  96. params := mux.Vars(r)
  97. tagName = params["tag"]
  98. }
  99. // Page title to use.
  100. var title = "Blog"
  101. if tagged {
  102. // Check for use of multi-tags.
  103. if strings.Contains(tagName, ",") {
  104. title = "Tagged Posts"
  105. if inc, exc, err := models.ParseMultitag(tagName); err == nil {
  106. isMultitag = true
  107. include = inc
  108. exclude = exc
  109. }
  110. } else {
  111. title = "Tagged as: " + tagName
  112. }
  113. } else if privacy == models.Draft {
  114. title = "Drafts"
  115. } else if privacy == models.Unlisted {
  116. title = "Unlisted"
  117. } else if privacy == models.Private {
  118. title = "Private"
  119. }
  120. v.V["title"] = title
  121. v.V["tag"] = tagName
  122. v.V["Multitag"] = struct {
  123. Is bool
  124. Include []string
  125. Exclude []string
  126. }{
  127. isMultitag,
  128. include,
  129. exclude,
  130. }
  131. v.V["privacy"] = privacy
  132. responses.RenderTemplate(w, r, "_builtin/blog/index.gohtml", v)
  133. }
  134. }
  135. // PostFragment at "/<fragment>" for viewing blog entries.
  136. func PostFragment(w http.ResponseWriter, r *http.Request) {
  137. fragment := strings.Trim(r.URL.Path, "/")
  138. post, err := models.Posts.LoadFragment(fragment)
  139. if err != nil {
  140. responses.NotFound(w, r)
  141. return
  142. }
  143. // Is it a private post and are we logged in?
  144. if post.Privacy != models.Public && post.Privacy != models.Unlisted && !authentication.LoggedIn(r) {
  145. responses.Forbidden(w, r, "Permission denied to view that post.")
  146. return
  147. }
  148. v := responses.NewTemplateVars(w, r)
  149. v.V["post"] = post
  150. // Render the body.
  151. v.V["rendered"] = post.HTML()
  152. responses.RenderTemplate(w, r, "_builtin/blog/view-post.gohtml", v)
  153. }
  154. // PartialBlogIndex is a template function to embed a blog index view on any page.
  155. func PartialBlogIndex(r *http.Request, tag, privacy string) template.HTML {
  156. html := bytes.NewBuffer([]byte{})
  157. v := responses.NewTemplateVars(html, r)
  158. page, _ := strconv.Atoi(r.FormValue("page"))
  159. var (
  160. posts models.PagedPosts
  161. err error
  162. )
  163. if tag != "" {
  164. posts, err = models.Posts.GetPostsByTag(tag, privacy, page, settings.Current.PostsPerPage)
  165. } else {
  166. posts, err = models.Posts.GetIndexPosts(privacy, page, settings.Current.PostsPerPage)
  167. }
  168. if err != nil && err.Error() != "sql: no rows in result set" {
  169. return template.HTML(fmt.Sprintf("[BlogIndex: %s]", err))
  170. }
  171. v.V["posts"] = posts.Posts
  172. v.V["paging"] = posts
  173. responses.PartialTemplate(html, r, "_builtin/blog/index.partial.gohtml", v)
  174. return template.HTML(html.String())
  175. }
  176. // TagIndex for "/tagged" to return all tags sorted by popularity.
  177. func TagIndex(w http.ResponseWriter, r *http.Request) {
  178. // If not logged in, only summarize public post tags.
  179. var public = !authentication.LoggedIn(r)
  180. tags := models.SummarizeTags(public)
  181. v := responses.NewTemplateVars(w, r)
  182. v.V["tags"] = tags
  183. responses.RenderTemplate(w, r, "_builtin/blog/tags.gohtml", v)
  184. }
  185. // BlogArchive shows the archive page of ALL blog posts.
  186. func BlogArchive(w http.ResponseWriter, r *http.Request) {
  187. v := responses.NewTemplateVars(w, r)
  188. // Show private and unlisted posts?
  189. showPrivate := authentication.LoggedIn(r)
  190. archive, err := models.Posts.GetArchive(showPrivate)
  191. if err != nil {
  192. responses.Error(w, r, http.StatusInternalServerError, err.Error())
  193. return
  194. }
  195. v.V["archive"] = archive
  196. responses.RenderTemplate(w, r, "_builtin/blog/archive.gohtml", v)
  197. }
  198. // BlogRandom handles the /blog/random route and picks a random post.
  199. func BlogRandom(w http.ResponseWriter, r *http.Request) {
  200. post, err := models.Posts.LoadRandom(models.Public)
  201. if err != nil {
  202. responses.Error(w, r, http.StatusInternalServerError, err.Error())
  203. return
  204. }
  205. responses.Redirect(w, r, "/"+post.Fragment)
  206. }
  207. // EditPost at "/blog/edit"
  208. func EditPost(w http.ResponseWriter, r *http.Request) {
  209. v := responses.NewTemplateVars(w, r)
  210. v.V["preview"] = ""
  211. // The blog post we're working with.
  212. var post = models.Posts.New()
  213. var isNew = true
  214. // Editing an existing post?
  215. if r.FormValue("id") != "" {
  216. id, _ := strconv.Atoi(r.FormValue("id"))
  217. if p, err := models.Posts.Load(id); err == nil {
  218. post = p
  219. isNew = false
  220. }
  221. }
  222. // POST handler: create the admin account.
  223. for r.Method == http.MethodPost {
  224. form, _ := forms.Parse(r)
  225. // Validate form parameters.
  226. val := form.Validator()
  227. val.Require("title")
  228. val.Require("body")
  229. post.ParseForm(form)
  230. if val.HasErrors() {
  231. v.ValidationError = val.ErrorMap()
  232. break
  233. }
  234. // Previewing or submitting the post?
  235. switch form.Get("submit") {
  236. case "preview":
  237. if post.ContentType == models.Markdown {
  238. v.V["preview"] = template.HTML(markdown.RenderTrustedMarkdown(post.Body))
  239. } else {
  240. v.V["preview"] = template.HTML(post.Body)
  241. }
  242. case "post":
  243. author, _ := authentication.CurrentUser(r)
  244. post.AuthorID = author.ID
  245. // When editing, allow to not touch the Last Updated time.
  246. var resetUpdated bool = !isNew && form.GetBool("no-update") == true
  247. err := post.Save()
  248. if resetUpdated {
  249. post.SetUpdated(post.CreatedAt)
  250. }
  251. if err != nil {
  252. v.Error = err
  253. } else {
  254. session.Flash(w, r, "Post created!")
  255. responses.Redirect(w, r, "/"+post.Fragment)
  256. }
  257. }
  258. break
  259. }
  260. v.V["post"] = post
  261. v.V["isNew"] = isNew
  262. responses.RenderTemplate(w, r, "_builtin/blog/edit.gohtml", v)
  263. }