Start work on forum category indexes

This commit is contained in:
Ben Visness 2021-04-28 23:52:27 -05:00
parent 314ae26e18
commit 285fd3eaf0
8 changed files with 234 additions and 11 deletions

View File

@ -0,0 +1,60 @@
package migrations
import (
"context"
"time"
"git.handmade.network/hmn/hmn/src/migration/types"
"git.handmade.network/hmn/hmn/src/oops"
"github.com/jackc/pgx/v4"
)
func init() {
registerMigration(RenameModeratedFields{})
}
type RenameModeratedFields struct{}
func (m RenameModeratedFields) Version() types.MigrationVersion {
return types.MigrationVersion(time.Date(2021, 4, 29, 4, 35, 48, 0, time.UTC))
}
func (m RenameModeratedFields) Name() string {
return "RenameModeratedFields"
}
func (m RenameModeratedFields) Description() string {
return "Rename 'moderated' to 'deleted'"
}
func (m RenameModeratedFields) Up(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx,
`
ALTER TABLE handmade_thread
RENAME moderated TO deleted;
ALTER TABLE handmade_post
RENAME moderated TO deleted;
`)
if err != nil {
return oops.New(err, "failed to rename columns")
}
_, err = tx.Exec(ctx,
`
ALTER TABLE handmade_thread
ALTER deleted TYPE bool USING CASE WHEN deleted = 0 THEN FALSE ELSE TRUE END;
ALTER TABLE handmade_thread ALTER COLUMN deleted SET DEFAULT FALSE;
ALTER TABLE handmade_post ALTER COLUMN deleted SET DEFAULT FALSE;
`,
)
if err != nil {
return oops.New(err, "failed to convert ints to bools")
}
return nil
}
func (m RenameModeratedFields) Down(ctx context.Context, tx pgx.Tx) error {
panic("Implement me")
}

View File

