595 lines
17 KiB
Go
595 lines
17 KiB
Go
package website
|
|
|
|
import (
|
|
"errors"
|
|
"math"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.handmade.network/hmn/hmn/src/db"
|
|
"git.handmade.network/hmn/hmn/src/hmnurl"
|
|
"git.handmade.network/hmn/hmn/src/models"
|
|
"git.handmade.network/hmn/hmn/src/oops"
|
|
"git.handmade.network/hmn/hmn/src/templates"
|
|
"git.handmade.network/hmn/hmn/src/utils"
|
|
)
|
|
|
|
type forumCategoryData struct {
|
|
templates.BaseData
|
|
|
|
NewThreadUrl string
|
|
MarkReadUrl string
|
|
Threads []templates.ThreadListItem
|
|
Pagination templates.Pagination
|
|
Subcategories []forumSubcategoryData
|
|
}
|
|
|
|
type forumSubcategoryData struct {
|
|
Name string
|
|
Url string
|
|
Threads []templates.ThreadListItem
|
|
TotalThreads int
|
|
}
|
|
|
|
func ForumCategory(c *RequestContext) ResponseData {
|
|
const threadsPerPage = 25
|
|
|
|
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)
|
|
|
|
c.Perf.StartBlock("SQL", "Fetch count of page 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
|
|
`,
|
|
currentCatId,
|
|
)
|
|
if err != nil {
|
|
panic(oops.New(err, "failed to get count of threads"))
|
|
}
|
|
c.Perf.EndBlock()
|
|
|
|
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(hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, 1), http.StatusSeeOther)
|
|
}
|
|
}
|
|
if page < 1 || numPages < page {
|
|
return c.Redirect(hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, utils.IntClamp(1, page, numPages)), http.StatusSeeOther)
|
|
}
|
|
|
|
howManyThreadsToSkip := (page - 1) * threadsPerPage
|
|
|
|
var currentUserId *int
|
|
if c.CurrentUser != nil {
|
|
currentUserId = &c.CurrentUser.ID
|
|
}
|
|
|
|
c.Perf.StartBlock("SQL", "Fetch page threads")
|
|
type threadQueryResult struct {
|
|
Thread models.Thread `db:"thread"`
|
|
FirstPost models.Post `db:"firstpost"`
|
|
LastPost models.Post `db:"lastpost"`
|
|
FirstUser *models.User `db:"firstuser"`
|
|
LastUser *models.User `db:"lastuser"`
|
|
ThreadLastReadTime *time.Time `db:"tlri.lastread"`
|
|
CatLastReadTime *time.Time `db:"clri.lastread"`
|
|
}
|
|
itMainThreads, 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 OFFSET $4
|
|
`,
|
|
currentCatId,
|
|
currentUserId,
|
|
threadsPerPage,
|
|
howManyThreadsToSkip,
|
|
)
|
|
if err != nil {
|
|
panic(oops.New(err, "failed to fetch threads"))
|
|
}
|
|
c.Perf.EndBlock()
|
|
defer itMainThreads.Close()
|
|
|
|
makeThreadListItem := func(row *threadQueryResult) templates.ThreadListItem {
|
|
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
|
|
}
|
|
|
|
return templates.ThreadListItem{
|
|
Title: row.Thread.Title,
|
|
Url: hmnurl.BuildForumThread(c.CurrentProject.Slug, lineageBuilder.GetSubforumLineageSlugs(row.Thread.CategoryID), row.Thread.ID, row.Thread.Title, 1),
|
|
FirstUser: templates.UserToTemplate(row.FirstUser, c.Theme),
|
|
FirstDate: row.FirstPost.PostDate,
|
|
LastUser: templates.UserToTemplate(row.LastUser, c.Theme),
|
|
LastDate: row.LastPost.PostDate,
|
|
|
|
Unread: !hasRead,
|
|
}
|
|
}
|
|
|
|
var threads []templates.ThreadListItem
|
|
for _, irow := range itMainThreads.ToSlice() {
|
|
row := irow.(*threadQueryResult)
|
|
threads = append(threads, makeThreadListItem(row))
|
|
}
|
|
|
|
// ---------------------
|
|
// Subcategory things
|
|
// ---------------------
|
|
|
|
var subcats []forumSubcategoryData
|
|
if page == 1 {
|
|
subcatNodes := categoryTree[currentCatId].Children
|
|
|
|
for _, catNode := range subcatNodes {
|
|
c.Perf.StartBlock("SQL", "Fetch count of subcategory threads")
|
|
// TODO(asaf): [PERF] [MINOR] Consider replacing querying count per subcat with a single query for all cats with GROUP BY.
|
|
numThreads, err := db.QueryInt(c.Context(), c.Conn,
|
|
`
|
|
SELECT COUNT(*)
|
|
FROM handmade_thread AS thread
|
|
WHERE
|
|
thread.category_id = $1
|
|
AND NOT thread.deleted
|
|
`,
|
|
catNode.ID,
|
|
)
|
|
if err != nil {
|
|
panic(oops.New(err, "failed to get count of threads"))
|
|
}
|
|
c.Perf.EndBlock()
|
|
|
|
c.Perf.StartBlock("SQL", "Fetch subcategory threads")
|
|
// TODO(asaf): [PERF] [MINOR] Consider batching these.
|
|
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
|
|
`,
|
|
catNode.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: *catNode.Name,
|
|
Url: hmnurl.BuildForumCategory(c.CurrentProject.Slug, lineageBuilder.GetSubforumLineageSlugs(catNode.ID), 1),
|
|
Threads: threads,
|
|
TotalThreads: numThreads,
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------
|
|
// Template assembly
|
|
// ---------------------
|
|
|
|
baseData := getBaseData(c)
|
|
baseData.Title = c.CurrentProject.Name + " Forums"
|
|
baseData.Breadcrumbs = []templates.Breadcrumb{ // TODO(ben): This is wrong; it needs to account for subcategories.
|
|
{
|
|
Name: c.CurrentProject.Name,
|
|
Url: hmnurl.BuildProjectHomepage(c.CurrentProject.Slug),
|
|
},
|
|
{
|
|
Name: "Forums",
|
|
Url: hmnurl.BuildForumCategory(c.CurrentProject.Slug, nil, 1),
|
|
Current: true,
|
|
},
|
|
}
|
|
|
|
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),
|
|
})
|
|
}
|
|
|
|
var res ResponseData
|
|
err = res.WriteTemplate("forum_category.html", forumCategoryData{
|
|
BaseData: baseData,
|
|
NewThreadUrl: hmnurl.BuildForumNewThread(c.CurrentProject.Slug, currentSubforumSlugs, false),
|
|
MarkReadUrl: hmnurl.BuildMarkRead(currentCatId),
|
|
Threads: threads,
|
|
Pagination: templates.Pagination{
|
|
Current: page,
|
|
Total: numPages,
|
|
|
|
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)),
|
|
},
|
|
Subcategories: subcats,
|
|
}, c.Perf)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
type forumThreadData struct {
|
|
templates.BaseData
|
|
|
|
Thread templates.Thread
|
|
Posts []templates.Post
|
|
|
|
CategoryUrl string
|
|
ReplyUrl string
|
|
Pagination templates.Pagination
|
|
}
|
|
|
|
var threadViewPostsPerPage = 15
|
|
|
|
func ForumThread(c *RequestContext) ResponseData {
|
|
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)
|
|
}
|
|
|
|
currentSubforumSlugs := lineageBuilder.GetSubforumLineageSlugs(currentCatId)
|
|
|
|
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
|
|
FROM
|
|
handmade_thread AS thread
|
|
JOIN handmade_category AS cat ON cat.id = thread.category_id
|
|
WHERE
|
|
thread.id = $1
|
|
AND NOT thread.deleted
|
|
AND cat.id = $2
|
|
`,
|
|
threadId,
|
|
currentCatId, // NOTE(asaf): This verifies that the requested thread is under the requested subforum.
|
|
)
|
|
c.Perf.EndBlock()
|
|
if err != nil {
|
|
if errors.Is(err, db.ErrNoMatchingRows) {
|
|
return FourOhFour(c)
|
|
} else {
|
|
panic(err)
|
|
}
|
|
}
|
|
thread := irow.(*threadQueryResult).Thread
|
|
|
|
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"))
|
|
}
|
|
page, numPages, ok := getPageInfo(c.PathParams["page"], numPosts, threadViewPostsPerPage)
|
|
if !ok {
|
|
urlNoPage := hmnurl.BuildForumThread(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, thread.Title, 1)
|
|
return c.Redirect(urlNoPage, http.StatusSeeOther)
|
|
}
|
|
pagination := templates.Pagination{
|
|
Current: page,
|
|
Total: numPages,
|
|
|
|
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)),
|
|
}
|
|
|
|
c.Perf.StartBlock("SQL", "Fetch posts")
|
|
type postsQueryResult struct {
|
|
Post models.Post `db:"post"`
|
|
Ver models.PostVersion `db:"ver"`
|
|
Author *models.User `db:"author"`
|
|
Editor *models.User `db:"editor"`
|
|
}
|
|
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
|
|
LEFT JOIN auth_user AS editor ON ver.editor_id = editor.id
|
|
WHERE
|
|
post.thread_id = $1
|
|
AND NOT post.deleted
|
|
ORDER BY postdate
|
|
LIMIT $2 OFFSET $3
|
|
`,
|
|
thread.ID,
|
|
threadViewPostsPerPage,
|
|
(page-1)*threadViewPostsPerPage,
|
|
)
|
|
c.Perf.EndBlock()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer itPosts.Close()
|
|
|
|
var posts []templates.Post
|
|
for _, irow := range itPosts.ToSlice() {
|
|
row := irow.(*postsQueryResult)
|
|
|
|
post := templates.PostToTemplate(&row.Post, row.Author, c.Theme)
|
|
post.AddContentVersion(row.Ver, row.Editor, c.Theme)
|
|
post.AddUrls(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, post.ID)
|
|
|
|
posts = append(posts, post)
|
|
}
|
|
|
|
baseData := getBaseData(c)
|
|
baseData.Title = thread.Title
|
|
// TODO(asaf): Set breadcrumbs
|
|
|
|
var res ResponseData
|
|
err = res.WriteTemplate("forum_thread.html", forumThreadData{
|
|
BaseData: baseData,
|
|
Thread: templates.ThreadToTemplate(&thread),
|
|
Posts: posts,
|
|
CategoryUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, 1),
|
|
ReplyUrl: hmnurl.BuildForumPostReply(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, *thread.FirstID),
|
|
Pagination: pagination,
|
|
}, c.Perf)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
func ForumPostRedirect(c *RequestContext) ResponseData {
|
|
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)
|
|
}
|
|
|
|
type editorData struct {
|
|
templates.BaseData
|
|
SubmitUrl string
|
|
PostTitle string
|
|
PostBody string
|
|
SubmitLabel string
|
|
PreviewLabel string
|
|
}
|
|
|
|
func ForumNewThread(c *RequestContext) ResponseData {
|
|
if c.Req.Method == http.MethodPost {
|
|
// TODO: Get preview data
|
|
}
|
|
|
|
baseData := getBaseData(c)
|
|
baseData.Title = "Create New Thread"
|
|
// TODO(ben): Set breadcrumbs
|
|
|
|
var res ResponseData
|
|
err := res.WriteTemplate("editor.html", editorData{
|
|
BaseData: baseData,
|
|
SubmitLabel: "Post New Thread",
|
|
PreviewLabel: "Preview",
|
|
}, c.Perf)
|
|
// err := res.WriteTemplate("forum_thread.html", forumThreadData{
|
|
// BaseData: baseData,
|
|
// Thread: templates.ThreadToTemplate(&thread),
|
|
// Posts: posts,
|
|
// CategoryUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, 1),
|
|
// ReplyUrl: hmnurl.BuildForumPostReply(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, *thread.FirstID),
|
|
// Pagination: pagination,
|
|
// }, c.Perf)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
func ForumNewThreadSubmit(c *RequestContext) ResponseData {
|
|
var res ResponseData
|
|
return res
|
|
}
|
|
|
|
func validateSubforums(lineageBuilder *models.CategoryLineageBuilder, project *models.Project, catPath string) (int, bool) {
|
|
if project.ForumID == nil {
|
|
return -1, false
|
|
}
|
|
|
|
subforumCatId := *project.ForumID
|
|
if len(catPath) == 0 {
|
|
return subforumCatId, true
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
return subforumCatId, valid
|
|
}
|