Add all the rest of the blog post operations
Still need to add UI for the blog index, and fix some aesthetic issues: - Wide posts can break the editor UI - Blog comments don't show the fancy reply UI - The post hash stuff on blog threads doesn't jump you to the correct post Probably other stuff, I dunno.
This commit is contained in:
parent
9945ab061d
commit
1f4dd335c5
|
@ -428,6 +428,13 @@ func BuildBlogThreadWithPostHash(projectSlug string, threadId int, title string,
|
|||
return ProjectUrlWithFragment(builder.String(), nil, projectSlug, strconv.Itoa(postId))
|
||||
}
|
||||
|
||||
var RegexBlogNewThread = regexp.MustCompile(`^/blog/new$`)
|
||||
|
||||
func BuildBlogNewThread(projectSlug string) string {
|
||||
defer CatchPanic()
|
||||
return ProjectUrl("/blog/new", nil, projectSlug)
|
||||
}
|
||||
|
||||
var RegexBlogPost = regexp.MustCompile(`^/blog/p/(?P<threadid>\d+)/e/(?P<postid>\d+)$`)
|
||||
|
||||
func BuildBlogPost(projectSlug string, threadId int, postId int) string {
|
||||
|
|
|
@ -3,13 +3,13 @@ package models
|
|||
type ThreadType int
|
||||
|
||||
const (
|
||||
ThreadTypeProjectArticle ThreadType = iota + 1
|
||||
ThreadTypeProjectBlogPost ThreadType = iota + 1
|
||||
ThreadTypeForumPost
|
||||
_ // formerly occupied by static pages, RIP
|
||||
_ // formerly occupied by who the hell knows what, RIP
|
||||
_ // formerly occupied by the wiki, RIP
|
||||
_ // formerly occupied by library discussions, RIP
|
||||
ThreadTypePersonalArticle
|
||||
ThreadTypePersonalBlogPost
|
||||
)
|
||||
|
||||
type Thread struct {
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
{{ template "base.html" . }}
|
||||
|
||||
{{ define "content" }}
|
||||
<div class="mw7 margin-center">
|
||||
<h3 class="mb3">Are you sure you want to delete this post?</h3>
|
||||
{{ template "forum_post_standalone.html" .Post }}
|
||||
<form action="{{ .SubmitUrl }}" method="POST" class="pv3 flex justify-end">
|
||||
{{ csrftoken .Session }}
|
||||
<input type="submit" value="Delete Post">
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
|
@ -58,6 +58,8 @@ func BlogThread(c *RequestContext) ResponseData {
|
|||
Thread: templates.ThreadToTemplate(&thread),
|
||||
MainPost: templatePosts[0],
|
||||
Comments: templatePosts[1:],
|
||||
ReplyLink: hmnurl.BuildBlogPostReply(c.CurrentProject.Slug, cd.ThreadID, posts[0].Post.ID),
|
||||
LoginLink: hmnurl.BuildLoginPage(c.FullUrl()),
|
||||
}, c.Perf)
|
||||
return res
|
||||
}
|
||||
|
@ -74,6 +76,71 @@ func BlogPostRedirectToThread(c *RequestContext) ResponseData {
|
|||
return c.Redirect(threadUrl, http.StatusFound)
|
||||
}
|
||||
|
||||
func BlogNewThread(c *RequestContext) ResponseData {
|
||||
baseData := getBaseData(c)
|
||||
baseData.Title = fmt.Sprintf("Create New Post | %s", c.CurrentProject.Name)
|
||||
baseData.MathjaxEnabled = true
|
||||
// TODO(ben): Set breadcrumbs
|
||||
|
||||
editData := getEditorDataForNew(baseData, nil)
|
||||
editData.SubmitUrl = hmnurl.BuildBlogNewThread(c.CurrentProject.Slug)
|
||||
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 {
|
||||
return ErrorResponse(http.StatusBadRequest, oops.New(err, "the form data was invalid"))
|
||||
}
|
||||
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(),
|
||||
`
|
||||
INSERT INTO handmade_thread (title, type, project_id, first_id, last_id)
|
||||
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
|
||||
CreateNewPost(c.Context(), tx, c.CurrentProject.ID, threadId, models.ThreadTypeProjectBlogPost, c.CurrentUser.ID, nil, unparsed, c.Req.Host)
|
||||
|
||||
err = tx.Commit(c.Context())
|
||||
if err != nil {
|
||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to create new blog post"))
|
||||
}
|
||||
|
||||
newThreadUrl := hmnurl.BuildBlogThread(c.CurrentProject.Slug, threadId, title)
|
||||
return c.Redirect(newThreadUrl, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func BlogPostEdit(c *RequestContext) ResponseData {
|
||||
cd, ok := getCommonBlogData(c)
|
||||
if !ok {
|
||||
|
@ -147,6 +214,135 @@ func BlogPostEditSubmit(c *RequestContext) ResponseData {
|
|||
return c.Redirect(postUrl, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func BlogPostReply(c *RequestContext) ResponseData {
|
||||
cd, ok := getCommonBlogData(c)
|
||||
if !ok {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
postData := FetchPostAndStuff(c.Context(), c.Conn, cd.ThreadID, cd.PostID)
|
||||
|
||||
baseData := getBaseData(c)
|
||||
baseData.Title = fmt.Sprintf("Replying to comment in \"%s\" | %s", postData.Thread.Title, c.CurrentProject.Name)
|
||||
baseData.MathjaxEnabled = true
|
||||
// TODO(ben): Set breadcrumbs
|
||||
|
||||
replyPost := templates.PostToTemplate(&postData.Post, postData.Author, c.Theme)
|
||||
replyPost.AddContentVersion(postData.CurrentVersion, postData.Editor)
|
||||
|
||||
editData := getEditorDataForNew(baseData, &replyPost)
|
||||
editData.SubmitUrl = hmnurl.BuildBlogPostReply(c.CurrentProject.Slug, cd.ThreadID, cd.PostID)
|
||||
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 {
|
||||
return ErrorResponse(http.StatusBadRequest, oops.New(nil, "the form data was invalid"))
|
||||
}
|
||||
unparsed := c.Req.Form.Get("body")
|
||||
if unparsed == "" {
|
||||
return RejectRequest(c, "Your reply cannot be empty.")
|
||||
}
|
||||
|
||||
newPostId, _ := CreateNewPost(c.Context(), tx, c.CurrentProject.ID, cd.ThreadID, models.ThreadTypeProjectBlogPost, c.CurrentUser.ID, &cd.PostID, unparsed, c.Req.Host)
|
||||
|
||||
err = tx.Commit(c.Context())
|
||||
if err != nil {
|
||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to reply to blog post"))
|
||||
}
|
||||
|
||||
newPostUrl := hmnurl.BuildBlogPost(c.CurrentProject.Slug, cd.ThreadID, newPostId)
|
||||
return c.Redirect(newPostUrl, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func BlogPostDelete(c *RequestContext) ResponseData {
|
||||
cd, ok := getCommonBlogData(c)
|
||||
if !ok {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
if !UserCanEditPost(c.Context(), c.Conn, *c.CurrentUser, cd.PostID) {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
postData := FetchPostAndStuff(c.Context(), c.Conn, cd.ThreadID, cd.PostID)
|
||||
|
||||
baseData := getBaseData(c)
|
||||
if postData.Thread.FirstID == postData.Post.ID {
|
||||
baseData.Title = fmt.Sprintf("Deleting \"%s\" | %s", postData.Thread.Title, c.CurrentProject.Name)
|
||||
} else {
|
||||
baseData.Title = fmt.Sprintf("Deleting comment in \"%s\" | %s", postData.Thread.Title, c.CurrentProject.Name)
|
||||
}
|
||||
baseData.MathjaxEnabled = true
|
||||
// TODO(ben): Set breadcrumbs
|
||||
|
||||
templatePost := templates.PostToTemplate(&postData.Post, postData.Author, c.Theme)
|
||||
templatePost.AddContentVersion(postData.CurrentVersion, postData.Editor)
|
||||
|
||||
type blogPostDeleteData struct {
|
||||
templates.BaseData
|
||||
Post templates.Post
|
||||
SubmitUrl string
|
||||
}
|
||||
|
||||
var res ResponseData
|
||||
res.MustWriteTemplate("blog_post_delete.html", blogPostDeleteData{
|
||||
BaseData: baseData,
|
||||
SubmitUrl: hmnurl.BuildBlogPostDelete(c.CurrentProject.Slug, cd.ThreadID, cd.PostID),
|
||||
Post: templatePost,
|
||||
}, c.Perf)
|
||||
return res
|
||||
}
|
||||
|
||||
func BlogPostDeleteSubmit(c *RequestContext) ResponseData {
|
||||
cd, ok := getCommonBlogData(c)
|
||||
if !ok {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
if !UserCanEditPost(c.Context(), c.Conn, *c.CurrentUser, cd.PostID) {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
tx, err := c.Conn.Begin(c.Context())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer tx.Rollback(c.Context())
|
||||
|
||||
threadDeleted := DeletePost(c.Context(), tx, cd.ThreadID, cd.PostID)
|
||||
|
||||
err = tx.Commit(c.Context())
|
||||
if err != nil {
|
||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete post"))
|
||||
}
|
||||
|
||||
if threadDeleted {
|
||||
projectUrl := hmnurl.BuildProjectHomepage(c.CurrentProject.Slug)
|
||||
return c.Redirect(projectUrl, http.StatusSeeOther)
|
||||
} else {
|
||||
thread := FetchThread(c.Context(), c.Conn, cd.ThreadID)
|
||||
threadUrl := hmnurl.BuildBlogThread(c.CurrentProject.Slug, thread.ID, thread.Title)
|
||||
return c.Redirect(threadUrl, http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
type commonBlogData struct {
|
||||
c *RequestContext
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ func Feed(c *RequestContext) ResponseData {
|
|||
AND deleted = FALSE
|
||||
AND post.thread_id IS NOT NULL
|
||||
`,
|
||||
[]models.ThreadType{models.ThreadTypeForumPost, models.ThreadTypeProjectArticle},
|
||||
[]models.ThreadType{models.ThreadTypeForumPost, models.ThreadTypeProjectBlogPost},
|
||||
)
|
||||
c.Perf.EndBlock()
|
||||
if err != nil {
|
||||
|
@ -339,7 +339,7 @@ func fetchAllPosts(c *RequestContext, lineageBuilder *models.SubforumLineageBuil
|
|||
LIMIT $3 OFFSET $4
|
||||
`,
|
||||
currentUserID,
|
||||
[]models.ThreadType{models.ThreadTypeForumPost, models.ThreadTypeProjectArticle},
|
||||
[]models.ThreadType{models.ThreadTypeForumPost, models.ThreadTypeProjectBlogPost},
|
||||
limit,
|
||||
offset,
|
||||
)
|
||||
|
|
|
@ -634,16 +634,22 @@ func ForumNewThreadSubmit(c *RequestContext) ResponseData {
|
|||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
c.Req.ParseForm()
|
||||
|
||||
err = c.Req.ParseForm()
|
||||
if err != nil {
|
||||
return ErrorResponse(http.StatusBadRequest, oops.New(err, "the form data was invalid"))
|
||||
}
|
||||
title := c.Req.Form.Get("title")
|
||||
unparsed := c.Req.Form.Get("body")
|
||||
sticky := false
|
||||
if c.CurrentUser.IsStaff && c.Req.Form.Get("sticky") != "" {
|
||||
sticky = true
|
||||
}
|
||||
|
||||
// TODO(ben): Validation (and error handling if ParseForm fails? might not need it since you'll get empty values)
|
||||
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
|
||||
|
@ -686,14 +692,14 @@ func ForumPostReply(c *RequestContext) ResponseData {
|
|||
postData := FetchPostAndStuff(c.Context(), c.Conn, cd.ThreadID, cd.PostID)
|
||||
|
||||
baseData := getBaseData(c)
|
||||
baseData.Title = fmt.Sprintf("Replying to \"%s\" | %s", postData.Thread.Title, cd.SubforumTree[cd.SubforumID].Name)
|
||||
baseData.Title = fmt.Sprintf("Replying to post | %s", cd.SubforumTree[cd.SubforumID].Name)
|
||||
baseData.MathjaxEnabled = true
|
||||
// TODO(ben): Set breadcrumbs
|
||||
|
||||
templatePost := templates.PostToTemplate(&postData.Post, postData.Author, c.Theme)
|
||||
templatePost.AddContentVersion(postData.CurrentVersion, postData.Editor)
|
||||
replyPost := templates.PostToTemplate(&postData.Post, postData.Author, c.Theme)
|
||||
replyPost.AddContentVersion(postData.CurrentVersion, postData.Editor)
|
||||
|
||||
editData := getEditorDataForNew(baseData, &templatePost)
|
||||
editData := getEditorDataForNew(baseData, &replyPost)
|
||||
editData.SubmitUrl = hmnurl.BuildForumPostReply(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), cd.ThreadID, cd.PostID)
|
||||
editData.SubmitLabel = "Submit Reply"
|
||||
|
||||
|
@ -714,9 +720,14 @@ func ForumPostReplySubmit(c *RequestContext) ResponseData {
|
|||
}
|
||||
defer tx.Rollback(c.Context())
|
||||
|
||||
c.Req.ParseForm()
|
||||
// TODO(ben): Validation
|
||||
err = c.Req.ParseForm()
|
||||
if err != nil {
|
||||
return ErrorResponse(http.StatusBadRequest, oops.New(nil, "the form data was invalid"))
|
||||
}
|
||||
unparsed := c.Req.Form.Get("body")
|
||||
if unparsed == "" {
|
||||
return RejectRequest(c, "Your reply cannot be empty.")
|
||||
}
|
||||
|
||||
newPostId, _ := CreateNewPost(c.Context(), tx, c.CurrentProject.ID, cd.ThreadID, models.ThreadTypeForumPost, c.CurrentUser.ID, &cd.PostID, unparsed, c.Req.Host)
|
||||
|
||||
|
|
|
@ -120,7 +120,7 @@ func Index(c *RequestContext) ResponseData {
|
|||
`,
|
||||
currentUserId,
|
||||
proj.ID,
|
||||
[]models.ThreadType{models.ThreadTypeProjectArticle, models.ThreadTypeForumPost},
|
||||
[]models.ThreadType{models.ThreadTypeProjectBlogPost, models.ThreadTypeForumPost},
|
||||
maxPosts,
|
||||
)
|
||||
c.Perf.EndBlock()
|
||||
|
@ -155,7 +155,7 @@ func Index(c *RequestContext) ResponseData {
|
|||
}
|
||||
|
||||
featurable := (!proj.IsHMN() &&
|
||||
projectPost.Post.ThreadType == models.ThreadTypeProjectArticle &&
|
||||
projectPost.Post.ThreadType == models.ThreadTypeProjectBlogPost &&
|
||||
projectPost.Thread.FirstID == projectPost.Post.ID &&
|
||||
landingPageProject.FeaturedPost == nil)
|
||||
|
||||
|
@ -269,7 +269,7 @@ func Index(c *RequestContext) ResponseData {
|
|||
LIMIT 1
|
||||
`,
|
||||
models.HMNProjectID,
|
||||
models.ThreadTypeProjectArticle,
|
||||
models.ThreadTypeProjectBlogPost,
|
||||
)
|
||||
if err != nil {
|
||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch news post"))
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
// NOTE(asaf): Please don't use this if you already know the kind of the post beforehand. Just call the appropriate build function.
|
||||
func UrlForGenericPost(thread *models.Thread, post *models.Post, lineageBuilder *models.SubforumLineageBuilder, projectSlug string) string {
|
||||
switch post.ThreadType {
|
||||
case models.ThreadTypeProjectArticle:
|
||||
case models.ThreadTypeProjectBlogPost:
|
||||
return hmnurl.BuildBlogThreadWithPostHash(projectSlug, post.ThreadID, thread.Title, post.ID)
|
||||
case models.ThreadTypeForumPost:
|
||||
return hmnurl.BuildForumPost(projectSlug, lineageBuilder.GetSubforumLineageSlugs(*thread.SubforumID), post.ThreadID, post.ID)
|
||||
|
@ -20,7 +20,7 @@ func UrlForGenericPost(thread *models.Thread, post *models.Post, lineageBuilder
|
|||
|
||||
var PostTypeMap = map[models.ThreadType][]templates.PostType{
|
||||
// { First post , Subsequent post }
|
||||
models.ThreadTypeProjectArticle: {templates.PostTypeBlogPost, templates.PostTypeBlogComment},
|
||||
models.ThreadTypeProjectBlogPost: {templates.PostTypeBlogPost, templates.PostTypeBlogComment},
|
||||
models.ThreadTypeForumPost: {templates.PostTypeForumThread, templates.PostTypeForumReply},
|
||||
}
|
||||
|
||||
|
|
|
@ -166,10 +166,16 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
|
|||
mainRoutes.GET(hmnurl.RegexForumPostDelete, authMiddleware(ForumPostDelete))
|
||||
mainRoutes.POST(hmnurl.RegexForumPostDelete, authMiddleware(csrfMiddleware(ForumPostDeleteSubmit)))
|
||||
|
||||
mainRoutes.GET(hmnurl.RegexBlogNewThread, authMiddleware(BlogNewThread))
|
||||
mainRoutes.POST(hmnurl.RegexBlogNewThread, authMiddleware(csrfMiddleware(BlogNewThreadSubmit)))
|
||||
mainRoutes.GET(hmnurl.RegexBlogThread, BlogThread)
|
||||
mainRoutes.GET(hmnurl.RegexBlogPost, BlogPostRedirectToThread)
|
||||
mainRoutes.GET(hmnurl.RegexBlogPostEdit, BlogPostEdit)
|
||||
mainRoutes.POST(hmnurl.RegexBlogPostEdit, BlogPostEditSubmit)
|
||||
mainRoutes.GET(hmnurl.RegexBlogPostReply, authMiddleware(BlogPostReply))
|
||||
mainRoutes.POST(hmnurl.RegexBlogPostReply, authMiddleware(csrfMiddleware(BlogPostReplySubmit)))
|
||||
mainRoutes.GET(hmnurl.RegexBlogPostEdit, authMiddleware(BlogPostEdit))
|
||||
mainRoutes.POST(hmnurl.RegexBlogPostEdit, authMiddleware(csrfMiddleware(BlogPostEditSubmit)))
|
||||
mainRoutes.GET(hmnurl.RegexBlogPostDelete, authMiddleware(BlogPostDelete))
|
||||
mainRoutes.POST(hmnurl.RegexBlogPostDelete, authMiddleware(csrfMiddleware(BlogPostDeleteSubmit)))
|
||||
|
||||
mainRoutes.GET(hmnurl.RegexPodcast, PodcastIndex)
|
||||
mainRoutes.GET(hmnurl.RegexPodcastEdit, PodcastEdit)
|
||||
|
|
|
@ -5,6 +5,6 @@ import (
|
|||
)
|
||||
|
||||
var ThreadTypeDisplayNames = map[models.ThreadType]string{
|
||||
models.ThreadTypeProjectArticle: "Blog",
|
||||
models.ThreadTypeProjectBlogPost: "Blog",
|
||||
models.ThreadTypeForumPost: "Forums",
|
||||
}
|
||||
|
|
|
@ -241,7 +241,7 @@ func CreateNewPost(
|
|||
`,
|
||||
time.Now(),
|
||||
threadId,
|
||||
models.ThreadTypeForumPost,
|
||||
threadType,
|
||||
-1,
|
||||
userId,
|
||||
projectId,
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
var TimelineTypeMap = map[models.ThreadType][]templates.TimelineType{
|
||||
// { First post , Subsequent post }
|
||||
models.ThreadTypeProjectArticle: {templates.TimelineTypeBlogPost, templates.TimelineTypeBlogComment},
|
||||
models.ThreadTypeProjectBlogPost: {templates.TimelineTypeBlogPost, templates.TimelineTypeBlogComment},
|
||||
models.ThreadTypeForumPost: {templates.TimelineTypeForumThread, templates.TimelineTypeForumReply},
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
|
||||
func BuildProjectRootResourceUrl(projectSlug string, kind models.ThreadType) string {
|
||||
switch kind {
|
||||
case models.ThreadTypeProjectArticle:
|
||||
case models.ThreadTypeProjectBlogPost:
|
||||
return hmnurl.BuildBlog(projectSlug, 1)
|
||||
case models.ThreadTypeForumPost:
|
||||
return hmnurl.BuildForum(projectSlug, nil, 1)
|
||||
|
|
Loading…
Reference in New Issue