2021-04-29 04:52:27 +00:00
|
|
|
package website
|
|
|
|
|
|
|
|
import (
|
2021-05-03 14:51:07 +00:00
|
|
|
"context"
|
2021-05-03 22:45:17 +00:00
|
|
|
"fmt"
|
2021-04-29 04:52:27 +00:00
|
|
|
"math"
|
|
|
|
"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-05-03 23:59:43 +00:00
|
|
|
"git.handmade.network/hmn/hmn/src/templates"
|
|
|
|
"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-03 23:59:43 +00:00
|
|
|
CategoryUrl string
|
|
|
|
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-04-29 04:52:27 +00:00
|
|
|
func ForumCategory(c *RequestContext) ResponseData {
|
|
|
|
const threadsPerPage = 25
|
|
|
|
|
|
|
|
catPath := c.PathParams["cats"]
|
|
|
|
catSlugs := strings.Split(catPath, "/")
|
2021-05-03 14:51:07 +00:00
|
|
|
currentCatId := fetchCatIdFromSlugs(c.Context(), c.Conn, catSlugs, c.CurrentProject.ID)
|
|
|
|
categoryUrls := GetProjectCategoryUrls(c.Context(), c.Conn, c.CurrentProject.ID)
|
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
|
|
|
|
|
|
|
numPages := int(math.Ceil(float64(numThreads) / threadsPerPage))
|
|
|
|
|
|
|
|
page := 1
|
|
|
|
pageString, hasPage := c.PathParams["page"]
|
|
|
|
if hasPage && pageString != "" {
|
|
|
|
if pageParsed, err := strconv.Atoi(pageString); err == nil {
|
|
|
|
page = pageParsed
|
|
|
|
} else {
|
|
|
|
return c.Redirect("/feed", http.StatusSeeOther) // TODO
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if page < 1 || numPages < page {
|
|
|
|
return c.Redirect("/feed", http.StatusSeeOther) // TODO
|
|
|
|
}
|
|
|
|
|
|
|
|
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-03 14:51:07 +00:00
|
|
|
Title: row.Thread.Title,
|
|
|
|
Url: ThreadUrl(row.Thread, models.CatKindForum, categoryUrls[currentCatId]),
|
|
|
|
|
|
|
|
FirstUser: templates.UserToTemplate(row.FirstUser),
|
|
|
|
FirstDate: row.FirstPost.PostDate,
|
|
|
|
LastUser: templates.UserToTemplate(row.LastUser),
|
|
|
|
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 {
|
|
|
|
c.Perf.StartBlock("SQL", "Fetch subcategories")
|
|
|
|
type subcatQueryResult struct {
|
|
|
|
Cat models.Category `db:"cat"`
|
|
|
|
}
|
|
|
|
itSubcats, err := db.Query(c.Context(), c.Conn, subcatQueryResult{},
|
|
|
|
`
|
|
|
|
SELECT $columns
|
|
|
|
FROM
|
|
|
|
handmade_category AS cat
|
|
|
|
WHERE
|
|
|
|
cat.parent_id = $1
|
|
|
|
`,
|
|
|
|
currentCatId,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
panic(oops.New(err, "failed to fetch subcategories"))
|
|
|
|
}
|
|
|
|
defer itSubcats.Close()
|
|
|
|
c.Perf.EndBlock()
|
|
|
|
|
|
|
|
for _, irow := range itSubcats.ToSlice() {
|
|
|
|
catRow := irow.(*subcatQueryResult)
|
|
|
|
|
|
|
|
c.Perf.StartBlock("SQL", "Fetch count of subcategory threads")
|
|
|
|
numThreads, err := db.QueryInt(c.Context(), c.Conn,
|
|
|
|
`
|
|
|
|
SELECT COUNT(*)
|
|
|
|
FROM handmade_thread AS thread
|
|
|
|
WHERE
|
|
|
|
thread.category_id = $1
|
|
|
|
AND NOT thread.deleted
|
|
|
|
`,
|
|
|
|
catRow.Cat.ID,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
panic(oops.New(err, "failed to get count of threads"))
|
|
|
|
}
|
|
|
|
c.Perf.EndBlock()
|
|
|
|
|
|
|
|
c.Perf.StartBlock("SQL", "Fetch subcategory threads")
|
|
|
|
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
|
|
|
|
`,
|
|
|
|
catRow.Cat.ID,
|
|
|
|
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{
|
|
|
|
Name: *catRow.Cat.Name,
|
|
|
|
Url: categoryUrls[catRow.Cat.ID],
|
|
|
|
Threads: threads,
|
|
|
|
TotalThreads: numThreads,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ---------------------
|
|
|
|
// Template assembly
|
|
|
|
// ---------------------
|
2021-05-03 14:51:07 +00:00
|
|
|
|
|
|
|
baseData := getBaseData(c)
|
2021-05-03 22:45:17 +00:00
|
|
|
baseData.Title = *c.CurrentProject.Name + " Forums"
|
|
|
|
baseData.Breadcrumbs = []templates.Breadcrumb{
|
|
|
|
{
|
|
|
|
Name: *c.CurrentProject.Name,
|
|
|
|
Url: hmnurl.ProjectUrl("/", nil, c.CurrentProject.Subdomain()),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "Forums",
|
|
|
|
Url: categoryUrls[currentCatId],
|
|
|
|
Current: true,
|
|
|
|
},
|
|
|
|
}
|
2021-05-03 14:51:07 +00:00
|
|
|
|
|
|
|
var res ResponseData
|
2021-05-03 22:45:17 +00:00
|
|
|
err = res.WriteTemplate("forum_category.html", forumCategoryData{
|
|
|
|
BaseData: baseData,
|
|
|
|
CategoryUrl: categoryUrls[currentCatId],
|
|
|
|
Threads: threads,
|
|
|
|
Pagination: templates.Pagination{
|
|
|
|
Current: page,
|
|
|
|
Total: numPages,
|
|
|
|
|
|
|
|
FirstUrl: categoryUrls[currentCatId],
|
|
|
|
LastUrl: fmt.Sprintf("%s/%d", categoryUrls[currentCatId], numPages),
|
|
|
|
NextUrl: fmt.Sprintf("%s/%d", categoryUrls[currentCatId], page+1),
|
|
|
|
PreviousUrl: fmt.Sprintf("%s/%d", categoryUrls[currentCatId], page-1),
|
|
|
|
},
|
2021-05-03 23:59:43 +00:00
|
|
|
Subcategories: subcats,
|
2021-05-03 14:51:07 +00:00
|
|
|
}, c.Perf)
|
2021-05-03 22:45:17 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
2021-05-03 14:51:07 +00:00
|
|
|
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
func fetchCatIdFromSlugs(ctx context.Context, conn *pgxpool.Pool, catSlugs []string, projectId int) int {
|
|
|
|
if len(catSlugs) == 1 {
|
|
|
|
var err error
|
|
|
|
currentCatId, err := db.QueryInt(ctx, conn,
|
|
|
|
`
|
|
|
|
SELECT cat.id
|
|
|
|
FROM
|
|
|
|
handmade_category AS cat
|
|
|
|
JOIN handmade_project AS proj ON proj.forum_id = cat.id
|
|
|
|
WHERE
|
|
|
|
proj.id = $1
|
|
|
|
AND cat.kind = $2
|
|
|
|
`,
|
|
|
|
projectId,
|
|
|
|
models.CatKindForum,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
panic(oops.New(err, "failed to get root category id"))
|
|
|
|
}
|
|
|
|
|
|
|
|
return currentCatId
|
|
|
|
} else {
|
|
|
|
var err error
|
|
|
|
currentCatId, err := db.QueryInt(ctx, conn,
|
|
|
|
`
|
2021-04-29 04:52:27 +00:00
|
|
|
SELECT id
|
|
|
|
FROM handmade_category
|
|
|
|
WHERE
|
|
|
|
slug = $1
|
|
|
|
AND kind = $2
|
|
|
|
AND project_id = $3
|
2021-05-03 14:51:07 +00:00
|
|
|
`,
|
|
|
|
catSlugs[len(catSlugs)-1],
|
|
|
|
models.CatKindForum,
|
|
|
|
projectId,
|
2021-04-29 04:52:27 +00:00
|
|
|
)
|
2021-05-03 14:51:07 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(oops.New(err, "failed to get current category id"))
|
|
|
|
}
|
2021-04-29 04:52:27 +00:00
|
|
|
|
2021-05-03 14:51:07 +00:00
|
|
|
return currentCatId
|
|
|
|
}
|
2021-04-29 04:52:27 +00:00
|
|
|
}
|