@ -24,7 +24,7 @@ type Post struct {
PostDate time.Time `db:"postdate"` PostDate time.Time `db:"postdate"`
IP net.IPNet `db:"ip"` IP net.IPNet `db:"ip"`
Sticky bool `db:"sticky"` Sticky bool `db:"sticky"`
Moderated bool `db:"moderated"` // TODO: I'm not sure this is ever meaningfully used. It always seems to be 0 / false? Deleted bool `db:"deleted"` // TODO: I'm not sure this is ever meaningfully used. It always seems to be 0 / false?
Hits int `db:"hits"` Hits int `db:"hits"`
Featured bool `db:"featured"` Featured bool `db:"featured"`
FeatureVotes int `db:"featurevotes"` // TODO: Remove this column from the db, it's never used FeatureVotes int `db:"featurevotes"` // TODO: Remove this column from the db, it's never used

View File

@ -10,7 +10,7 @@ type Thread struct {
ReplyCount int `db:"reply_count"` ReplyCount int `db:"reply_count"`
Sticky bool `db:"sticky"` Sticky bool `db:"sticky"`
Locked bool `db:"locked"` Locked bool `db:"locked"`
Moderated int `db:"moderated"` Deleted bool `db:"deleted"`
FirstID *int `db:"first_id"` FirstID *int `db:"first_id"`
LastID *int `db:"last_id"` LastID *int `db:"last_id"`

View File

@ -19,7 +19,6 @@ type Thread struct {
Locked bool Locked bool
Sticky bool Sticky bool
Moderated bool
} }
type Post struct { type Post struct {

View File

@ -32,7 +32,7 @@ func Feed(c *RequestContext) ResponseData {
handmade_post AS post handmade_post AS post
WHERE WHERE
post.category_kind = ANY ($1) post.category_kind = ANY ($1)
AND NOT moderated AND NOT deleted
`, `,
[]models.CategoryKind{models.CatKindForum, models.CatKindBlog, models.CatKindWiki, models.CatKindLibraryResource}, []models.CategoryKind{models.CatKindForum, models.CatKindBlog, models.CatKindWiki, models.CatKindLibraryResource},
) )
@ -41,7 +41,7 @@ func Feed(c *RequestContext) ResponseData {
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get count of feed posts")) return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get count of feed posts"))
} }
numPages := int(math.Ceil(float64(numPosts) / 30)) numPages := int(math.Ceil(float64(numPosts) / postsPerPage))
page := 1 page := 1
pageString, hasPage := c.PathParams["page"] pageString, hasPage := c.PathParams["page"]
@ -102,7 +102,7 @@ func Feed(c *RequestContext) ResponseData {
LEFT OUTER JOIN auth_user ON post.author_id = auth_user.id LEFT OUTER JOIN auth_user ON post.author_id = auth_user.id
WHERE WHERE
post.category_kind = ANY ($2) post.category_kind = ANY ($2)
AND post.moderated = FALSE AND post.deleted = FALSE
AND post.thread_id IS NOT NULL AND post.thread_id IS NOT NULL
ORDER BY postdate DESC ORDER BY postdate DESC
LIMIT $3 OFFSET $4 LIMIT $3 OFFSET $4

164
src/website/forums.go Normal file
View File

@ -0,0 +1,164 @@
package website
import (
"fmt"
"math"
"net/http"
"strconv"
"strings"
"time"
"git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
)
func ForumCategory(c *RequestContext) ResponseData {
const threadsPerPage = 25
catPath := c.PathParams["cats"]
catSlugs := strings.Split(catPath, "/")
catSlug := catSlugs[len(catSlugs)-1]
if len(catSlugs) == 1 {
catSlug = ""
}
// TODO: Is this query right? Do we need to do a better special case for when it's the root category?
currentCatId, err := db.QueryInt(c.Context(), c.Conn,
`
SELECT id
FROM handmade_category
WHERE
slug = $1
AND kind = $2
AND project_id = $3
`,
catSlug,
models.CatKindForum,
c.CurrentProject.ID,
)
if err != nil {
panic(oops.New(err, "failed to get current category id"))
}
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"))
}
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
}
type mainPostsQueryResult struct {
Thread models.Thread `db:"thread"`
FirstPost models.Post `db:"firstpost"`
LastPost models.Post `db:"lastpost"`
ThreadLastReadTime *time.Time `db:"tlri.lastread"`
CatLastReadTime *time.Time `db:"clri.lastread"`
}
itMainThreads, err := db.Query(c.Context(), c.Conn, mainPostsQueryResult{},
`
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 OUTER JOIN handmade_threadlastreadinfo AS tlri ON (
tlri.thread_id = thread.id
AND tlri.user_id = $2
)
LEFT OUTER JOIN handmade_categorylastreadinfo AS clri ON (
clri.category_id = $1
AND clri.user_id = $2
)
-- LEFT OUTER JOIN auth_user ON post.author_id = auth_user.id
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"))
}
var res ResponseData
for _, irow := range itMainThreads.ToSlice() {
row := irow.(*mainPostsQueryResult)
res.Write([]byte(fmt.Sprintf("%s\n", row.Thread.Title)))
}
// ---------------------
// Subcategory things
// ---------------------
c.Perf.StartBlock("SQL", "Fetch subcategories")
type queryResult struct {
Cat models.Category `db:"cat"`
}
itSubcats, err := db.Query(c.Context(), c.Conn, queryResult{},
`
WITH current AS (
SELECT id
FROM handmade_category
WHERE
slug = $1
AND kind = $2
AND project_id = $3
)
SELECT $columns
FROM
handmade_category AS cat,
current
WHERE
cat.id = current.id
OR cat.parent_id = current.id
`,
catSlug,
models.CatKindForum,
c.CurrentProject.ID,
)
if err != nil {
panic(oops.New(err, "failed to fetch subcategories"))
}
c.Perf.EndBlock()
_ = itSubcats // TODO: Actually query subcategory post data
return res
}

View File

@ -101,7 +101,7 @@ func Index(c *RequestContext) ResponseData {
WHERE WHERE
post.project_id = $2 post.project_id = $2
AND post.category_kind IN ($3, $4, $5, $6) AND post.category_kind IN ($3, $4, $5, $6)
AND post.moderated = FALSE AND post.deleted = FALSE
ORDER BY postdate DESC ORDER BY postdate DESC
LIMIT $7 LIMIT $7
`, `,
@ -258,7 +258,7 @@ func Index(c *RequestContext) ResponseData {
cat.project_id = $1 cat.project_id = $1
AND cat.kind = $2 AND cat.kind = $2
AND post.id = thread.first_id AND post.id = thread.first_id
AND thread.moderated = 0 AND NOT thread.deleted
ORDER BY post.postdate DESC ORDER BY post.postdate DESC
LIMIT 1 LIMIT 1
`, `,

View File

@ -72,7 +72,7 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
}) })
mainRoutes.GET(`^/feed(/(?P<page>.+)?)?$`, Feed) mainRoutes.GET(`^/feed(/(?P<page>.+)?)?$`, Feed)
// mainRoutes.GET(`^/(?P<cats>forums(/cat)*)$`, Category) mainRoutes.GET(`^/(?P<cats>forums(/.+?)*)$`, ForumCategory)
// mainRoutes.GET(`^/(?P<cats>forums(/cat)*)/t/(?P<threadid>\d+)/p/(?P<postid>\d+)$`, ForumPost) // mainRoutes.GET(`^/(?P<cats>forums(/cat)*)/t/(?P<threadid>\d+)/p/(?P<postid>\d+)$`, ForumPost)
mainRoutes.GET("^/assets/project.css$", ProjectCSS) mainRoutes.GET("^/assets/project.css$", ProjectCSS)