Start work on forum category indexes
This commit is contained in:
parent
314ae26e18
commit
285fd3eaf0
|
@ -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")
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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"`
|
||||||
|
|
|
@ -19,7 +19,6 @@ type Thread struct {
|
||||||
|
|
||||||
Locked bool
|
Locked bool
|
||||||
Sticky bool
|
Sticky bool
|
||||||
Moderated bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Post struct {
|
type Post struct {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
`,
|
`,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue