2021-07-30 19:59:48 +00:00
|
|
|
package website
|
|
|
|
|
|
|
|
import (
|
2021-09-14 04:13:58 +00:00
|
|
|
"errors"
|
2021-07-30 22:32:19 +00:00
|
|
|
"fmt"
|
2021-08-03 01:52:46 +00:00
|
|
|
"html/template"
|
2021-07-30 19:59:48 +00:00
|
|
|
"net/http"
|
|
|
|
"strconv"
|
2021-08-03 01:52:46 +00:00
|
|
|
"time"
|
2021-07-30 19:59:48 +00:00
|
|
|
|
|
|
|
"git.handmade.network/hmn/hmn/src/db"
|
2021-12-09 02:04:15 +00:00
|
|
|
"git.handmade.network/hmn/hmn/src/hmndata"
|
2021-07-30 19:59:48 +00:00
|
|
|
"git.handmade.network/hmn/hmn/src/hmnurl"
|
2021-07-30 22:32:19 +00:00
|
|
|
"git.handmade.network/hmn/hmn/src/models"
|
|
|
|
"git.handmade.network/hmn/hmn/src/oops"
|
2021-07-30 19:59:48 +00:00
|
|
|
"git.handmade.network/hmn/hmn/src/templates"
|
2021-08-03 01:52:46 +00:00
|
|
|
"git.handmade.network/hmn/hmn/src/utils"
|
2021-07-30 19:59:48 +00:00
|
|
|
)
|
|
|
|
|
2021-08-03 01:52:46 +00:00
|
|
|
func BlogIndex(c *RequestContext) ResponseData {
|
|
|
|
type blogIndexEntry struct {
|
|
|
|
Title string
|
|
|
|
Url string
|
|
|
|
Author templates.User
|
|
|
|
Date time.Time
|
|
|
|
Content template.HTML
|
|
|
|
}
|
|
|
|
type blogIndexData struct {
|
|
|
|
templates.BaseData
|
|
|
|
Posts []blogIndexEntry
|
|
|
|
Pagination templates.Pagination
|
2021-08-03 03:27:59 +00:00
|
|
|
|
|
|
|
CanCreatePost bool
|
|
|
|
NewPostUrl string
|
2021-08-03 01:52:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const postsPerPage = 5
|
|
|
|
|
2021-12-09 02:04:15 +00:00
|
|
|
numThreads, err := hmndata.CountThreads(c.Context(), c.Conn, c.CurrentUser, hmndata.ThreadsQuery{
|
2021-09-14 04:13:58 +00:00
|
|
|
ProjectIDs: []int{c.CurrentProject.ID},
|
|
|
|
ThreadTypes: []models.ThreadType{models.ThreadTypeProjectBlogPost},
|
|
|
|
})
|
2021-08-03 01:52:46 +00:00
|
|
|
if err != nil {
|
2021-08-28 12:21:03 +00:00
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch total number of blog posts"))
|
2021-08-03 01:52:46 +00:00
|
|
|
}
|
|
|
|
|
2021-11-10 17:13:56 +00:00
|
|
|
numPages := utils.NumPages(numThreads, postsPerPage)
|
2021-08-03 01:52:46 +00:00
|
|
|
page, ok := ParsePageNumber(c, "page", numPages)
|
|
|
|
if !ok {
|
2021-11-10 04:11:39 +00:00
|
|
|
c.Redirect(c.UrlContext.BuildBlog(page), http.StatusSeeOther)
|
2021-08-03 01:52:46 +00:00
|
|
|
}
|
|
|
|
|
2021-12-09 02:04:15 +00:00
|
|
|
threads, err := hmndata.FetchThreads(c.Context(), c.Conn, c.CurrentUser, hmndata.ThreadsQuery{
|
2021-12-11 22:18:58 +00:00
|
|
|
ProjectIDs: []int{c.CurrentProject.ID},
|
|
|
|
ThreadTypes: []models.ThreadType{models.ThreadTypeProjectBlogPost},
|
|
|
|
Limit: postsPerPage,
|
|
|
|
Offset: (page - 1) * postsPerPage,
|
|
|
|
OrderByCreated: true,
|
2021-09-14 04:13:58 +00:00
|
|
|
})
|
2021-08-03 01:52:46 +00:00
|
|
|
if err != nil {
|
2021-08-28 12:21:03 +00:00
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch blog posts for index"))
|
2021-08-03 01:52:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var entries []blogIndexEntry
|
2021-09-14 04:13:58 +00:00
|
|
|
for _, thread := range threads {
|
2021-08-03 01:52:46 +00:00
|
|
|
entries = append(entries, blogIndexEntry{
|
2021-09-14 04:13:58 +00:00
|
|
|
Title: thread.Thread.Title,
|
2021-11-10 04:11:39 +00:00
|
|
|
Url: c.UrlContext.BuildBlogThread(thread.Thread.ID, thread.Thread.Title),
|
2021-09-14 04:13:58 +00:00
|
|
|
Author: templates.UserToTemplate(thread.FirstPostAuthor, c.Theme),
|
|
|
|
Date: thread.FirstPost.PostDate,
|
|
|
|
Content: template.HTML(thread.FirstPostCurrentVersion.TextParsed),
|
2021-08-03 01:52:46 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-11-10 04:11:39 +00:00
|
|
|
baseData := getBaseData(c, fmt.Sprintf("%s Blog", c.CurrentProject.Name), []templates.Breadcrumb{BlogBreadcrumb(c.UrlContext)})
|
2021-08-03 01:52:46 +00:00
|
|
|
|
2021-08-03 03:27:59 +00:00
|
|
|
canCreate := false
|
2021-11-10 17:13:56 +00:00
|
|
|
if c.CurrentProject.HasBlog() && c.CurrentUser != nil {
|
2021-08-03 03:27:59 +00:00
|
|
|
isProjectOwner := false
|
2022-05-07 18:58:00 +00:00
|
|
|
owners, err := hmndata.FetchProjectOwners(c.Context(), c.Conn, c.CurrentUser, c.CurrentProject.ID)
|
2021-08-03 03:27:59 +00:00
|
|
|
if err != nil {
|
2021-08-28 12:21:03 +00:00
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch project owners"))
|
2021-08-03 03:27:59 +00:00
|
|
|
}
|
|
|
|
for _, owner := range owners {
|
|
|
|
if owner.ID == c.CurrentUser.ID {
|
|
|
|
isProjectOwner = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
canCreate = c.CurrentUser.IsStaff || isProjectOwner
|
|
|
|
}
|
|
|
|
|
2021-08-03 01:52:46 +00:00
|
|
|
var res ResponseData
|
|
|
|
res.MustWriteTemplate("blog_index.html", blogIndexData{
|
|
|
|
BaseData: baseData,
|
|
|
|
Posts: entries,
|
|
|
|
Pagination: templates.Pagination{
|
|
|
|
Current: page,
|
|
|
|
Total: numPages,
|
|
|
|
|
2021-11-10 04:11:39 +00:00
|
|
|
FirstUrl: c.UrlContext.BuildBlog(1),
|
|
|
|
LastUrl: c.UrlContext.BuildBlog(numPages),
|
|
|
|
PreviousUrl: c.UrlContext.BuildBlog(utils.IntClamp(1, page-1, numPages)),
|
|
|
|
NextUrl: c.UrlContext.BuildBlog(utils.IntClamp(1, page+1, numPages)),
|
2021-08-03 01:52:46 +00:00
|
|
|
},
|
2021-08-03 03:27:59 +00:00
|
|
|
|
|
|
|
CanCreatePost: canCreate,
|
2021-11-10 04:11:39 +00:00
|
|
|
NewPostUrl: c.UrlContext.BuildBlogNewThread(),
|
2021-08-03 01:52:46 +00:00
|
|
|
}, c.Perf)
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
2021-07-30 19:59:48 +00:00
|
|
|
func BlogThread(c *RequestContext) ResponseData {
|
|
|
|
type blogPostData struct {
|
|
|
|
templates.BaseData
|
|
|
|
Thread templates.Thread
|
|
|
|
MainPost templates.Post
|
|
|
|
Comments []templates.Post
|
|
|
|
ReplyLink string
|
|
|
|
LoginLink string
|
|
|
|
}
|
|
|
|
|
|
|
|
cd, ok := getCommonBlogData(c)
|
|
|
|
if !ok {
|
|
|
|
return FourOhFour(c)
|
|
|
|
}
|
|
|
|
|
2021-12-09 02:04:15 +00:00
|
|
|
thread, posts, err := hmndata.FetchThreadPosts(c.Context(), c.Conn, c.CurrentUser, cd.ThreadID, hmndata.PostsQuery{
|
2021-09-14 04:13:58 +00:00
|
|
|
ProjectIDs: []int{c.CurrentProject.ID},
|
|
|
|
ThreadTypes: []models.ThreadType{models.ThreadTypeProjectBlogPost},
|
|
|
|
})
|
|
|
|
if errors.Is(err, db.NotFound) {
|
|
|
|
return FourOhFour(c)
|
|
|
|
} else if err != nil {
|
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch posts for blog thread"))
|
|
|
|
}
|
2021-07-30 19:59:48 +00:00
|
|
|
|
|
|
|
var templatePosts []templates.Post
|
|
|
|
for _, p := range posts {
|
|
|
|
post := templates.PostToTemplate(&p.Post, p.Author, c.Theme)
|
|
|
|
post.AddContentVersion(p.CurrentVersion, p.Editor)
|
2021-11-10 04:11:39 +00:00
|
|
|
addBlogUrlsToPost(c.UrlContext, &post, &p.Thread, p.Post.ID)
|
2021-07-30 19:59:48 +00:00
|
|
|
|
|
|
|
if p.ReplyPost != nil {
|
|
|
|
reply := templates.PostToTemplate(p.ReplyPost, p.ReplyAuthor, c.Theme)
|
2021-11-10 04:11:39 +00:00
|
|
|
addBlogUrlsToPost(c.UrlContext, &reply, &p.Thread, p.Post.ID)
|
2021-07-30 19:59:48 +00:00
|
|
|
post.ReplyPost = &reply
|
|
|
|
}
|
|
|
|
|
|
|
|
templatePosts = append(templatePosts, post)
|
|
|
|
}
|
2021-09-05 20:16:35 +00:00
|
|
|
// Update thread last read info
|
|
|
|
if c.CurrentUser != nil {
|
|
|
|
c.Perf.StartBlock("SQL", "Update TLRI")
|
|
|
|
_, err := c.Conn.Exec(c.Context(),
|
|
|
|
`
|
2022-05-07 13:11:05 +00:00
|
|
|
INSERT INTO thread_last_read_info (thread_id, user_id, lastread)
|
2021-09-05 20:16:35 +00:00
|
|
|
VALUES ($1, $2, $3)
|
|
|
|
ON CONFLICT (thread_id, user_id) DO UPDATE
|
|
|
|
SET lastread = EXCLUDED.lastread
|
|
|
|
`,
|
|
|
|
cd.ThreadID,
|
|
|
|
c.CurrentUser.ID,
|
|
|
|
time.Now(),
|
|
|
|
)
|
|
|
|
c.Perf.EndBlock()
|
|
|
|
if err != nil {
|
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to update blog tlri"))
|
|
|
|
}
|
|
|
|
}
|
2021-07-30 19:59:48 +00:00
|
|
|
|
2021-11-10 04:11:39 +00:00
|
|
|
baseData := getBaseData(c, thread.Title, []templates.Breadcrumb{BlogBreadcrumb(c.UrlContext)})
|
2021-09-09 02:51:43 +00:00
|
|
|
baseData.OpenGraphItems = append(baseData.OpenGraphItems, templates.OpenGraphItem{
|
|
|
|
Property: "og:description",
|
2021-09-14 04:13:58 +00:00
|
|
|
Value: posts[0].Post.Preview,
|
2021-09-09 02:51:43 +00:00
|
|
|
})
|
2021-07-30 19:59:48 +00:00
|
|
|
|
|
|
|
var res ResponseData
|
|
|
|
res.MustWriteTemplate("blog_post.html", blogPostData{
|
2021-07-30 23:08:42 +00:00
|
|
|
BaseData: baseData,
|
|
|
|
Thread: templates.ThreadToTemplate(&thread),
|
|
|
|
MainPost: templatePosts[0],
|
|
|
|
Comments: templatePosts[1:],
|
2021-11-10 04:11:39 +00:00
|
|
|
ReplyLink: c.UrlContext.BuildBlogPostReply(cd.ThreadID, posts[0].Post.ID),
|
2021-07-30 23:08:42 +00:00
|
|
|
LoginLink: hmnurl.BuildLoginPage(c.FullUrl()),
|
2021-07-30 19:59:48 +00:00
|
|
|
}, c.Perf)
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
func BlogPostRedirectToThread(c *RequestContext) ResponseData {
|
|
|
|
cd, ok := getCommonBlogData(c)
|
|
|
|
if !ok {
|
|
|
|
return FourOhFour(c)
|
|
|
|
}
|
|
|
|
|
2021-12-09 02:04:15 +00:00
|
|
|
thread, err := hmndata.FetchThread(c.Context(), c.Conn, c.CurrentUser, cd.ThreadID, hmndata.ThreadsQuery{
|
2021-09-14 04:13:58 +00:00
|
|
|
ProjectIDs: []int{c.CurrentProject.ID},
|
|
|
|
ThreadTypes: []models.ThreadType{models.ThreadTypeProjectBlogPost},
|
|
|
|
})
|
|
|
|
if errors.Is(err, db.NotFound) {
|
|
|
|
return FourOhFour(c)
|
|
|
|
} else if err != nil {
|
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch thread for blog redirect"))
|
|
|
|
}
|
2021-07-30 19:59:48 +00:00
|
|
|
|
2021-11-10 04:11:39 +00:00
|
|
|
threadUrl := c.UrlContext.BuildBlogThreadWithPostHash(cd.ThreadID, thread.Thread.Title, cd.PostID)
|
2021-07-30 19:59:48 +00:00
|
|
|
return c.Redirect(threadUrl, http.StatusFound)
|
|
|
|
}
|
|
|
|
|
2021-07-30 23:08:42 +00:00
|
|
|
func BlogNewThread(c *RequestContext) ResponseData {
|
2021-09-01 18:25:09 +00:00
|
|
|
baseData := getBaseData(
|
|
|
|
c,
|
|
|
|
fmt.Sprintf("Create New Post | %s", c.CurrentProject.Name),
|
2021-11-10 04:11:39 +00:00
|
|
|
[]templates.Breadcrumb{BlogBreadcrumb(c.UrlContext)},
|
2021-09-01 18:25:09 +00:00
|
|
|
)
|
2021-07-30 23:08:42 +00:00
|
|
|
|
2021-11-10 04:11:39 +00:00
|
|
|
editData := getEditorDataForNew(c.UrlContext, c.CurrentUser, baseData, nil)
|
|
|
|
editData.SubmitUrl = c.UrlContext.BuildBlogNewThread()
|
2021-07-30 23:08:42 +00:00
|
|
|
editData.SubmitLabel = "Create Post"
|
|
|
|
|
|
|
|
var res ResponseData
|
|
|
|
res.MustWriteTemplate("editor.html", editData, c.Perf)
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
func BlogNewThreadSubmit(c *RequestContext) ResponseData {
|
|
|
|
tx, err := c.Conn.Begin(c.Context())
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
defer tx.Rollback(c.Context())
|
|
|
|
|
|
|
|
err = c.Req.ParseForm()
|
|
|
|
if err != nil {
|
2021-08-28 12:21:03 +00:00
|
|
|
return c.ErrorResponse(http.StatusBadRequest, oops.New(err, "the form data was invalid"))
|
2021-07-30 23:08:42 +00:00
|
|
|
}
|
|
|
|
title := c.Req.Form.Get("title")
|
|
|
|
unparsed := c.Req.Form.Get("body")
|
|
|
|
if title == "" {
|
|
|
|
return RejectRequest(c, "You must provide a title for your post.")
|
|
|
|
}
|
|
|
|
if unparsed == "" {
|
|
|
|
return RejectRequest(c, "You must provide a body for your post.")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create thread
|
|
|
|
var threadId int
|
|
|
|
err = tx.QueryRow(c.Context(),
|
|
|
|
`
|
2022-05-07 13:11:05 +00:00
|
|
|
INSERT INTO thread (title, type, project_id, first_id, last_id)
|
2021-07-30 23:08:42 +00:00
|
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
|
|
RETURNING id
|
|
|
|
`,
|
|
|
|
title,
|
|
|
|
models.ThreadTypeProjectBlogPost,
|
|
|
|
c.CurrentProject.ID,
|
|
|
|
-1,
|
|
|
|
-1,
|
|
|
|
).Scan(&threadId)
|
|
|
|
if err != nil {
|
|
|
|
panic(oops.New(err, "failed to create thread"))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create everything else
|
2021-12-09 02:04:15 +00:00
|
|
|
hmndata.CreateNewPost(c.Context(), tx, c.CurrentProject.ID, threadId, models.ThreadTypeProjectBlogPost, c.CurrentUser.ID, nil, unparsed, c.Req.Host)
|
2021-07-30 23:08:42 +00:00
|
|
|
|
|
|
|
err = tx.Commit(c.Context())
|
|
|
|
if err != nil {
|
2021-08-28 12:21:03 +00:00
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to create new blog post"))
|
2021-07-30 23:08:42 +00:00
|
|
|
}
|
|
|
|
|
2021-11-10 04:11:39 +00:00
|
|
|
newThreadUrl := c.UrlContext.BuildBlogThread(threadId, title)
|
2021-07-30 23:08:42 +00:00
|
|
|
return c.Redirect(newThreadUrl, http.StatusSeeOther)
|
|
|
|
}
|
|
|
|
|
2021-07-30 22:32:19 +00:00
|
|
|
func BlogPostEdit(c *RequestContext) ResponseData {
|
|
|
|
cd, ok := getCommonBlogData(c)
|
|
|
|
if !ok {
|
|
|
|
return FourOhFour(c)
|
|
|
|
}
|
|
|
|
|
2021-12-09 02:04:15 +00:00
|
|
|
if !hmndata.UserCanEditPost(c.Context(), c.Conn, *c.CurrentUser, cd.PostID) {
|
2021-07-30 22:32:19 +00:00
|
|
|
return FourOhFour(c)
|
|
|
|
}
|
|
|
|
|
2021-12-09 02:04:15 +00:00
|
|
|
post, err := hmndata.FetchThreadPost(c.Context(), c.Conn, c.CurrentUser, cd.ThreadID, cd.PostID, hmndata.PostsQuery{
|
2021-09-14 04:13:58 +00:00
|
|
|
ProjectIDs: []int{c.CurrentProject.ID},
|
|
|
|
ThreadTypes: []models.ThreadType{models.ThreadTypeProjectBlogPost},
|
|
|
|
})
|
|
|
|
if errors.Is(err, db.NotFound) {
|
|
|
|
return FourOhFour(c)
|
|
|
|
} else if err != nil {
|
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get blog post to edit"))
|
|
|
|
}
|
2021-07-30 22:32:19 +00:00
|
|
|
|
2021-09-01 18:25:09 +00:00
|
|
|
title := ""
|
2021-09-14 04:13:58 +00:00
|
|
|
if post.Thread.FirstID == post.Post.ID {
|
|
|
|
title = fmt.Sprintf("Editing \"%s\" | %s", post.Thread.Title, c.CurrentProject.Name)
|
2021-07-30 22:32:19 +00:00
|
|
|
} else {
|
2021-09-01 18:25:09 +00:00
|
|
|
title = fmt.Sprintf("Editing Post | %s", c.CurrentProject.Name)
|
2021-07-30 22:32:19 +00:00
|
|
|
}
|
2021-09-01 18:25:09 +00:00
|
|
|
baseData := getBaseData(
|
|
|
|
c,
|
|
|
|
title,
|
2021-11-10 04:11:39 +00:00
|
|
|
BlogThreadBreadcrumbs(c.UrlContext, &post.Thread),
|
2021-09-01 18:25:09 +00:00
|
|
|
)
|
2021-07-30 22:32:19 +00:00
|
|
|
|
2021-11-10 04:11:39 +00:00
|
|
|
editData := getEditorDataForEdit(c.UrlContext, c.CurrentUser, baseData, post)
|
|
|
|
editData.SubmitUrl = c.UrlContext.BuildBlogPostEdit(cd.ThreadID, cd.PostID)
|
2021-07-30 22:32:19 +00:00
|
|
|
editData.SubmitLabel = "Submit Edited Post"
|
2021-09-14 04:13:58 +00:00
|
|
|
if post.Thread.FirstID != post.Post.ID {
|
2021-07-30 22:32:19 +00:00
|
|
|
editData.SubmitLabel = "Submit Edited Comment"
|
|
|
|
}
|
|
|
|
|
|
|
|
var res ResponseData
|
|
|
|
res.MustWriteTemplate("editor.html", editData, c.Perf)
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
func BlogPostEditSubmit(c *RequestContext) ResponseData {
|
|
|
|
cd, ok := getCommonBlogData(c)
|
|
|
|
if !ok {
|
|
|
|
return FourOhFour(c)
|
|
|
|
}
|
|
|
|
|
2021-12-09 02:04:15 +00:00
|
|
|
if !hmndata.UserCanEditPost(c.Context(), c.Conn, *c.CurrentUser, cd.PostID) {
|
2021-07-30 22:32:19 +00:00
|
|
|
return FourOhFour(c)
|
|
|
|
}
|
|
|
|
|
|
|
|
tx, err := c.Conn.Begin(c.Context())
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
defer tx.Rollback(c.Context())
|
|
|
|
|
2021-12-09 02:04:15 +00:00
|
|
|
post, err := hmndata.FetchThreadPost(c.Context(), tx, c.CurrentUser, cd.ThreadID, cd.PostID, hmndata.PostsQuery{
|
2021-09-14 04:13:58 +00:00
|
|
|
ProjectIDs: []int{c.CurrentProject.ID},
|
|
|
|
ThreadTypes: []models.ThreadType{models.ThreadTypeProjectBlogPost},
|
|
|
|
})
|
|
|
|
if errors.Is(err, db.NotFound) {
|
|
|
|
return FourOhFour(c)
|
|
|
|
} else if err != nil {
|
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get blog post to submit edits"))
|
|
|
|
}
|
2021-07-30 22:32:19 +00:00
|
|
|
|
|
|
|
c.Req.ParseForm()
|
|
|
|
title := c.Req.Form.Get("title")
|
|
|
|
unparsed := c.Req.Form.Get("body")
|
|
|
|
editReason := c.Req.Form.Get("editreason")
|
2021-09-14 04:13:58 +00:00
|
|
|
if title != "" && post.Thread.FirstID != post.Post.ID {
|
2021-07-30 22:32:19 +00:00
|
|
|
return RejectRequest(c, "You can only edit the title by editing the first post.")
|
|
|
|
}
|
|
|
|
if unparsed == "" {
|
|
|
|
return RejectRequest(c, "You must provide a post body.")
|
|
|
|
}
|
|
|
|
|
2021-12-09 02:04:15 +00:00
|
|
|
hmndata.CreatePostVersion(c.Context(), tx, post.Post.ID, unparsed, c.Req.Host, editReason, &c.CurrentUser.ID)
|
2021-07-30 22:32:19 +00:00
|
|
|
|
2021-09-23 06:09:18 +00:00
|
|
|
if title != "" {
|
|
|
|
_, err := tx.Exec(c.Context(),
|
|
|
|
`
|
2022-05-07 13:11:05 +00:00
|
|
|
UPDATE thread SET title = $1 WHERE id = $2
|
2021-09-23 06:09:18 +00:00
|
|
|
`,
|
|
|
|
title,
|
|
|
|
post.Thread.ID,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to update thread title"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-30 22:32:19 +00:00
|
|
|
err = tx.Commit(c.Context())
|
|
|
|
if err != nil {
|
2021-08-28 12:21:03 +00:00
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to edit blog post"))
|
2021-07-30 22:32:19 +00:00
|
|
|
}
|
|
|
|
|
2021-11-10 04:11:39 +00:00
|
|
|
postUrl := c.UrlContext.BuildBlogThreadWithPostHash(cd.ThreadID, post.Thread.Title, cd.PostID)
|
2021-07-30 22:32:19 +00:00
|
|
|
return c.Redirect(postUrl, http.StatusSeeOther)
|
|
|
|
}
|
|
|
|
|
2021-07-30 23:08:42 +00:00
|
|
|
func BlogPostReply(c *RequestContext) ResponseData {
|
|
|
|
cd, ok := getCommonBlogData(c)
|
|
|
|
if !ok {
|
|
|
|
return FourOhFour(c)
|
|
|
|
}
|
|
|
|
|
2021-12-09 02:04:15 +00:00
|
|
|
post, err := hmndata.FetchThreadPost(c.Context(), c.Conn, c.CurrentUser, cd.ThreadID, cd.PostID, hmndata.PostsQuery{
|
2021-09-14 04:13:58 +00:00
|
|
|
ProjectIDs: []int{c.CurrentProject.ID},
|
|
|
|
ThreadTypes: []models.ThreadType{models.ThreadTypeProjectBlogPost},
|
|
|
|
})
|
|
|
|
if errors.Is(err, db.NotFound) {
|
|
|
|
return FourOhFour(c)
|
|
|
|
} else if err != nil {
|
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get blog post for reply"))
|
|
|
|
}
|
2021-07-30 23:08:42 +00:00
|
|
|
|
2021-09-01 18:25:09 +00:00
|
|
|
baseData := getBaseData(
|
|
|
|
c,
|
2021-09-14 04:13:58 +00:00
|
|
|
fmt.Sprintf("Replying to comment in \"%s\" | %s", post.Thread.Title, c.CurrentProject.Name),
|
2021-11-10 04:11:39 +00:00
|
|
|
BlogThreadBreadcrumbs(c.UrlContext, &post.Thread),
|
2021-09-01 18:25:09 +00:00
|
|
|
)
|
2021-07-30 23:08:42 +00:00
|
|
|
|
2021-09-14 04:13:58 +00:00
|
|
|
replyPost := templates.PostToTemplate(&post.Post, post.Author, c.Theme)
|
|
|
|
replyPost.AddContentVersion(post.CurrentVersion, post.Editor)
|
2021-07-30 23:08:42 +00:00
|
|
|
|
2021-11-10 04:11:39 +00:00
|
|
|
editData := getEditorDataForNew(c.UrlContext, c.CurrentUser, baseData, &replyPost)
|
|
|
|
editData.SubmitUrl = c.UrlContext.BuildBlogPostReply(cd.ThreadID, cd.PostID)
|
2021-07-30 23:08:42 +00:00
|
|
|
editData.SubmitLabel = "Submit Reply"
|
|
|
|
|
|
|
|
var res ResponseData
|
|
|
|
res.MustWriteTemplate("editor.html", editData, c.Perf)
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
func BlogPostReplySubmit(c *RequestContext) ResponseData {
|
|
|
|
cd, ok := getCommonBlogData(c)
|
|
|
|
if !ok {
|
|
|
|
return FourOhFour(c)
|
|
|
|
}
|
|
|
|
|
|
|
|
tx, err := c.Conn.Begin(c.Context())
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
defer tx.Rollback(c.Context())
|
|
|
|
|
|
|
|
err = c.Req.ParseForm()
|
|
|
|
if err != nil {
|
2021-08-28 12:21:03 +00:00
|
|
|
return c.ErrorResponse(http.StatusBadRequest, oops.New(nil, "the form data was invalid"))
|
2021-07-30 23:08:42 +00:00
|
|
|
}
|
|
|
|
unparsed := c.Req.Form.Get("body")
|
|
|
|
if unparsed == "" {
|
|
|
|
return RejectRequest(c, "Your reply cannot be empty.")
|
|
|
|
}
|
|
|
|
|
2021-12-09 02:04:15 +00:00
|
|
|
newPostId, _ := hmndata.CreateNewPost(c.Context(), tx, c.CurrentProject.ID, cd.ThreadID, models.ThreadTypeProjectBlogPost, c.CurrentUser.ID, &cd.PostID, unparsed, c.Req.Host)
|
2021-07-30 23:08:42 +00:00
|
|
|
|
|
|
|
err = tx.Commit(c.Context())
|
|
|
|
if err != nil {
|
2021-08-28 12:21:03 +00:00
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to reply to blog post"))
|
2021-07-30 23:08:42 +00:00
|
|
|
}
|
|
|
|
|
2021-11-10 04:11:39 +00:00
|
|
|
newPostUrl := c.UrlContext.BuildBlogPost(cd.ThreadID, newPostId)
|
2021-07-30 23:08:42 +00:00
|
|
|
return c.Redirect(newPostUrl, http.StatusSeeOther)
|
|
|
|
}
|
|
|
|
|
|
|
|
func BlogPostDelete(c *RequestContext) ResponseData {
|
|
|
|
cd, ok := getCommonBlogData(c)
|
|
|
|
if !ok {
|
|
|
|
return FourOhFour(c)
|
|
|
|
}
|
|
|
|
|
2021-12-09 02:04:15 +00:00
|
|
|
if !hmndata.UserCanEditPost(c.Context(), c.Conn, *c.CurrentUser, cd.PostID) {
|
2021-07-30 23:08:42 +00:00
|
|
|
return FourOhFour(c)
|
|
|
|
}
|
|
|
|
|
2021-12-09 02:04:15 +00:00
|
|
|
post, err := hmndata.FetchThreadPost(c.Context(), c.Conn, c.CurrentUser, cd.ThreadID, cd.PostID, hmndata.PostsQuery{
|
2021-09-14 04:13:58 +00:00
|
|
|
ProjectIDs: []int{c.CurrentProject.ID},
|
|
|
|
ThreadTypes: []models.ThreadType{models.ThreadTypeProjectBlogPost},
|
|
|
|
})
|
|
|
|
if errors.Is(err, db.NotFound) {
|
|
|
|
return FourOhFour(c)
|
|
|
|
} else if err != nil {
|
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get blog post to delete"))
|
|
|
|
}
|
2021-07-30 23:08:42 +00:00
|
|
|
|
2021-09-01 18:25:09 +00:00
|
|
|
title := ""
|
2021-09-14 04:13:58 +00:00
|
|
|
if post.Thread.FirstID == post.Post.ID {
|
|
|
|
title = fmt.Sprintf("Deleting \"%s\" | %s", post.Thread.Title, c.CurrentProject.Name)
|
2021-07-30 23:08:42 +00:00
|
|
|
} else {
|
2021-09-14 04:13:58 +00:00
|
|
|
title = fmt.Sprintf("Deleting comment in \"%s\" | %s", post.Thread.Title, c.CurrentProject.Name)
|
2021-07-30 23:08:42 +00:00
|
|
|
}
|
2021-09-01 18:25:09 +00:00
|
|
|
baseData := getBaseData(
|
|
|
|
c,
|
|
|
|
title,
|
2021-11-10 04:11:39 +00:00
|
|
|
BlogThreadBreadcrumbs(c.UrlContext, &post.Thread),
|
2021-09-01 18:25:09 +00:00
|
|
|
)
|
2021-07-30 23:08:42 +00:00
|
|
|
|
2021-09-14 04:13:58 +00:00
|
|
|
templatePost := templates.PostToTemplate(&post.Post, post.Author, c.Theme)
|
|
|
|
templatePost.AddContentVersion(post.CurrentVersion, post.Editor)
|
2021-07-30 23:08:42 +00:00
|
|
|
|
|
|
|
type blogPostDeleteData struct {
|
|
|
|
templates.BaseData
|
|
|
|
Post templates.Post
|
|
|
|
SubmitUrl string
|
|
|
|
}
|
|
|
|
|
|
|
|
var res ResponseData
|
|
|
|
res.MustWriteTemplate("blog_post_delete.html", blogPostDeleteData{
|
|
|
|
BaseData: baseData,
|
2021-11-10 04:11:39 +00:00
|
|
|
SubmitUrl: c.UrlContext.BuildBlogPostDelete(cd.ThreadID, cd.PostID),
|
2021-07-30 23:08:42 +00:00
|
|
|
Post: templatePost,
|
|
|
|
}, c.Perf)
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
func BlogPostDeleteSubmit(c *RequestContext) ResponseData {
|
|
|
|
cd, ok := getCommonBlogData(c)
|
|
|
|
if !ok {
|
|
|
|
return FourOhFour(c)
|
|
|
|
}
|
|
|
|
|
2021-12-09 02:04:15 +00:00
|
|
|
if !hmndata.UserCanEditPost(c.Context(), c.Conn, *c.CurrentUser, cd.PostID) {
|
2021-07-30 23:08:42 +00:00
|
|
|
return FourOhFour(c)
|
|
|
|
}
|
|
|
|
|
|
|
|
tx, err := c.Conn.Begin(c.Context())
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
defer tx.Rollback(c.Context())
|
|
|
|
|
2021-12-09 02:04:15 +00:00
|
|
|
threadDeleted := hmndata.DeletePost(c.Context(), tx, cd.ThreadID, cd.PostID)
|
2021-07-30 23:08:42 +00:00
|
|
|
|
|
|
|
err = tx.Commit(c.Context())
|
|
|
|
if err != nil {
|
2021-08-28 12:21:03 +00:00
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete post"))
|
2021-07-30 23:08:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if threadDeleted {
|
2021-11-10 04:11:39 +00:00
|
|
|
return c.Redirect(c.UrlContext.BuildHomepage(), http.StatusSeeOther)
|
2021-07-30 23:08:42 +00:00
|
|
|
} else {
|
2021-12-09 02:04:15 +00:00
|
|
|
thread, err := hmndata.FetchThread(c.Context(), c.Conn, c.CurrentUser, cd.ThreadID, hmndata.ThreadsQuery{
|
2021-09-14 04:13:58 +00:00
|
|
|
ProjectIDs: []int{c.CurrentProject.ID},
|
|
|
|
ThreadTypes: []models.ThreadType{models.ThreadTypeProjectBlogPost},
|
|
|
|
})
|
|
|
|
if errors.Is(err, db.NotFound) {
|
|
|
|
panic(oops.New(err, "the thread was supposedly not deleted after deleting a post in a blog, but the thread was not found afterwards"))
|
|
|
|
} else if err != nil {
|
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch thread after blog post delete"))
|
|
|
|
}
|
2021-11-10 04:11:39 +00:00
|
|
|
threadUrl := c.UrlContext.BuildBlogThread(thread.Thread.ID, thread.Thread.Title)
|
2021-07-30 23:08:42 +00:00
|
|
|
return c.Redirect(threadUrl, http.StatusSeeOther)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-30 19:59:48 +00:00
|
|
|
type commonBlogData struct {
|
|
|
|
c *RequestContext
|
|
|
|
|
|
|
|
ThreadID int
|
|
|
|
PostID int
|
|
|
|
}
|
|
|
|
|
|
|
|
func getCommonBlogData(c *RequestContext) (commonBlogData, bool) {
|
|
|
|
c.Perf.StartBlock("BLOGS", "Fetch common blog data")
|
|
|
|
defer c.Perf.EndBlock()
|
|
|
|
|
|
|
|
res := commonBlogData{
|
|
|
|
c: c,
|
|
|
|
}
|
|
|
|
|
|
|
|
if threadIdStr, hasThreadId := c.PathParams["threadid"]; hasThreadId {
|
|
|
|
threadId, err := strconv.Atoi(threadIdStr)
|
|
|
|
if err != nil {
|
|
|
|
return commonBlogData{}, false
|
|
|
|
}
|
|
|
|
res.ThreadID = threadId
|
|
|
|
|
|
|
|
c.Perf.StartBlock("SQL", "Verify that the thread exists")
|
2022-04-16 17:49:29 +00:00
|
|
|
threadExists, err := db.QueryOneScalar[bool](c.Context(), c.Conn,
|
2021-07-30 19:59:48 +00:00
|
|
|
`
|
|
|
|
SELECT COUNT(*) > 0
|
2022-05-07 13:11:05 +00:00
|
|
|
FROM thread
|
2021-07-30 19:59:48 +00:00
|
|
|
WHERE
|
|
|
|
id = $1
|
|
|
|
AND project_id = $2
|
|
|
|
`,
|
|
|
|
res.ThreadID,
|
|
|
|
c.CurrentProject.ID,
|
|
|
|
)
|
|
|
|
c.Perf.EndBlock()
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
if !threadExists {
|
|
|
|
return commonBlogData{}, false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if postIdStr, hasPostId := c.PathParams["postid"]; hasPostId {
|
|
|
|
postId, err := strconv.Atoi(postIdStr)
|
|
|
|
if err != nil {
|
|
|
|
return commonBlogData{}, false
|
|
|
|
}
|
|
|
|
res.PostID = postId
|
|
|
|
|
|
|
|
c.Perf.StartBlock("SQL", "Verify that the post exists")
|
2022-04-16 17:49:29 +00:00
|
|
|
postExists, err := db.QueryOneScalar[bool](c.Context(), c.Conn,
|
2021-07-30 19:59:48 +00:00
|
|
|
`
|
|
|
|
SELECT COUNT(*) > 0
|
2022-05-07 13:11:05 +00:00
|
|
|
FROM post
|
2021-07-30 19:59:48 +00:00
|
|
|
WHERE
|
|
|
|
id = $1
|
|
|
|
AND thread_id = $2
|
|
|
|
`,
|
|
|
|
res.PostID,
|
|
|
|
res.ThreadID,
|
|
|
|
)
|
|
|
|
c.Perf.EndBlock()
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
if !postExists {
|
|
|
|
return commonBlogData{}, false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return res, true
|
|
|
|
}
|
|
|
|
|
2021-11-10 04:11:39 +00:00
|
|
|
func addBlogUrlsToPost(urlContext *hmnurl.UrlContext, p *templates.Post, thread *models.Thread, postId int) {
|
|
|
|
p.Url = urlContext.BuildBlogThreadWithPostHash(thread.ID, thread.Title, postId)
|
|
|
|
p.DeleteUrl = urlContext.BuildBlogPostDelete(thread.ID, postId)
|
|
|
|
p.EditUrl = urlContext.BuildBlogPostEdit(thread.ID, postId)
|
|
|
|
p.ReplyUrl = urlContext.BuildBlogPostReply(thread.ID, postId)
|
2021-07-30 19:59:48 +00:00
|
|
|
}
|