2021-04-29 04:52:27 +00:00
package website
import (
2021-07-20 02:35:22 +00:00
"context"
2021-05-04 01:59:45 +00:00
"errors"
2021-07-20 02:35:22 +00:00
"fmt"
2021-04-29 04:52:27 +00:00
"math"
2021-07-04 22:48:08 +00:00
"net"
2021-04-29 04:52:27 +00:00
"net/http"
"strconv"
"strings"
"time"
"git.handmade.network/hmn/hmn/src/db"
2021-05-03 23:59:43 +00:00
"git.handmade.network/hmn/hmn/src/hmnurl"
2021-04-29 04:52:27 +00:00
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
2021-07-04 20:25:28 +00:00
"git.handmade.network/hmn/hmn/src/parsing"
2021-05-03 23:59:43 +00:00
"git.handmade.network/hmn/hmn/src/templates"
2021-05-06 05:57:14 +00:00
"git.handmade.network/hmn/hmn/src/utils"
2021-07-20 02:35:22 +00:00
"github.com/jackc/pgx/v4"
2021-07-22 04:42:34 +00:00
"github.com/jackc/pgx/v4/pgxpool"
2021-04-29 04:52:27 +00:00
)
2021-05-03 14:51:07 +00:00
type forumCategoryData struct {
templates . BaseData
2021-05-11 22:53:23 +00:00
NewThreadUrl string
MarkReadUrl string
2021-05-03 23:59:43 +00:00
Threads [ ] templates . ThreadListItem
Pagination templates . Pagination
Subcategories [ ] forumSubcategoryData
}
type forumSubcategoryData struct {
Name string
Url string
Threads [ ] templates . ThreadListItem
TotalThreads int
2021-05-03 14:51:07 +00:00
}
2021-07-20 02:35:22 +00:00
type editorData struct {
templates . BaseData
SubmitUrl string
2021-07-22 02:16:10 +00:00
Title string
2021-07-20 02:35:22 +00:00
SubmitLabel string
2021-07-22 01:41:23 +00:00
IsEditing bool // false if new post, true if updating existing one
EditInitialContents string
2021-07-20 02:35:22 +00:00
PostReplyingTo * templates . Post
}
2021-04-29 04:52:27 +00:00
func ForumCategory ( c * RequestContext ) ResponseData {
const threadsPerPage = 25
2021-05-11 22:53:23 +00:00
c . Perf . StartBlock ( "SQL" , "Fetch category tree" )
categoryTree := models . GetFullCategoryTree ( c . Context ( ) , c . Conn )
lineageBuilder := models . MakeCategoryLineageBuilder ( categoryTree )
c . Perf . EndBlock ( )
currentCatId , valid := validateSubforums ( lineageBuilder , c . CurrentProject , c . PathParams [ "cats" ] )
if ! valid {
return FourOhFour ( c )
}
currentSubforumSlugs := lineageBuilder . GetSubforumLineageSlugs ( currentCatId )
2021-04-29 04:52:27 +00:00
2021-05-03 23:59:43 +00:00
c . Perf . StartBlock ( "SQL" , "Fetch count of page threads" )
2021-04-29 04:52:27 +00:00
numThreads , err := db . QueryInt ( c . Context ( ) , c . Conn ,
`
SELECT COUNT ( * )
FROM handmade_thread AS thread
WHERE
thread . category_id = $ 1
AND NOT thread . deleted
` ,
currentCatId ,
)
if err != nil {
panic ( oops . New ( err , "failed to get count of threads" ) )
}
2021-05-03 23:59:43 +00:00
c . Perf . EndBlock ( )
2021-04-29 04:52:27 +00:00
2021-07-08 07:40:30 +00:00
numPages := utils . IntMax ( int ( math . Ceil ( float64 ( numThreads ) / threadsPerPage ) ) , 1 )
2021-04-29 04:52:27 +00:00
page := 1
pageString , hasPage := c . PathParams [ "page" ]
if hasPage && pageString != "" {
if pageParsed , err := strconv . Atoi ( pageString ) ; err == nil {
page = pageParsed
} else {
2021-05-11 22:53:23 +00:00
return c . Redirect ( hmnurl . BuildForumCategory ( c . CurrentProject . Slug , currentSubforumSlugs , 1 ) , http . StatusSeeOther )
2021-04-29 04:52:27 +00:00
}
}
if page < 1 || numPages < page {
2021-05-11 22:53:23 +00:00
return c . Redirect ( hmnurl . BuildForumCategory ( c . CurrentProject . Slug , currentSubforumSlugs , utils . IntClamp ( 1 , page , numPages ) ) , http . StatusSeeOther )
2021-04-29 04:52:27 +00:00
}
howManyThreadsToSkip := ( page - 1 ) * threadsPerPage
var currentUserId * int
if c . CurrentUser != nil {
currentUserId = & c . CurrentUser . ID
}
2021-05-03 23:59:43 +00:00
c . Perf . StartBlock ( "SQL" , "Fetch page threads" )
type threadQueryResult struct {
2021-04-29 04:52:27 +00:00
Thread models . Thread ` db:"thread" `
FirstPost models . Post ` db:"firstpost" `
LastPost models . Post ` db:"lastpost" `
2021-05-03 14:51:07 +00:00
FirstUser * models . User ` db:"firstuser" `
LastUser * models . User ` db:"lastuser" `
2021-04-29 04:52:27 +00:00
ThreadLastReadTime * time . Time ` db:"tlri.lastread" `
CatLastReadTime * time . Time ` db:"clri.lastread" `
}
2021-05-03 23:59:43 +00:00
itMainThreads , err := db . Query ( c . Context ( ) , c . Conn , threadQueryResult { } ,
2021-04-29 04:52:27 +00:00
`
SELECT $ columns
FROM
handmade_thread AS thread
JOIN handmade_post AS firstpost ON thread . first_id = firstpost . id
JOIN handmade_post AS lastpost ON thread . last_id = lastpost . id
2021-05-03 14:51:07 +00:00
LEFT JOIN auth_user AS firstuser ON firstpost . author_id = firstuser . id
LEFT JOIN auth_user AS lastuser ON lastpost . author_id = lastuser . id
LEFT JOIN handmade_threadlastreadinfo AS tlri ON (
2021-04-29 04:52:27 +00:00
tlri . thread_id = thread . id
AND tlri . user_id = $ 2
)
2021-05-03 14:51:07 +00:00
LEFT JOIN handmade_categorylastreadinfo AS clri ON (
2021-04-29 04:52:27 +00:00
clri . category_id = $ 1
AND clri . user_id = $ 2
)
WHERE
thread . category_id = $ 1
AND NOT thread . deleted
ORDER BY lastpost . postdate DESC
LIMIT $ 3 OFFSET $ 4
` ,
currentCatId ,
currentUserId ,
threadsPerPage ,
howManyThreadsToSkip ,
)
if err != nil {
panic ( oops . New ( err , "failed to fetch threads" ) )
}
2021-05-03 23:59:43 +00:00
c . Perf . EndBlock ( )
2021-05-03 14:51:07 +00:00
defer itMainThreads . Close ( )
2021-04-29 04:52:27 +00:00
2021-05-03 23:59:43 +00:00
makeThreadListItem := func ( row * threadQueryResult ) templates . ThreadListItem {
2021-05-03 22:53:28 +00:00
hasRead := false
if row . ThreadLastReadTime != nil && row . ThreadLastReadTime . After ( row . LastPost . PostDate ) {
hasRead = true
} else if row . CatLastReadTime != nil && row . CatLastReadTime . After ( row . LastPost . PostDate ) {
hasRead = true
}
2021-05-03 23:59:43 +00:00
return templates . ThreadListItem {
2021-05-11 22:53:23 +00:00
Title : row . Thread . Title ,
Url : hmnurl . BuildForumThread ( c . CurrentProject . Slug , lineageBuilder . GetSubforumLineageSlugs ( row . Thread . CategoryID ) , row . Thread . ID , row . Thread . Title , 1 ) ,
2021-05-25 13:12:20 +00:00
FirstUser : templates . UserToTemplate ( row . FirstUser , c . Theme ) ,
2021-05-03 14:51:07 +00:00
FirstDate : row . FirstPost . PostDate ,
2021-05-25 13:12:20 +00:00
LastUser : templates . UserToTemplate ( row . LastUser , c . Theme ) ,
2021-05-03 14:51:07 +00:00
LastDate : row . LastPost . PostDate ,
2021-05-03 22:53:28 +00:00
Unread : ! hasRead ,
2021-05-03 23:59:43 +00:00
}
}
var threads [ ] templates . ThreadListItem
for _ , irow := range itMainThreads . ToSlice ( ) {
row := irow . ( * threadQueryResult )
threads = append ( threads , makeThreadListItem ( row ) )
2021-04-29 04:52:27 +00:00
}
// ---------------------
// Subcategory things
// ---------------------
2021-05-03 23:59:43 +00:00
var subcats [ ] forumSubcategoryData
if page == 1 {
2021-05-11 22:53:23 +00:00
subcatNodes := categoryTree [ currentCatId ] . Children
2021-05-03 23:59:43 +00:00
2021-05-11 22:53:23 +00:00
for _ , catNode := range subcatNodes {
2021-05-03 23:59:43 +00:00
c . Perf . StartBlock ( "SQL" , "Fetch count of subcategory threads" )
2021-05-04 12:02:33 +00:00
// TODO(asaf): [PERF] [MINOR] Consider replacing querying count per subcat with a single query for all cats with GROUP BY.
2021-05-03 23:59:43 +00:00
numThreads , err := db . QueryInt ( c . Context ( ) , c . Conn ,
`
SELECT COUNT ( * )
FROM handmade_thread AS thread
WHERE
thread . category_id = $ 1
AND NOT thread . deleted
` ,
2021-05-11 22:53:23 +00:00
catNode . ID ,
2021-05-03 23:59:43 +00:00
)
if err != nil {
panic ( oops . New ( err , "failed to get count of threads" ) )
}
c . Perf . EndBlock ( )
c . Perf . StartBlock ( "SQL" , "Fetch subcategory threads" )
2021-05-04 12:02:33 +00:00
// TODO(asaf): [PERF] [MINOR] Consider batching these.
2021-05-03 23:59:43 +00:00
itThreads , err := db . Query ( c . Context ( ) , c . Conn , threadQueryResult { } ,
`
SELECT $ columns
FROM
handmade_thread AS thread
JOIN handmade_post AS firstpost ON thread . first_id = firstpost . id
JOIN handmade_post AS lastpost ON thread . last_id = lastpost . id
LEFT JOIN auth_user AS firstuser ON firstpost . author_id = firstuser . id
LEFT JOIN auth_user AS lastuser ON lastpost . author_id = lastuser . id
LEFT JOIN handmade_threadlastreadinfo AS tlri ON (
tlri . thread_id = thread . id
AND tlri . user_id = $ 2
)
LEFT JOIN handmade_categorylastreadinfo AS clri ON (
clri . category_id = $ 1
AND clri . user_id = $ 2
)
WHERE
thread . category_id = $ 1
AND NOT thread . deleted
ORDER BY lastpost . postdate DESC
LIMIT 3
` ,
2021-05-11 22:53:23 +00:00
catNode . ID ,
2021-05-03 23:59:43 +00:00
currentUserId ,
)
if err != nil {
panic ( err )
}
defer itThreads . Close ( )
c . Perf . EndBlock ( )
var threads [ ] templates . ThreadListItem
for _ , irow := range itThreads . ToSlice ( ) {
threadRow := irow . ( * threadQueryResult )
threads = append ( threads , makeThreadListItem ( threadRow ) )
}
subcats = append ( subcats , forumSubcategoryData {
2021-05-11 22:53:23 +00:00
Name : * catNode . Name ,
Url : hmnurl . BuildForumCategory ( c . CurrentProject . Slug , lineageBuilder . GetSubforumLineageSlugs ( catNode . ID ) , 1 ) ,
2021-05-03 23:59:43 +00:00
Threads : threads ,
TotalThreads : numThreads ,
} )
}
}
// ---------------------
// Template assembly
// ---------------------
2021-05-03 14:51:07 +00:00
baseData := getBaseData ( c )
2021-05-06 04:04:58 +00:00
baseData . Title = c . CurrentProject . Name + " Forums"
2021-05-06 05:57:14 +00:00
baseData . Breadcrumbs = [ ] templates . Breadcrumb { // TODO(ben): This is wrong; it needs to account for subcategories.
2021-05-03 22:45:17 +00:00
{
2021-05-06 04:04:58 +00:00
Name : c . CurrentProject . Name ,
2021-05-11 22:53:23 +00:00
Url : hmnurl . BuildProjectHomepage ( c . CurrentProject . Slug ) ,
2021-05-03 22:45:17 +00:00
} ,
{
Name : "Forums" ,
2021-05-11 22:53:23 +00:00
Url : hmnurl . BuildForumCategory ( c . CurrentProject . Slug , nil , 1 ) ,
2021-05-03 22:45:17 +00:00
Current : true ,
} ,
}
2021-05-03 14:51:07 +00:00
2021-05-11 22:53:23 +00:00
currentSubforums := lineageBuilder . GetSubforumLineage ( currentCatId )
for i , subforum := range currentSubforums {
baseData . Breadcrumbs = append ( baseData . Breadcrumbs , templates . Breadcrumb {
Name : * subforum . Name , // NOTE(asaf): All subforum categories must have names.
Url : hmnurl . BuildForumCategory ( c . CurrentProject . Slug , currentSubforumSlugs [ 0 : i + 1 ] , 1 ) ,
} )
}
2021-05-03 14:51:07 +00:00
var res ResponseData
2021-07-17 15:19:17 +00:00
res . MustWriteTemplate ( "forum_category.html" , forumCategoryData {
2021-05-11 22:53:23 +00:00
BaseData : baseData ,
2021-06-12 03:51:07 +00:00
NewThreadUrl : hmnurl . BuildForumNewThread ( c . CurrentProject . Slug , currentSubforumSlugs , false ) ,
2021-05-11 22:53:23 +00:00
MarkReadUrl : hmnurl . BuildMarkRead ( currentCatId ) ,
Threads : threads ,
2021-05-03 22:45:17 +00:00
Pagination : templates . Pagination {
Current : page ,
Total : numPages ,
2021-05-11 22:53:23 +00:00
FirstUrl : hmnurl . BuildForumCategory ( c . CurrentProject . Slug , currentSubforumSlugs , 1 ) ,
LastUrl : hmnurl . BuildForumCategory ( c . CurrentProject . Slug , currentSubforumSlugs , numPages ) ,
NextUrl : hmnurl . BuildForumCategory ( c . CurrentProject . Slug , currentSubforumSlugs , utils . IntClamp ( 1 , page + 1 , numPages ) ) ,
PreviousUrl : hmnurl . BuildForumCategory ( c . CurrentProject . Slug , currentSubforumSlugs , utils . IntClamp ( 1 , page - 1 , numPages ) ) ,
2021-05-03 22:45:17 +00:00
} ,
2021-05-03 23:59:43 +00:00
Subcategories : subcats ,
2021-05-03 14:51:07 +00:00
} , c . Perf )
return res
}
2021-05-04 01:59:45 +00:00
type forumThreadData struct {
templates . BaseData
2021-05-06 05:57:14 +00:00
2021-05-04 01:59:45 +00:00
Thread templates . Thread
Posts [ ] templates . Post
2021-05-06 05:57:14 +00:00
CategoryUrl string
ReplyUrl string
Pagination templates . Pagination
2021-05-04 01:59:45 +00:00
}
2021-06-25 13:52:43 +00:00
var threadViewPostsPerPage = 15
2021-05-04 01:59:45 +00:00
2021-06-25 13:52:43 +00:00
func ForumThread ( c * RequestContext ) ResponseData {
2021-05-11 22:53:23 +00:00
c . Perf . StartBlock ( "SQL" , "Fetch category tree" )
categoryTree := models . GetFullCategoryTree ( c . Context ( ) , c . Conn )
lineageBuilder := models . MakeCategoryLineageBuilder ( categoryTree )
c . Perf . EndBlock ( )
currentCatId , valid := validateSubforums ( lineageBuilder , c . CurrentProject , c . PathParams [ "cats" ] )
if ! valid {
return FourOhFour ( c )
}
2021-05-04 01:59:45 +00:00
threadId , err := strconv . Atoi ( c . PathParams [ "threadid" ] )
if err != nil {
return FourOhFour ( c )
}
2021-05-11 22:53:23 +00:00
currentSubforumSlugs := lineageBuilder . GetSubforumLineageSlugs ( currentCatId )
2021-05-04 01:59:45 +00:00
c . Perf . StartBlock ( "SQL" , "Fetch current thread" )
type threadQueryResult struct {
Thread models . Thread ` db:"thread" `
}
irow , err := db . QueryOne ( c . Context ( ) , c . Conn , threadQueryResult { } ,
`
SELECT $ columns
2021-05-06 05:57:14 +00:00
FROM
handmade_thread AS thread
JOIN handmade_category AS cat ON cat . id = thread . category_id
WHERE
thread . id = $ 1
AND NOT thread . deleted
2021-05-11 22:53:23 +00:00
AND cat . id = $ 2
2021-05-04 01:59:45 +00:00
` ,
threadId ,
2021-05-11 22:53:23 +00:00
currentCatId , // NOTE(asaf): This verifies that the requested thread is under the requested subforum.
2021-05-04 01:59:45 +00:00
)
c . Perf . EndBlock ( )
if err != nil {
if errors . Is ( err , db . ErrNoMatchingRows ) {
return FourOhFour ( c )
} else {
panic ( err )
}
}
thread := irow . ( * threadQueryResult ) . Thread
2021-05-06 05:57:14 +00:00
numPosts , err := db . QueryInt ( c . Context ( ) , c . Conn ,
`
SELECT COUNT ( * )
FROM handmade_post
WHERE
thread_id = $ 1
AND NOT deleted
` ,
thread . ID ,
)
if err != nil {
panic ( oops . New ( err , "failed to get count of posts for thread" ) )
}
2021-06-25 13:52:43 +00:00
page , numPages , ok := getPageInfo ( c . PathParams [ "page" ] , numPosts , threadViewPostsPerPage )
2021-05-04 01:59:45 +00:00
if ! ok {
2021-05-11 22:53:23 +00:00
urlNoPage := hmnurl . BuildForumThread ( c . CurrentProject . Slug , currentSubforumSlugs , thread . ID , thread . Title , 1 )
2021-05-04 01:59:45 +00:00
return c . Redirect ( urlNoPage , http . StatusSeeOther )
}
2021-05-06 05:57:14 +00:00
pagination := templates . Pagination {
Current : page ,
Total : numPages ,
2021-05-11 22:53:23 +00:00
FirstUrl : hmnurl . BuildForumThread ( c . CurrentProject . Slug , currentSubforumSlugs , thread . ID , thread . Title , 1 ) ,
LastUrl : hmnurl . BuildForumThread ( c . CurrentProject . Slug , currentSubforumSlugs , thread . ID , thread . Title , numPages ) ,
NextUrl : hmnurl . BuildForumThread ( c . CurrentProject . Slug , currentSubforumSlugs , thread . ID , thread . Title , utils . IntClamp ( 1 , page + 1 , numPages ) ) ,
PreviousUrl : hmnurl . BuildForumThread ( c . CurrentProject . Slug , currentSubforumSlugs , thread . ID , thread . Title , utils . IntClamp ( 1 , page - 1 , numPages ) ) ,
2021-05-06 05:57:14 +00:00
}
2021-05-04 01:59:45 +00:00
c . Perf . StartBlock ( "SQL" , "Fetch posts" )
type postsQueryResult struct {
2021-05-06 05:57:14 +00:00
Post models . Post ` db:"post" `
Ver models . PostVersion ` db:"ver" `
Author * models . User ` db:"author" `
Editor * models . User ` db:"editor" `
2021-07-20 03:07:15 +00:00
ReplyPost * models . Post ` db:"reply" `
ReplyAuthor * models . User ` db:"reply_author" `
2021-05-04 01:59:45 +00:00
}
itPosts , err := db . Query ( c . Context ( ) , c . Conn , postsQueryResult { } ,
`
SELECT $ columns
FROM
handmade_post AS post
JOIN handmade_postversion AS ver ON post . current_id = ver . id
LEFT JOIN auth_user AS author ON post . author_id = author . id
2021-05-06 05:57:14 +00:00
LEFT JOIN auth_user AS editor ON ver . editor_id = editor . id
2021-07-20 03:07:15 +00:00
LEFT JOIN handmade_post AS reply ON post . reply_id = reply . id
LEFT JOIN auth_user AS reply_author ON reply . author_id = reply_author . id
2021-05-04 01:59:45 +00:00
WHERE
post . thread_id = $ 1
2021-05-06 05:57:14 +00:00
AND NOT post . deleted
2021-07-20 03:07:15 +00:00
ORDER BY post . postdate
2021-05-04 01:59:45 +00:00
LIMIT $ 2 OFFSET $ 3
` ,
thread . ID ,
2021-06-25 13:52:43 +00:00
threadViewPostsPerPage ,
( page - 1 ) * threadViewPostsPerPage ,
2021-05-04 01:59:45 +00:00
)
c . Perf . EndBlock ( )
if err != nil {
panic ( err )
}
defer itPosts . Close ( )
var posts [ ] templates . Post
for _ , irow := range itPosts . ToSlice ( ) {
row := irow . ( * postsQueryResult )
2021-05-06 05:57:14 +00:00
2021-05-25 13:12:20 +00:00
post := templates . PostToTemplate ( & row . Post , row . Author , c . Theme )
2021-07-04 21:24:48 +00:00
post . AddContentVersion ( row . Ver , row . Editor )
2021-05-11 22:53:23 +00:00
post . AddUrls ( c . CurrentProject . Slug , currentSubforumSlugs , thread . ID , post . ID )
2021-05-06 05:57:14 +00:00
2021-07-20 03:07:15 +00:00
if row . ReplyPost != nil {
reply := templates . PostToTemplate ( row . ReplyPost , row . ReplyAuthor , c . Theme )
reply . AddUrls ( c . CurrentProject . Slug , currentSubforumSlugs , thread . ID , post . ID )
post . ReplyPost = & reply
}
2021-05-06 05:57:14 +00:00
posts = append ( posts , post )
2021-05-04 01:59:45 +00:00
}
baseData := getBaseData ( c )
2021-05-06 05:57:14 +00:00
baseData . Title = thread . Title
2021-05-04 12:02:33 +00:00
// TODO(asaf): Set breadcrumbs
2021-05-04 01:59:45 +00:00
var res ResponseData
2021-07-17 15:19:17 +00:00
res . MustWriteTemplate ( "forum_thread.html" , forumThreadData {
2021-05-06 05:57:14 +00:00
BaseData : baseData ,
Thread : templates . ThreadToTemplate ( & thread ) ,
Posts : posts ,
2021-05-11 22:53:23 +00:00
CategoryUrl : hmnurl . BuildForumCategory ( c . CurrentProject . Slug , currentSubforumSlugs , 1 ) ,
ReplyUrl : hmnurl . BuildForumPostReply ( c . CurrentProject . Slug , currentSubforumSlugs , thread . ID , * thread . FirstID ) ,
2021-05-06 05:57:14 +00:00
Pagination : pagination ,
2021-05-04 01:59:45 +00:00
} , c . Perf )
return res
}
2021-06-25 13:52:43 +00:00
func ForumPostRedirect ( c * RequestContext ) ResponseData {
2021-07-20 02:35:22 +00:00
// TODO(compression): This logic for fetching posts by thread id / post id is gonna
// show up in a lot of places. It's used multiple times for forums, and also for blogs.
// Consider compressing this later.
2021-06-25 13:52:43 +00:00
c . Perf . StartBlock ( "SQL" , "Fetch category tree" )
categoryTree := models . GetFullCategoryTree ( c . Context ( ) , c . Conn )
lineageBuilder := models . MakeCategoryLineageBuilder ( categoryTree )
c . Perf . EndBlock ( )
currentCatId , valid := validateSubforums ( lineageBuilder , c . CurrentProject , c . PathParams [ "cats" ] )
if ! valid {
return FourOhFour ( c )
}
requestedThreadId , err := strconv . Atoi ( c . PathParams [ "threadid" ] )
if err != nil {
return FourOhFour ( c )
}
requestedPostId , err := strconv . Atoi ( c . PathParams [ "postid" ] )
if err != nil {
return FourOhFour ( c )
}
c . Perf . StartBlock ( "SQL" , "Fetch post ids for thread" )
type postQuery struct {
PostID int ` db:"post.id" `
}
postQueryResult , err := db . Query ( c . Context ( ) , c . Conn , postQuery { } ,
`
SELECT $ columns
FROM
handmade_post AS post
WHERE
post . category_id = $ 1
AND post . thread_id = $ 2
AND NOT post . deleted
ORDER BY postdate
` ,
currentCatId ,
requestedThreadId ,
)
if err != nil {
return ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to fetch post ids" ) )
}
postQuerySlice := postQueryResult . ToSlice ( )
c . Perf . EndBlock ( )
postIdx := - 1
for i , id := range postQuerySlice {
if id . ( * postQuery ) . PostID == requestedPostId {
postIdx = i
break
}
}
if postIdx == - 1 {
return FourOhFour ( c )
}
c . Perf . StartBlock ( "SQL" , "Fetch thread title" )
type threadTitleQuery struct {
ThreadTitle string ` db:"thread.title" `
}
threadTitleQueryResult , err := db . QueryOne ( c . Context ( ) , c . Conn , threadTitleQuery { } ,
`
SELECT $ columns
FROM handmade_thread AS thread
WHERE thread . id = $ 1
` ,
requestedThreadId ,
)
if err != nil {
return ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to fetch thread title" ) )
}
c . Perf . EndBlock ( )
threadTitle := threadTitleQueryResult . ( * threadTitleQuery ) . ThreadTitle
page := ( postIdx / threadViewPostsPerPage ) + 1
return c . Redirect ( hmnurl . BuildForumThreadWithPostHash (
c . CurrentProject . Slug ,
lineageBuilder . GetSubforumLineageSlugs ( currentCatId ) ,
requestedThreadId ,
threadTitle ,
page ,
requestedPostId ,
) , http . StatusSeeOther )
}
2021-06-12 03:51:07 +00:00
func ForumNewThread ( c * RequestContext ) ResponseData {
baseData := getBaseData ( c )
baseData . Title = "Create New Thread"
2021-06-20 19:06:22 +00:00
baseData . MathjaxEnabled = true
2021-06-12 03:51:07 +00:00
// TODO(ben): Set breadcrumbs
2021-07-02 05:11:58 +00:00
c . Perf . StartBlock ( "SQL" , "Fetch category tree" )
categoryTree := models . GetFullCategoryTree ( c . Context ( ) , c . Conn )
lineageBuilder := models . MakeCategoryLineageBuilder ( categoryTree )
c . Perf . EndBlock ( )
currentCatId , valid := validateSubforums ( lineageBuilder , c . CurrentProject , c . PathParams [ "cats" ] )
if ! valid {
return FourOhFour ( c )
}
2021-06-12 03:51:07 +00:00
var res ResponseData
2021-07-17 15:19:17 +00:00
res . MustWriteTemplate ( "editor.html" , editorData {
2021-07-04 22:48:08 +00:00
BaseData : baseData ,
SubmitUrl : hmnurl . BuildForumNewThread ( c . CurrentProject . Slug , lineageBuilder . GetSubforumLineageSlugs ( currentCatId ) , true ) ,
SubmitLabel : "Post New Thread" ,
2021-06-12 03:51:07 +00:00
} , c . Perf )
return res
}
func ForumNewThreadSubmit ( c * RequestContext ) ResponseData {
2021-07-04 20:25:28 +00:00
tx , err := c . Conn . Begin ( c . Context ( ) )
if err != nil {
panic ( err )
}
2021-07-04 22:48:08 +00:00
defer tx . Rollback ( c . Context ( ) )
2021-07-04 20:25:28 +00:00
c . Perf . StartBlock ( "SQL" , "Fetch category tree" )
categoryTree := models . GetFullCategoryTree ( c . Context ( ) , c . Conn )
lineageBuilder := models . MakeCategoryLineageBuilder ( categoryTree )
c . Perf . EndBlock ( )
currentCatId , valid := validateSubforums ( lineageBuilder , c . CurrentProject , c . PathParams [ "cats" ] )
if ! valid {
return FourOhFour ( c )
}
c . Req . ParseForm ( )
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
}
// Create thread
var threadId int
err = tx . QueryRow ( c . Context ( ) ,
`
2021-07-04 22:48:08 +00:00
INSERT INTO handmade_thread ( title , sticky , category_id , first_id , last_id )
VALUES ( $ 1 , $ 2 , $ 3 , $ 4 , $ 5 )
2021-07-04 20:25:28 +00:00
RETURNING id
` ,
title ,
sticky ,
currentCatId ,
2021-07-04 22:48:08 +00:00
- 1 ,
- 1 ,
2021-07-04 20:25:28 +00:00
) . Scan ( & threadId )
if err != nil {
panic ( oops . New ( err , "failed to create thread" ) )
}
2021-07-22 01:41:23 +00:00
postId , _ := createNewForumPostAndVersion ( c . Context ( ) , tx , currentCatId , threadId , c . CurrentUser . ID , c . CurrentProject . ID , unparsed , c . Req . Host , nil )
2021-07-20 02:35:22 +00:00
// Update thread with post id
_ , err = tx . Exec ( c . Context ( ) ,
`
UPDATE handmade_thread
SET
first_id = $ 1 ,
last_id = $ 1
WHERE id = $ 2
` ,
postId ,
threadId ,
)
if err != nil {
panic ( oops . New ( err , "failed to set thread post ids" ) )
}
err = tx . Commit ( c . Context ( ) )
if err != nil {
return ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to create new forum thread" ) )
}
newThreadUrl := hmnurl . BuildForumThread ( c . CurrentProject . Slug , lineageBuilder . GetSubforumLineageSlugs ( currentCatId ) , threadId , title , 1 )
return c . Redirect ( newThreadUrl , http . StatusSeeOther )
}
func ForumPostReply ( c * RequestContext ) ResponseData {
// TODO(compression): This logic for fetching posts by thread id / post id is gonna
// show up in a lot of places. It's used multiple times for forums, and also for blogs.
// Consider compressing this later.
c . Perf . StartBlock ( "SQL" , "Fetch category tree" )
categoryTree := models . GetFullCategoryTree ( c . Context ( ) , c . Conn )
lineageBuilder := models . MakeCategoryLineageBuilder ( categoryTree )
c . Perf . EndBlock ( )
currentCatId , valid := validateSubforums ( lineageBuilder , c . CurrentProject , c . PathParams [ "cats" ] )
if ! valid {
return FourOhFour ( c )
}
requestedThreadId , err := strconv . Atoi ( c . PathParams [ "threadid" ] )
if err != nil {
return FourOhFour ( c )
}
requestedPostId , err := strconv . Atoi ( c . PathParams [ "postid" ] )
if err != nil {
return FourOhFour ( c )
}
2021-07-22 01:41:23 +00:00
c . Perf . StartBlock ( "SQL" , "Fetch post to reply to" )
2021-07-20 02:35:22 +00:00
// TODO: Scope this down to just what you need
type postQuery struct {
Thread models . Thread ` db:"thread" `
Post models . Post ` db:"post" `
CurrentVersion models . PostVersion ` db:"ver" `
Author * models . User ` db:"author" `
Editor * models . User ` db:"editor" `
}
postQueryResult , err := db . QueryOne ( c . Context ( ) , c . Conn , postQuery { } ,
`
SELECT $ columns
FROM
handmade_thread AS thread
JOIN handmade_post AS post ON post . thread_id = thread . id
JOIN handmade_postversion AS ver ON post . current_id = ver . id
LEFT JOIN auth_user AS author ON post . author_id = author . id
LEFT JOIN auth_user AS editor ON ver . editor_id = editor . id
WHERE
post . category_id = $ 1
AND post . thread_id = $ 2
AND post . id = $ 3
AND NOT post . deleted
ORDER BY postdate
` ,
currentCatId ,
requestedThreadId ,
requestedPostId ,
)
if err != nil {
if errors . Is ( err , db . ErrNoMatchingRows ) {
return FourOhFour ( c )
} else {
return ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to fetch reply post" ) )
}
}
result := postQueryResult . ( * postQuery )
baseData := getBaseData ( c )
baseData . Title = fmt . Sprintf ( "Replying to \"%s\" | %s" , result . Thread . Title , * categoryTree [ currentCatId ] . Name )
baseData . MathjaxEnabled = true
// TODO(ben): Set breadcrumbs
templatePost := templates . PostToTemplate ( & result . Post , result . Author , c . Theme )
templatePost . AddContentVersion ( result . CurrentVersion , result . Editor )
var res ResponseData
res . MustWriteTemplate ( "editor.html" , editorData {
BaseData : baseData ,
SubmitUrl : hmnurl . BuildForumPostReply ( c . CurrentProject . Slug , lineageBuilder . GetSubforumLineageSlugs ( currentCatId ) , requestedThreadId , requestedPostId ) ,
SubmitLabel : "Submit Reply" ,
2021-07-22 02:16:10 +00:00
Title : "Replying to post" ,
2021-07-20 02:35:22 +00:00
PostReplyingTo : & templatePost ,
} , c . Perf )
return res
}
func ForumPostReplySubmit ( c * RequestContext ) ResponseData {
tx , err := c . Conn . Begin ( c . Context ( ) )
if err != nil {
panic ( err )
}
defer tx . Rollback ( c . Context ( ) )
c . Perf . StartBlock ( "SQL" , "Fetch category tree" )
categoryTree := models . GetFullCategoryTree ( c . Context ( ) , c . Conn )
lineageBuilder := models . MakeCategoryLineageBuilder ( categoryTree )
c . Perf . EndBlock ( )
currentCatId , valid := validateSubforums ( lineageBuilder , c . CurrentProject , c . PathParams [ "cats" ] )
if ! valid {
return FourOhFour ( c )
}
threadId , err := strconv . Atoi ( c . PathParams [ "threadid" ] )
if err != nil {
return FourOhFour ( c )
}
2021-07-20 03:07:15 +00:00
postId , err := strconv . Atoi ( c . PathParams [ "postid" ] )
if err != nil {
return FourOhFour ( c )
}
2021-07-20 02:35:22 +00:00
c . Req . ParseForm ( )
unparsed := c . Req . Form . Get ( "body" )
2021-07-22 01:41:23 +00:00
newPostId , _ := createNewForumPostAndVersion ( c . Context ( ) , tx , currentCatId , threadId , c . CurrentUser . ID , c . CurrentProject . ID , unparsed , c . Req . Host , & postId )
2021-07-20 02:35:22 +00:00
err = tx . Commit ( c . Context ( ) )
if err != nil {
2021-07-22 01:41:23 +00:00
return ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to reply to forum post" ) )
2021-07-20 02:35:22 +00:00
}
2021-07-20 03:07:15 +00:00
newPostUrl := hmnurl . BuildForumPost ( c . CurrentProject . Slug , lineageBuilder . GetSubforumLineageSlugs ( currentCatId ) , threadId , newPostId )
2021-07-20 02:35:22 +00:00
return c . Redirect ( newPostUrl , http . StatusSeeOther )
}
2021-07-22 01:41:23 +00:00
func ForumPostEdit ( c * RequestContext ) ResponseData {
// TODO(compression): This logic for fetching posts by thread id / post id is gonna
// show up in a lot of places. It's used multiple times for forums, and also for blogs.
// Consider compressing this later.
c . Perf . StartBlock ( "SQL" , "Fetch category tree" )
categoryTree := models . GetFullCategoryTree ( c . Context ( ) , c . Conn )
lineageBuilder := models . MakeCategoryLineageBuilder ( categoryTree )
c . Perf . EndBlock ( )
2021-07-20 02:35:22 +00:00
2021-07-22 01:41:23 +00:00
currentCatId , valid := validateSubforums ( lineageBuilder , c . CurrentProject , c . PathParams [ "cats" ] )
if ! valid {
return FourOhFour ( c )
}
requestedThreadId , err := strconv . Atoi ( c . PathParams [ "threadid" ] )
if err != nil {
return FourOhFour ( c )
}
requestedPostId , err := strconv . Atoi ( c . PathParams [ "postid" ] )
if err != nil {
return FourOhFour ( c )
}
c . Perf . StartBlock ( "SQL" , "Fetch post to edit" )
// TODO: Scope this down to just what you need
type postQuery struct {
Thread models . Thread ` db:"thread" `
Post models . Post ` db:"post" `
CurrentVersion models . PostVersion ` db:"ver" `
Author * models . User ` db:"author" `
Editor * models . User ` db:"editor" `
}
postQueryResult , err := db . QueryOne ( c . Context ( ) , c . Conn , postQuery { } ,
`
SELECT $ columns
FROM
handmade_thread AS thread
JOIN handmade_post AS post ON post . thread_id = thread . id
JOIN handmade_postversion AS ver ON post . current_id = ver . id
LEFT JOIN auth_user AS author ON post . author_id = author . id
LEFT JOIN auth_user AS editor ON ver . editor_id = editor . id
WHERE
post . category_id = $ 1
AND post . thread_id = $ 2
AND post . id = $ 3
AND NOT post . deleted
ORDER BY postdate
` ,
currentCatId ,
requestedThreadId ,
requestedPostId ,
)
if err != nil {
if errors . Is ( err , db . ErrNoMatchingRows ) {
return FourOhFour ( c )
} else {
return ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to fetch reply post" ) )
}
}
result := postQueryResult . ( * postQuery )
2021-07-22 02:16:10 +00:00
// Ensure that the user is permitted to edit the post
2021-07-22 04:42:34 +00:00
canEdit , err := canEditPost ( c . Context ( ) , c . Conn , requestedPostId , * c . CurrentUser )
if err != nil {
return ErrorResponse ( http . StatusInternalServerError , err )
} else if ! canEdit {
2021-07-22 02:16:10 +00:00
return FourOhFour ( c )
}
2021-07-22 01:41:23 +00:00
baseData := getBaseData ( c )
baseData . Title = fmt . Sprintf ( "Editing \"%s\" | %s" , result . Thread . Title , * categoryTree [ currentCatId ] . Name )
baseData . MathjaxEnabled = true
// TODO(ben): Set breadcrumbs
templatePost := templates . PostToTemplate ( & result . Post , result . Author , c . Theme )
templatePost . AddContentVersion ( result . CurrentVersion , result . Editor )
var res ResponseData
res . MustWriteTemplate ( "editor.html" , editorData {
BaseData : baseData ,
SubmitUrl : hmnurl . BuildForumPostEdit ( c . CurrentProject . Slug , lineageBuilder . GetSubforumLineageSlugs ( currentCatId ) , requestedThreadId , requestedPostId ) ,
2021-07-22 02:16:10 +00:00
Title : result . Thread . Title ,
2021-07-22 01:41:23 +00:00
SubmitLabel : "Submit Edited Post" ,
IsEditing : true ,
EditInitialContents : result . CurrentVersion . TextRaw ,
} , c . Perf )
return res
}
func ForumPostEditSubmit ( c * RequestContext ) ResponseData {
tx , err := c . Conn . Begin ( c . Context ( ) )
if err != nil {
panic ( err )
2021-07-20 02:35:22 +00:00
}
2021-07-22 01:41:23 +00:00
defer tx . Rollback ( c . Context ( ) )
2021-07-20 02:35:22 +00:00
2021-07-22 01:41:23 +00:00
c . Perf . StartBlock ( "SQL" , "Fetch category tree" )
categoryTree := models . GetFullCategoryTree ( c . Context ( ) , c . Conn )
lineageBuilder := models . MakeCategoryLineageBuilder ( categoryTree )
c . Perf . EndBlock ( )
currentCatId , valid := validateSubforums ( lineageBuilder , c . CurrentProject , c . PathParams [ "cats" ] )
if ! valid {
return FourOhFour ( c )
}
threadId , err := strconv . Atoi ( c . PathParams [ "threadid" ] )
if err != nil {
return FourOhFour ( c )
}
postId , err := strconv . Atoi ( c . PathParams [ "postid" ] )
if err != nil {
return FourOhFour ( c )
}
2021-07-22 02:16:10 +00:00
// Ensure that the user is permitted to edit the post
2021-07-22 04:42:34 +00:00
canEdit , err := canEditPost ( c . Context ( ) , c . Conn , postId , * c . CurrentUser )
if err != nil {
return ErrorResponse ( http . StatusInternalServerError , err )
} else if ! canEdit {
return FourOhFour ( c )
}
c . Req . ParseForm ( )
unparsed := c . Req . Form . Get ( "body" )
editReason := c . Req . Form . Get ( "editreason" )
createForumPostVersion ( c . Context ( ) , tx , postId , unparsed , c . Req . Host , editReason , & c . CurrentUser . ID )
err = tx . Commit ( c . Context ( ) )
if err != nil {
return ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to edit forum post" ) )
}
postUrl := hmnurl . BuildForumPost ( c . CurrentProject . Slug , lineageBuilder . GetSubforumLineageSlugs ( currentCatId ) , threadId , postId )
return c . Redirect ( postUrl , http . StatusSeeOther )
}
func ForumPostDelete ( c * RequestContext ) ResponseData {
// TODO(compression): This logic for fetching posts by thread id / post id is gonna
// show up in a lot of places. It's used multiple times for forums, and also for blogs.
// Consider compressing this later.
c . Perf . StartBlock ( "SQL" , "Fetch category tree" )
categoryTree := models . GetFullCategoryTree ( c . Context ( ) , c . Conn )
lineageBuilder := models . MakeCategoryLineageBuilder ( categoryTree )
c . Perf . EndBlock ( )
currentCatId , valid := validateSubforums ( lineageBuilder , c . CurrentProject , c . PathParams [ "cats" ] )
if ! valid {
return FourOhFour ( c )
}
requestedThreadId , err := strconv . Atoi ( c . PathParams [ "threadid" ] )
if err != nil {
return FourOhFour ( c )
}
requestedPostId , err := strconv . Atoi ( c . PathParams [ "postid" ] )
if err != nil {
return FourOhFour ( c )
}
// Ensure that the user is allowed to delete this post
canEdit , err := canEditPost ( c . Context ( ) , c . Conn , requestedPostId , * c . CurrentUser )
if err != nil {
return ErrorResponse ( http . StatusInternalServerError , err )
} else if ! canEdit {
return FourOhFour ( c )
}
c . Perf . StartBlock ( "SQL" , "Fetch post to delete" )
type postQuery struct {
Thread models . Thread ` db:"thread" `
Post models . Post ` db:"post" `
CurrentVersion models . PostVersion ` db:"ver" `
Author * models . User ` db:"author" `
Editor * models . User ` db:"editor" `
2021-07-22 02:16:10 +00:00
}
2021-07-22 04:42:34 +00:00
postQueryResult , err := db . QueryOne ( c . Context ( ) , c . Conn , postQuery { } ,
2021-07-22 02:16:10 +00:00
`
SELECT $ columns
FROM
2021-07-22 04:42:34 +00:00
handmade_thread AS thread
JOIN handmade_post AS post ON post . thread_id = thread . id
JOIN handmade_postversion AS ver ON post . current_id = ver . id
2021-07-22 02:16:10 +00:00
LEFT JOIN auth_user AS author ON post . author_id = author . id
2021-07-22 04:42:34 +00:00
LEFT JOIN auth_user AS editor ON ver . editor_id = editor . id
2021-07-22 02:16:10 +00:00
WHERE
post . category_id = $ 1
AND post . thread_id = $ 2
AND post . id = $ 3
AND NOT post . deleted
ORDER BY postdate
` ,
currentCatId ,
2021-07-22 04:42:34 +00:00
requestedThreadId ,
requestedPostId ,
2021-07-22 02:16:10 +00:00
)
2021-07-22 04:42:34 +00:00
if err != nil {
if errors . Is ( err , db . ErrNoMatchingRows ) {
return FourOhFour ( c )
} else {
return ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to fetch post to delete" ) )
}
2021-07-22 02:16:10 +00:00
}
2021-07-22 04:42:34 +00:00
result := postQueryResult . ( * postQuery )
baseData := getBaseData ( c )
baseData . Title = fmt . Sprintf ( "Deleting post in \"%s\" | %s" , result . Thread . Title , * categoryTree [ currentCatId ] . Name )
baseData . MathjaxEnabled = true
// TODO(ben): Set breadcrumbs
templatePost := templates . PostToTemplate ( & result . Post , result . Author , c . Theme )
templatePost . AddContentVersion ( result . CurrentVersion , result . Editor )
type forumPostDeleteData struct {
templates . BaseData
Post templates . Post
SubmitUrl string
}
var res ResponseData
res . MustWriteTemplate ( "forum_post_delete.html" , forumPostDeleteData {
BaseData : baseData ,
SubmitUrl : hmnurl . BuildForumPostDelete ( c . CurrentProject . Slug , lineageBuilder . GetSubforumLineageSlugs ( currentCatId ) , requestedThreadId , requestedPostId ) ,
Post : templatePost ,
} , c . Perf )
return res
}
func ForumPostDeleteSubmit ( c * RequestContext ) ResponseData {
// TODO(compression): This logic for fetching posts by thread id / post id is gonna
// show up in a lot of places. It's used multiple times for forums, and also for blogs.
// Consider compressing this later.
c . Perf . StartBlock ( "SQL" , "Fetch category tree" )
categoryTree := models . GetFullCategoryTree ( c . Context ( ) , c . Conn )
lineageBuilder := models . MakeCategoryLineageBuilder ( categoryTree )
c . Perf . EndBlock ( )
2021-07-22 02:16:10 +00:00
2021-07-22 04:42:34 +00:00
currentCatId , valid := validateSubforums ( lineageBuilder , c . CurrentProject , c . PathParams [ "cats" ] )
if ! valid {
2021-07-22 02:16:10 +00:00
return FourOhFour ( c )
}
2021-07-22 01:41:23 +00:00
2021-07-22 04:42:34 +00:00
threadId , err := strconv . Atoi ( c . PathParams [ "threadid" ] )
if err != nil {
return FourOhFour ( c )
}
2021-07-22 01:41:23 +00:00
2021-07-22 04:42:34 +00:00
postId , err := strconv . Atoi ( c . PathParams [ "postid" ] )
if err != nil {
return FourOhFour ( c )
}
// Ensure that the user is allowed to delete this post
canEdit , err := canEditPost ( c . Context ( ) , c . Conn , postId , * c . CurrentUser )
if err != nil {
return ErrorResponse ( http . StatusInternalServerError , err )
} else if ! canEdit {
return FourOhFour ( c )
}
tx , err := c . Conn . Begin ( c . Context ( ) )
if err != nil {
panic ( err )
}
defer tx . Rollback ( c . Context ( ) )
isFirstPost , err := db . QueryBool ( c . Context ( ) , tx ,
`
SELECT thread . first_id = $ 1
FROM
handmade_thread AS thread
WHERE
thread . id = $ 2
` ,
postId ,
threadId ,
)
if err != nil {
return ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to check if post was the first post in the thread" ) )
}
if isFirstPost {
// Just delete the whole thread and all its posts.
_ , err = tx . Exec ( c . Context ( ) ,
`
UPDATE handmade_thread
SET deleted = TRUE
WHERE id = $ 1
` ,
threadId ,
)
_ , err = tx . Exec ( c . Context ( ) ,
`
UPDATE handmade_post
SET deleted = TRUE
WHERE thread_id = $ 1
` ,
threadId ,
)
err = tx . Commit ( c . Context ( ) )
if err != nil {
return ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to delete thread and posts when deleting the first post" ) )
}
forumUrl := hmnurl . BuildForumCategory ( c . CurrentProject . Slug , lineageBuilder . GetSubforumLineageSlugs ( currentCatId ) , 1 )
return c . Redirect ( forumUrl , http . StatusSeeOther )
}
_ , err = tx . Exec ( c . Context ( ) ,
`
UPDATE handmade_post
SET deleted = TRUE
WHERE
id = $ 1
` ,
postId ,
)
if err != nil {
return ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to mark forum post as deleted" ) )
}
err = fixThreadPostIds ( c . Context ( ) , tx , threadId )
if err != nil {
if errors . Is ( err , errThreadEmpty ) {
panic ( "it shouldn't be possible to delete the last remaining post in a thread, without it also being the first post in the thread and thus resulting in the whole thread getting deleted earlier" )
} else {
return ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to fix up thread post ids" ) )
}
}
2021-07-22 01:41:23 +00:00
err = tx . Commit ( c . Context ( ) )
if err != nil {
2021-07-22 04:42:34 +00:00
return ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to delete post" ) )
2021-07-22 01:41:23 +00:00
}
2021-07-22 04:42:34 +00:00
threadUrl := hmnurl . BuildForumThread ( c . CurrentProject . Slug , lineageBuilder . GetSubforumLineageSlugs ( currentCatId ) , threadId , "" , 1 ) // TODO: Go to the last page of the thread? Or the post before the post we just deleted?
return c . Redirect ( threadUrl , http . StatusSeeOther )
2021-07-22 01:41:23 +00:00
}
func createNewForumPostAndVersion ( ctx context . Context , tx pgx . Tx , catId , threadId , userId , projectId int , unparsedContent string , ipString string , replyId * int ) ( postId , versionId int ) {
2021-07-04 22:48:08 +00:00
// Create post
2021-07-20 02:35:22 +00:00
err := tx . QueryRow ( ctx ,
2021-07-04 22:48:08 +00:00
`
2021-07-22 02:16:10 +00:00
INSERT INTO handmade_post ( postdate , category_id , thread_id , current_id , author_id , category_kind , project_id , reply_id , preview )
VALUES ( $ 1 , $ 2 , $ 3 , $ 4 , $ 5 , $ 6 , $ 7 , $ 8 , $ 9 )
2021-07-04 22:48:08 +00:00
RETURNING id
` ,
2021-07-22 01:41:23 +00:00
time . Now ( ) ,
2021-07-20 02:35:22 +00:00
catId ,
2021-07-04 22:48:08 +00:00
threadId ,
- 1 ,
2021-07-20 02:35:22 +00:00
userId ,
2021-07-04 22:48:08 +00:00
models . CatKindForum ,
2021-07-20 02:35:22 +00:00
projectId ,
2021-07-20 03:07:15 +00:00
replyId ,
2021-07-22 02:16:10 +00:00
"" , // empty preview, will be updated later
2021-07-04 22:48:08 +00:00
) . Scan ( & postId )
if err != nil {
panic ( oops . New ( err , "failed to create post" ) )
}
2021-07-22 01:41:23 +00:00
versionId = createForumPostVersion ( ctx , tx , postId , unparsedContent , ipString , "" , nil )
return
}
func createForumPostVersion ( ctx context . Context , tx pgx . Tx , postId int , unparsedContent string , ipString string , editReason string , editorId * int ) ( versionId int ) {
parsed := parsing . ParsePostInput ( unparsedContent , parsing . RealMarkdown )
ip := net . ParseIP ( ipString )
const previewMaxLength = 100
parsedPlaintext := parsing . ParsePostInput ( unparsedContent , parsing . PlaintextMarkdown )
preview := parsedPlaintext
if len ( preview ) > previewMaxLength - 1 {
preview = preview [ : previewMaxLength - 1 ] + "…"
}
2021-07-04 20:25:28 +00:00
// Create post version
2021-07-22 01:41:23 +00:00
err := tx . QueryRow ( ctx ,
2021-07-04 20:25:28 +00:00
`
2021-07-22 01:41:23 +00:00
INSERT INTO handmade_postversion ( post_id , text_raw , text_parsed , ip , date , edit_reason , editor_id )
VALUES ( $ 1 , $ 2 , $ 3 , $ 4 , $ 5 , $ 6 , $ 7 )
2021-07-04 22:48:08 +00:00
RETURNING id
2021-07-04 20:25:28 +00:00
` ,
2021-07-04 22:48:08 +00:00
postId ,
2021-07-20 02:35:22 +00:00
unparsedContent ,
2021-07-04 20:25:28 +00:00
parsed ,
2021-07-04 22:48:08 +00:00
ip ,
2021-07-22 01:41:23 +00:00
time . Now ( ) ,
editReason ,
editorId ,
2021-07-04 22:48:08 +00:00
) . Scan ( & versionId )
if err != nil {
panic ( oops . New ( err , "failed to create post version" ) )
}
2021-07-22 01:41:23 +00:00
// Update post with version id and preview
2021-07-20 02:35:22 +00:00
_ , err = tx . Exec ( ctx ,
2021-07-04 22:48:08 +00:00
`
UPDATE handmade_post
2021-07-22 01:41:23 +00:00
SET current_id = $ 1 , preview = $ 2
WHERE id = $ 3
2021-07-04 22:48:08 +00:00
` ,
versionId ,
2021-07-22 01:41:23 +00:00
preview ,
2021-07-04 22:48:08 +00:00
postId ,
)
if err != nil {
2021-07-22 01:41:23 +00:00
panic ( oops . New ( err , "failed to set current post version and preview" ) )
2021-07-04 22:48:08 +00:00
}
2021-07-20 02:35:22 +00:00
return
2021-06-12 03:51:07 +00:00
}
2021-05-11 22:53:23 +00:00
func validateSubforums ( lineageBuilder * models . CategoryLineageBuilder , project * models . Project , catPath string ) ( int , bool ) {
if project . ForumID == nil {
return - 1 , false
}
2021-05-03 14:51:07 +00:00
2021-05-11 22:53:23 +00:00
subforumCatId := * project . ForumID
if len ( catPath ) == 0 {
return subforumCatId , true
}
2021-04-29 04:52:27 +00:00
2021-05-11 22:53:23 +00:00
catPath = strings . ToLower ( catPath )
valid := false
catSlugs := strings . Split ( catPath , "/" )
lastSlug := catSlugs [ len ( catSlugs ) - 1 ]
if len ( lastSlug ) > 0 {
lastSlugCatId := lineageBuilder . FindIdBySlug ( project . ID , lastSlug )
if lastSlugCatId != - 1 {
subforumSlugs := lineageBuilder . GetSubforumLineageSlugs ( lastSlugCatId )
allMatch := true
for i , subforum := range subforumSlugs {
if subforum != catSlugs [ i ] {
allMatch = false
break
}
}
valid = allMatch
}
if valid {
subforumCatId = lastSlugCatId
}
2021-05-03 14:51:07 +00:00
}
2021-05-11 22:53:23 +00:00
return subforumCatId , valid
2021-04-29 04:52:27 +00:00
}
2021-07-22 04:42:34 +00:00
func canEditPost ( ctx context . Context , conn * pgxpool . Pool , postId int , currentUser models . User ) ( bool , error ) {
if currentUser . IsStaff {
return true , nil
}
type postResult struct {
AuthorID * int ` db:"author.id" `
}
iresult , err := db . QueryOne ( ctx , conn , postResult { } ,
`
SELECT $ columns
FROM
handmade_post AS post
LEFT JOIN auth_user AS author ON post . author_id = author . id
WHERE
post . id = $ 1
AND NOT post . deleted
ORDER BY postdate
` ,
postId ,
)
if err != nil {
if errors . Is ( err , db . ErrNoMatchingRows ) {
return false , nil
} else {
return false , oops . New ( err , "failed to get author of post when checking permissions" )
}
}
result := iresult . ( * postResult )
return result . AuthorID != nil && * result . AuthorID == currentUser . ID , nil
}
var errThreadEmpty = errors . New ( "thread contained no non-deleted posts" )
/ *
Ensures that the first_id and last_id on the thread are still good .
Returns errThreadEmpty if the thread contains no visible posts any more .
You should probably mark the thread as deleted in this case .
* /
func fixThreadPostIds ( ctx context . Context , tx pgx . Tx , threadId int ) error {
postsIter , err := db . Query ( ctx , tx , models . Post { } ,
`
SELECT $ columns
FROM handmade_post
WHERE
thread_id = $ 1
AND NOT deleted
` ,
threadId ,
)
if err != nil {
return oops . New ( err , "failed to fetch posts when fixing up thread" )
}
var firstPost , lastPost * models . Post
for _ , ipost := range postsIter . ToSlice ( ) {
post := ipost . ( * models . Post )
if firstPost == nil || post . PostDate . Before ( firstPost . PostDate ) {
firstPost = post
}
if lastPost == nil || post . PostDate . After ( lastPost . PostDate ) {
lastPost = post
}
}
if firstPost == nil || lastPost == nil {
return errThreadEmpty
}
_ , err = tx . Exec ( ctx ,
`
UPDATE handmade_thread
SET first_id = $ 1 , last_id = $ 2
WHERE id = $ 3
` ,
firstPost . ID ,
lastPost . ID ,
threadId ,
)
if err != nil {
return oops . New ( err , "failed to update thread first/last ids" )
}
return nil
}